mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-22 20:47:28 +01:00
Merge pull request #1924 from tgstation/1077-GraphQL [NugetDeploy]
GraphQL API Initial Implementation + Webpanel Update
This commit is contained in:
@@ -40,6 +40,7 @@ env:
|
||||
TGS_WEBPANEL_NODE_VERSION: 20.x
|
||||
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
|
||||
PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }}
|
||||
Internal__EnableGraphQL: true
|
||||
|
||||
concurrency:
|
||||
group: "ci-${{ (github.event_name != 'push' && github.event_name != 'schedule' && github.event.inputs.pull_request_number) || github.run_id }}-${{ github.event_name }}"
|
||||
@@ -577,6 +578,7 @@ jobs:
|
||||
configuration: ["Debug", "Release"]
|
||||
env:
|
||||
TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt
|
||||
TGS_TEST_GRAPHQL: true
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Setup dotnet
|
||||
|
||||
+1
-4
@@ -15,6 +15,7 @@ artifacts/
|
||||
*.dmb
|
||||
*.int
|
||||
*.lk
|
||||
/src/Tgstation.Server.Client.GraphQL/schema.graphql
|
||||
/src/Tgstation.Server.Host/appsettings.*.json
|
||||
/src/Tgstation.Server.Host/appsettings.*.yml
|
||||
/src/Tgstation.Server.Host/wwwroot
|
||||
@@ -27,8 +28,4 @@ changelog.yml
|
||||
*nupkg
|
||||
*.sqlite3
|
||||
packaging/
|
||||
/src/Tgstation.Server.Common/node_modules
|
||||
/src/Tgstation.Server.Common/package.json
|
||||
/src/Tgstation.Server.Common/yarn.lock
|
||||
/src/Tgstation.Server.Common/logo_bg_white.svg
|
||||
yarn-error.log*
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
<TgsConfigVersion>5.2.0</TgsConfigVersion>
|
||||
<TgsApiVersion>10.9.0</TgsApiVersion>
|
||||
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
|
||||
<TgsApiLibraryVersion>14.1.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>17.1.0</TgsClientVersion>
|
||||
<TgsApiLibraryVersion>15.0.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>18.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.3.0</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.10.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.5.0</TgsHostWatchdogVersion>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
|
||||
<TgsWebpanelVersion>6.1.0</TgsWebpanelVersion>
|
||||
<TgsWebpanelVersion>6.2.0</TgsWebpanelVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about the local tgstation-server.
|
||||
/// </summary>
|
||||
public class LocalServerInformation : ServerInformationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// If the server is running on a windows operating system.
|
||||
/// </summary>
|
||||
public bool WindowsHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="OAuthProvider"/> to the <see cref="OAuthProviderInfo"/> for them.
|
||||
/// </summary>
|
||||
public Dictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// <summary>
|
||||
/// Information about a server in the swarm.
|
||||
/// </summary>
|
||||
public abstract class SwarmServer
|
||||
public abstract class SwarmServer : IEquatable<SwarmServer>
|
||||
{
|
||||
/// <summary>
|
||||
/// The public address of the server.
|
||||
@@ -24,5 +24,35 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string? Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwarmServer"/> class.
|
||||
/// </summary>
|
||||
protected SwarmServer()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwarmServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="SwarmServer"/> to copy.</param>
|
||||
protected SwarmServer(SwarmServer copy)
|
||||
{
|
||||
if (copy == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(copy));
|
||||
}
|
||||
|
||||
Address = copy.Address;
|
||||
PublicAddress = copy.PublicAddress;
|
||||
Identifier = copy.Identifier;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(SwarmServer other)
|
||||
=> other != null
|
||||
&& other.Identifier == Identifier
|
||||
&& other.PublicAddress == PublicAddress
|
||||
&& other.Address == Address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents information about a running <see cref="SwarmServer"/>.
|
||||
/// </summary>
|
||||
public class SwarmServerInformation : SwarmServer, IEquatable<SwarmServerInformation>
|
||||
{
|
||||
/// <summary>
|
||||
/// If the <see cref="SwarmServerResponse"/> is the controller.
|
||||
/// </summary>
|
||||
public bool Controller { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwarmServerInformation"/> class.
|
||||
/// </summary>
|
||||
public SwarmServerInformation()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwarmServerInformation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="SwarmServerInformation"/> to copy.</param>
|
||||
public SwarmServerInformation(SwarmServerInformation copy)
|
||||
: base(copy)
|
||||
{
|
||||
Controller = copy.Controller;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(SwarmServerInformation other)
|
||||
=> base.Equals(other)
|
||||
&& other.Controller == Controller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates data from the TGS update source.
|
||||
/// </summary>
|
||||
public class UpdateInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Major"/> is less than 4 the update cannot be applied due to API changes.
|
||||
/// </summary>
|
||||
public Version? LatestVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This response is cached. This field indicates the <see cref="DateTimeOffset"/> the <see cref="UpdateInformation"/> was generated.
|
||||
/// </summary>
|
||||
public DateTimeOffset? GeneratedAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,17 @@
|
||||
using System;
|
||||
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Response
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents administrative server information.
|
||||
/// </summary>
|
||||
public sealed class AdministrationResponse
|
||||
public sealed class AdministrationResponse : UpdateInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// The GitHub repository the server is built to receive updates from.
|
||||
/// </summary>
|
||||
public Uri? TrackedRepositoryUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Major"/> is not equal to 4 the update cannot be applied due to API changes.
|
||||
/// </summary>
|
||||
public Version? LatestVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This response is cached. This field indicates the <see cref="DateTimeOffset"/> when it was generated.
|
||||
/// </summary>
|
||||
public DateTimeOffset? GeneratedAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// <summary>
|
||||
/// Represents basic server information.
|
||||
/// </summary>
|
||||
public sealed class ServerInformationResponse : Internal.ServerInformationBase
|
||||
public sealed class ServerInformationResponse : Internal.LocalServerInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// The version of the host.
|
||||
@@ -23,11 +23,6 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// </summary>
|
||||
public Version? DMApiVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the server is running on a windows operating system.
|
||||
/// </summary>
|
||||
public bool WindowsHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If there is a server update in progress.
|
||||
/// </summary>
|
||||
@@ -38,10 +33,5 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// </summary>
|
||||
[ResponseOptions]
|
||||
public ICollection<SwarmServerResponse>? SwarmServers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="OAuthProvider"/> to the <see cref="OAuthProviderInfo"/> for them.
|
||||
/// </summary>
|
||||
public IDictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using System;
|
||||
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Response
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class SwarmServerResponse : SwarmServer
|
||||
public sealed class SwarmServerResponse : SwarmServerInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// If the <see cref="SwarmServerResponse"/> is the controller.
|
||||
/// Initializes a new instance of the <see cref="SwarmServerResponse"/> class.
|
||||
/// </summary>
|
||||
public bool Controller { get; set; }
|
||||
[Obsolete("For JSON deserialization only", true)]
|
||||
public SwarmServerResponse()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwarmServerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="swarmServerInfo">The <see cref="SwarmServerInformation"/> to copy.</param>
|
||||
public SwarmServerResponse(SwarmServerInformation swarmServerInfo)
|
||||
: base(swarmServerInfo)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public const string ApiRoot = "/api/";
|
||||
|
||||
/// <summary>
|
||||
/// The GraphQL route.
|
||||
/// </summary>
|
||||
public const string GraphQL = ApiRoot + "graphql";
|
||||
|
||||
/// <summary>
|
||||
/// The root route of all hubs.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"strawberryshake.tools": {
|
||||
"version": "13.9.12",
|
||||
"commands": [
|
||||
"dotnet-graphql"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"schema": "schema.graphql",
|
||||
"documents": "**/*.graphql",
|
||||
"extensions": {
|
||||
"strawberryShake": {
|
||||
"name": "GraphQLClient",
|
||||
"url": "../../artifacts/tgs-api.graphql",
|
||||
"namespace": "Tgstation.Server.Client.GraphQL",
|
||||
"records": {
|
||||
"inputs": false,
|
||||
"entities": false
|
||||
},
|
||||
"transportProfiles": [
|
||||
{
|
||||
"default": "Http",
|
||||
"subscription": "WebSocket"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
query ServerInformationQuery {
|
||||
swarm {
|
||||
metadata {
|
||||
apiVersion
|
||||
dmApiVersion
|
||||
updateInProgress
|
||||
version
|
||||
}
|
||||
localServer {
|
||||
information {
|
||||
instanceLimit
|
||||
minimumPasswordLength
|
||||
userGroupLimit
|
||||
userLimit
|
||||
validInstancePaths
|
||||
windowsHost
|
||||
oAuthProviderInfos {
|
||||
key
|
||||
value {
|
||||
clientId
|
||||
redirectUri
|
||||
serverUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
servers {
|
||||
address
|
||||
controller
|
||||
identifier
|
||||
publicAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Client.GraphQL
|
||||
{
|
||||
/// <inheritdoc />
|
||||
class GraphQLServerClient : IGraphQLServerClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IGraphQLClient"/> for the <see cref="GraphQLServerClient"/>.
|
||||
/// </summary>
|
||||
readonly IGraphQLClient graphQLClient;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAsyncDisposable"/> to be <see cref="DisposeAsync"/>'d with the <see cref="GraphQLServerClient"/>.
|
||||
/// </summary>
|
||||
readonly IAsyncDisposable serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GraphQLServerClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="graphQLClient">The value of <see cref="graphQLClient"/>.</param>
|
||||
/// <param name="serviceProvider">The value of <see cref="serviceProvider"/>.</param>
|
||||
public GraphQLServerClient(
|
||||
IGraphQLClient graphQLClient,
|
||||
IAsyncDisposable serviceProvider)
|
||||
{
|
||||
this.graphQLClient = graphQLClient ?? throw new ArgumentNullException(nameof(graphQLClient));
|
||||
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync() => serviceProvider.DisposeAsync();
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask RunQuery(Func<IGraphQLClient, ValueTask> queryExector)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryExector);
|
||||
return queryExector(graphQLClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Client.GraphQL.Serializers;
|
||||
|
||||
namespace Tgstation.Server.Client.GraphQL
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class GraphQLServerClientFactory : IGraphQLServerClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IRestServerClientFactory"/> for the <see cref="GraphQLServerClientFactory"/>.
|
||||
/// </summary>
|
||||
readonly IRestServerClientFactory restClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GraphQLServerClientFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="restClientFactory">The value of <see cref="restClientFactory"/>.</param>
|
||||
public GraphQLServerClientFactory(IRestServerClientFactory restClientFactory)
|
||||
{
|
||||
this.restClientFactory = restClientFactory ?? throw new ArgumentNullException(nameof(restClientFactory));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IAuthenticatedGraphQLServerClient> CreateFromLogin(Uri host, string username, string password, bool attemptLoginRefresh = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IAuthenticatedGraphQLServerClient> CreateFromOAuth(Uri host, string oAuthCode, OAuthProvider oAuthProvider, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAuthenticatedGraphQLServerClient CreateFromToken(Uri host, string token)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGraphQLServerClient CreateUnauthenticated(Uri host)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
|
||||
var clientBuilder = serviceCollection
|
||||
.AddGraphQLClient();
|
||||
var graphQLEndpoint = new Uri(host, Routes.GraphQL);
|
||||
clientBuilder.ConfigureHttpClient(client => client.BaseAddress = graphQLEndpoint);
|
||||
|
||||
serviceCollection.AddSerializer<UnsignedIntSerializer>();
|
||||
serviceCollection.AddSerializer<SemverSerializer>();
|
||||
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
|
||||
return new GraphQLServerClient(
|
||||
serviceProvider.GetRequiredService<IGraphQLClient>(),
|
||||
serviceProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Tgstation.Server.Client.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IGraphQLServerClient"/> known to be authenticated.
|
||||
/// </summary>
|
||||
public interface IAuthenticatedGraphQLServerClient : IGraphQLServerClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The REST <see cref="ITransferClient"/>.
|
||||
/// </summary>
|
||||
ITransferClient TransferClient { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Client.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper for using a TGS <see cref="IGraphQLClient"/>.
|
||||
/// </summary>
|
||||
public interface IGraphQLServerClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a given <paramref name="queryExector"/>. It may be invoked multiple times depending on the behavior of the <see cref="IGraphQLServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="queryExector">A <see cref="Func{T, TResult}"/> which executes a single query on a given <see cref="IGraphQLClient"/> and returns a <see cref="ValueTask"/> representing the running operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask RunQuery(Func<IGraphQLClient, ValueTask> queryExector);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Client.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="IGraphQLServerClient"/>s.
|
||||
/// </summary>
|
||||
public interface IGraphQLServerClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an unauthenticated <see cref="IGraphQLServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="host">The <see cref="Uri"/> of tgstation-server.</param>
|
||||
/// <returns>A new <see cref="IGraphQLServerClient"/>.</returns>
|
||||
IGraphQLServerClient CreateUnauthenticated(Uri host);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IGraphQLServerClient"/> using a password login.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="username">The username to for the <see cref="IGraphQLServerClient"/>.</param>
|
||||
/// <param name="password">The password for the <see cref="IGraphQLServerClient"/>.</param>
|
||||
/// <param name="attemptLoginRefresh">Attempt to refresh the received <see cref="TokenResponse"/> when it expires or becomes invalid. <paramref name="username"/> and <paramref name="password"/> will be stored in memory if this is <see langword="true"/>.</param>
|
||||
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IAuthenticatedGraphQLServerClient"/>.</returns>
|
||||
ValueTask<IAuthenticatedGraphQLServerClient> CreateFromLogin(
|
||||
Uri host,
|
||||
string username,
|
||||
string password,
|
||||
bool attemptLoginRefresh = true,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IGraphQLServerClient"/> using an OAuth login.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="oAuthCode">The OAuth code used to complete the flow.</param>
|
||||
/// <param name="oAuthProvider">The <see cref="OAuthProvider"/>.</param>
|
||||
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IAuthenticatedGraphQLServerClient"/>.</returns>
|
||||
ValueTask<IAuthenticatedGraphQLServerClient> CreateFromOAuth(
|
||||
Uri host,
|
||||
string oAuthCode,
|
||||
OAuthProvider oAuthProvider,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IRestServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="token">The <see cref="TokenResponse"/> to access the API with.</param>
|
||||
/// <returns>A new <see cref="IGraphQLServerClient"/>.</returns>
|
||||
IAuthenticatedGraphQLServerClient CreateFromToken(
|
||||
Uri host,
|
||||
string token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# tgstation-server GraphQL Client Library
|
||||
|
||||
This GraphQL library is used for accessing [tgstation-server](https://github.com/tgstation/tgstation-server) instances via .NET code.
|
||||
|
||||
## Examples
|
||||
|
||||
### Connecting to a Server:
|
||||
|
||||
!!!TODO!!!
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
using StrawberryShake.Serialization;
|
||||
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
|
||||
#pragma warning disable CA1812 // not detecting service provider usage
|
||||
|
||||
namespace Tgstation.Server.Client.GraphQL.Serializers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ScalarSerializer{TSerialized, TRuntime}"/> for <see cref="UInt32"/>s.
|
||||
/// </summary>
|
||||
sealed class SemverSerializer : ScalarSerializer<string, Version>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SemverSerializer"/> class.
|
||||
/// </summary>
|
||||
public SemverSerializer()
|
||||
: base("Semver")
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Version Parse(string serializedValue)
|
||||
=> Version.Parse(serializedValue ?? throw new ArgumentNullException(nameof(serializedValue)));
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string Format(Version runtimeValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtimeValue);
|
||||
return runtimeValue.Semver().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
using StrawberryShake.Serialization;
|
||||
|
||||
#pragma warning disable CA1812 // not detecting service provider usage
|
||||
|
||||
namespace Tgstation.Server.Client.GraphQL.Serializers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ScalarSerializer{TSerialized, TRuntime}"/> for <see cref="UInt32"/>s.
|
||||
/// </summary>
|
||||
sealed class UnsignedIntSerializer : ScalarSerializer<uint>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnsignedIntSerializer"/> class.
|
||||
/// </summary>
|
||||
public UnsignedIntSerializer()
|
||||
: base("UnsignedInt")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../../build/SrcCommon.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
|
||||
<Version>$(TgsApiVersion)</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GraphQL connector and code generator -->
|
||||
<PackageReference Include="StrawberryShake.Server" Version="13.9.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- https://github.com/dotnet/msbuild/issues/2661#issuecomment-338808147 -->
|
||||
<Target Name="WorkaroundSdk939" BeforeTargets="ImportGraphQLApiSchema">
|
||||
<MSBuild Projects="..\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
|
||||
</Target>
|
||||
|
||||
<Target Name="DeleteGeneratedFiles" BeforeTargets="ImportGraphQLApiSchema">
|
||||
<RemoveDir Directories="$(IntermediateOutputPath)berry" />
|
||||
</Target>
|
||||
|
||||
<!-- https://github.com/ChilliCream/graphql-platform/blob/c0c8df525ca0f47bf3b3b409a8b22cbe37f7a9c0/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets#L20 -->
|
||||
<Target Name="ImportGraphQLApiSchema" BeforeTargets="_GraphQLCodeGenerationRoot" Inputs="../../artifacts/tgs-api.graphql" Outputs="schema.graphql">
|
||||
<Copy SkipUnchangedFiles="true" SourceFiles="../../artifacts/tgs-api.graphql" DestinationFiles="schema.graphql" />
|
||||
</Target>
|
||||
|
||||
<Target Name="FixWarningsInGeneratedSchema" AfterTargets="GenerateGraphQLCode">
|
||||
<PropertyGroup>
|
||||
<InputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</InputFile>
|
||||
<OutputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</OutputFile>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile File="$(OutputFile)" Lines="$([System.IO.File]::ReadAllText($(InputFile)).Replace('/ <auto-generated/>','/ <auto-generated />%0d%0a#pragma warning disable'))" Overwrite="true" Encoding="Unicode" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
scalar _KeyFieldSet
|
||||
|
||||
directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT
|
||||
|
||||
directive @serializationType(name: String!) on SCALAR
|
||||
|
||||
directive @runtimeType(name: String!) on SCALAR
|
||||
|
||||
directive @enumValue(value: String!) on ENUM_VALUE
|
||||
|
||||
directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE
|
||||
|
||||
extend schema @key(fields: "id")
|
||||
|
||||
|
||||
extend scalar UnsignedInt @serializationType(name: "global::System.UInt32") @runtimeType(name: "global::System.UInt32")
|
||||
extend scalar Semver @serializationType(name: "global::System.String") @runtimeType(name: "global::System.Version")
|
||||
@@ -5,7 +5,7 @@ using System.Net.Http;
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Exceptions thrown by <see cref="IServerClient"/>s.
|
||||
/// Exceptions thrown by <see cref="IRestServerClient"/>s.
|
||||
/// </summary>
|
||||
public abstract class ClientException : Exception
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -8,14 +7,13 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Web interface for the API.
|
||||
/// </summary>
|
||||
interface IApiClient : IAsyncDisposable
|
||||
interface IApiClient : ITransferClient, IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ApiHeaders"/> the <see cref="IApiClient"/> uses.
|
||||
@@ -39,7 +37,7 @@ namespace Tgstation.Server.Client
|
||||
void AddRequestLogger(IRequestLogger requestLogger);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all job updates available to the <see cref="IServerClient"/>.
|
||||
/// Subscribe to all job updates available to the <see cref="IRestServerClient"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="THubImplementation">The <see cref="Type"/> of the hub being implemented.</typeparam>
|
||||
/// <param name="hubImplementation">The <typeparamref name="THubImplementation"/> to use for proxying the methods of the hub connection.</param>
|
||||
@@ -239,22 +237,5 @@ namespace Tgstation.Server.Client
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask<TResult> Delete<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken)
|
||||
where TBody : class;
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file <see cref="Stream"/> for a given <paramref name="ticket"/>.
|
||||
/// </summary>
|
||||
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the downloaded <see cref="Stream"/>.</returns>
|
||||
ValueTask<Stream> Download(FileTicketResponse ticket, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a given <paramref name="uploadStream"/> for a given <paramref name="ticket"/>.
|
||||
/// </summary>
|
||||
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
|
||||
/// <param name="uploadStream">The <see cref="Stream"/> to upload. <see langword="null"/> represents an empty file.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Client
|
||||
/// <summary>
|
||||
/// Main client for communicating with a server.
|
||||
/// </summary>
|
||||
public interface IServerClient : IAsyncDisposable
|
||||
public interface IRestServerClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The connected server's root <see cref="Uri"/>.
|
||||
@@ -51,14 +51,20 @@ namespace Tgstation.Server.Client
|
||||
IUserGroupsClient Groups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ServerInformationResponse"/> of the <see cref="IServerClient"/>.
|
||||
/// Access the <see cref="ITransferClient"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Most client methods handle transfers in their invocations. There is rarely any reason to use the <see cref="ITransferClient"/> directly.</remarks>
|
||||
ITransferClient Transfer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ServerInformationResponse"/> of the <see cref="IRestServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ServerInformationResponse"/> of the target server.</returns>
|
||||
ValueTask<ServerInformationResponse> ServerInformation(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all job updates available to the <see cref="IServerClient"/>.
|
||||
/// Subscribe to all job updates available to the <see cref="IRestServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="jobsReceiver">The <see cref="IJobsHub"/> to use to subscribe to updates.</param>
|
||||
/// <param name="retryPolicy">The optional <see cref="IRetryPolicy"/> to use for the backing connection. The default retry policy waits for 1, 2, 4, 8, and 16 seconds, then 30s repeatedly.</param>
|
||||
+15
-15
@@ -9,9 +9,9 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="IServerClient"/>s.
|
||||
/// Factory for creating <see cref="IRestServerClient"/>s.
|
||||
/// </summary>
|
||||
public interface IServerClientFactory
|
||||
public interface IRestServerClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ServerInformationResponse"/> for a given <paramref name="host"/>.
|
||||
@@ -28,17 +28,17 @@ namespace Tgstation.Server.Client
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IServerClient"/> using a password login.
|
||||
/// Create a <see cref="IRestServerClient"/> using a password login.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="username">The username to for the <see cref="IServerClient"/>.</param>
|
||||
/// <param name="password">The password for the <see cref="IServerClient"/>.</param>
|
||||
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
|
||||
/// <param name="username">The username to for the <see cref="IRestServerClient"/>.</param>
|
||||
/// <param name="password">The password for the <see cref="IRestServerClient"/>.</param>
|
||||
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IRestServerClient"/>.</param>
|
||||
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection.</param>
|
||||
/// <param name="attemptLoginRefresh">Attempt to refresh the received <see cref="TokenResponse"/> when it expires or becomes invalid. <paramref name="username"/> and <paramref name="password"/> will be stored in memory if this is <see langword="true"/>.</param>
|
||||
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IServerClient"/>.</returns>
|
||||
ValueTask<IServerClient> CreateFromLogin(
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IRestServerClient"/>.</returns>
|
||||
ValueTask<IRestServerClient> CreateFromLogin(
|
||||
Uri host,
|
||||
string username,
|
||||
string password,
|
||||
@@ -48,16 +48,16 @@ namespace Tgstation.Server.Client
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IServerClient"/> using am OAuth login.
|
||||
/// Create a <see cref="IRestServerClient"/> using an OAuth login.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="oAuthCode">The OAuth code used to complete the flow.</param>
|
||||
/// <param name="oAuthProvider">The <see cref="OAuthProvider"/>.</param>
|
||||
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
|
||||
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IRestServerClient"/>.</param>
|
||||
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection.</param>
|
||||
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IServerClient"/>.</returns>
|
||||
ValueTask<IServerClient> CreateFromOAuth(
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IRestServerClient"/>.</returns>
|
||||
ValueTask<IRestServerClient> CreateFromOAuth(
|
||||
Uri host,
|
||||
string oAuthCode,
|
||||
OAuthProvider oAuthProvider,
|
||||
@@ -66,12 +66,12 @@ namespace Tgstation.Server.Client
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IServerClient"/>.
|
||||
/// Create a <see cref="IRestServerClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="token">The <see cref="TokenResponse"/> to access the API with.</param>
|
||||
/// <returns>A new <see cref="IServerClient"/>.</returns>
|
||||
IServerClient CreateFromToken(
|
||||
/// <returns>A new <see cref="IRestServerClient"/>.</returns>
|
||||
IRestServerClient CreateFromToken(
|
||||
Uri host,
|
||||
TokenResponse token);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// For transferring data <see cref="Stream"/>s.
|
||||
/// </summary>
|
||||
public interface ITransferClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads a file <see cref="Stream"/> for a given <paramref name="ticket"/>.
|
||||
/// </summary>
|
||||
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the downloaded <see cref="Stream"/>.</returns>
|
||||
ValueTask<Stream> Download(FileTicketResponse ticket, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a given <paramref name="uploadStream"/> for a given <paramref name="ticket"/>.
|
||||
/// </summary>
|
||||
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
|
||||
/// <param name="uploadStream">The <see cref="Stream"/> to upload. <see langword="null"/> represents an empty file.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -12,7 +12,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ServerClient : IServerClient
|
||||
sealed class RestServerClient : IRestServerClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Uri Url => apiClient.Url;
|
||||
@@ -43,16 +43,19 @@ namespace Tgstation.Server.Client
|
||||
/// <inheritdoc />
|
||||
public IUserGroupsClient Groups { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ITransferClient Transfer => apiClient;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IApiClient"/> for the <see cref="ServerClient"/>.
|
||||
/// The <see cref="IApiClient"/> for the <see cref="RestServerClient"/>.
|
||||
/// </summary>
|
||||
readonly IApiClient apiClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerClient"/> class.
|
||||
/// Initializes a new instance of the <see cref="RestServerClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The value of <see cref="apiClient"/>.</param>
|
||||
public ServerClient(IApiClient apiClient)
|
||||
public RestServerClient(IApiClient apiClient)
|
||||
{
|
||||
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||
|
||||
+16
-16
@@ -12,37 +12,37 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Client
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public sealed class ServerClientFactory : IServerClientFactory
|
||||
public sealed class RestServerClientFactory : IRestServerClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IApiClientFactory"/> for the <see cref="ServerClientFactory"/>.
|
||||
/// The <see cref="IApiClientFactory"/> for the <see cref="RestServerClientFactory"/>.
|
||||
/// </summary>
|
||||
internal static IApiClientFactory ApiClientFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ProductHeaderValue"/> for the <see cref="ServerClientFactory"/>.
|
||||
/// The <see cref="ProductHeaderValue"/> for the <see cref="RestServerClientFactory"/>.
|
||||
/// </summary>
|
||||
readonly ProductHeaderValue productHeaderValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="ServerClientFactory"/> class.
|
||||
/// Initializes static members of the <see cref="RestServerClientFactory"/> class.
|
||||
/// </summary>
|
||||
static ServerClientFactory()
|
||||
static RestServerClientFactory()
|
||||
{
|
||||
ApiClientFactory = new ApiClientFactory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerClientFactory"/> class.
|
||||
/// Initializes a new instance of the <see cref="RestServerClientFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="productHeaderValue">The value of <see cref="productHeaderValue"/>.</param>
|
||||
public ServerClientFactory(ProductHeaderValue productHeaderValue)
|
||||
public RestServerClientFactory(ProductHeaderValue productHeaderValue)
|
||||
{
|
||||
this.productHeaderValue = productHeaderValue ?? throw new ArgumentNullException(nameof(productHeaderValue));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IServerClient> CreateFromLogin(
|
||||
public ValueTask<IRestServerClient> CreateFromLogin(
|
||||
Uri host,
|
||||
string username,
|
||||
string password,
|
||||
@@ -69,7 +69,7 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IServerClient> CreateFromOAuth(
|
||||
public ValueTask<IRestServerClient> CreateFromOAuth(
|
||||
Uri host,
|
||||
string oAuthCode,
|
||||
OAuthProvider oAuthProvider,
|
||||
@@ -93,7 +93,7 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServerClient CreateFromToken(Uri host, TokenResponse token)
|
||||
public IRestServerClient CreateFromToken(Uri host, TokenResponse token)
|
||||
{
|
||||
if (host == null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
@@ -102,7 +102,7 @@ namespace Tgstation.Server.Client
|
||||
if (token.Bearer == null)
|
||||
throw new InvalidOperationException("token.Bearer should not be null!");
|
||||
|
||||
var serverClient = new ServerClient(
|
||||
var serverClient = new RestServerClient(
|
||||
ApiClientFactory.CreateApiClient(
|
||||
host,
|
||||
new ApiHeaders(
|
||||
@@ -142,16 +142,16 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="IServerClient"/> from a login operation.
|
||||
/// Creates a <see cref="IRestServerClient"/> from a login operation.
|
||||
/// </summary>
|
||||
/// <param name="host">The URL to access TGS.</param>
|
||||
/// <param name="loginHeaders">The <see cref="ApiHeaders"/> to use for the login operation.</param>
|
||||
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
|
||||
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IRestServerClient"/>.</param>
|
||||
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection.</param>
|
||||
/// <param name="attemptLoginRefresh">If <paramref name="loginHeaders"/> may be used to re-login in the future.</param>
|
||||
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IServerClient"/>.</returns>
|
||||
async ValueTask<IServerClient> CreateWithNewToken(
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IRestServerClient"/>.</returns>
|
||||
async ValueTask<IRestServerClient> CreateWithNewToken(
|
||||
Uri host,
|
||||
ApiHeaders loginHeaders,
|
||||
IEnumerable<IRequestLogger>? requestLoggers,
|
||||
@@ -173,7 +173,7 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
|
||||
var apiHeaders = new ApiHeaders(productHeaderValue, token);
|
||||
var client = new ServerClient(
|
||||
var client = new RestServerClient(
|
||||
ApiClientFactory.CreateApiClient(
|
||||
host,
|
||||
apiHeaders,
|
||||
@@ -30,6 +30,21 @@
|
||||
/// </summary>
|
||||
public bool UsingDocker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used at compile time to write the GraphQL API schema to this path and exit.
|
||||
/// </summary>
|
||||
public string? DumpGraphQLApiPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables hosting the experimental GraphQL API in Release builds.
|
||||
/// </summary>
|
||||
public bool EnableGraphQL
|
||||
#if DEBUG
|
||||
=> true;
|
||||
#else
|
||||
{ get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// The base path for the app settings configuration files.
|
||||
/// </summary>
|
||||
|
||||
@@ -181,7 +181,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
UserGroupLimit = generalConfiguration.UserGroupLimit,
|
||||
ValidInstancePaths = generalConfiguration.ValidInstancePaths,
|
||||
WindowsHost = platformIdentifier.IsWindows,
|
||||
SwarmServers = swarmService.GetSwarmServers(),
|
||||
SwarmServers = swarmService
|
||||
.GetSwarmServers()
|
||||
?.Select(swarmServerInfo => new SwarmServerResponse(swarmServerInfo))
|
||||
.ToList(),
|
||||
OAuthProviderInfos = oAuthProviders.ProviderInfos(),
|
||||
UpdateInProgress = serverControl.UpdateInProgress,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ using Cyberboss.AspNetCore.AsyncInitializer;
|
||||
|
||||
using Elastic.CommonSchema.Serilog;
|
||||
|
||||
using HotChocolate.Types;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@@ -50,6 +52,8 @@ using Tgstation.Server.Host.Controllers;
|
||||
using Tgstation.Server.Host.Controllers.Results;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.GraphQL;
|
||||
using Tgstation.Server.Host.GraphQL.Types.Scalars;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Properties;
|
||||
@@ -283,6 +287,15 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddHttpClient();
|
||||
services.AddSingleton<IAbstractHttpClientFactory, AbstractHttpClientFactory>();
|
||||
|
||||
// configure graphql
|
||||
if (postSetupServices.InternalConfiguration.EnableGraphQL)
|
||||
services
|
||||
.AddGraphQLServer()
|
||||
.AddAuthorization()
|
||||
.AddType<UnsignedIntType>()
|
||||
.BindRuntimeType<Version, SemverType>()
|
||||
.AddQueryType<Query>();
|
||||
|
||||
void AddTypedContext<TContext>()
|
||||
where TContext : DatabaseContext
|
||||
{
|
||||
@@ -454,6 +467,7 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="ControlPanelConfiguration"/> to use.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SwarmConfiguration"/> to use.</param>
|
||||
/// <param name="internalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="InternalConfiguration"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="Microsoft.Extensions.Logging.ILogger"/> for the <see cref="Application"/>.</param>
|
||||
public void Configure(
|
||||
IApplicationBuilder applicationBuilder,
|
||||
@@ -464,6 +478,7 @@ namespace Tgstation.Server.Host.Core
|
||||
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions,
|
||||
IOptions<InternalConfiguration> internalConfigurationOptions,
|
||||
ILogger<Application> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(applicationBuilder);
|
||||
@@ -477,6 +492,7 @@ namespace Tgstation.Server.Host.Core
|
||||
var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
|
||||
var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
|
||||
var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
|
||||
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
@@ -596,6 +612,12 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
// majority of handling is done in the controllers
|
||||
endpoints.MapControllers();
|
||||
|
||||
if (internalConfiguration.EnableGraphQL)
|
||||
{
|
||||
logger.LogWarning("Enabling GraphQL. This API is experimental and breaking changes may occur at any time!");
|
||||
endpoints.MapGraphQL(Routes.GraphQL);
|
||||
}
|
||||
});
|
||||
|
||||
// 404 anything that gets this far
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace Tgstation.Server.Host.Extensions
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<ControlPanelConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GeneralConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwarmConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<InternalConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<ILogger<Application>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Tgstation.Server.Host.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// Root type for GraphQL mutations.
|
||||
/// </summary>
|
||||
public sealed class Mutation
|
||||
{
|
||||
// Intentionally left blank, use type extensions to properly scope operations to domains
|
||||
// https://chillicream.com/docs/hotchocolate/v13/defining-a-schema/extending-types
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma warning disable CA1724
|
||||
|
||||
using Tgstation.Server.Host.GraphQL.Types;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL
|
||||
{
|
||||
/// <summary>
|
||||
/// GraphQL query <see cref="global::System.Type"/>.
|
||||
/// </summary>
|
||||
public sealed class Query
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ServerSwarm"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="ServerSwarm"/>.</returns>
|
||||
public ServerSwarm Swarm() => new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a database entity.
|
||||
/// </summary>
|
||||
public abstract class Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
public long Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Entity"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The value of <see cref="Id"/>.</param>
|
||||
protected Entity(long id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
using HotChocolate;
|
||||
using HotChocolate.Authorization;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Security.OAuth;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the local tgstation-server.
|
||||
/// </summary>
|
||||
public sealed class LocalServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets <see cref="LocalServerInformation"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthProviders">The <see cref="IOAuthProviders"/> to use.</param>
|
||||
/// <param name="platformIdentifier">The <see cref="IPlatformIdentifier"/> to use.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
|
||||
/// <returns>A new <see cref="LocalServerInformation"/>.</returns>
|
||||
[AllowAnonymous]
|
||||
public LocalServerInformation Information(
|
||||
[Service] IOAuthProviders oAuthProviders,
|
||||
[Service] IPlatformIdentifier platformIdentifier,
|
||||
[Service] IOptionsSnapshot<GeneralConfiguration> generalConfigurationOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(oAuthProviders);
|
||||
ArgumentNullException.ThrowIfNull(platformIdentifier);
|
||||
ArgumentNullException.ThrowIfNull(generalConfigurationOptions);
|
||||
|
||||
var generalConfiguration = generalConfigurationOptions.Value;
|
||||
return new LocalServerInformation
|
||||
{
|
||||
MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
|
||||
InstanceLimit = generalConfiguration.InstanceLimit,
|
||||
UserLimit = generalConfiguration.UserLimit,
|
||||
UserGroupLimit = generalConfiguration.UserGroupLimit,
|
||||
ValidInstancePaths = generalConfiguration.ValidInstancePaths,
|
||||
WindowsHost = platformIdentifier.IsWindows,
|
||||
OAuthProviderInfos = oAuthProviders.ProviderInfos(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="Entity"/> with a <see cref="Name"/>.
|
||||
/// </summary>
|
||||
public abstract class NamedEntity : Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the <see cref="NamedEntity"/>.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedEntity"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID for the <see cref="Entity"/>.</param>
|
||||
/// <param name="name">The value of <see cref="Name"/>.</param>
|
||||
protected NamedEntity(long id, string name)
|
||||
: base(id)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a valid OAuth connection.
|
||||
/// </summary>
|
||||
public sealed class OAuthConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthProvider"/> of the <see cref="OAuthConnection"/>.
|
||||
/// </summary>]
|
||||
public OAuthProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user in the <see cref="Provider"/>.
|
||||
/// </summary>
|
||||
public string ExternalUserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConnection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="externalUserId">The value of <see cref="ExternalUserId"/>.</param>
|
||||
/// <param name="provider">The value of <see cref="OAuthProvider"/>.</param>
|
||||
public OAuthConnection(string externalUserId, OAuthProvider provider)
|
||||
{
|
||||
ExternalUserId = externalUserId ?? throw new ArgumentNullException(nameof(externalUserId));
|
||||
Provider = provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a set of permissions for the server.
|
||||
/// </summary>
|
||||
public sealed class PermissionSet : Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Rights.AdministrationRights"/> for the <see cref="PermissionSet"/>.
|
||||
/// </summary>
|
||||
public AdministrationRights AdministrationRights { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Rights.InstanceManagerRights"/> for the <see cref="PermissionSet"/>.
|
||||
/// </summary>
|
||||
public InstanceManagerRights InstanceManagerRights { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PermissionSet"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Entity.Id"/>.</param>
|
||||
/// <param name="administrationRights">The value of <see cref="AdministrationRights"/>.</param>
|
||||
/// <param name="instanceManagerRights">The value of <see cref="InstanceManagerRights"/>.</param>
|
||||
public PermissionSet(long id, AdministrationRights administrationRights, InstanceManagerRights instanceManagerRights)
|
||||
: base(id)
|
||||
{
|
||||
AdministrationRights = administrationRights;
|
||||
InstanceManagerRights = instanceManagerRights;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
|
||||
using HotChocolate.Language;
|
||||
using HotChocolate.Types;
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types.Scalars
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ScalarType{TRuntimeType, TLiteral}"/> for semantic <see cref="Version"/>s.
|
||||
/// </summary>
|
||||
public sealed class SemverType : ScalarType<Version, StringValueNode>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SemverType"/> class.
|
||||
/// </summary>
|
||||
public SemverType()
|
||||
: base("Semver")
|
||||
{
|
||||
Description = "Represents a version in semver format as defined by https://semver.org/spec/v2.0.0.html";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IValueNode ParseResult(object? resultValue)
|
||||
=> ParseValue(resultValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool TryDeserialize(object? resultValue, out object? runtimeValue)
|
||||
{
|
||||
if (resultValue is not string resultString)
|
||||
{
|
||||
runtimeValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = Version.TryParse(resultString, out var resultVersion);
|
||||
runtimeValue = resultVersion;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool TrySerialize(object? runtimeValue, out object? resultValue)
|
||||
{
|
||||
if (runtimeValue is not Version runtimeVersion)
|
||||
{
|
||||
resultValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
resultValue = runtimeVersion.Semver().ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Version ParseLiteral(StringValueNode valueSyntax)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(valueSyntax);
|
||||
return Version.Parse(valueSyntax.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override StringValueNode ParseValue(Version runtimeValue)
|
||||
=> new StringValueNode(runtimeValue.Semver().ToString());
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsInstanceOfType(StringValueNode valueSyntax)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(valueSyntax);
|
||||
return IsInstanceOfType(valueSyntax.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsInstanceOfType(Version runtimeValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtimeValue);
|
||||
return runtimeValue.Build != -1 && runtimeValue.Revision == -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using HotChocolate;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a tgstation-server swarm.
|
||||
/// </summary>
|
||||
public sealed class ServerSwarm
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="SwarmMetadata"/> for the swarm.
|
||||
/// </summary>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
|
||||
/// <param name="serverControl">The <see cref="IServerControl"/> to use.</param>
|
||||
/// <returns>A new <see cref="SwarmMetadata"/>.</returns>
|
||||
public SwarmMetadata Metadata(
|
||||
[Service] IAssemblyInformationProvider assemblyInformationProvider,
|
||||
[Service] IServerControl serverControl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
|
||||
ArgumentNullException.ThrowIfNull(serverControl);
|
||||
return new SwarmMetadata(assemblyInformationProvider, serverControl.UpdateInProgress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local <see cref="Types.LocalServer"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="Types.LocalServer"/>.</returns>
|
||||
public LocalServer LocalServer() => new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="SwarmServerInformation"/> for all servers in a swarm.
|
||||
/// </summary>
|
||||
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
|
||||
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmServerInformation"/>s if the local server is part of a swarm, <see langword="null"/> otherwise.</returns>
|
||||
public List<SwarmServerInformation>? Servers(
|
||||
[Service] ISwarmService swarmService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(swarmService);
|
||||
return swarmService.GetSwarmServers();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Host.Components.Interop;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents information that is constant across all servers in a <see cref="ServerSwarm"/>.
|
||||
/// </summary>
|
||||
public sealed class SwarmMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The version of the host.
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api"/> version of the host.
|
||||
/// </summary>
|
||||
public Version ApiVersion => ApiHeaders.Version;
|
||||
|
||||
/// <summary>
|
||||
/// The DMAPI interop version the server uses.
|
||||
/// </summary>
|
||||
public Version DMApiVersion => DMApiConstants.InteropVersion;
|
||||
|
||||
/// <summary>
|
||||
/// If there is a server update in progress.
|
||||
/// </summary>
|
||||
public bool UpdateInProgress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SwarmMetadata"/> class.
|
||||
/// </summary>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> used to derive the <see cref="Version"/>.</param>
|
||||
/// <param name="updateInProgress">The value of <see cref="UpdateInProgress"/>.</param>
|
||||
public SwarmMetadata(IAssemblyInformationProvider assemblyInformationProvider, bool updateInProgress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
|
||||
Version = assemblyInformationProvider.Version;
|
||||
UpdateInProgress = updateInProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A user registered in the server.
|
||||
/// </summary>
|
||||
public sealed class User : NamedEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// If the <see cref="User"/> is enabled since users cannot be deleted. System users cannot be disabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When the <see cref="User"/> was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively.
|
||||
/// </summary>
|
||||
public string? SystemIdentifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Entity.Id"/> of the <see cref="CreatedBy"/> <see cref="User"/>.
|
||||
/// </summary>
|
||||
readonly long createdById;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="User"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Entity.Id"/>.</param>
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/>.</param>
|
||||
/// <param name="systemIdentifier">The value of <see cref="SystemIdentifier"/>.</param>
|
||||
/// <param name="createdById">The value of <see cref="createdById"/>.</param>
|
||||
/// <param name="createdAt">The value of <see cref="CreatedAt"/>.</param>
|
||||
/// <param name="enabled">The value of <see cref="Enabled"/>.</param>
|
||||
public User(
|
||||
long id,
|
||||
string name,
|
||||
string? systemIdentifier,
|
||||
DateTimeOffset createdAt,
|
||||
long createdById,
|
||||
bool enabled)
|
||||
: base(id, name)
|
||||
{
|
||||
SystemIdentifier = systemIdentifier;
|
||||
CreatedAt = createdAt;
|
||||
this.createdById = createdById;
|
||||
Enabled = enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> who created this <see cref="User"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="User"/> who created this <see cref="User"/>, if any.</returns>
|
||||
public ValueTask<User?> CreatedBy()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// List of <see cref="OAuthConnection"/>s associated with the user if OAuth is configured.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="List{T}"/> of <see cref="OAuthConnection"/>s for the <see cref="User"/> if OAuth is configured.</returns>
|
||||
public ValueTask<List<OAuthConnection>>? OAuthConnections()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Types.PermissionSet"/> directly associated with the <see cref="User"/>, if any.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="Types.PermissionSet"/> directly associated with the <see cref="User"/>, if any.</returns>
|
||||
public ValueTask<PermissionSet?> PermissionSet()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="UserGroup"/> asociated with the user, if any.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="UserGroup"/> associated with the <see cref="User"/>, if any.</returns>
|
||||
public ValueTask<UserGroup?> Group()
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate.Types;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a group of <see cref="User"/>s.
|
||||
/// </summary>
|
||||
public sealed class UserGroup : NamedEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Entity.Id"/> of the <see cref="PermissionSet"/>.
|
||||
/// </summary>
|
||||
readonly long permissionSetId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGroup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Entity.Id"/>.</param>
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/>.</param>
|
||||
/// <param name="permissionSetId">The value of <see cref="permissionSetId"/>.</param>
|
||||
public UserGroup(
|
||||
long id,
|
||||
string name,
|
||||
long permissionSetId)
|
||||
: base(id, name)
|
||||
{
|
||||
this.permissionSetId = permissionSetId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="PermissionSet"/> of the <see cref="UserGroup"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="Types.PermissionSet"/> for the <see cref="UserGroup"/>.</returns>
|
||||
public ValueTask<PermissionSet> PermissionSet()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="User"/>s in the <see cref="UserGroup"/>.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="List{T}"/> of <see cref="User"/>s in the <see cref="UserGroup"/>.</returns>
|
||||
[UsePaging(IncludeTotalCount = true)]
|
||||
public List<User> Users()
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate.Execution;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -13,6 +16,7 @@ using Microsoft.Extensions.Options;
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host
|
||||
@@ -137,6 +141,9 @@ namespace Tgstation.Server.Host
|
||||
{
|
||||
using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
|
||||
{
|
||||
if (await DumpGraphQLSchemaIfRequested(Host.Services, cancellationToken))
|
||||
return;
|
||||
|
||||
var generalConfigurationOptions = Host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
|
||||
generalConfiguration = generalConfigurationOptions.Value;
|
||||
await Host.RunAsync(cancellationTokenSource.Token);
|
||||
@@ -264,6 +271,31 @@ namespace Tgstation.Server.Host
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if <see cref="InternalConfiguration.DumpGraphQLApiPath"/> is set and dumps the GraphQL API Schema to it if so.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceProvider"/> to resolve services from.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the GraphQL API was dumped, <see langword="false"/> otherwise.</returns>
|
||||
async ValueTask<bool> DumpGraphQLSchemaIfRequested(IServiceProvider services, CancellationToken cancellationToken)
|
||||
{
|
||||
var internalConfigurationOptions = services.GetRequiredService<IOptions<InternalConfiguration>>();
|
||||
var apiDumpPath = internalConfigurationOptions.Value.DumpGraphQLApiPath;
|
||||
if (String.IsNullOrWhiteSpace(apiDumpPath))
|
||||
return false;
|
||||
|
||||
logger!.LogInformation("Dumping GraphQL API spec to {path} and exiting...", apiDumpPath);
|
||||
|
||||
// https://github.com/ChilliCream/graphql-platform/discussions/5885
|
||||
var resolver = services.GetRequiredService<IRequestExecutorResolver>();
|
||||
var executor = await resolver.GetRequestExecutorAsync(cancellationToken: cancellationToken);
|
||||
var sdl = executor.Schema.Print();
|
||||
|
||||
var ioManager = services.GetRequiredService<IIOManager>();
|
||||
await ioManager.WriteAllBytes(apiDumpPath, Encoding.UTF8.GetBytes(sdl), cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> if the <see cref="IServerControl"/> cannot be used.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Host.Swarm
|
||||
{
|
||||
@@ -15,8 +15,8 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <summary>
|
||||
/// Pass in an updated list of <paramref name="swarmServers"/> to the node.
|
||||
/// </summary>
|
||||
/// <param name="swarmServers">An <see cref="IEnumerable{T}"/> of the updated <see cref="SwarmServerResponse"/>s.</param>
|
||||
void UpdateSwarmServersList(IEnumerable<SwarmServerResponse> swarmServers);
|
||||
/// <param name="swarmServers">An <see cref="IEnumerable{T}"/> of the updated <see cref="SwarmServerInformation"/>s.</param>
|
||||
void UpdateSwarmServersList(IEnumerable<SwarmServerInformation> swarmServers);
|
||||
|
||||
/// <summary>
|
||||
/// Notify the node of an update request from the controller.
|
||||
@@ -36,11 +36,11 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <summary>
|
||||
/// Attempt to register a given <paramref name="node"/> with the controller.
|
||||
/// </summary>
|
||||
/// <param name="node">The <see cref="SwarmServerResponse"/> that is registering.</param>
|
||||
/// <param name="node">The <see cref="SwarmServer"/> that is registering.</param>
|
||||
/// <param name="registrationId">The registration <see cref="Guid"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the registration was successful, <see langword="false"/> otherwise.</returns>
|
||||
ValueTask<bool> RegisterNode(Api.Models.Internal.SwarmServer node, Guid registrationId, CancellationToken cancellationToken);
|
||||
ValueTask<bool> RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to unregister a node with a given <paramref name="registrationId"/> with the controller.
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Swarm
|
||||
@@ -35,9 +35,9 @@ namespace Tgstation.Server.Host.Swarm
|
||||
ValueTask<SwarmCommitResult> CommitUpdate(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <see cref="SwarmServerResponse"/>s in the swarm, including the current one.
|
||||
/// Gets the list of <see cref="SwarmServerInformation"/>s in the swarm, including the current one.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmServerResponse"/>s in the swarm. If the server is not part of a swarm, <see langword="null"/> will be returned.</returns>
|
||||
List<SwarmServerResponse>? GetSwarmServers();
|
||||
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmServerInformation"/>s in the swarm. If the server is not part of a swarm, <see langword="null"/> will be returned.</returns>
|
||||
List<SwarmServerInformation>? GetSwarmServers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Host.Swarm
|
||||
{
|
||||
@@ -11,9 +11,9 @@ namespace Tgstation.Server.Host.Swarm
|
||||
public sealed class SwarmServersUpdateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ICollection{T}"/> of updated <see cref="SwarmServerResponse"/>s.
|
||||
/// The <see cref="ICollection{T}"/> of updated <see cref="SwarmServerInformation"/>s.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public ICollection<SwarmServerResponse>? SwarmServers { get; set; }
|
||||
public ICollection<SwarmServerInformation>? SwarmServers { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
using Tgstation.Server.Common.Http;
|
||||
@@ -104,12 +105,12 @@ namespace Tgstation.Server.Host.Swarm
|
||||
readonly CancellationTokenSource? serverHealthCheckCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="List{T}"/> of connected <see cref="SwarmServerResponse"/>s.
|
||||
/// <see cref="List{T}"/> of connected <see cref="SwarmServerInformation"/>s.
|
||||
/// </summary>
|
||||
readonly List<SwarmServerResponse>? swarmServers;
|
||||
readonly List<SwarmServerInformation>? swarmServers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Dictionary{TKey, TValue}"/> of <see cref="Api.Models.Internal.SwarmServer.Identifier"/>s to registration <see cref="Guid"/>s and when they were created.
|
||||
/// <see cref="Dictionary{TKey, TValue}"/> of <see cref="SwarmServer.Identifier"/>s to registration <see cref="Guid"/>s and when they were created.
|
||||
/// </summary>
|
||||
readonly Dictionary<string, (Guid RegistrationId, DateTimeOffset RegisteredAt)>? registrationIdsAndTimes;
|
||||
|
||||
@@ -196,9 +197,9 @@ namespace Tgstation.Server.Host.Swarm
|
||||
serverHealthCheckCancellationTokenSource = new CancellationTokenSource();
|
||||
forceHealthCheckTcs = new TaskCompletionSource();
|
||||
|
||||
swarmServers = new List<SwarmServerResponse>
|
||||
swarmServers = new List<SwarmServerInformation>
|
||||
{
|
||||
new SwarmServerResponse
|
||||
new SwarmServerInformation
|
||||
{
|
||||
Address = swarmConfiguration.Address,
|
||||
PublicAddress = swarmConfiguration.PublicAddress,
|
||||
@@ -315,7 +316,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
// on the controller, we first need to signal for nodes to go ahead
|
||||
// if anything fails at this point, there's nothing we can do
|
||||
logger.LogDebug("Sending remote commit message to nodes...");
|
||||
async ValueTask SendRemoteCommitUpdate(SwarmServerResponse swarmServer)
|
||||
async ValueTask SendRemoteCommitUpdate(SwarmServerInformation swarmServer)
|
||||
{
|
||||
using var request = PrepareSwarmRequest(
|
||||
swarmServer,
|
||||
@@ -349,7 +350,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<SwarmServerResponse>? GetSwarmServers()
|
||||
public List<SwarmServerInformation>? GetSwarmServers()
|
||||
{
|
||||
if (!SwarmMode)
|
||||
return null;
|
||||
@@ -427,7 +428,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
{
|
||||
logger.LogTrace("Begin Shutdown");
|
||||
|
||||
async ValueTask SendUnregistrationRequest(SwarmServerResponse? swarmServer)
|
||||
async ValueTask SendUnregistrationRequest(SwarmServerInformation? swarmServer)
|
||||
{
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
using var request = PrepareSwarmRequest(
|
||||
@@ -503,7 +504,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateSwarmServersList(IEnumerable<SwarmServerResponse> swarmServers)
|
||||
public void UpdateSwarmServersList(IEnumerable<SwarmServerInformation> swarmServers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(swarmServers);
|
||||
|
||||
@@ -539,7 +540,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> RegisterNode(Api.Models.Internal.SwarmServer node, Guid registrationId, CancellationToken cancellationToken)
|
||||
public async ValueTask<bool> RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
|
||||
@@ -586,7 +587,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
registrationIdsAndTimes.Remove(node.Identifier);
|
||||
}
|
||||
|
||||
swarmServers.Add(new SwarmServerResponse
|
||||
swarmServers.Add(new SwarmServerInformation
|
||||
{
|
||||
PublicAddress = node.PublicAddress,
|
||||
Address = node.Address,
|
||||
@@ -693,7 +694,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
logger.LogInformation("Aborting swarm update!");
|
||||
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
async ValueTask SendRemoteAbort(SwarmServerResponse swarmServer)
|
||||
async ValueTask SendRemoteAbort(SwarmServerInformation swarmServer)
|
||||
{
|
||||
using var request = PrepareSwarmRequest(
|
||||
swarmServer,
|
||||
@@ -719,7 +720,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
}
|
||||
|
||||
if (!swarmController)
|
||||
return SendRemoteAbort(new SwarmServerResponse
|
||||
return SendRemoteAbort(new SwarmServerInformation
|
||||
{
|
||||
Address = swarmConfiguration.ControllerAddress,
|
||||
});
|
||||
@@ -735,10 +736,10 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <summary>
|
||||
/// Create the <see cref="RequestFileStreamProvider"/> for an update package retrieval from a given <paramref name="sourceNode"/>.
|
||||
/// </summary>
|
||||
/// <param name="sourceNode">The <see cref="SwarmServerResponse"/> to download the update package from.</param>
|
||||
/// <param name="sourceNode">The <see cref="SwarmServerInformation"/> to download the update package from.</param>
|
||||
/// <param name="ticket">The <see cref="FileTicketResponse"/> to use for the download.</param>
|
||||
/// <returns>A new <see cref="RequestFileStreamProvider"/> for the update package.</returns>
|
||||
RequestFileStreamProvider CreateUpdateStreamProvider(SwarmServerResponse sourceNode, FileTicketResponse ticket)
|
||||
RequestFileStreamProvider CreateUpdateStreamProvider(SwarmServerInformation sourceNode, FileTicketResponse ticket)
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
try
|
||||
@@ -791,8 +792,8 @@ namespace Tgstation.Server.Host.Swarm
|
||||
SwarmUpdateOperation localUpdateOperation;
|
||||
try
|
||||
{
|
||||
SwarmServerResponse? sourceNode = null;
|
||||
List<SwarmServerResponse> currentNodes;
|
||||
SwarmServerInformation? sourceNode = null;
|
||||
List<SwarmServerInformation> currentNodes;
|
||||
lock (swarmServers)
|
||||
{
|
||||
currentNodes = swarmServers
|
||||
@@ -1082,12 +1083,12 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// Create a <see cref="FileTicketResponse"/> for downloading the content of a given <paramref name="initiatorProvider"/> for the rest of the swarm nodes.
|
||||
/// </summary>
|
||||
/// <param name="initiatorProvider">The <see cref="ISeekableFileStreamProvider"/> containing the server update package.</param>
|
||||
/// <param name="involvedServers">An <see cref="IEnumerable{T}"/> of the involved <see cref="SwarmServerResponse"/>.</param>
|
||||
/// <param name="involvedServers">An <see cref="IEnumerable{T}"/> of the involved <see cref="SwarmServerInformation"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="Dictionary{TKey, TValue}"/> of unique <see cref="FileTicketResponse"/>s keyed by their <see cref="Api.Models.Internal.SwarmServer.Identifier"/>.</returns>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="Dictionary{TKey, TValue}"/> of unique <see cref="FileTicketResponse"/>s keyed by their <see cref="SwarmServer.Identifier"/>.</returns>
|
||||
async ValueTask<Dictionary<string, FileTicketResponse>> CreateDownloadTickets(
|
||||
ISeekableFileStreamProvider initiatorProvider,
|
||||
IReadOnlyCollection<SwarmServerResponse> involvedServers,
|
||||
IReadOnlyCollection<SwarmServerInformation> involvedServers,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// we need to ensure this thing is loaded before we start providing downloads or it'll create unnecessary delays
|
||||
@@ -1126,12 +1127,12 @@ namespace Tgstation.Server.Host.Swarm
|
||||
{
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
List<SwarmServerResponse> currentSwarmServers;
|
||||
List<SwarmServerInformation> currentSwarmServers;
|
||||
lock (swarmServers!)
|
||||
currentSwarmServers = swarmServers.ToList();
|
||||
|
||||
var registrationIdsAndTimes = this.registrationIdsAndTimes!;
|
||||
async ValueTask HealthRequestForServer(SwarmServerResponse swarmServer)
|
||||
async ValueTask HealthRequestForServer(SwarmServerInformation swarmServer)
|
||||
{
|
||||
using var request = PrepareSwarmRequest(
|
||||
swarmServer,
|
||||
@@ -1320,7 +1321,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
async ValueTask SendUpdatedServerListToNodes(CancellationToken cancellationToken)
|
||||
{
|
||||
List<SwarmServerResponse> currentSwarmServers;
|
||||
List<SwarmServerInformation> currentSwarmServers;
|
||||
lock (swarmServers!)
|
||||
{
|
||||
serversDirty = false;
|
||||
@@ -1336,7 +1337,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
logger.LogDebug("Sending updated server list to all {nodeCount} nodes...", currentSwarmServers.Count - 1);
|
||||
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
async ValueTask UpdateRequestForServer(SwarmServerResponse swarmServer)
|
||||
async ValueTask UpdateRequestForServer(SwarmServerInformation swarmServer)
|
||||
{
|
||||
using var request = PrepareSwarmRequest(
|
||||
swarmServer,
|
||||
@@ -1374,20 +1375,20 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <summary>
|
||||
/// Prepares a <see cref="HttpRequestMessage"/> for swarm communication.
|
||||
/// </summary>
|
||||
/// <param name="swarmServer">The <see cref="SwarmServerResponse"/> the message is for. Must have <see cref="Api.Models.Internal.SwarmServer.Address"/> and <see cref="Api.Models.Internal.SwarmServer.Identifier"/> set. If <see langword="null"/>, will be sent to swarm controller.</param>
|
||||
/// <param name="swarmServer">The <see cref="SwarmServerInformation"/> the message is for. Must have <see cref="SwarmServer.Address"/> and <see cref="SwarmServer.Identifier"/> set. If <see langword="null"/>, will be sent to swarm controller.</param>
|
||||
/// <param name="httpMethod">The <see cref="HttpMethod"/>.</param>
|
||||
/// <param name="route">The route on <see cref="SwarmConstants.ControllerRoute"/> to use.</param>
|
||||
/// <param name="body">The body <see cref="object"/> if any.</param>
|
||||
/// <param name="registrationIdOverride">An optional override to the <see cref="SwarmConstants.RegistrationIdHeader"/>.</param>
|
||||
/// <returns>A new <see cref="HttpRequestMessage"/>.</returns>
|
||||
HttpRequestMessage PrepareSwarmRequest(
|
||||
SwarmServerResponse? swarmServer,
|
||||
SwarmServerInformation? swarmServer,
|
||||
HttpMethod httpMethod,
|
||||
string route,
|
||||
object? body,
|
||||
Guid? registrationIdOverride = null)
|
||||
{
|
||||
swarmServer ??= new SwarmServerResponse
|
||||
swarmServer ??= new SwarmServerInformation
|
||||
{
|
||||
Address = swarmConfiguration.ControllerAddress,
|
||||
};
|
||||
@@ -1518,10 +1519,10 @@ namespace Tgstation.Server.Host.Swarm
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Api.Models.Internal.SwarmServer.Identifier"/> from a given <paramref name="registrationId"/>.
|
||||
/// Gets the <see cref="SwarmServer.Identifier"/> from a given <paramref name="registrationId"/>.
|
||||
/// </summary>
|
||||
/// <param name="registrationId">The registration <see cref="Guid"/>.</param>
|
||||
/// <returns>The registered <see cref="Api.Models.Internal.SwarmServer.Identifier"/> or <see langword="null"/> if it does not exist.</returns>
|
||||
/// <returns>The registered <see cref="SwarmServer.Identifier"/> or <see langword="null"/> if it does not exist.</returns>
|
||||
string? NodeIdentifierFromRegistration(Guid registrationId)
|
||||
{
|
||||
if (!swarmController)
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
|
||||
namespace Tgstation.Server.Host.Swarm
|
||||
{
|
||||
@@ -16,7 +15,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <summary>
|
||||
/// All of the <see cref="SwarmServer"/>s that are involved in the updates.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SwarmServerResponse> InvolvedServers => initialInvolvedServers ?? throw new InvalidOperationException("This property can only be checked on controller SwarmUpdateOperations!");
|
||||
public IReadOnlyList<SwarmServerInformation> InvolvedServers => initialInvolvedServers ?? throw new InvalidOperationException("This property can only be checked on controller SwarmUpdateOperations!");
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Version"/> being updated to.
|
||||
@@ -31,7 +30,7 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="InvolvedServers"/>.
|
||||
/// </summary>
|
||||
readonly IReadOnlyList<SwarmServerResponse>? initialInvolvedServers;
|
||||
readonly IReadOnlyList<SwarmServerInformation>? initialInvolvedServers;
|
||||
|
||||
/// <summary>
|
||||
/// The backing <see cref="TaskCompletionSource{TResult}"/> for <see cref="CommitGate"/>.
|
||||
@@ -58,9 +57,9 @@ namespace Tgstation.Server.Host.Swarm
|
||||
/// Initializes a new instance of the <see cref="SwarmUpdateOperation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="targetVersion">The value of <see cref="TargetVersion"/>.</param>
|
||||
/// <param name="currentNodes">An <see cref="IEnumerable{T}"/> of the controller's current nodes as <see cref="SwarmServerResponse"/>s. Must have <see cref="SwarmServer.Address"/> and <see cref="SwarmServer.Identifier"/> set.</param>
|
||||
/// <param name="currentNodes">An <see cref="IEnumerable{T}"/> of the controller's current nodes as <see cref="SwarmServerInformation"/>s. Must have <see cref="SwarmServer.Address"/> and <see cref="SwarmServer.Identifier"/> set.</param>
|
||||
/// <remarks>This is the variant for use by the controller.</remarks>
|
||||
public SwarmUpdateOperation(Version targetVersion, IEnumerable<SwarmServerResponse> currentNodes)
|
||||
public SwarmUpdateOperation(Version targetVersion, IEnumerable<SwarmServerInformation> currentNodes)
|
||||
: this(targetVersion)
|
||||
{
|
||||
initialInvolvedServers = currentNodes?.ToList() ?? throw new ArgumentNullException(nameof(currentNodes));
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
<Exec WorkingDirectory="ClientApp" Command="yarn run msbuild" />
|
||||
</Target>
|
||||
|
||||
<Target Name="ExportGraphQLApiSchema" AfterTargets="AfterBuild" Outputs="../../artifacts/tgs-api.graphql">
|
||||
<Message Text="Exporting GraphQL API Schema..." Importance="high" />
|
||||
<Exec Command="dotnet $(TargetPath)" EnvironmentVariables="General__SetupWizardMode=Never;Internal__DumpGraphQLApiPath=../../artifacts/tgs-api.graphql;Internal__EnableGraphQL=true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="NpmClean" AfterTargets="Clean">
|
||||
<Message Text="Cleaning web control panel..." Importance="high" />
|
||||
<RemoveDir Directories="wwwroot" />
|
||||
@@ -61,11 +66,8 @@
|
||||
|
||||
<Target Condition="'$(TGS_TELEMETRY_KEY_FILE)' != ''" Name="ApplyTelemetryAppSerializedKey" BeforeTargets="CoreCompile">
|
||||
<Error Condition="!Exists('$(TGS_TELEMETRY_KEY_FILE)')" Text="TGS_TELEMETRY_KEY_FILE set but does not exist!" />
|
||||
<ReadLinesFromFile
|
||||
File="$(TGS_TELEMETRY_KEY_FILE)" >
|
||||
<Output
|
||||
TaskParameter="Lines"
|
||||
ItemName="SerializedTelemetryKey"/>
|
||||
<ReadLinesFromFile File="$(TGS_TELEMETRY_KEY_FILE)">
|
||||
<Output TaskParameter="Lines" ItemName="SerializedTelemetryKey" />
|
||||
</ReadLinesFromFile>
|
||||
<ItemGroup>
|
||||
<TelemetryAppSerializedKeyAssemblyAttributes Include="Tgstation.Server.Host.Properties.TelemetryAppSerializedKeyAttribute">
|
||||
@@ -79,7 +81,7 @@
|
||||
</Target>
|
||||
|
||||
<Target Condition="'$(TGS_TELEMETRY_KEY_FILE)' == '' And '$(CI)' != ''" Name="FailBuildInCIWithoutTelemetryKey" BeforeTargets="CoreCompile">
|
||||
<Error Text="The TGS_TELEMETRY_KEY_FILE environment variable should be set in CI!"/>
|
||||
<Error Text="The TGS_TELEMETRY_KEY_FILE environment variable should be set in CI!" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -97,6 +99,12 @@
|
||||
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.11.1" />
|
||||
<!-- Usage: GitLab interop -->
|
||||
<PackageReference Include="GitLabApiClient" Version="1.8.0" />
|
||||
<!-- Usage: GraphQL API Engine -->
|
||||
<PackageReference Include="HotChocolate.AspNetCore" Version="13.9.12" />
|
||||
<!-- Usage: GraphQL Authorization Plugin -->
|
||||
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="13.9.12" />
|
||||
<!-- Usage: GraphQL additional scalar type definitions -->
|
||||
<PackageReference Include="HotChocolate.Types.Scalars" Version="13.9.12" />
|
||||
<!-- Usage: git interop -->
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
|
||||
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
|
||||
@@ -172,6 +180,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Authority\" />
|
||||
<Folder Include="GraphQL\DataLoaders\" />
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -10,8 +10,8 @@ namespace Tgstation.Server.Client.Tests
|
||||
[TestMethod]
|
||||
public void TestConstruction()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new ServerClientFactory(null));
|
||||
new ServerClientFactory(new ProductHeaderValue("Tgstation.Server.Client.Tests", GetType().Assembly.GetName().Version.ToString()));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new RestServerClientFactory(null));
|
||||
new RestServerClientFactory(new ProductHeaderValue("Tgstation.Server.Client.Tests", GetType().Assembly.GetName().Version.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,37 +40,43 @@ namespace Tgstation.Server.Host.Core.Tests
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(mockServiceCollection, null, null));
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null, null, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null, null, null, null, null, null));
|
||||
|
||||
var mockAppBuilder = new Mock<IApplicationBuilder>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null, null, null, null, null, null));
|
||||
|
||||
var mockServerControl = new Mock<IServerControl>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null, null, null, null, null, null));
|
||||
|
||||
var mockTokenFactory = new Mock<ITokenFactory>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null, null));
|
||||
|
||||
var mockServerPortProvider = new Mock<IServerPortProvider>();
|
||||
mockServerPortProvider.SetupGet(x => x.HttpApiPort).Returns(5345);
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null, null));
|
||||
|
||||
var mockAssemblyInformationProvider = Mock.Of<IAssemblyInformationProvider>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null, null));
|
||||
|
||||
var mockControlPanelOptions = new Mock<IOptions<ControlPanelConfiguration>>();
|
||||
mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null, null));
|
||||
|
||||
var mockGeneralOptions = new Mock<IOptions<GeneralConfiguration>>();
|
||||
mockGeneralOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()).Verifiable();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null, null));
|
||||
|
||||
var mockSwarmOptions = new Mock<IOptions<SwarmConfiguration>>();
|
||||
mockSwarmOptions.SetupGet(x => x.Value).Returns(new SwarmConfiguration()).Verifiable();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockSwarmOptions.Object, null));
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockSwarmOptions.Object, null, null));
|
||||
|
||||
var mockInternalOptions = new Mock<IOptions<InternalConfiguration>>();
|
||||
mockInternalOptions.SetupGet(x => x.Value).Returns(new InternalConfiguration()).Verifiable();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockSwarmOptions.Object, mockInternalOptions.Object, null));
|
||||
|
||||
mockControlPanelOptions.VerifyAll();
|
||||
mockInternalOptions.VerifyAll();
|
||||
mockSwarmOptions.VerifyAll();
|
||||
mockGeneralOptions.VerifyAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
{
|
||||
sealed class JobsHubTests : IJobsHub
|
||||
{
|
||||
readonly IServerClient permedUser;
|
||||
readonly IServerClient permlessUser;
|
||||
readonly IRestServerClient permedUser;
|
||||
readonly IRestServerClient permlessUser;
|
||||
|
||||
readonly TaskCompletionSource finishTcs;
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
long? permlessPsId;
|
||||
|
||||
public JobsHubTests(IServerClient permedUser, IServerClient permlessUser)
|
||||
public JobsHubTests(IRestServerClient permedUser, IRestServerClient permlessUser)
|
||||
{
|
||||
this.permedUser = permedUser;
|
||||
this.permlessUser = permlessUser;
|
||||
|
||||
@@ -24,12 +24,12 @@ namespace Tgstation.Server.Tests.Live
|
||||
{
|
||||
public const string TestInstanceName = "IntegrationTestInstance";
|
||||
|
||||
readonly IServerClient serverClient;
|
||||
readonly IRestServerClient serverClient;
|
||||
readonly IInstanceManagerClient instanceManagerClient;
|
||||
readonly IUsersClient usersClient;
|
||||
readonly string testRootPath;
|
||||
|
||||
public InstanceManagerTest(IServerClient serverClient, string testRootPath)
|
||||
public InstanceManagerTest(IRestServerClient serverClient, string testRootPath)
|
||||
{
|
||||
this.serverClient = serverClient ?? throw new ArgumentNullException(nameof(serverClient));
|
||||
instanceManagerClient = serverClient.Instances;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Tgstation.Server.Client;
|
||||
using Tgstation.Server.Client.GraphQL;
|
||||
|
||||
namespace Tgstation.Server.Tests.Live
|
||||
{
|
||||
sealed class MultiServerClient
|
||||
{
|
||||
readonly IRestServerClient restServerClient;
|
||||
readonly IGraphQLServerClient graphQLServerClient;
|
||||
|
||||
readonly bool useGraphQL;
|
||||
|
||||
public MultiServerClient(IRestServerClient restServerClient, IGraphQLServerClient graphQLServerClient)
|
||||
{
|
||||
this.restServerClient = restServerClient ?? throw new ArgumentNullException(nameof(restServerClient));
|
||||
this.graphQLServerClient = graphQLServerClient ?? throw new ArgumentNullException(nameof(graphQLServerClient));
|
||||
this.useGraphQL = Boolean.TryParse(Environment.GetEnvironmentVariable("TGS_TEST_GRAPHQL"), out var result) && result;
|
||||
}
|
||||
|
||||
public ValueTask Execute(
|
||||
Func<IRestServerClient, ValueTask> restAction,
|
||||
Func<IGraphQLClient, ValueTask> graphQLAction)
|
||||
{
|
||||
if (useGraphQL)
|
||||
return graphQLServerClient.RunQuery(graphQLAction);
|
||||
|
||||
return restAction(restServerClient);
|
||||
}
|
||||
|
||||
public async ValueTask ExecuteReadOnlyConfirmEquivalence<TRestResult, TGraphQLResult>(
|
||||
Func<IRestServerClient, ValueTask<TRestResult>> restAction,
|
||||
Func<IGraphQLClient, ValueTask<TGraphQLResult>> graphQLAction,
|
||||
Func<TRestResult, TGraphQLResult, bool> comparison)
|
||||
{
|
||||
var restTask = restAction(this.restServerClient);
|
||||
TGraphQLResult graphQLResult = default;
|
||||
await this.graphQLServerClient.RunQuery(async gqlClient => graphQLResult = await graphQLAction(gqlClient));
|
||||
|
||||
var restResult = await restTask;
|
||||
Assert.IsTrue(comparison(restResult, graphQLResult), "REST/GraphQL results differ!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
{
|
||||
static class RawRequestTests
|
||||
{
|
||||
static async Task TestRequestValidation(IServerClient serverClient, CancellationToken cancellationToken)
|
||||
static async Task TestRequestValidation(IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = serverClient.Url;
|
||||
var token = serverClient.Token.Bearer;
|
||||
@@ -196,7 +196,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
}
|
||||
}
|
||||
|
||||
static async Task TestServerInformation(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
|
||||
static async Task TestServerInformation(IRestServerClientFactory clientFactory, IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
var serverInfo = await serverClient.ServerInformation(default);
|
||||
|
||||
@@ -220,7 +220,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
await ApiAssert.ThrowsException<UnauthorizedException, ServerInformationResponse>(() => badClient.ServerInformation(cancellationToken));
|
||||
}
|
||||
|
||||
static async Task TestOAuthFails(IServerClient serverClient, CancellationToken cancellationToken)
|
||||
static async Task TestOAuthFails(IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = serverClient.Url;
|
||||
var token = serverClient.Token.Bearer;
|
||||
@@ -242,7 +242,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
}
|
||||
}
|
||||
|
||||
static async Task TestInvalidTransfers(IServerClient serverClient, CancellationToken cancellationToken)
|
||||
static async Task TestInvalidTransfers(IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = serverClient.Url;
|
||||
var token = serverClient.Token.Bearer;
|
||||
@@ -317,7 +317,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RegressionTestForLeakedPasswordHashesBug(IServerClient serverClient, CancellationToken cancellationToken)
|
||||
static async Task RegressionTestForLeakedPasswordHashesBug(IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
// See what https://github.com/tgstation/tgstation-server/commit/6c8dc87c4af36620885b262175d7974aca2b3c2b fixed
|
||||
|
||||
@@ -361,7 +361,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
=> ProxyFunc(job, cancellationToken);
|
||||
}
|
||||
|
||||
static async Task TestSignalRUsage(IServerClientFactory serverClientFactory, IServerClient serverClient, CancellationToken cancellationToken)
|
||||
static async Task TestSignalRUsage(IRestServerClientFactory serverClientFactory, IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
{
|
||||
// test regular creation works without error
|
||||
var hubConnectionBuilder = new HubConnectionBuilder();
|
||||
@@ -374,7 +374,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => tokenRetrivalFunc();
|
||||
((IApiClient)typeof(ServerClient)
|
||||
((IApiClient)typeof(RestServerClient)
|
||||
.GetField(
|
||||
"apiClient",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
@@ -457,7 +457,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
}
|
||||
}
|
||||
|
||||
public static Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
|
||||
public static Task Run(IRestServerClientFactory clientFactory, IRestServerClient serverClient, CancellationToken cancellationToken)
|
||||
=> Task.WhenAll(
|
||||
TestRequestValidation(serverClient, cancellationToken),
|
||||
TestOAuthFails(serverClient, cancellationToken),
|
||||
|
||||
@@ -35,6 +35,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Client;
|
||||
using Tgstation.Server.Client.Components;
|
||||
using Tgstation.Server.Client.GraphQL;
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
@@ -71,7 +72,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
_ = mainDMPort.Value;
|
||||
}
|
||||
|
||||
readonly ServerClientFactory clientFactory = new (new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
||||
readonly RestServerClientFactory clientFactory = new (new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
||||
|
||||
public static List<System.Diagnostics.Process> GetEngineServerProcessesOnPort(EngineType engineType, ushort? port)
|
||||
{
|
||||
@@ -205,7 +206,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
await CachingFileDownloader.InitializeAndInjectForLiveTests(default);
|
||||
|
||||
DummyChatProvider.RandomDisconnections(true);
|
||||
ServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory();
|
||||
RestServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory();
|
||||
|
||||
var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING");
|
||||
if (String.IsNullOrWhiteSpace(connectionString))
|
||||
@@ -290,7 +291,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
var serverTask = server.Run(cancellationToken).AsTask();
|
||||
try
|
||||
{
|
||||
async ValueTask<ServerUpdateResponse> TestWithoutAndWithPermission(Func<ValueTask<ServerUpdateResponse>> action, IServerClient client, AdministrationRights right)
|
||||
async ValueTask<ServerUpdateResponse> TestWithoutAndWithPermission(Func<ValueTask<ServerUpdateResponse>> action, IRestServerClient client, AdministrationRights right)
|
||||
{
|
||||
var ourUser = await client.Users.Read(cancellationToken);
|
||||
var update = new UserUpdateRequest
|
||||
@@ -321,7 +322,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/" + ApiHeaders.Version);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.OAuthAuthenticationScheme, adminClient.Token.Bearer);
|
||||
request.Headers.Add(ApiHeaders.OAuthProviderHeader, OAuthProvider.GitHub.ToString());
|
||||
request.Headers.Add(ApiHeaders.OAuthProviderHeader, Api.Models.OAuthProvider.GitHub.ToString());
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
@@ -875,7 +876,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
var controllerInfo = await controllerClient.ServerInformation(cancellationToken);
|
||||
|
||||
async Task WaitForSwarmServerUpdate(IServerClient client, int currentServerCount)
|
||||
async Task WaitForSwarmServerUpdate(IRestServerClient client, int currentServerCount)
|
||||
{
|
||||
ServerInformationResponse serverInformation;
|
||||
do
|
||||
@@ -1371,7 +1372,43 @@ namespace Tgstation.Server.Tests.Live
|
||||
await ApiAssert.ThrowsException<UnauthorizedException, UserResponse>(() => tokenOnlyClient.Users.Read(cancellationToken), null);
|
||||
}
|
||||
|
||||
async ValueTask<IServerClient> CreateUserWithNoInstancePerms()
|
||||
// basic graphql test, to be used everywhere eventually
|
||||
await using (var graphQLClient = new GraphQLServerClientFactory(clientFactory).CreateUnauthenticated(server.RootUrl))
|
||||
{
|
||||
// test getting server info
|
||||
var multiClient = new MultiServerClient(firstAdminClient, graphQLClient);
|
||||
|
||||
await multiClient.ExecuteReadOnlyConfirmEquivalence(
|
||||
restClient => restClient.ServerInformation(cancellationToken),
|
||||
async gqlClient => (await gqlClient.ServerInformationQuery.ExecuteAsync(cancellationToken)).Data,
|
||||
(restServerInfo, gqlServerInfo) => restServerInfo.UpdateInProgress == gqlServerInfo.Swarm.Metadata.UpdateInProgress
|
||||
&& restServerInfo.Version == gqlServerInfo.Swarm.Metadata.Version
|
||||
&& restServerInfo.DMApiVersion == gqlServerInfo.Swarm.Metadata.DmApiVersion
|
||||
&& restServerInfo.InstanceLimit == gqlServerInfo.Swarm.LocalServer.Information.InstanceLimit
|
||||
&& restServerInfo.UserGroupLimit == gqlServerInfo.Swarm.LocalServer.Information.UserGroupLimit
|
||||
&& restServerInfo.ValidInstancePaths.SequenceEqual(gqlServerInfo.Swarm.LocalServer.Information.ValidInstancePaths)
|
||||
&& restServerInfo.UserLimit == gqlServerInfo.Swarm.LocalServer.Information.UserLimit
|
||||
&& restServerInfo.MinimumPasswordLength == gqlServerInfo.Swarm.LocalServer.Information.MinimumPasswordLength
|
||||
&& (restServerInfo.SwarmServers == gqlServerInfo.Swarm.Servers
|
||||
|| restServerInfo.SwarmServers.SequenceEqual(gqlServerInfo.Swarm.Servers.Select(x => new SwarmServerResponse(new Api.Models.Internal.SwarmServerInformation
|
||||
{
|
||||
Address = x.Address,
|
||||
PublicAddress = x.PublicAddress,
|
||||
Controller = x.Controller,
|
||||
Identifier = x.Identifier,
|
||||
}))))
|
||||
&& (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.LocalServer.Information.OAuthProviderInfos
|
||||
|| restServerInfo.OAuthProviderInfos.All(kvp =>
|
||||
{
|
||||
var info = gqlServerInfo.Swarm.LocalServer.Information.OAuthProviderInfos.FirstOrDefault(x => (int)x.Key == (int)kvp.Key);
|
||||
return info != null
|
||||
&& info.Value.ServerUrl == kvp.Value.ServerUrl
|
||||
&& info.Value.ClientId == kvp.Value.ClientId
|
||||
&& info.Value.RedirectUri == kvp.Value.RedirectUri;
|
||||
})));
|
||||
}
|
||||
|
||||
async ValueTask<IRestServerClient> CreateUserWithNoInstancePerms()
|
||||
{
|
||||
var createRequest = new UserCreateRequest()
|
||||
{
|
||||
@@ -1457,10 +1494,10 @@ namespace Tgstation.Server.Tests.Live
|
||||
}
|
||||
|
||||
var instanceTest = new InstanceTest(
|
||||
firstAdminClient.Instances,
|
||||
fileDownloader,
|
||||
GetInstanceManager(),
|
||||
(ushort)server.ApiUrl.Port);
|
||||
firstAdminClient.Instances,
|
||||
fileDownloader,
|
||||
GetInstanceManager(),
|
||||
(ushort)server.ApiUrl.Port);
|
||||
|
||||
async Task RunInstanceTests()
|
||||
{
|
||||
@@ -1811,7 +1848,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
await serverTask;
|
||||
}
|
||||
|
||||
async Task<IServerClient> CreateAdminClient(Uri url, CancellationToken cancellationToken)
|
||||
async Task<IRestServerClient> CreateAdminClient(Uri url, CancellationToken cancellationToken)
|
||||
{
|
||||
url = new Uri(url.ToString().Replace(Routes.ApiRoot, String.Empty));
|
||||
var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2);
|
||||
|
||||
@@ -17,9 +17,9 @@ namespace Tgstation.Server.Tests.Live
|
||||
{
|
||||
sealed class UsersTest
|
||||
{
|
||||
readonly IServerClient serverClient;
|
||||
readonly IRestServerClient serverClient;
|
||||
|
||||
public UsersTest(IServerClient serverClient)
|
||||
public UsersTest(IRestServerClient serverClient)
|
||||
{
|
||||
this.serverClient = serverClient ?? throw new ArgumentNullException(nameof(serverClient));
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ namespace Tgstation.Server.Tests
|
||||
var versionString = versionsPropertyGroup.Element(xmlNamespace + "TgsClientVersion").Value + ".0";
|
||||
Assert.IsNotNull(versionString);
|
||||
Assert.IsTrue(Version.TryParse(versionString, out var expected));
|
||||
var actual = typeof(ServerClientFactory).Assembly.GetName().Version;
|
||||
var actual = typeof(RestServerClientFactory).Assembly.GetName().Version;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
|
||||
<ProjectReference Include="..\..\src\Tgstation.Server.Client.GraphQL\Tgstation.Server.Client.GraphQL.csproj" />
|
||||
<ProjectReference Include="..\..\src\Tgstation.Server.Host.Watchdog\Tgstation.Server.Host.Watchdog.csproj" />
|
||||
<ProjectReference Include="..\..\src\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -270,6 +270,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Shared", "
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Shared.Tests", "tests\Tgstation.Server.Shared.Tests\Tgstation.Server.Shared.Tests.csproj", "{EAB84FD0-5514-4254-B188-7D90ACB7284D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Client.GraphQL", "src\Tgstation.Server.Client.GraphQL\Tgstation.Server.Client.GraphQL.csproj", "{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -564,6 +566,18 @@ Global
|
||||
{EAB84FD0-5514-4254-B188-7D90ACB7284D}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU
|
||||
{EAB84FD0-5514-4254-B188-7D90ACB7284D}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EAB84FD0-5514-4254-B188-7D90ACB7284D}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BF95E2D-FD27-470C-82B7-C21AC01BFBD7}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -72,7 +72,7 @@ static class Program
|
||||
assemblyName.Version!.Semver().ToString());
|
||||
|
||||
var serverUrl = new Uri($"http://localhost:{apiPort}");
|
||||
var clientFactory = new ServerClientFactory(productInfoHeaderValue.Product);
|
||||
var clientFactory = new RestServerClientFactory(productInfoHeaderValue.Product);
|
||||
var TGS6Client = await clientFactory.CreateFromLogin(
|
||||
serverUrl,
|
||||
DefaultCredentials.AdminUserName,
|
||||
|
||||
@@ -476,8 +476,8 @@ try
|
||||
var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(MaxWaitMinutes);
|
||||
|
||||
var serverUrl = new Uri($"http://localhost:{configuredApiPort}");
|
||||
var clientFactory = new ServerClientFactory(productInfoHeaderValue.Product);
|
||||
IServerClient TGS6Client;
|
||||
var clientFactory = new RestServerClientFactory(productInfoHeaderValue.Product);
|
||||
IRestServerClient TGS6Client;
|
||||
for (var I = 1; ; ++I)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user