2using System.Collections.Generic;
5using System.Linq.Expressions;
6using System.Reflection;
8using System.Threading.Tasks;
10using Microsoft.AspNetCore.Mvc;
11using Microsoft.EntityFrameworkCore;
12using Microsoft.Extensions.Logging;
13using Microsoft.Extensions.Options;
37#pragma warning disable CA1506
96 ILogger<InstanceController> logger,
102 IOptions<GeneralConfiguration> generalConfigurationOptions,
103 IOptions<SwarmConfiguration> swarmConfigurationOptions)
106 authenticationContextFactory,
114 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
115 swarmConfiguration = swarmConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(swarmConfigurationOptions));
132 ArgumentNullException.ThrowIfNull(model);
134 if (String.IsNullOrWhiteSpace(model.Name))
137 var unNormalizedPath = model.Path;
139 model.Path = targetInstancePath;
143 bool InstanceIsChildOf(
string otherPath)
145 if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
148 bool sameLength = targetInstancePath.Length == otherPath.Length;
149 char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
151 || dirSeparatorChar == Path.DirectorySeparatorChar
152 || dirSeparatorChar == Path.AltDirectorySeparatorChar;
155 if (InstanceIsChildOf(installationDirectoryPath))
159 IActionResult earlyOut =
null;
160 ulong countOfOtherInstances = 0;
161 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
163 var newCancellationToken = cts.Token;
170 .Select(x =>
new Models.Instance
179 else if (InstanceIsChildOf(otherInstance.Path))
182 if (earlyOut !=
null && !newCancellationToken.IsCancellationRequested)
185 newCancellationToken);
187 catch (OperationCanceledException)
189 cancellationToken.ThrowIfCancellationRequested();
193 if (earlyOut !=
null)
199 .Any(path => InstanceIsChildOf(path)) ??
true))
202 async Task<bool> DirExistsAndIsNotEmpty()
210 var files = await filesTask;
211 var dirs = await dirsTask;
213 return files.Concat(dirs).Any();
216 var dirExistsTask = DirExistsAndIsNotEmpty();
217 bool attached =
false;
225 if (newInstance ==
null)
249 catch (IOException e)
253 AdditionalData = e.Message,
258 "{userName} {attachedOrCreated} instance {instanceName}: {instanceId} ({instancePath})",
260 attached ?
"attached" :
"created",
265 var api = newInstance.ToApi();
266 api.Accessible =
true;
267 return attached ? Json(api) :
Created(api);
280 [ProducesResponseType(204)]
282 public async Task<IActionResult>
Delete(
long id, CancellationToken cancellationToken)
287 .Where(x => x.Id ==
id && x.SwarmIdentifer == swarmConfiguration.Identifier)
288 .FirstOrDefaultAsync(cancellationToken);
289 if (originalModel ==
default)
291 if (originalModel.Online.Value)
296 var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
299 if (await ioManager.DirectoryExists(originalModel.Path, cancellationToken))
300 await ioManager.WriteAllBytes(attachFileName, Array.Empty<
byte>(), cancellationToken);
302 catch (OperationCanceledException)
305 await ioManager.DeleteFile(attachFileName, CancellationToken.None);
327#pragma warning disable CA1502
330 ArgumentNullException.ThrowIfNull(model);
335 .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier);
337 var moveJob = await InstanceQuery()
338 .SelectMany(x => x.Jobs).
339 Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
343 }).FirstOrDefaultAsync(cancellationToken);
345 if (moveJob !=
default)
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)
358 .FirstOrDefaultAsync(cancellationToken);
359 if (originalModel ==
default(Models.Instance))
362 if (ValidateInstanceOnlineStatus(originalModel))
366 bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression,
InstanceManagerRights requiredRight)
368 var memberSelectorExpression = (MemberExpression)expression.Body;
369 var property = (PropertyInfo)memberSelectorExpression.Member;
371 var newVal =
property.GetValue(model);
374 if (!userRights.HasFlag(requiredRight) &&
property.GetValue(originalModel) != newVal)
377 property.SetValue(originalModel, newVal);
381 string originalModelPath =
null;
382 string rawPath =
null;
383 if (model.Path !=
null)
385 rawPath = NormalizePath(model.Path);
387 if (rawPath != originalModel.Path)
391 if (originalModel.Online.Value && model.Online !=
true)
394 var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
395 if (await ioManager.FileExists(model.Path, cancellationToken) || await dirExistsTask)
398 originalModelPath = originalModel.Path;
399 originalModel.Path = rawPath;
403 var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
404 var originalOnline = originalModel.Online.Value;
405 var renamed = model.Name !=
null && originalModel.Name != model.Name;
414 if (model.ChatBotLimit.HasValue)
419 .Where(x => x.InstanceId == originalModel.Id)
420 .CountAsync(cancellationToken);
422 if (countOfExistingChatBots > model.ChatBotLimit.Value)
431 await WithComponentInstance(
432 async componentInstance =>
434 await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken);
440 var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
443 if (originalOnline && model.Online ==
false)
445 else if (!originalOnline && model.Online ==
true)
449 originalModel.DreamDaemonSettings.AutoStart =
false;
450 await InstanceOperations.OnlineInstance(originalModel, cancellationToken);
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;
469 Id = originalModel.
Id,
472 var moving = originalModelPath !=
null;
477 Description = $
"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}",
479 CancelRightsType =
RightsType.InstanceManager,
484 await jobManager.RegisterOperation(
486 (core, databaseContextFactory, paramJob, progressHandler, ct)
487 => InstanceOperations.MoveInstance(originalModel, originalModelPath, ct),
489 api.MoveJob = job.
ToApi();
492 if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
495 await WithComponentInstance(
496 async componentInstance =>
498 await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value);
504 await CheckAccessible(api, cancellationToken);
505 return moving ? Accepted(api) : Json(api);
507#pragma warning restore CA1502
520 public async Task<IActionResult>
List(
521 [FromQuery]
int? page,
522 [FromQuery]
int? pageSize,
523 CancellationToken cancellationToken)
530 .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier);
534 .Where(x => x.InstancePermissionSets.Any(instanceUser =>
543 return query.Select(x => x);
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);
553 var needsUpdate =
false;
555 () => Task.FromResult(
558 .OrderBy(x => x.Id))),
561 needsUpdate |= ValidateInstanceOnlineStatus(instance);
562 instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id)?.ToApi();
563 await CheckAccessible(instance, cancellationToken);
587 public async Task<IActionResult>
GetId(
long id, CancellationToken cancellationToken)
595 .Where(x => x.Id ==
id && x.SwarmIdentifer == swarmConfiguration.Identifier);
598 query = query.Include(x => x.InstancePermissionSets);
602 var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken);
604 if (instance ==
null)
607 if (ValidateInstanceOnlineStatus(instance))
620 var api = instance.ToApi();
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);
641 [ProducesResponseType(204)]
643 public async Task<IActionResult>
GrantPermissions(
long id, CancellationToken cancellationToken)
648 .Where(x => x.Id ==
id && x.SwarmIdentifer == swarmConfiguration.Identifier);
651 var usersInstancePermissionSet = await BaseQuery()
652 .SelectMany(x => x.InstancePermissionSets)
654 .FirstOrDefaultAsync(cancellationToken);
655 if (usersInstancePermissionSet ==
default)
658 var instanceExists = await BaseQuery()
659 .AnyAsync(cancellationToken);
664 var instanceAdminUser = InstanceAdminPermissionSet(
null);
665 instanceAdminUser.InstanceId = id;
669 InstanceAdminPermissionSet(usersInstancePermissionSet);
684 var ddPort = await portAllocator.GetAvailablePort(1024,
false, cancellationToken);
685 if (!ddPort.HasValue)
689 const ushort DefaultDreamDaemonPort = 1337;
690 if (ddPort.Value < DefaultDreamDaemonPort)
691 ddPort = await portAllocator.GetAvailablePort(DefaultDreamDaemonPort,
false, cancellationToken) ?? ddPort;
693 const ushort DefaultApiValidationPort = 1339;
694 var dmPort = await portAllocator
696 Math.Min((ushort)(ddPort.Value + 1), DefaultApiValidationPort),
699 if (!dmPort.HasValue)
703 if (dmPort < DefaultApiValidationPort)
704 dmPort = await portAllocator.GetAvailablePort(DefaultApiValidationPort,
false, cancellationToken) ?? dmPort;
711 AllowWebClient =
false,
717 HealthCheckSeconds = 60,
718 DumpOnHealthCheckRestart =
false,
719 TopicRequestTimeout = generalConfiguration.ByondTopicTimeout,
720 AdditionalParameters = String.Empty,
721 StartProfiler =
false,
726 ApiValidationPort = dmPort,
728 RequireDMApiValidation =
true,
729 Timeout = TimeSpan.FromHours(1),
731 Name = initialSettings.
Name,
733 Path = initialSettings.
Path,
734 AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0,
735 ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
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,
748 InstancePermissionSets =
new List<InstancePermissionSet>
750 InstanceAdminPermissionSet(
null),
752 SwarmIdentifer = swarmConfiguration.Identifier,
774 return permissionSetToModify;
787 path = ioManager.ResolvePath(path);
788 if (platformIdentifier.IsWindows)
789 path = path.ToUpperInvariant().Replace(
'\\',
'/');
806 .AnyAsync(cancellationToken);
virtual ? long Id
The ID of the entity.
Metadata about a server instance.
string? Path
The path to where the Instance is located. Can only be changed while the Instance is offline....
string? Identifier
The server's identifier.
virtual ? string Name
The name of the entity represented by the NamedEntity.
InstanceManagerRights? InstanceManagerRights
The Rights.InstanceManagerRights for the user.
A request to create an Instance.
A request to update an Instance.
Represents an error message returned by the server.
Server response for Instances.
Represents a paginated set of models.
Routes to a server actions.
const string InstanceManager
The Models.Instance controller.
const string List
The postfix for list operations.
General configuration options.
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.
ApiController for operations on IInstanceCores.
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.
User User
The authenticated user.
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.
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.
For creating and accessing authentication contexts.
Gets unassigned ports for use by TGS.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
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.
ChatBotRights
Rights for chat bots.
ConfigurationRights
Rights for Models.IConfigurationFiles.
DreamMakerRights
Rights for deployment.
RightsType
The type of rights a model uses.
RepositoryRights
Rights for the git repository.
InstancePermissionSetRights
Rights for an Models.Instance.
DreamDaemonRights
Rights for managing DreamDaemon.
InstanceManagerRights
Rights for managing Models.Instances.