-
-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1986 from tgstation/AdminAuthority [GQLDeploy]
Implement some administrative functions in GraphQL
- Loading branch information
Showing
23 changed files
with
803 additions
and
319 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
src/Tgstation.Server.Client.GraphQL/GQL/Mutations/RepositoryBasedServerUpdate.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
mutation RepositoryBasedServerUpdate($targetVersion: Semver!) { | ||
changeServerNodeVersionViaTrackedRepository(input: { targetVersion: $targetVersion }) { | ||
errors { | ||
... on ErrorMessageError { | ||
additionalData | ||
errorCode | ||
message | ||
} | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/Tgstation.Server.Client.GraphQL/GQL/Mutations/RestartServer.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
mutation RestartServer() { | ||
restartServerNode() { | ||
errors { | ||
... on ErrorMessageError { | ||
additionalData | ||
errorCode | ||
message | ||
} | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUpdateInformation.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
query GetUpdateInformation($forceFresh: Boolean!) { | ||
swarm { | ||
updateInformation { | ||
generatedAt | ||
latestVersion(forceFresh: $forceFresh) | ||
updateInProgress | ||
trackedRepositoryUrl(forceFresh: $forceFresh) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
241 changes: 241 additions & 0 deletions
241
src/Tgstation.Server.Host/Authority/AdministrationAuthority.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
using Microsoft.Extensions.Caching.Memory; | ||
using Microsoft.Extensions.Logging; | ||
|
||
using Octokit; | ||
|
||
using Tgstation.Server.Api.Models; | ||
using Tgstation.Server.Api.Models.Response; | ||
using Tgstation.Server.Api.Rights; | ||
using Tgstation.Server.Host.Authority.Core; | ||
using Tgstation.Server.Host.Core; | ||
using Tgstation.Server.Host.Database; | ||
using Tgstation.Server.Host.Security; | ||
using Tgstation.Server.Host.Transfer; | ||
using Tgstation.Server.Host.Utils.GitHub; | ||
|
||
namespace Tgstation.Server.Host.Authority | ||
{ | ||
/// <inheritdoc cref="IAdministrationAuthority" /> | ||
sealed class AdministrationAuthority : AuthorityBase, IAdministrationAuthority | ||
{ | ||
/// <summary> | ||
/// Default <see cref="Exception.Message"/> for <see cref="ApiException"/>s. | ||
/// </summary> | ||
const string OctokitException = "Bad GitHub API response, check configuration!"; | ||
|
||
/// <summary> | ||
/// The <see cref="IMemoryCache"/> key for <see cref="GetUpdateInformation(bool, CancellationToken)"/>. | ||
/// </summary> | ||
static readonly object ReadCacheKey = new(); | ||
|
||
/// <summary> | ||
/// The <see cref="IGitHubServiceFactory"/> for the <see cref="AdministrationAuthority"/>. | ||
/// </summary> | ||
readonly IGitHubServiceFactory gitHubServiceFactory; | ||
|
||
/// <summary> | ||
/// The <see cref="IServerControl"/> for the <see cref="AdministrationAuthority"/>. | ||
/// </summary> | ||
readonly IServerControl serverControl; | ||
|
||
/// <summary> | ||
/// The <see cref="IServerUpdateInitiator"/> for the <see cref="AdministrationAuthority"/>. | ||
/// </summary> | ||
readonly IServerUpdateInitiator serverUpdateInitiator; | ||
|
||
/// <summary> | ||
/// The <see cref="IFileTransferTicketProvider"/> for the <see cref="AdministrationAuthority"/>. | ||
/// </summary> | ||
readonly IFileTransferTicketProvider fileTransferService; | ||
|
||
/// <summary> | ||
/// The <see cref="IMemoryCache"/> for the <see cref="AdministrationAuthority"/>. | ||
/// </summary> | ||
readonly IMemoryCache cacheService; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="AdministrationAuthority"/> class. | ||
/// </summary> | ||
/// <param name="authenticationContext">The <see cref="IAuthenticationContext"/> to use.</param> | ||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to use.</param> | ||
/// <param name="logger">The <see cref="ILogger"/> to use.</param> | ||
/// <param name="gitHubServiceFactory">The value of <see cref="gitHubServiceFactory"/>.</param> | ||
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param> | ||
/// <param name="serverUpdateInitiator">The value of <see cref="serverUpdateInitiator"/>.</param> | ||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param> | ||
/// <param name="cacheService">The value of <see cref="cacheService"/>.</param> | ||
public AdministrationAuthority( | ||
IAuthenticationContext authenticationContext, | ||
IDatabaseContext databaseContext, | ||
ILogger<UserAuthority> logger, | ||
IGitHubServiceFactory gitHubServiceFactory, | ||
IServerControl serverControl, | ||
IServerUpdateInitiator serverUpdateInitiator, | ||
IFileTransferTicketProvider fileTransferService, | ||
IMemoryCache cacheService) | ||
: base( | ||
authenticationContext, | ||
databaseContext, | ||
logger) | ||
{ | ||
this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); | ||
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); | ||
this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator)); | ||
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); | ||
this.cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public async ValueTask<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
async Task<AdministrationResponse> CacheFactory() | ||
{ | ||
Version? greatestVersion = null; | ||
Uri? repoUrl = null; | ||
try | ||
{ | ||
var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); | ||
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken); | ||
var releases = await gitHubService.GetTgsReleases(cancellationToken); | ||
|
||
foreach (var kvp in releases) | ||
{ | ||
var version = kvp.Key; | ||
var release = kvp.Value; | ||
if (version.Major > 3 // Forward/backward compatible but not before TGS4 | ||
&& (greatestVersion == null || version > greatestVersion)) | ||
greatestVersion = version; | ||
} | ||
|
||
repoUrl = await repositoryUrlTask; | ||
} | ||
catch (NotFoundException e) | ||
{ | ||
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!"); | ||
} | ||
|
||
return new AdministrationResponse | ||
{ | ||
LatestVersion = greatestVersion, | ||
TrackedRepositoryUrl = repoUrl, | ||
GeneratedAt = DateTimeOffset.UtcNow, | ||
}; | ||
} | ||
|
||
var ttl = TimeSpan.FromMinutes(30); | ||
Task<AdministrationResponse> task; | ||
if (forceFresh || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject)) | ||
{ | ||
using var entry = cacheService.CreateEntry(ReadCacheKey); | ||
entry.AbsoluteExpirationRelativeToNow = ttl; | ||
entry.Value = task = CacheFactory(); | ||
} | ||
else | ||
task = (Task<AdministrationResponse>)rawCacheObject!; | ||
|
||
return new AuthorityResponse<AdministrationResponse>(await task); | ||
} | ||
catch (RateLimitExceededException e) | ||
{ | ||
return RateLimit<AdministrationResponse>(e); | ||
} | ||
catch (ApiException e) | ||
{ | ||
Logger.LogWarning(e, OctokitException); | ||
return new AuthorityResponse<AdministrationResponse>( | ||
new ErrorMessageResponse(ErrorCode.RemoteApiError) | ||
{ | ||
AdditionalData = e.Message, | ||
}, | ||
HttpFailureResponse.FailedDependency); | ||
} | ||
} | ||
|
||
/// <inheritdoc /> | ||
public async ValueTask<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken) | ||
{ | ||
var attemptingUpload = uploadZip == true; | ||
if (attemptingUpload) | ||
{ | ||
if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion)) | ||
return Forbid<ServerUpdateResponse>(); | ||
} | ||
else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion)) | ||
return Forbid<ServerUpdateResponse>(); | ||
|
||
if (targetVersion.Major < 4) | ||
return BadRequest<ServerUpdateResponse>(ErrorCode.CannotChangeServerSuite); | ||
|
||
if (!serverControl.WatchdogPresent) | ||
return new AuthorityResponse<ServerUpdateResponse>( | ||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog), | ||
HttpFailureResponse.UnprocessableEntity); | ||
|
||
IFileUploadTicket? uploadTicket = attemptingUpload | ||
? fileTransferService.CreateUpload(FileUploadStreamKind.None) | ||
: null; | ||
|
||
ServerUpdateResult updateResult; | ||
try | ||
{ | ||
try | ||
{ | ||
updateResult = await serverUpdateInitiator.InitiateUpdate(uploadTicket, targetVersion, cancellationToken); | ||
} | ||
catch | ||
{ | ||
if (attemptingUpload) | ||
await uploadTicket!.DisposeAsync(); | ||
|
||
throw; | ||
} | ||
} | ||
catch (RateLimitExceededException ex) | ||
{ | ||
return RateLimit<ServerUpdateResponse>(ex); | ||
} | ||
catch (ApiException e) | ||
{ | ||
Logger.LogWarning(e, OctokitException); | ||
return new AuthorityResponse<ServerUpdateResponse>( | ||
new ErrorMessageResponse(ErrorCode.RemoteApiError) | ||
{ | ||
AdditionalData = e.Message, | ||
}, | ||
HttpFailureResponse.FailedDependency); | ||
} | ||
|
||
return updateResult switch | ||
{ | ||
ServerUpdateResult.Started => new AuthorityResponse<ServerUpdateResponse>(new ServerUpdateResponse(targetVersion, uploadTicket?.Ticket.FileTicket), HttpSuccessResponse.Accepted), | ||
ServerUpdateResult.ReleaseMissing => Gone<ServerUpdateResponse>(), | ||
ServerUpdateResult.UpdateInProgress => BadRequest<ServerUpdateResponse>(ErrorCode.ServerUpdateInProgress), | ||
ServerUpdateResult.SwarmIntegrityCheckFailed => new AuthorityResponse<ServerUpdateResponse>( | ||
new ErrorMessageResponse(ErrorCode.SwarmIntegrityCheckFailed), | ||
HttpFailureResponse.FailedDependency), | ||
_ => throw new InvalidOperationException($"Unexpected ServerUpdateResult: {updateResult}"), | ||
}; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public async ValueTask<AuthorityResponse> TriggerServerRestart() | ||
{ | ||
if (!serverControl.WatchdogPresent) | ||
{ | ||
Logger.LogDebug("Restart request failed due to lack of host watchdog!"); | ||
return new AuthorityResponse( | ||
new ErrorMessageResponse(ErrorCode.MissingHostWatchdog), | ||
HttpFailureResponse.UnprocessableEntity); | ||
} | ||
|
||
await serverControl.Restart(); | ||
return new AuthorityResponse(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
src/Tgstation.Server.Host/Authority/IAdministrationAuthority.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
using Tgstation.Server.Api.Models.Response; | ||
using Tgstation.Server.Api.Rights; | ||
using Tgstation.Server.Host.Authority.Core; | ||
using Tgstation.Server.Host.Security; | ||
|
||
namespace Tgstation.Server.Host.Authority | ||
{ | ||
/// <summary> | ||
/// <see cref="IAuthority"/> for administrative server operations. | ||
/// </summary> | ||
public interface IAdministrationAuthority : IAuthority | ||
{ | ||
/// <summary> | ||
/// Gets the <see cref="AdministrationResponse"/> containing server update information. | ||
/// </summary> | ||
/// <param name="forceFresh">Bypass the caching that the authority performs for this request, forcing it to contact GitHub.</param> | ||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param> | ||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AdministrationResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns> | ||
[TgsAuthorize(AdministrationRights.ChangeVersion)] | ||
ValueTask<AuthorityResponse<AdministrationResponse>> GetUpdateInformation(bool forceFresh, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Triggers a restart of tgstation-server without terminating running game instances, setting its version to a given <paramref name="targetVersion"/>. | ||
/// </summary> | ||
/// <param name="targetVersion">The <see cref="Version"/> TGS will switch to upon reboot.</param> | ||
/// <param name="uploadZip">If <see langword="true"/> a <see cref="FileTicketResponse.FileTicket"/> will be returned and the call must provide an uploaded zip file containing the update data to the file transfer service.</param> | ||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param> | ||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="ServerUpdateResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns> | ||
[TgsAuthorize(AdministrationRights.ChangeVersion | AdministrationRights.UploadVersion)] | ||
ValueTask<AuthorityResponse<ServerUpdateResponse>> TriggerServerVersionChange(Version targetVersion, bool uploadZip, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Triggers a restart of tgstation-server without terminating running game instances. | ||
/// </summary> | ||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse"/>.</returns> | ||
[TgsAuthorize(AdministrationRights.RestartHost)] | ||
ValueTask<AuthorityResponse> TriggerServerRestart(); | ||
} | ||
} |
Oops, something went wrong.