GitHubService and factory + AdministrationController replacement

This commit is contained in:
Dominion
2023-06-04 00:33:05 -04:00
parent 0fb61510b6
commit fe7d91a06b
8 changed files with 283 additions and 32 deletions
@@ -19,7 +19,6 @@ using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -40,9 +39,9 @@ namespace Tgstation.Server.Host.Controllers
const string OctokitException = "Bad GitHub API response, check configuration!";
/// <summary>
/// The <see cref="IGitHubClientFactory"/> for the <see cref="AdministrationController"/>.
/// The <see cref="IGitHubService"/> for the <see cref="AdministrationController"/>.
/// </summary>
readonly IGitHubClientFactory gitHubClientFactory;
readonly IGitHubService gitHubService;
/// <summary>
/// The <see cref="IServerControl"/> for the <see cref="AdministrationController"/>.
@@ -74,11 +73,6 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly IFileTransferTicketProvider fileTransferService;
/// <summary>
/// The <see cref="UpdatesConfiguration"/> for the <see cref="AdministrationController"/>.
/// </summary>
readonly UpdatesConfiguration updatesConfiguration;
/// <summary>
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="AdministrationController"/>.
/// </summary>
@@ -89,7 +83,7 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
/// <param name="gitHubService">The value of <see cref="gitHubService"/>.</param>
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param>
/// <param name="serverUpdateInitiator">The value of <see cref="serverUpdateInitiator"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
@@ -97,12 +91,11 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
/// <param name="updatesConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="updatesConfiguration"/>.</param>
/// <param name="fileLoggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="fileLoggingConfiguration"/>.</param>
public AdministrationController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IGitHubClientFactory gitHubClientFactory,
IGitHubService gitHubService,
IServerControl serverControl,
IServerUpdateInitiator serverUpdateInitiator,
IAssemblyInformationProvider assemblyInformationProvider,
@@ -110,7 +103,6 @@ namespace Tgstation.Server.Host.Controllers
IPlatformIdentifier platformIdentifier,
IFileTransferTicketProvider fileTransferService,
ILogger<AdministrationController> logger,
IOptions<UpdatesConfiguration> updatesConfigurationOptions,
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions)
: base(
databaseContext,
@@ -118,14 +110,13 @@ namespace Tgstation.Server.Host.Controllers
logger,
true)
{
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService));
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
}
@@ -150,26 +141,19 @@ namespace Tgstation.Server.Host.Controllers
Uri repoUrl = null;
try
{
var gitHubClient = gitHubClientFactory.CreateClient();
var repositoryTask = gitHubClient
.Repository
.Get(updatesConfiguration.GitHubRepositoryId)
.WithToken(cancellationToken);
var releases = (await gitHubClient
.Repository
.Release
.GetAll(updatesConfiguration.GitHubRepositoryId)
.WithToken(cancellationToken))
.Where(x => x.TagName.StartsWith(
updatesConfiguration.GitTagPrefix,
StringComparison.InvariantCulture));
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
var releases = await gitHubService.GetTgsReleases(cancellationToken);
foreach (var release in releases)
if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
&& version.Major > 3 // Forward/backward compatible but not before TGS4
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 = new Uri((await repositoryTask).HtmlUrl);
}
repoUrl = await repositoryUrlTask;
}
catch (NotFoundException e)
{
@@ -370,7 +370,10 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<IFileDownloader, FileDownloader>();
services.AddSingleton<IServerPortProvider, ServerPortProivder>();
services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
services.AddSingleton<IGitHubServiceFactory, GitHubServiceFactory>();
services.AddSingleton(x => x.GetRequiredService<IGitHubServiceFactory>().CreateService());
// configure root services
services.AddSingleton<IJobService, JobService>();
@@ -67,7 +67,9 @@ namespace Tgstation.Server.Host.Utils
public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken);
/// <inheritdoc />
public IGitHubClient CreateClient(string accessToken) => GetOrCreateClient(accessToken ?? throw new ArgumentNullException(nameof(accessToken)));
public IGitHubClient CreateClient(string accessToken)
=> GetOrCreateClient(
accessToken ?? throw new ArgumentNullException(nameof(accessToken)));
/// <summary>
/// Retrieve a <see cref="GitHubClient"/> from the <see cref="clientCache"/> or add a new one based on a given <paramref name="accessToken"/>.
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Octokit;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
namespace Tgstation.Server.Host.Utils
{
/// <inheritdoc />
sealed class GitHubService : IGitHubService
{
/// <summary>
/// The <see cref="IGitHubClient"/> for the <see cref="GitHubService"/>.
/// </summary>
readonly IGitHubClient gitHubClient;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="GitHubService"/>.
/// </summary>
readonly ILogger<GitHubService> logger;
/// <summary>
/// The <see cref="UpdatesConfiguration"/> for the <see cref="GitHubService"/>.
/// </summary>
readonly UpdatesConfiguration updatesConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="GitHubService"/> class.
/// </summary>
/// <param name="gitHubClient">The value of <see cref="gitHubClient"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="updatesConfiguration">The value of <see cref="updatesConfiguration"/>.</param>
public GitHubService(IGitHubClient gitHubClient, ILogger<GitHubService> logger, UpdatesConfiguration updatesConfiguration)
{
this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.updatesConfiguration = updatesConfiguration ?? throw new ArgumentNullException(nameof(updatesConfiguration));
}
/// <inheritdoc />
public async Task<Dictionary<Version, Release>> GetTgsReleases(CancellationToken cancellationToken)
{
logger.LogTrace("GetTgsReleases");
var allReleases = await gitHubClient
.Repository
.Release
.GetAll(updatesConfiguration.GitHubRepositoryId)
.WithToken(cancellationToken);
logger.LogTrace("{totalReleases} total releases", allReleases.Count);
var releases = allReleases
.Select(release =>
{
if (!release.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)
|| !Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version))
return null;
return Tuple.Create(version, release);
})
.Where(tuple => tuple != null)
.ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2);
logger.LogTrace("{parsedReleases} parsed releases", releases.Count);
return releases;
}
/// <inheritdoc />
public async Task<Uri> GetUpdatesRepositoryUrl(CancellationToken cancellationToken)
{
logger.LogTrace("GetUpdatesRepositoryUrl");
var repository = await gitHubClient
.Repository
.Get(updatesConfiguration.GitHubRepositoryId)
.WithToken(cancellationToken);
var repoUrl = new Uri(repository.HtmlUrl);
logger.LogTrace("Maps to {repostioryUrl}", repoUrl);
return repoUrl;
}
}
}
@@ -0,0 +1,66 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Octokit;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Utils
{
/// <inheritdoc />
sealed class GitHubServiceFactory : IGitHubServiceFactory
{
/// <summary>
/// The <see cref="IGitHubClientFactory"/> for the <see cref="GitHubServiceFactory"/>.
/// </summary>
readonly IGitHubClientFactory gitHubClientFactory;
/// <summary>
/// The <see cref="ILoggerFactory"/> for the <see cref="GitHubServiceFactory"/>.
/// </summary>
readonly ILoggerFactory loggerFactory;
/// <summary>
/// The <see cref="UpdatesConfiguration"/> for the <see cref="GitHubServiceFactory"/>.
/// </summary>
readonly UpdatesConfiguration updatesConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="GitHubServiceFactory"/> class.
/// </summary>
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="updatesConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="updatesConfiguration"/>.</param>
public GitHubServiceFactory(
IGitHubClientFactory gitHubClientFactory,
ILoggerFactory loggerFactory,
IOptions<UpdatesConfiguration> updatesConfigurationOptions)
{
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
}
/// <inheritdoc />
public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient());
/// <inheritdoc />
public IGitHubService CreateService(string accessToken)
=> CreateServiceImpl(
gitHubClientFactory.CreateClient(
accessToken ?? throw new ArgumentNullException(nameof(accessToken))));
/// <summary>
/// Create a <see cref="GitHubService"/>.
/// </summary>
/// <param name="gitHubClient">The <see cref="IGitHubClient"/> for the <see cref="GitHubService"/>.</param>
/// <returns>A new <see cref="GitHubService"/>.</returns>
GitHubService CreateServiceImpl(IGitHubClient gitHubClient)
=> new (
gitHubClient,
loggerFactory.CreateLogger<GitHubService>(),
updatesConfiguration);
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Octokit;
namespace Tgstation.Server.Host.Utils
{
/// <summary>
/// Service for interacting with GitHub.
/// </summary>
public interface IGitHubService
{
/// <summary>
/// Gets the <see cref="Uri"/> of the repository designated as the updates repository.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Uri"/> of the designated updates repository.</returns>
Task<Uri> GetUpdatesRepositoryUrl(CancellationToken cancellationToken);
/// <summary>
/// Get all valid TGS <see cref="Release"/>s from the configured update source.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of TGS <see cref="Release"/>s keyed by their <see cref="Version"/>.</returns>
Task<Dictionary<Version, Release>> GetTgsReleases(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,21 @@
namespace Tgstation.Server.Host.Utils
{
/// <summary>
/// Factory for <see cref="IGitHubService"/>s.
/// </summary>
public interface IGitHubServiceFactory
{
/// <summary>
/// Create a <see cref="IGitHubService"/>.
/// </summary>
/// <returns>A new <see cref="IGitHubService"/>.</returns>
public IGitHubService CreateService();
/// <summary>
/// Create a <see cref="IGitHubService"/>.
/// </summary>
/// <param name="accessToken">The access token to use for communication with GitHub.</param>
/// <returns>A new <see cref="IGitHubService"/>.</returns>
public IGitHubService CreateService(string accessToken);
}
}
@@ -0,0 +1,57 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Octokit;
using Tgstation.Server.Host.Configuration;
namespace Tgstation.Server.Host.Utils.Tests
{
[TestClass]
public sealed class TestGitHubServiceFactory
{
[TestMethod]
public void TestConstructor()
{
Assert.ThrowsException<ArgumentNullException>(() => new GitHubServiceFactory(null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new GitHubServiceFactory(Mock.Of<IGitHubClientFactory>(), null, null));
Assert.ThrowsException<ArgumentNullException>(() => new GitHubServiceFactory(Mock.Of<IGitHubClientFactory>(), Mock.Of<ILoggerFactory>(), null));
var mockOptions = new Mock<IOptions<UpdatesConfiguration>>();
mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration());
_ = new GitHubServiceFactory(Mock.Of<IGitHubClientFactory>(), Mock.Of<ILoggerFactory>(), mockOptions.Object);
}
[TestMethod]
public void TestCreateService()
{
var mockFactory = new Mock<IGitHubClientFactory>();
mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of<IGitHubClient>()).Verifiable();
var mockToken = "asdf";
mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of<IGitHubClient>()).Verifiable();
var mockOptions = new Mock<IOptions<UpdatesConfiguration>>();
mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration());
var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of<ILoggerFactory>(), mockOptions.Object);
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateService(null));
Assert.AreEqual(0, mockFactory.Invocations.Count);
var result1 = factory.CreateService();
Assert.IsNotNull(result1);
var result2 = factory.CreateService(mockToken);
Assert.IsNotNull(result2);
mockFactory.VerifyAll();
}
}
}