diff --git a/build/Version.props b/build/Version.props index 05c5365352..31e62df8db 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,7 +5,7 @@ 6.0.0 5.0.0 - 10.0.0 + 9.13.0 6.0.1 12.0.0 13.0.0 diff --git a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs index 1564e5949a..94ec3c5246 100644 --- a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs @@ -2,6 +2,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models.Internal @@ -27,7 +29,14 @@ namespace Tgstation.Server.Api.Models.Internal /// The of the . /// [NotMapped] - public EngineRights? EngineRights { get; set; } + [JsonIgnore] + public EngineRights? EngineRights + { +#pragma warning disable CS0618 // Type or member is obsolete + get => ByondRights; + set => ByondRights = value; +#pragma warning restore CS0618 // Type or member is obsolete + } /// /// The legacy of the . diff --git a/src/Tgstation.Server.Host/Controllers/Legacy/ByondController.cs b/src/Tgstation.Server.Host/Controllers/Legacy/ByondController.cs new file mode 100644 index 0000000000..2ca7bafb47 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Legacy/ByondController.cs @@ -0,0 +1,345 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Controllers.Legacy.Models; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Transfer; + +namespace Tgstation.Server.Host.Controllers.Legacy +{ + /// + /// Controller for managing BYOND installations. + /// + [Route(Routes.Root + "Byond")] + public sealed class ByondController : InstanceRequiredController + { + /// + /// The for the . + /// + readonly IJobManager jobManager; + + /// + /// The for the . + /// + readonly IFileTransferTicketProvider fileTransferService; + + /// + /// Create an for a given legacy formatted BYOND . + /// + /// The legacy BYOND . + /// A . + static EngineVersion CreateEngineVersionFromLegacyByondVersion(Version version) => new EngineVersion + { + Version = new Version(version.Major, version.Minor), + Engine = EngineType.Byond, + CustomIteration = version.Build <= 0 ? null : version.Build, + }; + + /// + /// Create a legacy formated BYOND for a given . + /// + /// The . + /// A legacy BYOND . + static Version CreateLegacyByondVersionFromEngineVersion(EngineVersion engineVersion) + => new (engineVersion.Version.Major, engineVersion.Version.Minor, engineVersion.CustomIteration ?? 0); + + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The value of . + /// The value of . + public ByondController( + IDatabaseContext databaseContext, + IAuthenticationContextFactory authenticationContextFactory, + ILogger logger, + IInstanceManager instanceManager, + IJobManager jobManager, + IFileTransferTicketProvider fileTransferService) + : base( + databaseContext, + authenticationContextFactory, + logger, + instanceManager) + { + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); + } + + /// + /// Gets the active . + /// + /// A resulting in the for the operation. + /// Retrieved version information successfully. + [HttpGet] + [TgsAuthorize(EngineRights.ReadActive)] + [ProducesResponseType(typeof(ByondResponse), 200)] + public ValueTask Read() + => WithComponentInstance(instance => + { + var version = instance.EngineManager.ActiveVersion; + return ValueTask.FromResult( + Json(new ByondResponse + { + Version = version?.Engine.Value == EngineType.Byond + ? CreateLegacyByondVersionFromEngineVersion(version) + : null, + })); + }); + + /// + /// Lists installed s. + /// + /// The current page. + /// The page size. + /// The for the operation. + /// A resulting in the for the operation. + /// Retrieved version information successfully. + [HttpGet(Routes.List)] + [TgsAuthorize(EngineRights.ListInstalled)] + [ProducesResponseType(typeof(PaginatedResponse), 200)] + public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) + => WithComponentInstance( + instance => Paginated( + () => ValueTask.FromResult( + new PaginatableResult( + instance + .EngineManager + .InstalledVersions + .Where(x => x.Engine.Value == EngineType.Byond) + .Select(x => new ByondResponse + { + Version = CreateLegacyByondVersionFromEngineVersion(x), + }) + .AsQueryable() + .OrderBy(x => x.Version))), + null, + page, + pageSize, + cancellationToken)); + + /// + /// Changes the active BYOND version to the one specified in a given . + /// + /// The containing the to switch to. + /// The for the operation. + /// A resulting in the for the operation. + /// Switched active version successfully. + /// Created to install and switch active version successfully. + [HttpPost] + [TgsAuthorize(EngineRights.InstallOfficialOrChangeActiveByondVersion | EngineRights.InstallCustomByondVersion)] + [ProducesResponseType(typeof(ByondInstallResponse), 200)] + [ProducesResponseType(typeof(ByondInstallResponse), 202)] +#pragma warning disable CA1506 // TODO: Decomplexify + public async ValueTask Update([FromBody] ByondVersionRequest model, CancellationToken cancellationToken) +#pragma warning restore CA1506 + { + ArgumentNullException.ThrowIfNull(model); + + var uploadingZip = model.UploadCustomZip == true; + + if (model.Version == null + || model.Version.Revision != -1 + || (uploadingZip && model.Version.Build > 0)) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + + var userEngineRights = AuthenticationContext.InstancePermissionSet.EngineRights.Value; + if ((!userEngineRights.HasFlag(EngineRights.InstallOfficialOrChangeActiveByondVersion) && !uploadingZip) + || (!userEngineRights.HasFlag(EngineRights.InstallCustomByondVersion) && uploadingZip)) + return Forbid(); + + // remove cruff fields + var result = new ByondInstallResponse(); + return await WithComponentInstance( + async instance => + { + var byondManager = instance.EngineManager; + var engineVersion = CreateEngineVersionFromLegacyByondVersion(model.Version); + var versionAlreadyInstalled = !uploadingZip + && byondManager + .InstalledVersions + .Any(x => x.Equals(engineVersion)); + if (versionAlreadyInstalled) + { + Logger.LogInformation( + "User ID {userId} changing instance ID {instanceId} BYOND version to {newByondVersion}", + AuthenticationContext.User.Id, + Instance.Id, + engineVersion); + + try + { + await byondManager.ChangeVersion( + null, + engineVersion, + null, + false, + cancellationToken); + } + catch (InvalidOperationException ex) + { + Logger.LogDebug( + ex, + "Race condition: BYOND version {version} uninstalled before we could switch to it. Creating install job instead...", + engineVersion); + versionAlreadyInstalled = false; + } + } + + if (!versionAlreadyInstalled) + { + if (engineVersion.CustomIteration.HasValue) + return BadRequest(new ErrorMessageResponse(ErrorCode.EngineNonExistentCustomVersion)); + + Logger.LogInformation( + "User ID {userId} installing BYOND version to {newByondVersion} on instance ID {instanceId}", + AuthenticationContext.User.Id, + engineVersion, + Instance.Id); + + // run the install through the job manager + var job = new Host.Models.Job + { + Description = $"Install {(!uploadingZip ? string.Empty : "custom ")}BYOND version {engineVersion}", + StartedBy = AuthenticationContext.User, + CancelRightsType = RightsType.Engine, + CancelRight = (ulong)EngineRights.CancelInstall, + Instance = Instance, + }; + + IFileUploadTicket fileUploadTicket = null; + if (uploadingZip) + fileUploadTicket = fileTransferService.CreateUpload(FileUploadStreamKind.None); + + try + { + await jobManager.RegisterOperation( + job, + async (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => + { + Stream zipFileStream = null; + if (fileUploadTicket != null) + await using (fileUploadTicket) + { + var uploadStream = await fileUploadTicket.GetResult(jobCancellationToken) ?? throw new JobException(ErrorCode.FileUploadExpired); + zipFileStream = new MemoryStream(); + try + { + await uploadStream.CopyToAsync(zipFileStream, jobCancellationToken); + } + catch + { + await zipFileStream.DisposeAsync(); + throw; + } + } + + await using (zipFileStream) + await core.EngineManager.ChangeVersion( + progressHandler, + engineVersion, + zipFileStream, + true, + jobCancellationToken); + }, + cancellationToken); + + result.InstallJob = job.ToApi(); + result.FileTicket = fileUploadTicket?.Ticket.FileTicket; + } + catch + { + if (fileUploadTicket != null) + await fileUploadTicket.DisposeAsync(); + + throw; + } + } + + return result.InstallJob != null ? Accepted(result) : Json(result); + }); + } + + /// + /// Attempts to delete the BYOND version specified in a given from the instance. + /// + /// The containing the to delete. + /// The for the operation. + /// A resulting in the for the operation. + /// Created to delete target version successfully. + /// Attempted to delete the active BYOND . + /// The specified was not installed. + [HttpDelete] + [TgsAuthorize(EngineRights.DeleteInstall)] + [ProducesResponseType(typeof(JobResponse), 202)] + [ProducesResponseType(typeof(ErrorMessageResponse), 409)] + [ProducesResponseType(typeof(ErrorMessageResponse), 410)] + public async ValueTask Delete([FromBody] ByondVersionDeleteRequest model, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(model); + + if (model.Version == null + || model.Version.Revision != -1) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + + var engineVersion = CreateEngineVersionFromLegacyByondVersion(model.Version); + var notInstalledResponse = await WithComponentInstance( + instance => + { + var engineManager = instance.EngineManager; + + if (engineVersion.Equals(engineManager.ActiveVersion)) + return ValueTask.FromResult( + Conflict(new ErrorMessageResponse(ErrorCode.EngineCannotDeleteActiveVersion))); + + var versionNotInstalled = !engineManager.InstalledVersions.Any(x => x.Equals(engineVersion)); + + return ValueTask.FromResult( + versionNotInstalled + ? this.Gone() + : null); + }); + + if (notInstalledResponse != null) + return notInstalledResponse; + + // run the install through the job manager + var job = new Host.Models.Job + { + Description = $"Delete installed BYOND version {engineVersion}", + StartedBy = AuthenticationContext.User, + CancelRightsType = RightsType.Engine, + CancelRight = (ulong)(engineVersion.CustomIteration.HasValue ? EngineRights.InstallOfficialOrChangeActiveByondVersion : EngineRights.InstallCustomByondVersion), + Instance = Instance, + }; + + await jobManager.RegisterOperation( + job, + (instanceCore, databaseContextFactory, job, progressReporter, jobCancellationToken) + => instanceCore.EngineManager.DeleteVersion(progressReporter, engineVersion, jobCancellationToken), + cancellationToken); + + var apiResponse = job.ToApi(); + return Accepted(apiResponse); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondInstallResponse.cs b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondInstallResponse.cs new file mode 100644 index 0000000000..5cda990b2c --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondInstallResponse.cs @@ -0,0 +1,27 @@ +using System; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Response; + +namespace Tgstation.Server.Host.Controllers.Legacy.Models +{ + /// + /// Represents a BYOND installation job. is used to upload custom BYOND version zip files. + /// + public sealed class ByondInstallResponse : FileTicketResponse + { + /// + /// The being used to install a new . + /// + [ResponseOptions] + public JobResponse InstallJob { get; set; } + + /// + [ResponseOptions] + public override string FileTicket + { + get => base.FileTicket; + set => base.FileTicket = value; + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondResponse.cs b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondResponse.cs new file mode 100644 index 0000000000..d541717e8d --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondResponse.cs @@ -0,0 +1,18 @@ +using System; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Controllers.Legacy.Models +{ + /// + /// Represents an installed BYOND . + /// + public sealed class ByondResponse + { + /// + /// The installed BYOND . BYOND itself only considers the and numbers. This older API uses the number to represent installed custom versions. + /// + [ResponseOptions] + public Version Version { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondVersionDeleteRequest.cs b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondVersionDeleteRequest.cs new file mode 100644 index 0000000000..9293129298 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondVersionDeleteRequest.cs @@ -0,0 +1,18 @@ +using System; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Controllers.Legacy.Models +{ + /// + /// A request to delete a specific . + /// + public class ByondVersionDeleteRequest + { + /// + /// The BYOND version to delete. + /// + [RequestOptions(FieldPresence.Required)] + public Version Version { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondVersionRequest.cs b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondVersionRequest.cs new file mode 100644 index 0000000000..59aebebacc --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/Legacy/Models/ByondVersionRequest.cs @@ -0,0 +1,23 @@ +using System; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Controllers.Legacy.Models +{ + /// + /// A request to install a BYOND . + /// + public sealed class ByondVersionRequest + { + /// + /// The BYOND version to install. + /// + [RequestOptions(FieldPresence.Required)] + public Version Version { get; set; } + + /// + /// If a custom BYOND version is to be uploaded. + /// + public bool? UploadCustomZip { get; set; } + } +} diff --git a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs index 4b66154d43..680307cd23 100644 --- a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs +++ b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs @@ -43,7 +43,7 @@ namespace Tgstation.Server.Tests var logger = loggerFactory.CreateLogger("CachingFileDownloader"); var cfd = new CachingFileDownloader(loggerFactory.CreateLogger()); - var edgeVersion = await ByondTest.GetEdgeVersion(Api.Models.EngineType.Byond, cfd, cancellationToken); + var edgeVersion = await EngineTest.GetEdgeVersion(Api.Models.EngineType.Byond, cfd, cancellationToken); await InitializeByondVersion(logger, edgeVersion.Version, new PlatformIdentifier().IsWindows, cancellationToken); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs similarity index 87% rename from tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs rename to tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs index 991281ac02..6ca6ac32fb 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs @@ -26,9 +26,9 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests.Live.Instance { - sealed class ByondTest(IEngineClient byondClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata, EngineType engineType) : JobsRequiredTest(jobsClient) + sealed class EngineTest(IEngineClient engineClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata, EngineType engineType) : JobsRequiredTest(jobsClient) { - readonly IEngineClient byondClient = byondClient ?? throw new ArgumentNullException(nameof(byondClient)); + readonly IEngineClient engineClient = engineClient ?? throw new ArgumentNullException(nameof(engineClient)); readonly IFileDownloader fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); readonly Api.Models.Instance metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); @@ -111,7 +111,7 @@ namespace Tgstation.Server.Tests.Live.Instance ValueTask TestInstallNullVersion(CancellationToken cancellationToken) => ApiAssert.ThrowsException( - () => byondClient.SetActiveVersion( + () => engineClient.SetActiveVersion( new EngineVersionRequest { EngineVersion = new EngineVersion @@ -133,7 +133,7 @@ namespace Tgstation.Server.Tests.Live.Instance async Task TestDeletes(CancellationToken cancellationToken) { - var deleteThisOneBecauseItWasntPartOfTheOriginalTest = await byondClient.DeleteVersion(new EngineVersionDeleteRequest + var deleteThisOneBecauseItWasntPartOfTheOriginalTest = await engineClient.DeleteVersion(new EngineVersionDeleteRequest { EngineVersion = new EngineVersion { @@ -144,7 +144,7 @@ namespace Tgstation.Server.Tests.Live.Instance }, cancellationToken); await WaitForJob(deleteThisOneBecauseItWasntPartOfTheOriginalTest, 30, false, null, cancellationToken); - var nonExistentUninstallResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( + var nonExistentUninstallResponseTask = ApiAssert.ThrowsException(() => engineClient.DeleteVersion( new EngineVersionDeleteRequest { EngineVersion = new EngineVersion @@ -155,7 +155,7 @@ namespace Tgstation.Server.Tests.Live.Instance }, cancellationToken), ErrorCode.ResourceNotPresent); - var uninstallResponseTask = byondClient.DeleteVersion( + var uninstallResponseTask = engineClient.DeleteVersion( new EngineVersionDeleteRequest { EngineVersion = new EngineVersion @@ -167,7 +167,7 @@ namespace Tgstation.Server.Tests.Live.Instance }, cancellationToken); - var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( + var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => engineClient.DeleteVersion( new EngineVersionDeleteRequest { EngineVersion = new EngineVersion @@ -194,7 +194,7 @@ namespace Tgstation.Server.Tests.Live.Instance var byondDir = Path.Combine(metadata.Path, "Byond", testVersion.ToString()); Assert.IsFalse(Directory.Exists(byondDir)); - var newVersions = await byondClient.InstalledVersions(null, cancellationToken); + var newVersions = await engineClient.InstalledVersions(null, cancellationToken); Assert.IsNotNull(newVersions); Assert.AreEqual(1, newVersions.Count); Assert.AreEqual(testVersion.Version.Semver(), newVersions[0].EngineVersion.Version.Semver()); @@ -211,11 +211,11 @@ namespace Tgstation.Server.Tests.Live.Instance } }; - await ApiAssert.ThrowsException(() => byondClient.SetActiveVersion(newModel, null, cancellationToken), ErrorCode.ModelValidationFailure); + await ApiAssert.ThrowsException(() => engineClient.SetActiveVersion(newModel, null, cancellationToken), ErrorCode.ModelValidationFailure); newModel.EngineVersion.Engine = testEngine; - var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken); + var test = await engineClient.SetActiveVersion(newModel, null, cancellationToken); Assert.IsNotNull(test.InstallJob); await WaitForJob(test.InstallJob, 60, true, ErrorCode.EngineDownloadFail, cancellationToken); } @@ -231,10 +231,10 @@ namespace Tgstation.Server.Tests.Live.Instance SourceSHA = testVersion.SourceSHA, } }; - var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken); + var test = await engineClient.SetActiveVersion(newModel, null, cancellationToken); Assert.IsNotNull(test.InstallJob); await WaitForJob(test.InstallJob, 180, false, null, cancellationToken); - var currentShit = await byondClient.ActiveVersion(cancellationToken); + var currentShit = await engineClient.ActiveVersion(cancellationToken); Assert.AreEqual(newModel.EngineVersion, currentShit.EngineVersion); Assert.IsFalse(currentShit.EngineVersion.CustomIteration.HasValue); @@ -253,8 +253,8 @@ namespace Tgstation.Server.Tests.Live.Instance async Task TestNoVersion(CancellationToken cancellationToken) { - var allVersionsTask = byondClient.InstalledVersions(null, cancellationToken); - var currentShit = await byondClient.ActiveVersion(cancellationToken); + var allVersionsTask = engineClient.InstalledVersions(null, cancellationToken); + var currentShit = await engineClient.ActiveVersion(cancellationToken); Assert.IsNotNull(currentShit); Assert.IsNull(currentShit.EngineVersion); var otherShit = await allVersionsTask; @@ -289,7 +289,7 @@ namespace Tgstation.Server.Tests.Live.Instance // get the bytes for stable await using var stableBytesMs = await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion(testVersion, null, cancellationToken), cancellationToken); - var test = await byondClient.SetActiveVersion( + var test = await engineClient.SetActiveVersion( new EngineVersionRequest { EngineVersion = new EngineVersion @@ -308,7 +308,7 @@ namespace Tgstation.Server.Tests.Live.Instance // do it again. #1501 stableBytesMs.Seek(0, SeekOrigin.Begin); - var test2 = await byondClient.SetActiveVersion( + var test2 = await engineClient.SetActiveVersion( new EngineVersionRequest { EngineVersion = new EngineVersion @@ -326,12 +326,12 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(test2.InstallJob); await WaitForJob(test2.InstallJob, 30, false, null, cancellationToken); - var newSettings = await byondClient.ActiveVersion(cancellationToken); + var newSettings = await engineClient.ActiveVersion(cancellationToken); Assert.AreEqual(new Version(testVersion.Version.Major, testVersion.Version.Minor, 0), newSettings.EngineVersion.Version); Assert.AreEqual(2, newSettings.EngineVersion.CustomIteration); // test a few switches - var installResponse = await byondClient.SetActiveVersion(new EngineVersionRequest + var installResponse = await engineClient.SetActiveVersion(new EngineVersionRequest { EngineVersion = new EngineVersion { @@ -341,7 +341,7 @@ namespace Tgstation.Server.Tests.Live.Instance } }, null, cancellationToken); Assert.IsNull(installResponse.InstallJob); - await ApiAssert.ThrowsException(() => byondClient.SetActiveVersion(new EngineVersionRequest + await ApiAssert.ThrowsException(() => engineClient.SetActiveVersion(new EngineVersionRequest { EngineVersion = new EngineVersion { @@ -351,7 +351,7 @@ namespace Tgstation.Server.Tests.Live.Instance } }, null, cancellationToken), ErrorCode.EngineNonExistentCustomVersion); - installResponse = await byondClient.SetActiveVersion(new EngineVersionRequest + installResponse = await engineClient.SetActiveVersion(new EngineVersionRequest { EngineVersion = new EngineVersion { diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index c50f4d2567..26f00486bd 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -34,6 +35,22 @@ namespace Tgstation.Server.Tests.Live.Instance readonly InstanceManager instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); readonly ushort serverPort = serverPort; + public async Task RunLegacyByondTest( + IInstanceClient instanceClient, + CancellationToken cancellationToken) + { + var testVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + await new LegacyByondTest( + instanceClient.Jobs, + fileDownloader, + new LegacyByondClient( + (IApiClient)instanceClient.Engine.GetType().GetProperty("ApiClient", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(instanceClient.Engine), + instanceClient.Metadata), + testVersion.Version, + instanceClient.Metadata) + .Run(cancellationToken); + } + public async Task RunTests( IInstanceClient instanceClient, ushort dmPort, @@ -42,14 +59,14 @@ namespace Tgstation.Server.Tests.Live.Instance bool lowPrioDeployment, CancellationToken cancellationToken) { - var testVersion = await ByondTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); - var byondTest = new ByondTest(instanceClient.Engine, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, testVersion.Engine.Value); + var testVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + var engineTest = new EngineTest(instanceClient.Engine, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, testVersion.Engine.Value); var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata); var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata); var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs); var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs, dmPort, ddPort, lowPrioDeployment, testVersion.Engine.Value); - var byondTask = byondTest.Run(cancellationToken, out var firstInstall); + var byondTask = engineTest.Run(cancellationToken, out var firstInstall); var chatTask = chatTest.RunPreWatchdog(cancellationToken); var repoLongJob = await repoTest.RunLongClone(cancellationToken); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/LegacyByondClient.cs b/tests/Tgstation.Server.Tests/Live/Instance/LegacyByondClient.cs new file mode 100644 index 0000000000..9793401d24 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/Instance/LegacyByondClient.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Client; +using Tgstation.Server.Host.Controllers.Legacy.Models; + +namespace Tgstation.Server.Tests.Live.Instance +{ + /// + sealed class LegacyByondClient : PaginatedClient + { + const string Route = Routes.Root + "Byond"; + + /// + /// The for the . + /// + readonly Api.Models.Instance instance; + + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The value of . + public LegacyByondClient(IApiClient apiClient, Api.Models.Instance instance) + : base(apiClient) + { + this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + } + + /// + public ValueTask ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read(Route, instance.Id!.Value, cancellationToken); + + /// + public ValueTask DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken) + => ApiClient.Delete(Route, deleteRequest, instance.Id!.Value, cancellationToken); + + /// + public ValueTask> InstalledVersions(PaginationSettings paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Route), instance.Id, cancellationToken); + + /// + public async ValueTask SetActiveVersion(ByondVersionRequest installRequest, Stream zipFileStream, CancellationToken cancellationToken) + { + if (installRequest == null) + throw new ArgumentNullException(nameof(installRequest)); + if (installRequest.UploadCustomZip == true && zipFileStream == null) + throw new ArgumentNullException(nameof(zipFileStream)); + + var result = await ApiClient.Update( + Route, + installRequest, + instance.Id!.Value, + cancellationToken) + .ConfigureAwait(false); + + if (installRequest.UploadCustomZip == true) + await ApiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false); + + return result; + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/LegacyByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/LegacyByondTest.cs new file mode 100644 index 0000000000..ef13665456 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/Instance/LegacyByondTest.cs @@ -0,0 +1,240 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Threading; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Models.Request; +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Configuration; + +using Tgstation.Server.Host.IO; + +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Controllers.Legacy.Models; +using Tgstation.Server.Client.Components; + +namespace Tgstation.Server.Tests.Live.Instance +{ + internal class LegacyByondTest : JobsRequiredTest + { + readonly LegacyByondClient byondClient; + readonly Version testVersion; + readonly Api.Models.Instance metadata; + + readonly IFileDownloader fileDownloader; + + public LegacyByondTest(IJobsClient jobsClient, IFileDownloader fileDownloader, LegacyByondClient byondClient, Version testVersion, Api.Models.Instance metadata) + : base(jobsClient) + { + this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); + this.byondClient = byondClient ?? throw new ArgumentNullException(nameof(byondClient)); + this.testVersion = testVersion ?? throw new ArgumentNullException(nameof(testVersion)); + this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + } + + public async Task Run(CancellationToken cancellationToken) + { + await TestNoVersion(cancellationToken); + await TestInstallNullVersion(cancellationToken); + await TestInstallStable(cancellationToken); + await TestInstallFakeVersion(cancellationToken); + await TestCustomInstalls(cancellationToken); + await TestDeletes(cancellationToken); + } + + ValueTask TestInstallNullVersion(CancellationToken cancellationToken) + => ApiAssert.ThrowsException( + () => byondClient.SetActiveVersion( + new ByondVersionRequest(), + null, + cancellationToken), + ErrorCode.ModelValidationFailure); + + async Task TestDeletes(CancellationToken cancellationToken) + { + var deleteThisOneBecauseItWasntPartOfTheOriginalTest = await byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = new Version(testVersion.Major, testVersion.Minor, 2), + }, cancellationToken); + await WaitForJob(deleteThisOneBecauseItWasntPartOfTheOriginalTest, 30, false, null, cancellationToken); + + var nonExistentUninstallResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = new(509, 1000), + }, + cancellationToken), ErrorCode.ResourceNotPresent); + + var uninstallResponseTask = byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = testVersion + }, + cancellationToken); + + var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = new Version(testVersion.Major, testVersion.Minor, 1), + }, + cancellationToken), ErrorCode.EngineCannotDeleteActiveVersion); + + await badBecauseActiveResponseTask; + + var uninstallJob = await uninstallResponseTask; + Assert.IsNotNull(uninstallJob); + + // Has to wait on deployment test possibly + var uninstallTask = WaitForJob(uninstallJob, 120, false, null, cancellationToken); + + await nonExistentUninstallResponseTask; + + await uninstallTask; + var byondDir = Path.Combine(metadata.Path, "Byond", testVersion.ToString()); + Assert.IsFalse(Directory.Exists(byondDir)); + + var newVersions = await byondClient.InstalledVersions(null, cancellationToken); + Assert.IsNotNull(newVersions); + Assert.AreEqual(1, newVersions.Count); + Assert.AreEqual(new Version(testVersion.Major, testVersion.Minor, 1), newVersions[0].Version); + } + + async Task TestInstallFakeVersion(CancellationToken cancellationToken) + { + var newModel = new ByondVersionRequest + { + Version = new Version(5011, 1385), + }; + + var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken); + Assert.IsNotNull(test.InstallJob); + await WaitForJob(test.InstallJob, 60, true, ErrorCode.EngineDownloadFail, cancellationToken); + } + + async Task TestInstallStable(CancellationToken cancellationToken) + { + var newModel = new ByondVersionRequest + { + Version = testVersion, + }; + var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken); + Assert.IsNotNull(test.InstallJob); + await WaitForJob(test.InstallJob, 180, false, null, cancellationToken); + var currentShit = await byondClient.ActiveVersion(cancellationToken); + Assert.AreEqual(newModel.Version.Semver(), currentShit.Version.Semver()); + + var dreamMaker = "DreamMaker"; + if (new PlatformIdentifier().IsWindows) + dreamMaker += ".exe"; + + var dreamMakerDir = Path.Combine(metadata.Path, "Byond", newModel.Version.ToString(), "byond", "bin"); + + Assert.IsTrue(Directory.Exists(dreamMakerDir), $"Directory {dreamMakerDir} does not exist!"); + Assert.IsTrue( + File.Exists( + Path.Combine(dreamMakerDir, dreamMaker)), + $"Missing DreamMaker executable! Dir contents: {string.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}"); + } + + async Task TestNoVersion(CancellationToken cancellationToken) + { + var allVersionsTask = byondClient.InstalledVersions(null, cancellationToken); + var currentShit = await byondClient.ActiveVersion(cancellationToken); + Assert.IsNotNull(currentShit); + Assert.IsNull(currentShit.Version); + var otherShit = await allVersionsTask; + Assert.IsNotNull(otherShit); + Assert.AreEqual(0, otherShit.Count); + } + + async Task TestCustomInstalls(CancellationToken cancellationToken) + { + var generalConfigOptionsMock = new Mock>(); + generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); + var sessionConfigOptionsMock = new Mock>(); + sessionConfigOptionsMock.SetupGet(x => x.Value).Returns(new SessionConfiguration()); + + var assemblyInformationProvider = new AssemblyInformationProvider(); + + IEngineInstaller byondInstaller = new PlatformIdentifier().IsWindows + ? new WindowsByondInstaller( + Mock.Of(), + Mock.Of(), + fileDownloader, + generalConfigOptionsMock.Object, + Mock.Of>()) + : new PosixByondInstaller( + Mock.Of(), + Mock.Of(), + fileDownloader, + Mock.Of>()); + + using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; + + // get the bytes for stable + await using var stableBytesMs = await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion(new EngineVersion + { + Version = testVersion, + Engine = EngineType.Byond, + }, null, cancellationToken), cancellationToken); + + var test = await byondClient.SetActiveVersion( + new ByondVersionRequest + { + Version = testVersion, + UploadCustomZip = true, + }, + stableBytesMs, + cancellationToken); + + Assert.IsNotNull(test.InstallJob); + await WaitForJob(test.InstallJob, 30, false, null, cancellationToken); + + // do it again. #1501 + stableBytesMs.Seek(0, SeekOrigin.Begin); + var test2 = await byondClient.SetActiveVersion( + new ByondVersionRequest + { + Version = testVersion, + UploadCustomZip = true, + }, + stableBytesMs, + cancellationToken); + + Assert.IsNotNull(test2.InstallJob); + await WaitForJob(test2.InstallJob, 30, false, null, cancellationToken); + + var newSettings = await byondClient.ActiveVersion(cancellationToken); + Assert.AreEqual(new Version(testVersion.Major, testVersion.Minor, 2), newSettings.Version); + + // test a few switches + var installResponse = await byondClient.SetActiveVersion(new ByondVersionRequest + { + Version = testVersion, + }, null, cancellationToken); + Assert.IsNull(installResponse.InstallJob); + await ApiAssert.ThrowsException(() => byondClient.SetActiveVersion(new ByondVersionRequest + { + Version = new Version(testVersion.Major, testVersion.Minor, 3), + }, null, cancellationToken), ErrorCode.EngineNonExistentCustomVersion); + + installResponse = await byondClient.SetActiveVersion(new ByondVersionRequest + { + Version = new Version(testVersion.Major, testVersion.Minor, 1), + }, null, cancellationToken); + Assert.IsNull(installResponse.InstallJob); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 229bdeade6..0b576de6d7 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -1141,9 +1141,11 @@ namespace Tgstation.Server.Tests.Live var instanceManagerTest = new InstanceManagerTest(adminClient, server.Directory); var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken); var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken); + var byondApiCompatInstanceTask = instanceManagerTest.CreateTestInstance("BCAPITestsInstance", cancellationToken); instance = await instanceManagerTest.CreateTestInstance("LiveTestsInstance", cancellationToken); var compatInstance = await compatInstanceTask; var odInstance = await odInstanceTask; + var byondApiCompatInstance = await byondApiCompatInstanceTask; var instancesTest = FailFast(instanceManagerTest.RunPreTest(cancellationToken)); Assert.IsTrue(Directory.Exists(instance.Path)); var instanceClient = adminClient.Instances.CreateClient(instance); @@ -1158,10 +1160,19 @@ namespace Tgstation.Server.Tests.Live async Task RunInstanceTests() { + var byondApiCompatTests = FailFast( + instanceTest + .RunLegacyByondTest( + adminClient.Instances.CreateClient(byondApiCompatInstance), + cancellationToken)); + + if (TestingUtils.RunningInGitHubActions) // they only have 2 cores, can't handle intense parallelization + await byondApiCompatTests; + var odCompatTests = FailFast( instanceTest .RunCompatTests( - await ByondTest.GetEdgeVersion(EngineType.OpenDream, fileDownloader, cancellationToken), + await EngineTest.GetEdgeVersion(EngineType.OpenDream, fileDownloader, cancellationToken), adminClient.Instances.CreateClient(odInstance), odDMPort, odDDPort, @@ -1204,6 +1215,7 @@ namespace Tgstation.Server.Tests.Live await compatTests; await odCompatTests; + await byondApiCompatTests; } var instanceTests = RunInstanceTests(); @@ -1367,7 +1379,7 @@ namespace Tgstation.Server.Tests.Live preStartupTime = DateTimeOffset.UtcNow; serverTask = server.Run(cancellationToken).AsTask(); long expectedCompileJobId, expectedStaged; - var edgeVersion = await ByondTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) { var instanceClient = adminClient.Instances.CreateClient(instance);