tgstation-server 5.12.7
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.IO;
4using System.Linq;
5using System.Linq.Expressions;
6using System.Reflection;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.AspNetCore.Mvc;
11using Microsoft.EntityFrameworkCore;
12using Microsoft.Extensions.Logging;
13using Microsoft.Extensions.Options;
14
30
32{
36 [Route(Routes.InstanceManager)]
37#pragma warning disable CA1506 // TODO: Decomplexify
39 {
43 public const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
44
48 const string MoveInstanceJobPrefix = "Move instance ID ";
49
54
59
64
69
74
79
94 IDatabaseContext databaseContext,
95 IAuthenticationContextFactory authenticationContextFactory,
96 ILogger<InstanceController> logger,
102 IOptions<GeneralConfiguration> generalConfigurationOptions,
103 IOptions<SwarmConfiguration> swarmConfigurationOptions)
104 : base(
105 databaseContext,
106 authenticationContextFactory,
107 logger,
109 {
110 this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
111 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
112 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
113 this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
114 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
115 swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
116 }
117
126 [HttpPut]
127 [TgsAuthorize(InstanceManagerRights.Create)]
128 [ProducesResponseType(typeof(InstanceResponse), 200)]
129 [ProducesResponseType(typeof(InstanceResponse), 201)]
130 public async Task<IActionResult> Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken)
131 {
132 ArgumentNullException.ThrowIfNull(model);
133
134 if (String.IsNullOrWhiteSpace(model.Name))
135 return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceName));
136
137 var unNormalizedPath = model.Path;
138 var targetInstancePath = NormalizePath(unNormalizedPath);
139 model.Path = targetInstancePath;
140
141 var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
142
143 bool InstanceIsChildOf(string otherPath)
144 {
145 if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
146 return false;
147
148 bool sameLength = targetInstancePath.Length == otherPath.Length;
149 char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
150 return sameLength
151 || dirSeparatorChar == Path.DirectorySeparatorChar
152 || dirSeparatorChar == Path.AltDirectorySeparatorChar;
153 }
154
155 if (InstanceIsChildOf(installationDirectoryPath))
156 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
157
158 // Validate it's not a child of any other instance
159 IActionResult earlyOut = null;
160 ulong countOfOtherInstances = 0;
161 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
162 {
163 var newCancellationToken = cts.Token;
164 try
165 {
166 await DatabaseContext
167 .Instances
168 .AsQueryable()
169 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier)
170 .Select(x => new Models.Instance
171 {
172 Path = x.Path,
173 })
174 .ForEachAsync(
175 otherInstance =>
176 {
177 if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
178 earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached));
179 else if (InstanceIsChildOf(otherInstance.Path))
180 earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath));
181
182 if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
183 cts.Cancel();
184 },
185 newCancellationToken);
186 }
187 catch (OperationCanceledException)
188 {
189 cancellationToken.ThrowIfCancellationRequested();
190 }
191 }
192
193 if (earlyOut != null)
194 return earlyOut;
195
196 // Last test, ensure it's in the list of valid paths
198 .Select(path => NormalizePath(path))
199 .Any(path => InstanceIsChildOf(path)) ?? true))
200 return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceNotAtWhitelistedPath));
201
202 async Task<bool> DirExistsAndIsNotEmpty()
203 {
204 if (!await ioManager.DirectoryExists(model.Path, cancellationToken))
205 return false;
206
207 var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
208 var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
209
210 var files = await filesTask;
211 var dirs = await dirsTask;
212
213 return files.Concat(dirs).Any();
214 }
215
216 var dirExistsTask = DirExistsAndIsNotEmpty();
217 bool attached = false;
218 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
219 if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken))
220 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
221 else
222 attached = true;
223
224 var newInstance = await CreateDefaultInstance(model, cancellationToken);
225 if (newInstance == null)
226 return Conflict(new ErrorMessageResponse(ErrorCode.NoPortsAvailable));
227
228 DatabaseContext.Instances.Add(newInstance);
229 try
230 {
231 await DatabaseContext.Save(cancellationToken);
232
233 try
234 {
235 // actually reserve it now
236 await ioManager.CreateDirectory(unNormalizedPath, cancellationToken);
237 await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken);
238 }
239 catch
240 {
241 // oh shit delete the model
242 DatabaseContext.Instances.Remove(newInstance);
243
244 // DCT: Operation must always run
245 await DatabaseContext.Save(CancellationToken.None);
246 throw;
247 }
248 }
249 catch (IOException e)
250 {
251 return Conflict(new ErrorMessageResponse(ErrorCode.IOError)
252 {
253 AdditionalData = e.Message,
254 });
255 }
256
257 Logger.LogInformation(
258 "{userName} {attachedOrCreated} instance {instanceName}: {instanceId} ({instancePath})",
260 attached ? "attached" : "created",
261 newInstance.Name,
262 newInstance.Id,
263 newInstance.Path);
264
265 var api = newInstance.ToApi();
266 api.Accessible = true; // instances are always accessible by their creator
267 return attached ? Json(api) : Created(api);
268 }
269
278 [HttpDelete("{id}")]
279 [TgsAuthorize(InstanceManagerRights.Delete)]
280 [ProducesResponseType(204)]
281 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
282 public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
283 {
284 var originalModel = await DatabaseContext
285 .Instances
286 .AsQueryable()
287 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier)
288 .FirstOrDefaultAsync(cancellationToken);
289 if (originalModel == default)
290 return this.Gone();
291 if (originalModel.Online.Value)
292 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceDetachOnline));
293
294 DatabaseContext.Instances.Remove(originalModel);
295
296 var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
297 try
298 {
299 if (await ioManager.DirectoryExists(originalModel.Path, cancellationToken))
300 await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), cancellationToken);
301 }
302 catch (OperationCanceledException)
303 {
304 // DCT: Operation must always run
305 await ioManager.DeleteFile(attachFileName, CancellationToken.None);
306 throw;
307 }
308
309 await DatabaseContext.Save(cancellationToken); // cascades everything
310 return NoContent();
311 }
312
322 [HttpPost]
323 [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
324 [ProducesResponseType(typeof(InstanceResponse), 200)]
325 [ProducesResponseType(typeof(InstanceResponse), 202)]
326 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
327#pragma warning disable CA1502 // TODO: Decomplexify
328 public async Task<IActionResult> Update([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken)
329 {
330 ArgumentNullException.ThrowIfNull(model);
331
332 IQueryable<Models.Instance> InstanceQuery() => DatabaseContext
333 .Instances
334 .AsQueryable()
335 .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier);
336
337 var moveJob = await InstanceQuery()
338 .SelectMany(x => x.Jobs).
339 Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
340 .Select(x => new Job
341 {
342 Id = x.Id,
343 }).FirstOrDefaultAsync(cancellationToken);
344
345 if (moveJob != default)
346 {
347 // don't allow them to cancel it if they can't start it.
349 return Forbid();
350 await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken); // cancel it now
351 }
352
353 var originalModel = await InstanceQuery()
354 .Include(x => x.RepositorySettings)
355 .Include(x => x.ChatSettings)
356 .ThenInclude(x => x.Channels)
357 .Include(x => x.DreamDaemonSettings) // need these for onlining
358 .FirstOrDefaultAsync(cancellationToken);
359 if (originalModel == default(Models.Instance))
360 return this.Gone();
361
362 if (ValidateInstanceOnlineStatus(originalModel))
363 await DatabaseContext.Save(cancellationToken);
364
365 var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
366 bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
367 {
368 var memberSelectorExpression = (MemberExpression)expression.Body;
369 var property = (PropertyInfo)memberSelectorExpression.Member;
370
371 var newVal = property.GetValue(model);
372 if (newVal == null)
373 return false;
374 if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
375 return true;
376
377 property.SetValue(originalModel, newVal);
378 return false;
379 }
380
381 string originalModelPath = null;
382 string rawPath = null;
383 if (model.Path != null)
384 {
385 rawPath = NormalizePath(model.Path);
386
387 if (rawPath != originalModel.Path)
388 {
389 if (!userRights.HasFlag(InstanceManagerRights.Relocate))
390 return Forbid();
391 if (originalModel.Online.Value && model.Online != true)
392 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceRelocateOnline));
393
394 var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
395 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
396 return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtExistingPath));
397
398 originalModelPath = originalModel.Path;
399 originalModel.Path = rawPath;
400 }
401 }
402
403 var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
404 var originalOnline = originalModel.Online.Value;
405 var renamed = model.Name != null && originalModel.Name != model.Name;
406
407 if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
408 || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
409 || CheckModified(x => x.Name, InstanceManagerRights.Rename)
410 || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
411 || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
412 return Forbid();
413
414 if (model.ChatBotLimit.HasValue)
415 {
416 var countOfExistingChatBots = await DatabaseContext
417 .ChatBots
418 .AsQueryable()
419 .Where(x => x.InstanceId == originalModel.Id)
420 .CountAsync(cancellationToken);
421
422 if (countOfExistingChatBots > model.ChatBotLimit.Value)
423 return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax));
424 }
425
426 await DatabaseContext.Save(cancellationToken);
427
428 if (renamed)
429 {
430 // ignoring retval because we don't care if it's offline
431 await WithComponentInstance(
432 async componentInstance =>
433 {
434 await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken);
435 return null;
436 },
437 originalModel);
438 }
439
440 var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
441 try
442 {
443 if (originalOnline && model.Online == false)
444 await InstanceOperations.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken);
445 else if (!originalOnline && model.Online == true)
446 {
447 // force autostart false here because we don't want any long running jobs right now
448 // remember to document this
449 originalModel.DreamDaemonSettings.AutoStart = false;
450 await InstanceOperations.OnlineInstance(originalModel, cancellationToken);
451 }
452 }
453 catch (Exception e)
454 {
455 if (e is not OperationCanceledException)
456 Logger.LogError(e, "Error changing instance online state!");
457 originalModel.Online = originalOnline;
458 originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
459 if (originalModelPath != null)
460 originalModel.Path = originalModelPath;
461
462 // DCT: Operation must always run
463 await DatabaseContext.Save(CancellationToken.None);
464 throw;
465 }
466
467 var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new InstanceResponse
468 {
469 Id = originalModel.Id,
470 };
471
472 var moving = originalModelPath != null;
473 if (moving)
474 {
475 var job = new Job
476 {
477 Description = $"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}",
478 Instance = originalModel,
479 CancelRightsType = RightsType.InstanceManager,
480 CancelRight = (ulong)InstanceManagerRights.Relocate,
481 StartedBy = AuthenticationContext.User,
482 };
483
484 await jobManager.RegisterOperation(
485 job,
486 (core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline
487 => InstanceOperations.MoveInstance(originalModel, originalModelPath, ct),
488 cancellationToken);
489 api.MoveJob = job.ToApi();
490 }
491
492 if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
493 {
494 // ignoring retval because we don't care if it's offline
495 await WithComponentInstance(
496 async componentInstance =>
497 {
498 await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value);
499 return null;
500 },
501 originalModel);
502 }
503
504 await CheckAccessible(api, cancellationToken);
505 return moving ? Accepted(api) : Json(api);
506 }
507#pragma warning restore CA1502
508
517 [HttpGet(Routes.List)]
518 [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
519 [ProducesResponseType(typeof(PaginatedResponse<InstanceResponse>), 200)]
520 public async Task<IActionResult> List(
521 [FromQuery] int? page,
522 [FromQuery] int? pageSize,
523 CancellationToken cancellationToken)
524 {
525 IQueryable<Models.Instance> GetBaseQuery()
526 {
527 var query = DatabaseContext
528 .Instances
529 .AsQueryable()
530 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier);
532 query = query
533 .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value))
534 .Where(x => x.InstancePermissionSets.Any(instanceUser =>
535 instanceUser.ByondRights != ByondRights.None ||
536 instanceUser.ChatBotRights != ChatBotRights.None ||
537 instanceUser.ConfigurationRights != ConfigurationRights.None ||
538 instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
539 instanceUser.DreamMakerRights != DreamMakerRights.None ||
540 instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None));
541
542 // Hack for EF IAsyncEnumerable BS
543 return query.Select(x => x);
544 }
545
546 var moveJobs = await GetBaseQuery()
547 .SelectMany(x => x.Jobs)
548 .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
549 .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
550 .Include(x => x.Instance)
551 .ToListAsync(cancellationToken);
552
553 var needsUpdate = false;
554 var result = await Paginated<Models.Instance, InstanceResponse>(
555 () => Task.FromResult(
557 GetBaseQuery()
558 .OrderBy(x => x.Id))),
559 async instance =>
560 {
561 needsUpdate |= ValidateInstanceOnlineStatus(instance);
562 instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id)?.ToApi();
563 await CheckAccessible(instance, cancellationToken);
564 },
565 page,
566 pageSize,
567 cancellationToken);
568
569 if (needsUpdate)
570 await DatabaseContext.Save(cancellationToken);
571
572 return result;
573 }
574
583 [HttpGet("{id}")]
584 [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
585 [ProducesResponseType(typeof(InstanceResponse), 200)]
586 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
587 public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
588 {
590 IQueryable<Models.Instance> QueryForUser()
591 {
592 var query = DatabaseContext
593 .Instances
594 .AsQueryable()
595 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier);
596
597 if (cantList)
598 query = query.Include(x => x.InstancePermissionSets);
599 return query;
600 }
601
602 var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken);
603
604 if (instance == null)
605 return this.Gone();
606
607 if (ValidateInstanceOnlineStatus(instance))
608 await DatabaseContext.Save(cancellationToken);
609
610 if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value &&
611 (instanceUser.RepositoryRights != RepositoryRights.None ||
612 instanceUser.ByondRights != ByondRights.None ||
613 instanceUser.ChatBotRights != ChatBotRights.None ||
614 instanceUser.ConfigurationRights != ConfigurationRights.None ||
615 instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
616 instanceUser.DreamMakerRights != DreamMakerRights.None ||
617 instanceUser.InstancePermissionSetRights != InstancePermissionSetRights.None)))
618 return Forbid();
619
620 var api = instance.ToApi();
621
622 var moveJob = await QueryForUser()
623 .SelectMany(x => x.Jobs)
624 .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
625 .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
626 .FirstOrDefaultAsync(cancellationToken);
627 api.MoveJob = moveJob?.ToApi();
628 await CheckAccessible(api, cancellationToken);
629 return Json(api);
630 }
631
639 [HttpPatch("{id}")]
640 [TgsAuthorize(InstanceManagerRights.GrantPermissions)]
641 [ProducesResponseType(204)]
642 [ProducesResponseType(typeof(ErrorMessageResponse), 410)]
643 public async Task<IActionResult> GrantPermissions(long id, CancellationToken cancellationToken)
644 {
645 IQueryable<Models.Instance> BaseQuery() => DatabaseContext
646 .Instances
647 .AsQueryable()
648 .Where(x => x.Id == id && x.SwarmIdentifer == swarmConfiguration.Identifier);
649
650 // ensure the current user has write privilege on the instance
651 var usersInstancePermissionSet = await BaseQuery()
652 .SelectMany(x => x.InstancePermissionSets)
653 .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value)
654 .FirstOrDefaultAsync(cancellationToken);
655 if (usersInstancePermissionSet == default)
656 {
657 // does the instance actually exist?
658 var instanceExists = await BaseQuery()
659 .AnyAsync(cancellationToken);
660
661 if (!instanceExists)
662 return this.Gone();
663
664 var instanceAdminUser = InstanceAdminPermissionSet(null);
665 instanceAdminUser.InstanceId = id;
666 DatabaseContext.InstancePermissionSets.Add(instanceAdminUser);
667 }
668 else
669 InstanceAdminPermissionSet(usersInstancePermissionSet);
670
671 await DatabaseContext.Save(cancellationToken);
672
673 return NoContent();
674 }
675
682 async Task<Models.Instance> CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
683 {
684 var ddPort = await portAllocator.GetAvailablePort(1024, false, cancellationToken);
685 if (!ddPort.HasValue)
686 return null;
687
688 // try to use the old default if possible
689 const ushort DefaultDreamDaemonPort = 1337;
690 if (ddPort.Value < DefaultDreamDaemonPort)
691 ddPort = await portAllocator.GetAvailablePort(DefaultDreamDaemonPort, false, cancellationToken) ?? ddPort;
692
693 const ushort DefaultApiValidationPort = 1339;
694 var dmPort = await portAllocator
695 .GetAvailablePort(
696 Math.Min((ushort)(ddPort.Value + 1), DefaultApiValidationPort),
697 false,
698 cancellationToken);
699 if (!dmPort.HasValue)
700 return null;
701
702 // try to use the old default if possible
703 if (dmPort < DefaultApiValidationPort)
704 dmPort = await portAllocator.GetAvailablePort(DefaultApiValidationPort, false, cancellationToken) ?? dmPort;
705
706 return new Models.Instance
707 {
708 ConfigurationType = initialSettings.ConfigurationType ?? ConfigurationType.Disallowed,
710 {
711 AllowWebClient = false,
712 AutoStart = false,
713 Port = ddPort,
714 SecurityLevel = DreamDaemonSecurity.Safe,
715 Visibility = DreamDaemonVisibility.Public,
716 StartupTimeout = 60,
717 HealthCheckSeconds = 60,
718 DumpOnHealthCheckRestart = false,
719 TopicRequestTimeout = generalConfiguration.ByondTopicTimeout,
720 AdditionalParameters = String.Empty,
721 StartProfiler = false,
722 LogOutput = false,
723 },
725 {
726 ApiValidationPort = dmPort,
727 ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
728 RequireDMApiValidation = true,
729 Timeout = TimeSpan.FromHours(1),
730 },
731 Name = initialSettings.Name,
732 Online = false,
733 Path = initialSettings.Path,
734 AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0,
735 ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
737 {
738 CommitterEmail = Components.Repository.Repository.DefaultCommitterEmail,
739 CommitterName = Components.Repository.Repository.DefaultCommitterName,
740 PushTestMergeCommits = false,
741 ShowTestMergeCommitters = false,
742 AutoUpdatesKeepTestMerges = false,
743 AutoUpdatesSynchronize = false,
744 PostTestMergeComment = false,
745 CreateGitHubDeployments = false,
746 UpdateSubmodules = true,
747 },
748 InstancePermissionSets = new List<InstancePermissionSet> // give this user full privileges on the instance
749 {
750 InstanceAdminPermissionSet(null),
751 },
752 SwarmIdentifer = swarmConfiguration.Identifier,
753 };
754 }
755
762 {
763 permissionSetToModify ??= new InstancePermissionSet()
764 {
765 PermissionSetId = AuthenticationContext.PermissionSet.Id.Value,
766 };
767 permissionSetToModify.ByondRights = RightsHelper.AllRights<ByondRights>();
768 permissionSetToModify.ChatBotRights = RightsHelper.AllRights<ChatBotRights>();
769 permissionSetToModify.ConfigurationRights = RightsHelper.AllRights<ConfigurationRights>();
770 permissionSetToModify.DreamDaemonRights = RightsHelper.AllRights<DreamDaemonRights>();
771 permissionSetToModify.DreamMakerRights = RightsHelper.AllRights<DreamMakerRights>();
772 permissionSetToModify.RepositoryRights = RightsHelper.AllRights<RepositoryRights>();
773 permissionSetToModify.InstancePermissionSetRights = RightsHelper.AllRights<InstancePermissionSetRights>();
774 return permissionSetToModify;
775 }
776
782 string NormalizePath(string path)
783 {
784 if (path == null)
785 return null;
786
787 path = ioManager.ResolvePath(path);
788 if (platformIdentifier.IsWindows)
789 path = path.ToUpperInvariant().Replace('\\', '/');
790
791 return path;
792 }
793
800 async Task CheckAccessible(InstanceResponse instanceResponse, CancellationToken cancellationToken)
801 {
802 instanceResponse.Accessible = await DatabaseContext
804 .AsQueryable()
805 .Where(x => x.InstanceId == instanceResponse.Id && x.PermissionSetId == AuthenticationContext.PermissionSet.Id)
806 .AnyAsync(cancellationToken);
807 }
808 }
809}
virtual ? long Id
The ID of the entity.
Definition: EntityId.cs:13
Metadata about a server instance.
Definition: Instance.cs:9
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:21
virtual ? string Name
The name of the entity represented by the NamedEntity.
Definition: NamedEntity.cs:16
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:38
const string List
The postfix for list operations.
Definition: Routes.cs:103
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.
const string MoveInstanceJobPrefix
Prefix for move JobResponses.
async Task< IActionResult > Update([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken)
Modify an Api.Models.Instance's settings.
InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< InstanceController > logger, IInstanceManager instanceManager, IJobManager jobManager, IIOManager ioManager, IPortAllocator portAllocator, IPlatformIdentifier platformIdentifier, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< SwarmConfiguration > swarmConfigurationOptions)
Initializes a new instance of the InstanceController class.
readonly IJobManager jobManager
The IJobManager for the InstanceController.
async Task CheckAccessible(InstanceResponse instanceResponse, CancellationToken cancellationToken)
Populate the InstanceResponse.Accessible property of a given instanceResponse .
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceController.
readonly IPortAllocator portAllocator
The IPortAllocator for the InstanceController.
async Task< IActionResult > List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
List Api.Models.Instances.
async Task< Models.Instance > CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken)
Creates a default Models.Instance from initialSettings .
string NormalizePath(string path)
Normalize a given path for an instance.
readonly SwarmConfiguration swarmConfiguration
The SwarmConfiguration for the InstanceController.
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Instance.
const string InstanceAttachFileName
File name to allow attaching instances.
async Task< IActionResult > Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken)
Create or attach an Api.Models.Instance.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController.
async Task< IActionResult > GrantPermissions(long id, CancellationToken cancellationToken)
Gives the current user full permissions on a given instance id .
InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet permissionSetToModify)
Generate an InstancePermissionSet with full rights.
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Detach an Api.Models.Instance with the given id .
readonly IIOManager ioManager
The IIOManager for the InstanceController.
Helper for returning paginated models.
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
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 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 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
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:11
DreamDaemonVisibility
The visibility setting for DreamDaemon.
ConfigurationType
The type of configuration allowed on an Instance.
DreamDaemonSecurity
DreamDaemon's security level.
ByondRights
Rights for BYOND version management.
Definition: ByondRights.cs:10
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
RepositoryRights
Rights for the git repository.
InstancePermissionSetRights
Rights for an Models.Instance.
DreamDaemonRights
Rights for managing DreamDaemon.
InstanceManagerRights
Rights for managing Models.Instances.