tgstation-server 6.8.0
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
16using NCrontab;
17
34
36{
40 [Route(Routes.InstanceManager)]
41#pragma warning disable CA1506 // TODO: Decomplexify
43 {
47 public const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
48
53
58
63
68
73
78
83
100 IDatabaseContext databaseContext,
101 IAuthenticationContext authenticationContext,
102 ILogger<InstanceController> logger,
109 IOptions<GeneralConfiguration> generalConfigurationOptions,
110 IOptions<SwarmConfiguration> swarmConfigurationOptions,
111 IApiHeadersProvider apiHeaders)
112 : base(
113 databaseContext,
114 authenticationContext,
115 logger,
117 apiHeaders,
118 false)
119 {
120 this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
121 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
122 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
123 this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
124 this.permissionsUpdateNotifyee = permissionsUpdateNotifyee ?? throw new ArgumentNullException(nameof(permissionsUpdateNotifyee));
125
126 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
127 swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
128 }
129
138 [HttpPut]
139 [TgsAuthorize(InstanceManagerRights.Create)]
140 [ProducesResponseType(typeof(InstanceResponse), 200)]
141 [ProducesResponseType(typeof(InstanceResponse), 201)]
142 public async ValueTask<IActionResult> Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken)
143 {
144 ArgumentNullException.ThrowIfNull(model);
145
146 if (String.IsNullOrWhiteSpace(model.Name) || String.IsNullOrWhiteSpace(model.Path))
147 return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceNameOrPath));
148
149 IActionResult? earlyOut = ValidateCronSetting(model);
150 if (earlyOut != null)
151 return earlyOut;
152
153 var targetInstancePath = NormalizePath(model.Path!);
154 model.Path = targetInstancePath;
155
156 var installationDirectoryPath = DefaultIOManager.CurrentDirectory;
157 if (await ioManager.PathIsChildOf(installationDirectoryPath, targetInstancePath, cancellationToken))
158 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
159
160 // Validate it's not a child of any other instance
161 var instancePaths = await DatabaseContext
162 .Instances
163 .AsQueryable()
164 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier)
165 .Select(x => new Models.Instance
166 {
167 Path = x.Path,
168 })
169 .ToListAsync(cancellationToken);
170
171 if ((instancePaths.Count + 1) >= generalConfiguration.InstanceLimit)
172 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached));
173
174 var instancePathChecks = instancePaths
175 .Select(otherInstance => ioManager.PathIsChildOf(otherInstance.Path!, targetInstancePath, cancellationToken))
176 .ToArray();
177
178 await Task.WhenAll(instancePathChecks);
179
180 if (instancePathChecks.Any(task => task.Result))
181 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
182
183 // Last test, ensure it's in the list of valid paths
185 .Select(path => ioManager.PathIsChildOf(path, targetInstancePath, cancellationToken))
186 .ToArray()
187 ?? Enumerable.Empty<Task<bool>>();
188 await Task.WhenAll(pathChecks);
189 if (!pathChecks.All(task => task.Result))
190 return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceNotAtWhitelistedPath));
191
192 async ValueTask<bool> DirExistsAndIsNotEmpty()
193 {
194 if (!await ioManager.DirectoryExists(targetInstancePath, cancellationToken))
195 return false;
196
197 var filesTask = ioManager.GetFiles(targetInstancePath, cancellationToken);
198 var dirsTask = ioManager.GetDirectories(targetInstancePath, cancellationToken);
199
200 var files = await filesTask;
201 var dirs = await dirsTask;
202
203 return files.Concat(dirs).Any();
204 }
205
206 var dirExistsTask = DirExistsAndIsNotEmpty();
207 bool attached = false;
208 if (await ioManager.FileExists(targetInstancePath, cancellationToken) || await dirExistsTask)
209 if (!await ioManager.FileExists(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken))
210 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
211 else
212 attached = true;
213
214 var newInstance = await CreateDefaultInstance(model, cancellationToken);
215 if (newInstance == null)
216 return Conflict(new ErrorMessageResponse(ErrorCode.NoPortsAvailable));
217
218 DatabaseContext.Instances.Add(newInstance);
219 try
220 {
221 await DatabaseContext.Save(cancellationToken);
222
223 try
224 {
225 // actually reserve it now
226 await ioManager.CreateDirectory(targetInstancePath, cancellationToken);
227 await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken);
228 }
229 catch
230 {
231 // oh shit delete the model
232 DatabaseContext.Instances.Remove(newInstance);
233
234 // DCT: Operation must always run
235 await DatabaseContext.Save(CancellationToken.None);
236 throw;
237 }
238 }
239 catch (IOException e)
240 {
241 return Conflict(new ErrorMessageResponse(ErrorCode.IOError)
242 {
243 AdditionalData = e.Message,
244 });
245 }
246
247 Logger.LogInformation(
248 "{userName} {attachedOrCreated} instance {instanceName}: {instanceId} ({instancePath})",
250 attached ? "attached" : "created",
251 newInstance.Name,
252 newInstance.Id,
253 newInstance.Path);
254
256 newInstance.InstancePermissionSets.First(),
257 cancellationToken);
258
259 var api = newInstance.ToApi();
260 api.Accessible = true; // instances are always accessible by their creator
261 return attached ? Json(api) : Created(api);
262 }
263
272 [HttpDelete("{id}")]
273 [TgsAuthorize(InstanceManagerRights.Delete)]
274 [ProducesResponseType(204)]
275 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
276 public async ValueTask<IActionResult> Delete(long id, CancellationToken cancellationToken)
277 {
278 var originalModel = await DatabaseContext
279 .Instances
280 .AsQueryable()
281 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier)
282 .FirstOrDefaultAsync(cancellationToken);
283 if (originalModel == default)
284 return this.Gone();
285 if (originalModel.Online!.Value)
286 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceDetachOnline));
287
288 var originalPath = originalModel.Path!;
289 var attachFileName = ioManager.ConcatPath(originalPath, InstanceAttachFileName);
290 try
291 {
292 if (await ioManager.DirectoryExists(originalPath, cancellationToken))
293 await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), cancellationToken);
294 }
295 catch (OperationCanceledException)
296 {
297 // DCT: Operation must always run
298 await ioManager.DeleteFile(attachFileName, CancellationToken.None);
299 throw;
300 }
301
302 try
303 {
304 // yes this is racy af. I hate it
305 // there's a bug where removing the root instance doesn't work sometimes
306 await DatabaseContext
308 .AsQueryable()
309 .Where(x => x.Job!.Instance!.Id == id)
310 .ExecuteDeleteAsync(cancellationToken);
311 await DatabaseContext
313 .AsQueryable()
314 .Where(x => x.RevisionInformation.InstanceId == id)
315 .ExecuteDeleteAsync(cancellationToken);
316 await DatabaseContext
318 .AsQueryable()
319 .Where(x => x.InstanceId == id)
320 .ExecuteDeleteAsync(cancellationToken);
321
322 DatabaseContext.Instances.Remove(originalModel);
323 await DatabaseContext.Save(cancellationToken); // cascades everything else
324 }
325 catch
326 {
327 await ioManager.DeleteFile(attachFileName, CancellationToken.None); // DCT: Shouldn't be cancelled
328 throw;
329 }
330
331 return NoContent();
332 }
333
343 [HttpPost]
344 [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
345 [ProducesResponseType(typeof(InstanceResponse), 200)]
346 [ProducesResponseType(typeof(InstanceResponse), 202)]
347 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
348#pragma warning disable CA1502 // TODO: Decomplexify
349 public async ValueTask<IActionResult> Update([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken)
350 {
351 ArgumentNullException.ThrowIfNull(model);
352
353 IQueryable<Models.Instance> InstanceQuery() => DatabaseContext
354 .Instances
355 .AsQueryable()
356 .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier);
357
358 var moveJob = await InstanceQuery()
359 .SelectMany(x => x.Jobs)
360 .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move)
361 .Select(x => new Job(x.Id!.Value))
362 .FirstOrDefaultAsync(cancellationToken);
363
364 if (moveJob != null)
365 {
366 // don't allow them to cancel it if they can't start it.
368 return Forbid();
369 await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken); // cancel it now
370 }
371
372 var originalModel = await InstanceQuery()
373 .Include(x => x.RepositorySettings)
374 .Include(x => x.ChatSettings)
375 .ThenInclude(x => x.Channels)
376 .Include(x => x.DreamDaemonSettings) // need these for onlining
377 .FirstOrDefaultAsync(cancellationToken);
378 if (originalModel == default(Models.Instance))
379 return this.Gone();
380
381 if (ValidateInstanceOnlineStatus(originalModel))
382 await DatabaseContext.Save(cancellationToken);
383
384 var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
385 bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
386 {
387 var memberSelectorExpression = (MemberExpression)expression.Body;
388 var property = (PropertyInfo)memberSelectorExpression.Member;
389
390 var newVal = property.GetValue(model);
391 if (newVal == null)
392 return false;
393 if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
394 return true;
395
396 property.SetValue(originalModel, newVal);
397 return false;
398 }
399
400 string? originalModelPath = null;
401 string? normalizedPath = null;
402 var originalOnline = originalModel.Online!.Value;
403 if (model.Path != null)
404 {
405 normalizedPath = NormalizePath(model.Path);
406
407 if (normalizedPath != originalModel.Path)
408 {
409 if (!userRights.HasFlag(InstanceManagerRights.Relocate))
410 return Forbid();
411 if (originalOnline && model.Online != true)
412 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceRelocateOnline));
413
414 var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
415 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
416 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
417
418 originalModelPath = originalModel.Path;
419 originalModel.Path = normalizedPath;
420 }
421 }
422
423 var oldAutoUpdateInterval = originalModel.AutoUpdateInterval!.Value;
424 var oldAutoUpdateCron = originalModel.AutoUpdateCron;
425
426 var earlyOut = ValidateCronSetting(model);
427 if (earlyOut != null)
428 return earlyOut;
429
430 var changedAutoInterval = model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval;
431 var changedAutoCron = model.AutoUpdateCron != null && oldAutoUpdateCron != model.AutoUpdateCron;
432
433 var renamed = model.Name != null && originalModel.Name != model.Name;
434
435 if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
436 || CheckModified(x => x.AutoUpdateCron, InstanceManagerRights.SetAutoUpdate)
437 || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
438 || CheckModified(x => x.Name, InstanceManagerRights.Rename)
439 || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
440 || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
441 return Forbid();
442
443 if (model.ChatBotLimit.HasValue)
444 {
445 var countOfExistingChatBots = await DatabaseContext
446 .ChatBots
447 .AsQueryable()
448 .Where(x => x.InstanceId == originalModel.Id)
449 .CountAsync(cancellationToken);
450
451 if (countOfExistingChatBots > model.ChatBotLimit.Value)
452 return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax));
453 }
454
455 if (changedAutoCron)
456 model.AutoUpdateInterval = 0;
457 else if (changedAutoInterval)
458 model.AutoUpdateCron = String.Empty;
459
460 await DatabaseContext.Save(cancellationToken);
461
462 if (renamed)
463 {
464 // ignoring retval because we don't care if it's offline
466 async componentInstance =>
467 {
468 await componentInstance.InstanceRenamed(originalModel.Name!, cancellationToken);
469 return null;
470 },
471 originalModel);
472 }
473
474 var oldAutoStart = originalModel.DreamDaemonSettings!.AutoStart;
475 try
476 {
477 if (originalOnline && model.Online == false)
478 await InstanceOperations.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken);
479 else if (!originalOnline && model.Online == true)
480 {
481 // force autostart false here because we don't want any long running jobs right now
482 // remember to document this
483 originalModel.DreamDaemonSettings.AutoStart = false;
484 await InstanceOperations.OnlineInstance(originalModel, cancellationToken);
485 }
486 }
487 catch (Exception e)
488 {
489 if (e is not OperationCanceledException)
490 Logger.LogError(e, "Error changing instance online state!");
491 originalModel.Online = originalOnline;
492 originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
493 if (originalModelPath != null)
494 originalModel.Path = originalModelPath;
495
496 // DCT: Operation must always run
497 await DatabaseContext.Save(CancellationToken.None);
498 throw;
499 }
500
501 var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new InstanceResponse
502 {
503 Id = originalModel.Id,
504 };
505
506 var moving = originalModelPath != null;
507 if (moving)
508 {
509 var description = $"Move instance ID {originalModel.Id} from {originalModelPath} to {normalizedPath}";
510 var job = Job.Create(JobCode.Move, AuthenticationContext.User, originalModel, InstanceManagerRights.Relocate);
511 job.Description = description;
512
514 job,
515 (core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline
516 => InstanceOperations.MoveInstance(originalModel, originalModelPath!, ct),
517 cancellationToken);
518 api.MoveJob = job.ToApi();
519 }
520
521 if (changedAutoInterval || changedAutoCron)
522 {
523 // ignoring retval because we don't care if it's offline
525 async componentInstance =>
526 {
527 await componentInstance.ScheduleAutoUpdate(model.AutoUpdateInterval!.Value, model.AutoUpdateCron);
528 return null;
529 },
530 originalModel);
531 }
532
533 await CheckAccessible(api, cancellationToken);
534 return moving ? Accepted(api) : Json(api);
535 }
536#pragma warning restore CA1502
537
546 [HttpGet(Routes.List)]
547 [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
548 [ProducesResponseType(typeof(PaginatedResponse<InstanceResponse>), 200)]
549 public async ValueTask<IActionResult> List(
550 [FromQuery] int? page,
551 [FromQuery] int? pageSize,
552 CancellationToken cancellationToken)
553 {
554 IQueryable<Models.Instance> GetBaseQuery()
555 {
556 var query = DatabaseContext
557 .Instances
558 .AsQueryable()
559 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier);
561 query = query
562 .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id))
563 .Where(x => x.InstancePermissionSets.Any(instanceUser =>
564 instanceUser.EngineRights != EngineRights.None ||
565 instanceUser.ChatBotRights != ChatBotRights.None ||
566 instanceUser.ConfigurationRights != ConfigurationRights.None ||
567 instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
568 instanceUser.DreamMakerRights != DreamMakerRights.None ||
569 instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None));
570
571 // Hack for EF IAsyncEnumerable BS
572 return query.Select(x => x);
573 }
574
575 var moveJobs = await GetBaseQuery()
576 .SelectMany(x => x.Jobs)
577 .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move)
578 .Include(x => x.StartedBy!)
579 .ThenInclude(x => x.CreatedBy)
580 .Include(x => x.Instance)
581 .ToListAsync(cancellationToken);
582
583 var needsUpdate = false;
584 var result = await Paginated<Models.Instance, InstanceResponse>(
585 () => ValueTask.FromResult(
587 GetBaseQuery()
588 .OrderBy(x => x.Id))),
589 async instance =>
590 {
591 needsUpdate |= ValidateInstanceOnlineStatus(instance);
592 instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance!.Id == instance.Id)?.ToApi();
593 await CheckAccessible(instance, cancellationToken);
594 },
595 page,
596 pageSize,
597 cancellationToken);
598
599 if (needsUpdate)
600 await DatabaseContext.Save(cancellationToken);
601
602 return result;
603 }
604
613 [HttpGet("{id}")]
614 [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
615 [ProducesResponseType(typeof(InstanceResponse), 200)]
616 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
617 public async ValueTask<IActionResult> GetId(long id, CancellationToken cancellationToken)
618 {
620 IQueryable<Models.Instance> QueryForUser()
621 {
622 var query = DatabaseContext
623 .Instances
624 .AsQueryable()
625 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier);
626
627 if (cantList)
628 query = query.Include(x => x.InstancePermissionSets);
629 return query;
630 }
631
632 var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken);
633
634 if (instance == null)
635 return this.Gone();
636
637 if (ValidateInstanceOnlineStatus(instance))
638 await DatabaseContext.Save(cancellationToken);
639
640 if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Require(x => x.Id)
641 && (instanceUser.RepositoryRights != RepositoryRights.None ||
642 instanceUser.EngineRights != EngineRights.None ||
643 instanceUser.ChatBotRights != ChatBotRights.None ||
644 instanceUser.ConfigurationRights != ConfigurationRights.None ||
645 instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
646 instanceUser.DreamMakerRights != DreamMakerRights.None ||
647 instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None)))
648 return Forbid();
649
650 var api = instance.ToApi();
651
652 var moveJob = await QueryForUser()
653 .SelectMany(x => x.Jobs)
654 .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move)
655 .Include(x => x.StartedBy!)
656 .ThenInclude(x => x.CreatedBy)
657 .Include(x => x.Instance)
658 .FirstOrDefaultAsync(cancellationToken);
659 api.MoveJob = moveJob?.ToApi();
660 await CheckAccessible(api, cancellationToken);
661 return Json(api);
662 }
663
671 [HttpPatch("{id}")]
672 [TgsAuthorize(InstanceManagerRights.GrantPermissions)]
673 [ProducesResponseType(204)]
674 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
675 public async ValueTask<IActionResult> GrantPermissions(long id, CancellationToken cancellationToken)
676 {
677 IQueryable<Models.Instance> BaseQuery() => DatabaseContext
678 .Instances
679 .AsQueryable()
680 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier);
681
682 // ensure the current user has write privilege on the instance
683 var usersInstancePermissionSet = await BaseQuery()
684 .SelectMany(x => x.InstancePermissionSets)
685 .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id)
686 .FirstOrDefaultAsync(cancellationToken);
687 if (usersInstancePermissionSet == default)
688 {
689 // does the instance actually exist?
690 var instanceExists = await BaseQuery()
691 .AnyAsync(cancellationToken);
692
693 if (!instanceExists)
694 return this.Gone();
695
696 var instanceAdminUser = InstanceAdminPermissionSet(null);
697 instanceAdminUser.InstanceId = id;
698 DatabaseContext.InstancePermissionSets.Add(instanceAdminUser);
699 }
700 else
701 InstanceAdminPermissionSet(usersInstancePermissionSet);
702
703 await DatabaseContext.Save(cancellationToken);
704
705 return NoContent();
706 }
707
714 async ValueTask<Models.Instance?> CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
715 {
716 var ddPort = await portAllocator.GetAvailablePort(1024, false, cancellationToken);
717 if (!ddPort.HasValue)
718 return null;
719
720 // try to use the old default if possible
721 const ushort DefaultDreamDaemonPort = 1337;
722 if (ddPort.Value < DefaultDreamDaemonPort)
723 ddPort = await portAllocator.GetAvailablePort(DefaultDreamDaemonPort, false, cancellationToken) ?? ddPort;
724
725 const ushort DefaultApiValidationPort = 1339;
726 var dmPort = await portAllocator
728 Math.Min((ushort)(ddPort.Value + 1), DefaultApiValidationPort),
729 false,
730 cancellationToken);
731 if (!dmPort.HasValue)
732 return null;
733
734 // try to use the old default if possible
735 if (dmPort < DefaultApiValidationPort)
736 dmPort = await portAllocator.GetAvailablePort(DefaultApiValidationPort, false, cancellationToken) ?? dmPort;
737
738 return new Models.Instance
739 {
740 ConfigurationType = initialSettings.ConfigurationType ?? ConfigurationType.Disallowed,
742 {
743 AllowWebClient = false,
744 AutoStart = false,
745 Port = ddPort,
746 OpenDreamTopicPort = 0,
747 SecurityLevel = DreamDaemonSecurity.Safe,
748 Visibility = DreamDaemonVisibility.Public,
749 StartupTimeout = 60,
750 HealthCheckSeconds = 60,
751 DumpOnHealthCheckRestart = false,
752 TopicRequestTimeout = generalConfiguration.ByondTopicTimeout,
753 AdditionalParameters = String.Empty,
754 StartProfiler = false,
755 LogOutput = false,
756 MapThreads = 0,
757 Minidumps = true,
758 },
760 {
761 ApiValidationPort = dmPort,
762 ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
763 RequireDMApiValidation = true,
764 Timeout = TimeSpan.FromHours(1),
765 CompilerAdditionalArguments = null,
766 },
767 Name = initialSettings.Name,
768 Online = false,
769 Path = initialSettings.Path,
770 AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0,
771 AutoUpdateCron = initialSettings.AutoUpdateCron ?? String.Empty,
772 ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
774 {
775 CommitterEmail = Components.Repository.Repository.DefaultCommitterEmail,
776 CommitterName = Components.Repository.Repository.DefaultCommitterName,
777 PushTestMergeCommits = false,
778 ShowTestMergeCommitters = false,
779 AutoUpdatesKeepTestMerges = false,
780 AutoUpdatesSynchronize = false,
781 PostTestMergeComment = false,
782 CreateGitHubDeployments = false,
783 UpdateSubmodules = true,
784 },
785 InstancePermissionSets = new List<InstancePermissionSet> // give this user full privileges on the instance
786 {
788 },
789 SwarmIdentifer = swarmConfiguration.Identifier,
790 };
791 }
792
799 {
800 permissionSetToModify ??= new InstancePermissionSet()
801 {
803 PermissionSetId = AuthenticationContext.PermissionSet.Require(x => x.Id),
804 };
805 permissionSetToModify.EngineRights = RightsHelper.AllRights<EngineRights>();
806 permissionSetToModify.ChatBotRights = RightsHelper.AllRights<ChatBotRights>();
807 permissionSetToModify.ConfigurationRights = RightsHelper.AllRights<ConfigurationRights>();
808 permissionSetToModify.DreamDaemonRights = RightsHelper.AllRights<DreamDaemonRights>();
809 permissionSetToModify.DreamMakerRights = RightsHelper.AllRights<DreamMakerRights>();
810 permissionSetToModify.RepositoryRights = RightsHelper.AllRights<RepositoryRights>();
811 permissionSetToModify.InstancePermissionSetRights = RightsHelper.AllRights<InstancePermissionSetRights>();
812 return permissionSetToModify;
813 }
814
820 [return: NotNullIfNotNull(nameof(path))]
821 string? NormalizePath(string? path)
822 {
823 if (path == null)
824 return null;
825
826 path = ioManager.ResolvePath(path);
828
829 return path;
830 }
831
838 async ValueTask CheckAccessible(InstanceResponse instanceResponse, CancellationToken cancellationToken)
839 {
840 instanceResponse.Accessible = await DatabaseContext
842 .AsQueryable()
843 .Where(x => x.InstanceId == instanceResponse.Id && x.PermissionSetId == AuthenticationContext.PermissionSet.Id)
844 .AnyAsync(cancellationToken);
845 }
846
852 BadRequestObjectResult? ValidateCronSetting(Api.Models.Instance instance)
853 {
854 if (!String.IsNullOrWhiteSpace(instance.AutoUpdateCron))
855 {
856 if ((instance.AutoUpdateInterval.HasValue && instance.AutoUpdateInterval.Value != 0)
857 || (CrontabSchedule.TryParse(
858 instance.AutoUpdateCron,
859 new CrontabSchedule.ParseOptions
860 {
861 IncludingSeconds = true,
862 }) == null))
863 return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
864
865 instance.AutoUpdateInterval = 0;
866 }
867 else
868 instance.AutoUpdateCron = String.Empty;
869
870 return null;
871 }
872 }
873}
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
uint ByondTopicTimeout
The timeout in milliseconds for sending and receiving topics to/from DreamDaemon. Note that a single ...
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.
async ValueTask< IActionResult?> WithComponentInstanceNullable(Func< IInstanceCore, ValueTask< IActionResult?> > action, Models.Instance? instance=null)
Run a given action with the relevant IInstance.
IInstanceOperations InstanceOperations
Access the IInstanceOperations instance.
readonly IInstanceManager instanceManager
The IInstanceManager for the ComponentInterfacingController.
bool ValidateInstanceOnlineStatus(Api.Models.Instance metadata)
Corrects discrepencies between the Api.Models.Instance.Online status of IInstances in the database vs...
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.
InstanceController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ILogger< InstanceController > logger, IInstanceManager instanceManager, IJobManager jobManager, IIOManager ioManager, IPlatformIdentifier platformIdentifier, IPortAllocator portAllocator, IPermissionsUpdateNotifyee permissionsUpdateNotifyee, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< SwarmConfiguration > swarmConfigurationOptions, IApiHeadersProvider apiHeaders)
Initializes a new instance of the InstanceController class.
readonly IJobManager jobManager
The IJobManager for the InstanceController.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceController.
BadRequestObjectResult? ValidateCronSetting(Api.Models.Instance instance)
Validates a given instance 's Api.Models.Instance.AutoUpdateCron setting.
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 .
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.
DbSet< CompileJob > CompileJobs
The CompileJobs 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.
DbSet< RevInfoTestMerge > RevInfoTestMerges
The RevInfoTestMerges in the DatabaseContext.
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations 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...
ValueTask OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance.
ValueTask MoveInstance(Models.Instance metadata, string oldPath, CancellationToken cancellationToken)
Move an IInstance.
ValueTask OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken)
Offline an IInstance.
Interface for using filesystems.
Definition: IIOManager.cs:13
Task< IReadOnlyList< string > > GetFiles(string path, CancellationToken cancellationToken)
Returns full file names in a given path .
Task< bool > PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken)
Check if a given parentPath is a parent of a given parentPath .
string ResolvePath()
Retrieve the full path of the current working directory.
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 .
ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
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
ValueTask< Job?> CancelJob(Job job, User? user, bool blocking, CancellationToken cancellationToken)
Cancels a give job .
ValueTask RegisterOperation(Job job, JobEntrypoint operation, CancellationToken cancellationToken)
Registers a given Job and begins running it.
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.
string NormalizePath(string path)
Normalize a path for consistency.
Gets unassigned ports for use by TGS.
ValueTask< ushort?> GetAvailablePort(ushort basePort, bool checkOne, CancellationToken cancellationToken)
Gets a port not currently in use by TGS.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition: ErrorCode.cs:12
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.