tgstation-server  4.3.2
The /tg/station 13 server suite
AdministrationController.cs
Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using Microsoft.Extensions.Primitives;
5 using Octokit;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Net;
11 using System.Threading;
12 using System.Threading.Tasks;
13 using Tgstation.Server.Api;
20 using Tgstation.Server.Host.IO;
23 
24 namespace Tgstation.Server.Host.Controllers
25 {
29  [Route(Routes.Administration)]
31  {
32  const string OctokitException = "Bad GitHub API response, check configuration! Exception: {0}";
33 
38 
43 
48 
53 
58 
63 
68 
83  IDatabaseContext databaseContext,
84  IAuthenticationContextFactory authenticationContextFactory,
85  IGitHubClientFactory gitHubClientFactory,
86  IServerControl serverUpdater,
87  IAssemblyInformationProvider assemblyInformationProvider,
88  IIOManager ioManager,
89  IPlatformIdentifier platformIdentifier,
90  ILogger<AdministrationController> logger,
91  IOptions<UpdatesConfiguration> updatesConfigurationOptions,
92  IOptions<GeneralConfiguration> generalConfigurationOptions)
93  : base(
94  databaseContext,
95  authenticationContextFactory,
96  logger,
97  false,
98  true)
99  {
100  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
101  this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
102  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
103  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
104  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
105  updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
106  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
107  }
108 
109  StatusCodeResult RateLimit(RateLimitExceededException exception)
110  {
111  Logger.LogWarning("Exceeded GitHub rate limit! Exception {0}", exception);
112  var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
113  Response.Headers.Add("Retry-After", new StringValues(secondsString));
114  return StatusCode(429);
115  }
116 
123  async Task<IActionResult> CheckReleasesAndApplyUpdate(Version newVersion, CancellationToken cancellationToken)
124  {
125  Logger.LogDebug("Looking for GitHub releases version {0}...", newVersion);
126  IEnumerable<Release> releases;
127  try
128  {
129  var gitHubClient = GetGitHubClient();
130  releases = await gitHubClient
131  .Repository
132  .Release
133  .GetAll(updatesConfiguration.GitHubRepositoryId)
134  .WithToken(cancellationToken)
135  .ConfigureAwait(false);
136  }
137  catch (RateLimitExceededException e)
138  {
139  return RateLimit(e);
140  }
141  catch (ApiException e)
142  {
143  Logger.LogWarning(OctokitException, e);
144  return StatusCode((int)HttpStatusCode.FailedDependency);
145  }
146 
147  releases = releases.Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
148 
149  Logger.LogTrace("Release query complete!");
150 
151  foreach (var release in releases)
152  if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == newVersion)
153  {
154  var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault();
155  if (asset == default)
156  continue;
157 
158  if (!serverUpdater.ApplyUpdate(version, new Uri(asset.BrowserDownloadUrl), ioManager))
159  return Conflict(new ErrorMessage(ErrorCode.ServerUpdateInProgress));
160  return Accepted(new Administration
161  {
162  WindowsHost = platformIdentifier.IsWindows,
163  NewVersion = newVersion
164  }); // gtfo of here before all the cancellation tokens fire
165  }
166 
167  return StatusCode((int)HttpStatusCode.Gone);
168  }
169 
170  IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken);
171 
179  [HttpGet]
180  [TgsAuthorize]
181  [ProducesResponseType(typeof(Administration), 200)]
182  [ProducesResponseType(424)]
183  [ProducesResponseType(typeof(ErrorMessage), 429)]
184  public async Task<IActionResult> Read()
185  {
186  try
187  {
188  Version greatestVersion = null;
189  Uri repoUrl = null;
190  try
191  {
192  var gitHubClient = GetGitHubClient();
193  var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId);
194  var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
195 
196  foreach (var I in releases)
197  if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
198  && version.Major == assemblyInformationProvider.Version.Major
199  && (greatestVersion == null || version > greatestVersion))
200  greatestVersion = version;
201  repoUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
202  }
203  catch (NotFoundException e)
204  {
205  Logger.LogWarning("Not found exception while retrieving upstream repository info: {0}", e);
206  }
207 
208  return Json(new Administration
209  {
210  LatestVersion = greatestVersion,
211  TrackedRepositoryUrl = repoUrl,
212  WindowsHost = platformIdentifier.IsWindows
213  });
214  }
215  catch (RateLimitExceededException e)
216  {
217  return RateLimit(e);
218  }
219  catch (ApiException e)
220  {
221  Logger.LogWarning(OctokitException, e);
222  return StatusCode((int)HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.GitHubApiError)
223  {
224  AdditionalData = e.Message
225  });
226  }
227  }
228 
240  [HttpPost]
241  [TgsAuthorize(AdministrationRights.ChangeVersion)]
242  [ProducesResponseType(typeof(Administration), 202)]
243  [ProducesResponseType(410)]
244  [ProducesResponseType(typeof(ErrorMessage), 422)]
245  [ProducesResponseType(424)]
246  [ProducesResponseType(typeof(ErrorMessage), 429)]
247  public async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
248  {
249  if (model == null)
250  throw new ArgumentNullException(nameof(model));
251 
252  if (model.NewVersion == null)
253  return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)
254  {
255  AdditionalData = "newVersion is required!"
256  });
257 
258  if (model.NewVersion.Major != assemblyInformationProvider.Version.Major)
259  return BadRequest(new ErrorMessage(ErrorCode.CannotChangeServerSuite));
260 
261  if(!serverUpdater.WatchdogPresent)
262  return UnprocessableEntity(new ErrorMessage(ErrorCode.MissingHostWatchdog));
263 
264  return await CheckReleasesAndApplyUpdate(model.NewVersion, cancellationToken).ConfigureAwait(false);
265  }
266 
273  [HttpDelete]
274  [TgsAuthorize(AdministrationRights.RestartHost)]
275  [ProducesResponseType(204)]
276  [ProducesResponseType(typeof(ErrorMessage), 422)]
277  public async Task<IActionResult> Delete()
278  {
279  try
280  {
281  if (!serverUpdater.WatchdogPresent)
282  {
283  Logger.LogDebug("Restart request failed due to lack of host watchdog!");
284  return UnprocessableEntity(new ErrorMessage(ErrorCode.MissingHostWatchdog));
285  }
286 
287  await serverUpdater.Restart().ConfigureAwait(false);
288  return NoContent();
289  }
290  catch (InvalidOperationException)
291  {
292  return StatusCode((int)HttpStatusCode.ServiceUnavailable);
293  }
294  }
295  }
296 }
AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger< AdministrationController > logger, IOptions< UpdatesConfiguration > updatesConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct an AdministrationController
async Task< IActionResult > Read()
Get Administration server information.
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
async Task< IActionResult > Delete()
Attempts to restart the server.
readonly IIOManager ioManager
The IIOManager for the AdministrationController
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the AdministrationController
readonly UpdatesConfiguration updatesConfiguration
The UpdatesConfiguration for the AdministrationController
AdministrationRights
Rights for Models.Administration
const string Administration
The Models.Administration controller
Definition: Routes.cs:19
StatusCodeResult RateLimit(RateLimitExceededException exception)
Represents administrative server information
Configuration for the automatic update system
Routes to a server actions
Definition: Routes.cs:9
async Task< IActionResult > Update([FromBody] Administration model, CancellationToken cancellationToken)
Attempt to perform a server upgrade.
async Task< IActionResult > CheckReleasesAndApplyUpdate(Version newVersion, CancellationToken cancellationToken)
Try to download and apply an update with a given newVersion .
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the AdministrationController
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController
Interface for using filesystems
Definition: IIOManager.cs:11
For identifying the current platform
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
readonly IServerControl serverUpdater
The IServerControl for the AdministrationController
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the AdministrationController
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...