tgstation-server 6.1.2
The /tg/station 13 server suite
Loading...
Searching...
No Matches
InstanceController.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics.CodeAnalysis;
4using System.IO;
5using System.Linq;
6using System.Linq.Expressions;
7using System.Reflection;
8using System.Threading;
9using System.Threading.Tasks;
10
11using Microsoft.AspNetCore.Mvc;
12using Microsoft.EntityFrameworkCore;
13using Microsoft.Extensions.Logging;
14using Microsoft.Extensions.Options;
15
32
34{
38 [Route(Routes.InstanceManager)]
39#pragma warning disable CA1506 // TODO: Decomplexify
41 {
45 public const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
46
51
56
61
66
71
76
81
98 IDatabaseContext databaseContext,
99 IAuthenticationContext authenticationContext,
100 ILogger<InstanceController> logger,
107 IOptions<GeneralConfiguration> generalConfigurationOptions,
108 IOptions<SwarmConfiguration> swarmConfigurationOptions,
109 IApiHeadersProvider apiHeaders)
110 : base(
111 databaseContext,
112 authenticationContext,
113 logger,
115 apiHeaders,
116 false)
117 {
118 this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
119 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
120 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
121 this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
122 this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
123
124 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
125 swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
126 }
127
136 [HttpPut]
137 [TgsAuthorize(InstanceManagerRights.Create)]
138 [ProducesResponseType(typeof(InstanceResponse), 200)]
139 [ProducesResponseType(typeof(InstanceResponse), 201)]
140 public async ValueTask<IActionResult> Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken)
141 {
142 ArgumentNullException.ThrowIfNull(model);
143
144 if (String.IsNullOrWhiteSpace(model.Name) || String.IsNullOrWhiteSpace(model.Path))
145 return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceNameOrPath));
146
147 var unNormalizedPath = model.Path;
148 var targetInstancePath = NormalizePath(unNormalizedPath);
149 model.Path = targetInstancePath;
150
151 var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
152
153 bool InstanceIsChildOf(string otherPath)
154 {
155 if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
156 return false;
157
158 bool sameLength = targetInstancePath.Length == otherPath.Length;
159 char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
160 return sameLength
161 || dirSeparatorChar == Path.DirectorySeparatorChar
162 || dirSeparatorChar == Path.AltDirectorySeparatorChar;
163 }
164
165 if (InstanceIsChildOf(installationDirectoryPath))
166 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
167
168 // Validate it's not a child of any other instance
169 IActionResult? earlyOut = null;
170 ulong countOfOtherInstances = 0;
171 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
172 {
173 var newCancellationToken = cts.Token;
174 try
175 {
176 await DatabaseContext
177 .Instances
178 .AsQueryable()
179 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier)
180 .Select(x => new Models.Instance
181 {
182 Path = x.Path,
183 })
184 .ForEachAsync(
185 otherInstance =>
186 {
187 if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
188 earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached));
189 else if (InstanceIsChildOf(otherInstance.Path!))
190 earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
191
192 if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
193 cts.Cancel();
194 },
195 newCancellationToken);
196 }
197 catch (OperationCanceledException)
198 {
199 cancellationToken.ThrowIfCancellationRequested();
200 }
201 }
202
203 if (earlyOut != null)
204 return earlyOut;
205
206 // Last test, ensure it's in the list of valid paths
208 .Select(path => NormalizePath(path))
209 .Any(path => InstanceIsChildOf(path)) ?? true))
210 return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceNotAtWhitelistedPath));
211
212 async ValueTask<bool> DirExistsAndIsNotEmpty()
213 {
214 if (!await ioManager.DirectoryExists(model.Path, cancellationToken))
215 return false;
216
217 var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
218 var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
219
220 var files = await filesTask;
221 var dirs = await dirsTask;
222
223 return files.Concat(dirs).Any();
224 }
225
226 var dirExistsTask = DirExistsAndIsNotEmpty();
227 bool attached = false;
228 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
229 if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken))
230 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
231 else
232 attached = true;
233
234 var newInstance = await CreateDefaultInstance(model, cancellationToken);
235 if (newInstance == null)
236 return Conflict(new ErrorMessageResponse(ErrorCode.NoPortsAvailable));
237
238 DatabaseContext.Instances.Add(newInstance);
239 try
240 {
241 await DatabaseContext.Save(cancellationToken);
242
243 try
244 {
245 // actually reserve it now
246 await ioManager.CreateDirectory(unNormalizedPath, cancellationToken);
247 await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken);
248 }
249 catch
250 {
251 // oh shit delete the model
252 DatabaseContext.Instances.Remove(newInstance);
253
254 // DCT: Operation must always run
255 await DatabaseContext.Save(CancellationToken.None);
256 throw;
257 }
258 }
259 catch (IOException e)
260 {
261 return Conflict(new ErrorMessageResponse(ErrorCode.IOError)
262 {
263 AdditionalData = e.Message,
264 });
265 }
266
267 Logger.LogInformation(
268 "{userName} {attachedOrCreated} instance {instanceName}: {instanceId} ({instancePath})",
270 attached ? "attached" : "created",
271 newInstance.Name,
272 newInstance.Id,
273 newInstance.Path);
274
276 newInstance.InstancePermissionSets.First(),
277 cancellationToken);
278
279 var api = newInstance.ToApi();
280 api.Accessible = true; // instances are always accessible by their creator
281 return attached ? Json(api) : Created(api);
282 }
283
292 [HttpDelete("{id}")]
293 [TgsAuthorize(InstanceManagerRights.Delete)]
294 [ProducesResponseType(204)]
295 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
296 public async ValueTask<IActionResult> Delete(long id, CancellationToken cancellationToken)
297 {
298 var originalModel = await DatabaseContext
299 .Instances
300 .AsQueryable()
301 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier)
302 .FirstOrDefaultAsync(cancellationToken);
303 if (originalModel == default)
304 return this.Gone();
305 if (originalModel.Online!.Value)
306 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceDetachOnline));
307
308 DatabaseContext.Instances.Remove(originalModel);
309
310 var originalPath = originalModel.Path!;
311 var attachFileName = ioManager.ConcatPath(originalPath, InstanceAttachFileName);
312 try
313 {
314 if (await ioManager.DirectoryExists(originalPath, cancellationToken))
315 await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), cancellationToken);
316 }
317 catch (OperationCanceledException)
318 {
319 // DCT: Operation must always run
320 await ioManager.DeleteFile(attachFileName, CancellationToken.None);
321 throw;
322 }
323
324 await DatabaseContext.Save(cancellationToken); // cascades everything
325 return NoContent();
326 }
327
337 [HttpPost]
338 [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
339 [ProducesResponseType(typeof(InstanceResponse), 200)]
340 [ProducesResponseType(typeof(InstanceResponse), 202)]
341 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
342#pragma warning disable CA1502 // TODO: Decomplexify
343 public async ValueTask<IActionResult> Update([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken)
344 {
345 ArgumentNullException.ThrowIfNull(model);
346
347 IQueryable<Models.Instance> InstanceQuery() => DatabaseContext
348 .Instances
349 .AsQueryable()
350 .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier);
351
352 var moveJob = await InstanceQuery()
353 .SelectMany(x => x.Jobs)
354 .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move)
355 .Select(x => new Job(x.Id!.Value))
356 .FirstOrDefaultAsync(cancellationToken);
357
358 if (moveJob != null)
359 {
360 // don't allow them to cancel it if they can't start it.
362 return Forbid();
363 await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken); // cancel it now
364 }
365
366 var originalModel = await InstanceQuery()
367 .Include(x => x.RepositorySettings)
368 .Include(x => x.ChatSettings)
369 .ThenInclude(x => x.Channels)
370 .Include(x => x.DreamDaemonSettings) // need these for onlining
371 .FirstOrDefaultAsync(cancellationToken);
372 if (originalModel == default(Models.Instance))
373 return this.Gone();
374
375 if (ValidateInstanceOnlineStatus(originalModel))
376 await DatabaseContext.Save(cancellationToken);
377
378 var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
379 bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
380 {
381 var memberSelectorExpression = (MemberExpression)expression.Body;
382 var property = (PropertyInfo)memberSelectorExpression.Member;
383
384 var newVal = property.GetValue(model);
385 if (newVal == null)
386 return false;
387 if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
388 return true;
389
390 property.SetValue(originalModel, newVal);
391 return false;
392 }
393
394 string? originalModelPath = null;
395 string? rawPath = null;
396 var originalOnline = originalModel.Online!.Value;
397 if (model.Path != null)
398 {
399 rawPath = NormalizePath(model.Path);
400
401 if (rawPath != originalModel.Path)
402 {
403 if (!userRights.HasFlag(InstanceManagerRights.Relocate))
404 return Forbid();
405 if (originalOnline && model.Online != true)
406 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceRelocateOnline));
407
408 var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
409 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
410 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
411
412 originalModelPath = originalModel.Path;
413 originalModel.Path = rawPath;
414 }
415 }
416
417 var oldAutoUpdateInterval = originalModel.AutoUpdateInterval!.Value;
418 var renamed = model.Name != null && originalModel.Name != model.Name;
419
420 if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
421 || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
422 || CheckModified(x => x.Name, InstanceManagerRights.Rename)
423 || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
424 || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
425 return Forbid();
426
427 if (model.ChatBotLimit.HasValue)
428 {
429 var countOfExistingChatBots = await DatabaseContext
430 .ChatBots
431 .AsQueryable()
432 .Where(x => x.InstanceId == originalModel.Id)
433 .CountAsync(cancellationToken);
434
435 if (countOfExistingChatBots > model.ChatBotLimit.Value)
436 return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax));
437 }
438
439 await DatabaseContext.Save(cancellationToken);
440
441 if (renamed)
442 {
443 // ignoring retval because we don't care if it's offline
444 await WithComponentInstanceNullable(
445 async componentInstance =>
446 {
447 await componentInstance.InstanceRenamed(originalModel.Name!, cancellationToken);
448 return null;
449 },
450 originalModel);
451 }
452
453 var oldAutoStart = originalModel.DreamDaemonSettings!.AutoStart;
454 try
455 {
456 if (originalOnline && model.Online == false)
457 await InstanceOperations.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken);
458 else if (!originalOnline && model.Online == true)
459 {
460 // force autostart false here because we don't want any long running jobs right now
461 // remember to document this
462 originalModel.DreamDaemonSettings.AutoStart = false;
463 await InstanceOperations.OnlineInstance(originalModel, cancellationToken);
464 }
465 }
466 catch (Exception e)
467 {
468 if (e is not OperationCanceledException)
469 Logger.LogError(e, "Error changing instance online state!");
470 originalModel.Online = originalOnline;
471 originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
472 if (originalModelPath != null)
473 originalModel.Path = originalModelPath;
474
475 // DCT: Operation must always run
476 await DatabaseContext.Save(CancellationToken.None);
477 throw;
478 }
479
480 var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new InstanceResponse
481 {
482 Id = originalModel.Id,
483 };
484
485 var moving = originalModelPath != null;
486 if (moving)
487 {
488 var description = $"Move instance ID {originalModel.Id} from {originalModelPath} to {rawPath}";
489 var job = Job.Create(JobCode.Move, AuthenticationContext.User, originalModel, InstanceManagerRights.Relocate);
490 job.Description = description;
491
492 await jobManager.RegisterOperation(
493 job,
494 (core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline
495 => InstanceOperations.MoveInstance(originalModel, originalModelPath!, ct),
496 cancellationToken);
497 api.MoveJob = job.ToApi();
498 }
499
500 if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
501 {
502 // ignoring retval because we don't care if it's offline
503 await WithComponentInstanceNullable(
504 async componentInstance =>
505 {
506 await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value);
507 return null;
508 },
509 originalModel);
510 }
511
512 await CheckAccessible(api, cancellationToken);
513 return moving ? Accepted(api) : Json(api);
514 }
515#pragma warning restore CA1502
516
525 [HttpGet(Routes.List)]
526 [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
527 [ProducesResponseType(typeof(PaginatedResponse<InstanceResponse>), 200)]
528 public async ValueTask<IActionResult> List(
529 [FromQuery] int? page,
530 [FromQuery] int? pageSize,
531 CancellationToken cancellationToken)
532 {
533 IQueryable<Models.Instance> GetBaseQuery()
534 {
535 var query = DatabaseContext
536 .Instances
537 .AsQueryable()
538 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier);
540 query = query
541 .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id))
542 .Where(x => x.InstancePermissionSets.Any(instanceUser =>
543 instanceUser.EngineRights != EngineRights.None ||
544 instanceUser.ChatBotRights != ChatBotRights.None ||
545 instanceUser.ConfigurationRights != ConfigurationRights.None ||
546 instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
547 instanceUser.DreamMakerRights != DreamMakerRights.None ||
548 instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None));
549
550 // Hack for EF IAsyncEnumerable BS
551 return query.Select(x => x);
552 }
553
554 var moveJobs = await GetBaseQuery()
555 .SelectMany(x => x.Jobs)
556 .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move)
557 .Include(x => x.StartedBy!)
558 .ThenInclude(x => x.CreatedBy)
559 .Include(x => x.Instance)
560 .ToListAsync(cancellationToken);
561
562 var needsUpdate = false;
563 var result = await Paginated<Models.Instance, InstanceResponse>(
564 () => ValueTask.FromResult(
566 GetBaseQuery()
567 .OrderBy(x => x.Id))),
568 async instance =>
569 {
570 needsUpdate |= ValidateInstanceOnlineStatus(instance);
571 instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance!.Id == instance.Id)?.ToApi();
572 await CheckAccessible(instance, cancellationToken);
573 },
574 page,
575 pageSize,
576 cancellationToken);
577
578 if (needsUpdate)
579 await DatabaseContext.Save(cancellationToken);
580
581 return result;
582 }
583
592 [HttpGet("{id}")]
593 [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
594 [ProducesResponseType(typeof(InstanceResponse), 200)]
595 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
596 public async ValueTask<IActionResult> GetId(long id, CancellationToken cancellationToken)
597 {
599 IQueryable<Models.Instance> QueryForUser()
600 {
601 var query = DatabaseContext
602 .Instances
603 .AsQueryable()
604 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier);
605
606 if (cantList)
607 query = query.Include(x => x.InstancePermissionSets);
608 return query;
609 }
610
611 var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken);
612
613 if (instance == null)
614 return this.Gone();
615
616 if (ValidateInstanceOnlineStatus(instance))
617 await DatabaseContext.Save(cancellationToken);
618
619 if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Require(x => x.Id)
620 && (instanceUser.RepositoryRights != RepositoryRights.None ||
621 instanceUser.EngineRights != EngineRights.None ||
622 instanceUser.ChatBotRights != ChatBotRights.None ||
623 instanceUser.ConfigurationRights != ConfigurationRights.None ||
624 instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
625 instanceUser.DreamMakerRights != DreamMakerRights.None ||
626 instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None)))
627 return Forbid();
628
629 var api = instance.ToApi();
630
631 var moveJob = await QueryForUser()
632 .SelectMany(x => x.Jobs)
633 .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move)
634 .Include(x => x.StartedBy!)
635 .ThenInclude(x => x.CreatedBy)
636 .Include(x => x.Instance)
637 .FirstOrDefaultAsync(cancellationToken);
638 api.MoveJob = moveJob?.ToApi();
639 await CheckAccessible(api, cancellationToken);
640 return Json(api);
641 }
642
650 [HttpPatch("{id}")]
651 [TgsAuthorize(InstanceManagerRights.GrantPermissions)]
652 [ProducesResponseType(204)]
653 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
654 public async ValueTask<IActionResult> GrantPermissions(long id, CancellationToken cancellationToken)
655 {
656 IQueryable<Models.Instance> BaseQuery() => DatabaseContext
657 .Instances
658 .AsQueryable()
659 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier);
660
661 // ensure the current user has write privilege on the instance
662 var usersInstancePermissionSet = await BaseQuery()
663 .SelectMany(x => x.InstancePermissionSets)
664 .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id)
665 .FirstOrDefaultAsync(cancellationToken);
666 if (usersInstancePermissionSet == default)
667 {
668 // does the instance actually exist?
669 var instanceExists = await BaseQuery()
670 .AnyAsync(cancellationToken);
671
672 if (!instanceExists)
673 return this.Gone();
674
675 var instanceAdminUser = InstanceAdminPermissionSet(null);
676 instanceAdminUser.InstanceId = id;
677 DatabaseContext.InstancePermissionSets.Add(instanceAdminUser);
678 }
679 else
680 InstanceAdminPermissionSet(usersInstancePermissionSet);
681
682 await DatabaseContext.Save(cancellationToken);
683
684 return NoContent();
685 }
686
693 async ValueTask<Models.Instance?> CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
694 {
695 var ddPort = await portAllocator.GetAvailablePort(1024, false, cancellationToken);
696 if (!ddPort.HasValue)
697 return null;
698
699 // try to use the old default if possible
700 const ushort DefaultDreamDaemonPort = 1337;
701 if (ddPort.Value < DefaultDreamDaemonPort)
702 ddPort = await portAllocator.GetAvailablePort(DefaultDreamDaemonPort, false, cancellationToken) ?? ddPort;
703
704 const ushort DefaultApiValidationPort = 1339;
705 var dmPort = await portAllocator
706 .GetAvailablePort(
707 Math.Min((ushort)(ddPort.Value + 1), DefaultApiValidationPort),
708 false,
709 cancellationToken);
710 if (!dmPort.HasValue)
711 return null;
712
713 // try to use the old default if possible
714 if (dmPort < DefaultApiValidationPort)
715 dmPort = await portAllocator.GetAvailablePort(DefaultApiValidationPort, false, cancellationToken) ?? dmPort;
716
717 return new Models.Instance
718 {
719 ConfigurationType = initialSettings.ConfigurationType ?? ConfigurationType.Disallowed,
721 {
722 AllowWebClient = false,
723 AutoStart = false,
724 Port = ddPort,
725 SecurityLevel = DreamDaemonSecurity.Safe,
726 Visibility = DreamDaemonVisibility.Public,
727 StartupTimeout = 60,
728 HealthCheckSeconds = 60,
729 DumpOnHealthCheckRestart = false,
730 TopicRequestTimeout = generalConfiguration.ByondTopicTimeout,
731 AdditionalParameters = String.Empty,
732 StartProfiler = false,
733 LogOutput = false,
734 MapThreads = 0,
735 },
737 {
738 ApiValidationPort = dmPort,
739 ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
740 RequireDMApiValidation = true,
741 Timeout = TimeSpan.FromHours(1),
742 },
743 Name = initialSettings.Name,
744 Online = false,
745 Path = initialSettings.Path,
746 AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0,
747 ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
749 {
750 CommitterEmail = Components.Repository.Repository.DefaultCommitterEmail,
751 CommitterName = Components.Repository.Repository.DefaultCommitterName,
752 PushTestMergeCommits = false,
753 ShowTestMergeCommitters = false,
754 AutoUpdatesKeepTestMerges = false,
755 AutoUpdatesSynchronize = false,
756 PostTestMergeComment = false,
757 CreateGitHubDeployments = false,
758 UpdateSubmodules = true,
759 },
760 InstancePermissionSets = new List<InstancePermissionSet> // give this user full privileges on the instance
761 {
762 InstanceAdminPermissionSet(null),
763 },
764 SwarmIdentifer = swarmConfiguration.Identifier,
765 };
766 }
767
774 {
775 permissionSetToModify ??= new InstancePermissionSet()
776 {
778 PermissionSetId = AuthenticationContext.PermissionSet.Require(x => x.Id),
779 };
780 permissionSetToModify.EngineRights = RightsHelper.AllRights<EngineRights>();
781 permissionSetToModify.ChatBotRights = RightsHelper.AllRights<ChatBotRights>();
782 permissionSetToModify.ConfigurationRights = RightsHelper.AllRights<ConfigurationRights>();
783 permissionSetToModify.DreamDaemonRights = RightsHelper.AllRights<DreamDaemonRights>();
784 permissionSetToModify.DreamMakerRights = RightsHelper.AllRights<DreamMakerRights>();
785 permissionSetToModify.RepositoryRights = RightsHelper.AllRights<RepositoryRights>();
786 permissionSetToModify.InstancePermissionSetRights = RightsHelper.AllRights<InstancePermissionSetRights>();
787 return permissionSetToModify;
788 }
789
795 [return: NotNullIfNotNull(nameof(path))]
796 string? NormalizePath(string? path)
797 {
798 if (path == null)
799 return null;
800
801 path = ioManager.ResolvePath(path);
802 if (platformIdentifier.IsWindows)
803 path = path.ToUpperInvariant().Replace('\\', '/');
804
805 return path;
806 }
807
814 async ValueTask CheckAccessible(InstanceResponse instanceResponse, CancellationToken cancellationToken)
815 {
816 instanceResponse.Accessible = await DatabaseContext
818 .AsQueryable()
819 .Where(x => x.InstanceId == instanceResponse.Id && x.PermissionSetId == AuthenticationContext.PermissionSet.Id)
820 .AnyAsync(cancellationToken);
821 }
822 }
823}
virtual ? long Id
The ID of the entity.
Definition: EntityId.cs:13
string? Path
The path to where the Instance is located. Can only be changed while the Instance is offline....
Definition: Instance.cs:15
ICollection< string >? ValidInstancePaths
Limits the locations instances may be created or attached from.
uint InstanceLimit
The maximum number of Instances allowed.
string? Identifier
The server's identifier.
Definition: SwarmServer.cs:26
virtual ? string Name
The name of the entity represented by the NamedEntity.
Definition: NamedEntity.cs:16
Represents a set of server permissions.
InstanceManagerRights? InstanceManagerRights
The Rights.InstanceManagerRights for the user.
Represents an error message returned by the server.
Routes to a server actions.
Definition: Routes.cs:9
const string InstanceManager
The Models.Instance controller.
Definition: Routes.cs:43
const string List
The postfix for list operations.
Definition: Routes.cs:108
Configuration for the server swarm system.
ObjectResult Created(object payload)
Generic 201 response with a given payload .
ILogger< ApiController > Logger
The ILogger for the ApiController.
readonly IInstanceManager instanceManager
The IInstanceManager for the ComponentInterfacingController.
ApiController for managing Components.Instances.
async ValueTask< Models.Instance?> CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
Creates a default Models.Instance from initialSettings .
async ValueTask< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Instance.
readonly IJobManager jobManager
The IJobManager for the InstanceController.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceController.
string? NormalizePath(string? path)
Normalize a given path for an instance.
readonly IPortAllocator portAllocator
The IPortAllocator for the InstanceController.
async ValueTask< IActionResult > Update([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken)
Modify an Api.Models.Instance's settings.
InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet? permissionSetToModify)
Generate an InstancePermissionSet with full rights.
readonly SwarmConfiguration swarmConfiguration
The SwarmConfiguration for the InstanceController.
async ValueTask< IActionResult > List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
List Api.Models.Instances.
const string InstanceAttachFileName
File name to allow attaching instances.
async ValueTask< IActionResult > Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken)
Create or attach an Api.Models.Instance.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController.
readonly IPermissionsUpdateNotifyee permissionsUpdateNotifyee
The IPermissionsUpdateNotifyee for the InstanceController.
async ValueTask< IActionResult > Delete(long id, CancellationToken cancellationToken)
Detach an Api.Models.Instance with the given id .
async ValueTask CheckAccessible(InstanceResponse instanceResponse, CancellationToken cancellationToken)
Populate the InstanceResponse.Accessible property of a given instanceResponse .
InstanceController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ILogger< InstanceController > logger, IInstanceManager instanceManager, IJobManager jobManager, IIOManager ioManager, IPortAllocator portAllocator, IPlatformIdentifier platformIdentifier, IPermissionsUpdateNotifyee permissionsUpdateNotifyee, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< SwarmConfiguration > swarmConfigurationOptions, IApiHeadersProvider apiHeaders)
Initializes a new instance of the InstanceController class.
readonly IIOManager ioManager
The IIOManager for the InstanceController.
async ValueTask< IActionResult > GrantPermissions(long id, CancellationToken cancellationToken)
Gives the current user full permissions on a given instance id .
Backend abstract implementation of IDatabaseContext.
DbSet< Instance > Instances
The Instances in the DatabaseContext.
DbSet< InstancePermissionSet > InstancePermissionSets
The InstancePermissionSets in the DatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext. A Task representing the running operation.
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
IIOManager that resolves paths to Environment.CurrentDirectory.
const string CurrentDirectory
Path to the current working directory for the IIOManager.
Represents an Api.Models.Instance in the database.
Definition: Instance.cs:11
static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance)
Creates a new job for registering in the Jobs.IJobService.
PermissionSet PermissionSet
The User's effective PermissionSet.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType . The value of rightsType . Note that if InstancePermissionSet is...
Interface for using filesystems.
Definition: IIOManager.cs:13
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns full file names in a given path .
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task< IReadOnlyList< string > > GetDirectories(string path, CancellationToken cancellationToken)
Returns full directory names in a given path .
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path .
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path .
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
Task< bool > DirectoryExists(string path, CancellationToken cancellationToken)
Check that the directory at path exists.
Manages the runtime of Jobs.
Definition: IJobManager.cs:13
Represents the currently authenticated Models.User.
Receives notifications about permissions updates.
ValueTask InstancePermissionSetCreated(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken)
Called when a given instancePermissionSet is successfully created.
For identifying the current platform.
Gets unassigned ports for use by TGS.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition: ErrorCode.cs:13
DreamDaemonVisibility
The visibility setting for DreamDaemon.
JobCode
The different types of Response.JobResponse.
Definition: JobCode.cs:9
ConfigurationType
The type of configuration allowed on an Instance.
DreamDaemonSecurity
DreamDaemon's security level.
ChatBotRights
Rights for chat bots.
ConfigurationRights
Rights for Models.IConfigurationFiles.
DreamMakerRights
Rights for deployment.
RightsType
The type of rights a model uses.
Definition: RightsType.cs:7
EngineRights
Rights for engine version management.
Definition: EngineRights.cs:10
RepositoryRights
Rights for the git repository.
InstancePermissionSetRights
Rights for an Models.Instance.
DreamDaemonRights
Rights for managing DreamDaemon.
InstanceManagerRights
Rights for managing Models.Instances.