diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 233c7fb51e5..1c6c65e278c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -36,6 +36,8 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. On Linux, you also need the `libgdiplus` package installed to generate icons. +You need to run `corepack enable` to configure node to correctly build the webpanel. + The recommended IDE is Visual Studio 2022 or VSCode. In order to build the service version and/or the Windows installer you need a to run on Windows. diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 6cfb58ff5b2..8704b477d8a 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -36,6 +36,7 @@ env: OD_MIN_COMPAT_DOTNET_VERSION: 7 OD_DOTNET_VERSION: 8 TGS_DOTNET_QUALITY: ga + TGS_WEBPANEL_NODE_VERSION: 20.x TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }} TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }} @@ -52,6 +53,8 @@ jobs: permissions: security-events: write actions: read + env: + TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt steps: - name: Setup dotnet uses: actions/setup-dotnet@v4 @@ -74,9 +77,16 @@ jobs: with: languages: csharp + - name: Setup Telemetry Key File + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true + - name: Delete Telemetry Key File + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: @@ -356,6 +366,8 @@ jobs: docker-build: name: Build Docker Image runs-on: ubuntu-latest + env: + TGS_TELEMETRY_KEY_FILE: tgs_telemetry_key.txt steps: - name: Checkout (Branch) uses: actions/checkout@v4 @@ -367,8 +379,16 @@ jobs: with: ref: "refs/pull/${{ github.event.inputs.pull_request_number }}/merge" + - name: Setup Telemetry Key File + shell: bash + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build Docker Image - run: docker build . -f build/Dockerfile + run: docker build . -f build/Dockerfile --build-arg TGS_TELEMETRY_KEY_FILE=${{ env.TGS_TELEMETRY_KEY_FILE }} + + - name: Delete Telemetry Key File + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} linux-unit-tests: name: Linux Tests @@ -379,6 +399,7 @@ jobs: env: TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }} + TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt runs-on: ubuntu-latest steps: - name: Install x86 libc Dependencies @@ -393,6 +414,11 @@ jobs: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }} + - name: Checkout (Branch) uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' @@ -406,9 +432,19 @@ jobs: - name: Restore run: dotnet restore + - name: Enable Corepack + run: corepack enable + + - name: Setup Telemetry Key File + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build run: dotnet build -c ${{ matrix.configuration }}NoWindows + - name: Delete Telemetry Key File + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Cache BYOND .zips uses: actions/cache@v4 id: cache-byond @@ -434,6 +470,7 @@ jobs: env: TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }} + TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt runs-on: windows-latest steps: - name: Setup dotnet @@ -442,6 +479,11 @@ jobs: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }} + - name: Checkout (Branch) uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' @@ -455,9 +497,21 @@ jobs: - name: Restore run: dotnet restore + - name: Enable Corepack + run: corepack enable + + - name: Setup Telemetry Key File + shell: bash + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build run: dotnet build -c ${{ matrix.configuration }}NoWix + - name: Delete Telemetry Key File + shell: bash + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Cache BYOND .zips uses: actions/cache@v4 id: cache-byond @@ -483,6 +537,8 @@ jobs: database-type: [ 'SqlServer', 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ] watchdog-type: [ 'Basic', 'Advanced' ] configuration: [ 'Debug', 'Release' ] + env: + TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt runs-on: windows-latest steps: - name: Wait for LocalDB Connection # Do this first because we don't want to find out it's failing later @@ -501,6 +557,11 @@ jobs: ${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }} + - name: Set TGS_TEST_DUMP_API_SPEC if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }} run: echo "TGS_TEST_DUMP_API_SPEC=yes" >> $Env:GITHUB_ENV @@ -570,9 +631,21 @@ jobs: - name: Restore run: dotnet restore + - name: Enable Corepack + run: corepack enable + + - name: Setup Telemetry Key File + shell: bash + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build run: dotnet build -c ${{ matrix.configuration }} tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj + - name: Delete Telemetry Key File + shell: bash + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Cache BYOND .zips uses: actions/cache@v4 id: cache-byond @@ -713,6 +786,8 @@ jobs: database-type: [ 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ] watchdog-type: [ 'Basic', 'Advanced' ] configuration: [ 'Debug', 'Release' ] + env: + TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt runs-on: ubuntu-latest steps: - name: Disable ptrace_scope @@ -733,6 +808,11 @@ jobs: ${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }} + - name: Set Sqlite Connection Info if: ${{ matrix.database-type == 'Sqlite' }} run: | @@ -775,9 +855,19 @@ jobs: - name: Restore run: dotnet restore + - name: Enable Corepack + run: corepack enable + + - name: Setup Telemetry Key File + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build run: dotnet build -c ${{ matrix.configuration }}NoWindows tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj + - name: Delete Telemetry Key File + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Cache BYOND .zips uses: actions/cache@v4 id: cache-byond @@ -1117,12 +1207,14 @@ jobs: build-deb: name: Build .deb Package # Can't do i386 due to https://github.com/dotnet/core/issues/4595 runs-on: ubuntu-latest + env: + TGS_TELEMETRY_KEY_FILE: /tmp/tgs_telemetry_key.txt steps: - name: Install Native Dependencies run: | sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2 xmlstarlet libgdiplus + sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 - name: Import GPG Key if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev')) @@ -1160,27 +1252,37 @@ jobs: with: ref: "refs/pull/${{ github.event.inputs.pull_request_number }}/merge" - - name: Parse TGS version - run: | - echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV - - name: Grab Most Recent Changelog run: curl -L https://raw.githubusercontent.com/tgstation/tgstation-server/gh-pages/changelog.yml -o changelog.yml + - name: Setup Telemetry Key File + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Execute Build Script (Unsigned) - if: (!(github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev'))) + if: (!(github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && github.event.ref == 'refs/heads/master')) run: sudo -E build/package/deb/build_package.sh - name: Execute Build Script (Signed) - if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev')) + if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && github.event.ref == 'refs/heads/master') env: PACKAGING_KEYGRIP: ${{ vars.PACKAGING_KEYGRIP }} + run: sudo -E build/package/deb/build_package.sh + + - name: Parse TGS version run: | - sudo -E build/package/deb/build_package.sh + echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV + + - name: Verify Package Files are Signed + if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && github.event.ref == 'refs/heads/master') + run: gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1.dsc gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.changes gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.buildinfo + - name: Delete Telemetry Key File + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Test Install run: | sudo mkdir /etc/tgstation-server @@ -1219,6 +1321,8 @@ jobs: build-msi: name: Build Windows Installer .exe runs-on: windows-latest + env: + TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt steps: - name: Install winget uses: Cyberboss/install-winget@v1 @@ -1231,6 +1335,11 @@ jobs: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }} + - name: Checkout (Branch) uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' @@ -1252,9 +1361,21 @@ jobs: - name: Restore run: dotnet restore + - name: Enable Corepack + run: corepack enable + + - name: Setup Telemetry Key File + shell: bash + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build Host run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj + - name: Delete Telemetry Key File + shell: bash + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build Service run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -1620,6 +1741,8 @@ jobs: needs: [deploy-dm, deploy-http, deployment-gate] runs-on: windows-latest if: github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]') + env: + TGS_TELEMETRY_KEY_FILE: C:/tgs_telemetry_key.txt steps: - name: Setup dotnet uses: actions/setup-dotnet@v4 @@ -1627,6 +1750,11 @@ jobs: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup Node.JS + uses: actions/setup-node@v4 + with: + node-version: ${{ env.TGS_WEBPANEL_NODE_VERSION }} + - name: Checkout uses: actions/checkout@v4 @@ -1638,9 +1766,21 @@ jobs: cd build/package/winget dotnet tool restore - - name: Build Host # We need to rebuild the installer.exe so it can be properly signed + - name: Enable Corepack + run: corepack enable + + - name: Setup Telemetry Key File + shell: bash + run: echo "${{ secrets.TGS_TELEMETRY_KEY }}" > ${{ env.TGS_TELEMETRY_KEY_FILE }} + + - name: Build Host # We need to rebuild the installer.exe so it can be properly signed run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj + - name: Delete Telemetry Key File + shell: bash + if: always() + run: rm ${{ env.TGS_TELEMETRY_KEY_FILE }} + - name: Build Service run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj diff --git a/README.md b/README.md index 58b341d571a..ea51ba7f332 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,12 @@ The following providers use the `ServerUrl` setting: - Keycloak - InvisionCommunity +- `Telemetry:DisableVersionReporting`: Prevents you installation and the version you're using from being reported on the source repository's deployments list + +- `Telemetry:ServerFriendlyName`: Prevents anonymous TGS version usage statistics from being sent to be displayed on the repository. + +- `Telemetry:VersionReportingRepositoryId`: The repository telemetry is sent to. For security reasons, this is not the main TGS repo. See the [tgstation-server-deployments](https://github.com/tgstation/tgstation-server-deployments) repository for more information. + ### Database Configuration If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk. diff --git a/build/Dockerfile b/build/Dockerfile index cba330156e2..b301591833e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,5 +1,8 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build +# Set in CI +ARG TGS_TELEMETRY_KEY_FILE= + # install node and npm # replace shell with bash so we can source files RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.1/install.sh | sh @@ -17,7 +20,8 @@ RUN . $NVM_DIR/nvm.sh \ && apt-get install -y \ dos2unix \ libgdiplus \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && corepack enable # Build web control panel WORKDIR /repo/build @@ -45,7 +49,9 @@ RUN dotnet publish -c Release -o /app \ && build/RemoveUnsupportedRuntimes.sh /app WORKDIR /repo/src/Tgstation.Server.Host -RUN dotnet publish -c Release -o /app/lib/Default \ + +RUN export TGS_TELEMETRY_KEY_FILE="../../${TGS_TELEMETRY_KEY_FILE}" \ + && dotnet publish -c Release -o /app/lib/Default \ && cd ../.. \ && build/RemoveUnsupportedRuntimes.sh /app/lib/Default \ && mv /app/lib/Default/appsettings* /app diff --git a/build/Version.props b/build/Version.props index 4f6a8945320..f524e2ee699 100644 --- a/build/Version.props +++ b/build/Version.props @@ -21,6 +21,5 @@ 11.4.2 - 1.22.21 diff --git a/build/WebpanelVersion.props b/build/WebpanelVersion.props index 806c5ed542d..2613f5510c0 100644 --- a/build/WebpanelVersion.props +++ b/build/WebpanelVersion.props @@ -1,6 +1,6 @@ - 5.9.0 + 6.1.0 diff --git a/build/package/deb/build_package.sh b/build/package/deb/build_package.sh index 1a25546da88..a48071f2193 100755 --- a/build/package/deb/build_package.sh +++ b/build/package/deb/build_package.sh @@ -18,7 +18,7 @@ apt-get install -y \ devscripts \ ca-certificates \ curl \ - gnupg \ + gnupg2 \ xmlstarlet \ libgdiplus @@ -35,6 +35,8 @@ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.co apt-get update apt-get install nodejs dotnet-sdk-8.0 -y +corepack enable + CURRENT_COMMIT=$(git rev-parse HEAD) rm -rf packaging @@ -67,7 +69,6 @@ cp build/tgstation-server.service debian/ SIGN_COMMAND="$SCRIPT_DIR/wrap_gpg.sh" rm -f /tmp/tgs_wrap_gpg_output.log - set +e if [[ -z "$PACKAGING_KEYGRIP" ]]; then diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 4e37e7d008b..9a04c0ac94e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -79,13 +79,13 @@ await databaseContextFactory.UseContext( IGitHubService gitHubService; if (instanceAuthenticated) { - authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); + authenticatedGitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken); gitHubService = authenticatedGitHubService; } else { authenticatedGitHubService = null; - gitHubService = gitHubServiceFactory.CreateService(); + gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); } var repoOwner = remoteInformation.RemoteRepositoryOwner!; @@ -175,8 +175,8 @@ public override async ValueTask> RemoveMergedTest } var gitHubService = repositorySettings.AccessToken != null - ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) - : gitHubServiceFactory.CreateService(); + ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken) + : await gitHubServiceFactory.CreateService(cancellationToken); var tasks = revisionInformation .ActiveTestMerges @@ -255,7 +255,7 @@ protected override async ValueTask CommentOnTestMergeSource( int testMergeNumber, CancellationToken cancellationToken) { - var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); + var gitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken); try { @@ -343,7 +343,7 @@ await databaseContextFactory.UseContext( return; } - var gitHubService = gitHubServiceFactory.CreateService(gitHubAccessToken); + var gitHubService = await gitHubServiceFactory.CreateService(gitHubAccessToken, cancellationToken); try { diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 6109f490bb7..69289bb5d40 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -49,8 +49,8 @@ public GitHubRemoteFeatures(IGitHubServiceFactory gitHubServiceFactory, ILogger< CancellationToken cancellationToken) { var gitHubService = repositorySettings.AccessToken != null - ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) - : gitHubServiceFactory.CreateService(); + ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken) + : await gitHubServiceFactory.CreateService(cancellationToken); PullRequest? pr = null; ApiException? exception = null; diff --git a/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs b/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs new file mode 100644 index 00000000000..7ec0361759d --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/TelemetryConfiguration.cs @@ -0,0 +1,33 @@ +namespace Tgstation.Server.Host.Configuration +{ + /// + /// Configuration options for telemetry. + /// + public sealed class TelemetryConfiguration + { + /// + /// The key for the the resides in. + /// + public const string Section = "Telemetry"; + + /// + /// The default value of . + /// + private const long DefaultVersionReportingRepositoryId = 841149827; // https://github.com/tgstation/tgstation-server-deployments + + /// + /// If version reporting telemetry is disabled. + /// + public bool DisableVersionReporting { get; set; } + + /// + /// The friendly name used on GitHub deployments for version reporting. If only the server will be shown. + /// + public string? ServerFriendlyName { get; set; } + + /// + /// The GitHub repository ID used for version reporting. + /// + public long? VersionReportingRepositoryId { get; set; } = DefaultVersionReportingRepositoryId; + } +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index d4c1335df77..c41113069d2 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -165,7 +165,7 @@ async Task CacheFactory() Uri? repoUrl = null; try { - var gitHubService = gitHubServiceFactory.CreateService(); + var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken); var releases = await gitHubService.GetTgsReleases(cancellationToken); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 36970000c59..7e981066c0d 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -143,6 +143,7 @@ public void ConfigureServices( services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); + services.UseStandardConfig(Configuration); // enable options which give us config reloading services.AddOptions(); @@ -423,6 +424,7 @@ void AddTypedContext() services.AddSingleton(); services.AddSingleton(); services.AddHostedService(); + services.AddHostedService(); services.AddFileDownloader(); services.AddGitHub(); diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index 8c9d9681687..1170537a12d 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -288,7 +288,7 @@ async ValueTask BeginUpdateImpl( { logger.LogDebug("Looking for GitHub releases version {version}...", newVersion); - var gitHubService = gitHubServiceFactory.CreateService(); + var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var releases = await gitHubService.GetTgsReleases(cancellationToken); foreach (var kvp in releases) { diff --git a/src/Tgstation.Server.Host/Core/VersionReportingService.cs b/src/Tgstation.Server.Host/Core/VersionReportingService.cs new file mode 100644 index 00000000000..dd0eef6f786 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/VersionReportingService.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Octokit; + +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Properties; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Handles TGS version reporting, if enabled. + /// + sealed class VersionReportingService : BackgroundService + { + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly TelemetryConfiguration telemetryConfiguration; + + /// + /// The passed to . + /// + CancellationToken shutdownCancellationToken; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The containing the value of . + /// The value of . + public VersionReportingService( + IGitHubClientFactory gitHubClientFactory, + IIOManager ioManager, + IAsyncDelayer asyncDelayer, + IAssemblyInformationProvider assemblyInformationProvider, + IOptions telemetryConfigurationOptions, + ILogger logger) + { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + telemetryConfiguration = telemetryConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(telemetryConfigurationOptions)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public override Task StopAsync(CancellationToken cancellationToken) + { + shutdownCancellationToken = cancellationToken; + return base.StopAsync(cancellationToken); + } + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (telemetryConfiguration.DisableVersionReporting) + { + logger.LogDebug("Version telemetry disabled"); + return; + } + + if (!telemetryConfiguration.VersionReportingRepositoryId.HasValue) + { + logger.LogError("Version reporting repository is misconfigured. Telemetry cannot be sent!"); + return; + } + + var attribute = TelemetryAppSerializedKeyAttribute.Instance; + if (attribute == null) + { + logger.LogDebug("TGS build configuration does not allow for version telemetry"); + return; + } + + logger.LogDebug("Starting..."); + + try + { + var telemetryIdFile = ioManager.ResolvePath( + ioManager.ConcatPath( + ioManager.GetPathInLocalDirectory(assemblyInformationProvider), + "telemetry.id")); + + Guid telemetryId; + if (!await ioManager.FileExists(telemetryIdFile, stoppingToken)) + { + telemetryId = Guid.NewGuid(); + await ioManager.WriteAllBytes(telemetryIdFile, Encoding.UTF8.GetBytes(telemetryId.ToString()), stoppingToken); + logger.LogInformation("Generated telemetry ID {telemetryId} and wrote to {file}", telemetryId, telemetryIdFile); + } + else + { + var contents = await ioManager.ReadAllBytes(telemetryIdFile, stoppingToken); + + string guidStr; + try + { + guidStr = Encoding.UTF8.GetString(contents); + } + catch (Exception ex) + { + logger.LogError(ex, "Cannot decode telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile); + return; + } + + if (!Guid.TryParse(guidStr, out telemetryId)) + { + logger.LogError("Cannot parse telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile); + return; + } + } + + try + { + while (!stoppingToken.IsCancellationRequested) + { + var nextDelayHours = await TryReportVersion( + telemetryId, + attribute.SerializedKey, + telemetryConfiguration.VersionReportingRepositoryId.Value, + false, + stoppingToken) + ? 24 + : 1; + + logger.LogDebug("Next version report in {hours} hours", nextDelayHours); + await asyncDelayer.Delay(TimeSpan.FromHours(nextDelayHours), stoppingToken); + } + } + catch (OperationCanceledException ex) + { + logger.LogTrace(ex, "Inner cancellation"); + } + + shutdownCancellationToken.ThrowIfCancellationRequested(); + + logger.LogDebug("Sending shutdown telemetry"); + await TryReportVersion( + telemetryId, + attribute.SerializedKey, + telemetryConfiguration.VersionReportingRepositoryId.Value, + true, + shutdownCancellationToken); + } + catch (OperationCanceledException ex) + { + logger.LogTrace(ex, "Exiting due to outer cancellation..."); + } + catch (Exception ex) + { + logger.LogError(ex, "Crashed!"); + } + } + + /// + /// Make an attempt to report the current to the configured GitHub repository. + /// + /// The telemetry for the installation. + /// The serialized authentication for the . + /// The ID of the repository to send telemetry to. + /// If this is shutdown telemetry. + /// The for the operation. + /// A resulting in if telemetry was reported successfully, otherwise. + async ValueTask TryReportVersion(Guid telemetryId, string serializedPem, long repositoryId, bool shutdown, CancellationToken cancellationToken) + { + logger.LogDebug("Sending version telemetry..."); + + var serverFriendlyName = telemetryConfiguration.ServerFriendlyName; + if (String.IsNullOrWhiteSpace(serverFriendlyName)) + serverFriendlyName = null; + + logger.LogTrace( + "Repository ID: {repoId}, Server friendly name: {friendlyName}", + repositoryId, + serverFriendlyName == null + ? "(null)" + : $"\"{serverFriendlyName}\""); + try + { + var gitHubClient = await gitHubClientFactory.CreateInstallationClient( + serializedPem, + repositoryId, + cancellationToken); + + if (gitHubClient == null) + { + logger.LogWarning("Could not create GitHub client to connect to repository ID {repoId}!", repositoryId); + return false; + } + + // remove this lookup once https://github.com/octokit/octokit.net/pull/2960 is merged and released + var repository = await gitHubClient.Repository.Get(repositoryId); + + logger.LogTrace("Repository ID {id} resolved to {owner}/{name}", repositoryId, repository.Owner.Name, repository.Name); + + var inputs = new Dictionary + { + { "telemetry_id", telemetryId.ToString() }, + { "tgs_semver", assemblyInformationProvider.Version.Semver().ToString() }, + { "shutdown", shutdown ? "true" : "false" }, + }; + + if (serverFriendlyName != null) + inputs.Add("server_friendly_name", serverFriendlyName); + + await gitHubClient.Actions.Workflows.CreateDispatch( + repository.Owner.Login, + repository.Name, + ".github/workflows/tgs_deployments_telemetry.yml", + new CreateWorkflowDispatch("main") + { + Inputs = inputs, + }); + + logger.LogTrace("Telemetry sent successfully"); + + return true; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to report version!"); + return false; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs b/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs new file mode 100644 index 00000000000..6d2ff1be5b8 --- /dev/null +++ b/src/Tgstation.Server.Host/Properties/TelemetryAppSerializedKeyAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.Reflection; + +namespace Tgstation.Server.Host.Properties +{ + /// + /// Attribute for bundling the GitHub App serialized private key used for version telemetry. + /// + [AttributeUsage(AttributeTargets.Assembly)] + sealed class TelemetryAppSerializedKeyAttribute : Attribute + { + /// + /// Return the 's instance of the . + /// + public static TelemetryAppSerializedKeyAttribute? Instance => Assembly + .GetExecutingAssembly() + .GetCustomAttribute(); + + /// + /// The serialized GitHub App Client ID and private key. + /// + public string SerializedKey { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public TelemetryAppSerializedKeyAttribute(string serializedKey) + { + SerializedKey = serializedKey ?? throw new ArgumentNullException(nameof(serializedKey)); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index 3f3180f9da2..d535d978843 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -60,12 +60,12 @@ public GitHubOAuthValidator( { logger.LogTrace("Validating response code..."); - var gitHubService = gitHubServiceFactory.CreateService(); + var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken); var token = await gitHubService.CreateOAuthAccessToken(oAuthConfiguration, code, cancellationToken); if (token == null) return null; - var authenticatedClient = gitHubServiceFactory.CreateService(token); + var authenticatedClient = await gitHubServiceFactory.CreateService(token, cancellationToken); logger.LogTrace("Getting user details..."); var userId = await authenticatedClient.GetCurrentUserId(cancellationToken); diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 5c77ecffebc..dacc6734107 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -950,6 +950,31 @@ async ValueTask ParseAddress(string question) }; } + /// + /// Prompts the user to create a . + /// + /// The for the operation. + /// A resulting in the new . + async ValueTask ConfigureTelemetry(CancellationToken cancellationToken) + { + bool enableReporting = await PromptYesNo("Enable version telemetry? This anonymously reports the TGS version in use.", true, cancellationToken); + + string? serverFriendlyName = null; + if (enableReporting) + { + await console.WriteAsync("(Optional) Publically associate your reported version with a friendly name:", false, cancellationToken); + serverFriendlyName = await console.ReadLineAsync(false, cancellationToken); + if (String.IsNullOrWhiteSpace(serverFriendlyName)) + serverFriendlyName = null; + } + + return new TelemetryConfiguration + { + DisableVersionReporting = !enableReporting, + ServerFriendlyName = serverFriendlyName, + }; + } + /// /// Saves a given set to . /// @@ -961,6 +986,7 @@ async ValueTask ParseAddress(string question) /// The to save. /// The to save. /// The to save. + /// The to save. /// The for the operation. /// A representing the running operation. async ValueTask SaveConfiguration( @@ -972,6 +998,7 @@ async ValueTask SaveConfiguration( ElasticsearchConfiguration? elasticsearchConfiguration, ControlPanelConfiguration controlPanelConfiguration, SwarmConfiguration? swarmConfiguration, + TelemetryConfiguration? telemetryConfiguration, CancellationToken cancellationToken) { newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort; @@ -984,6 +1011,7 @@ async ValueTask SaveConfiguration( { ElasticsearchConfiguration.Section, elasticsearchConfiguration }, { ControlPanelConfiguration.Section, controlPanelConfiguration }, { SwarmConfiguration.Section, swarmConfiguration }, + { TelemetryConfiguration.Section, telemetryConfiguration }, }; var versionConverter = new VersionConverter(); @@ -1055,6 +1083,8 @@ async ValueTask RunWizard(string userConfigFileName, CancellationToken cancellat var swarmConfiguration = await ConfigureSwarm(cancellationToken); + var telemetryConfiguration = await ConfigureTelemetry(cancellationToken); + await console.WriteAsync(null, true, cancellationToken); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken); @@ -1067,6 +1097,7 @@ await SaveConfiguration( elasticSearchConfiguration, controlPanelConfiguration, swarmConfiguration, + telemetryConfiguration, cancellationToken); } @@ -1184,6 +1215,7 @@ await SaveConfiguration( AllowAnyOrigin = true, }, null, + null, cancellationToken); } else diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 7e29d1943bd..1c2937a5b21 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -29,13 +29,13 @@ - + - + @@ -45,21 +45,43 @@ - + <_Parameter1>$(TgsConfigVersion) <_Parameter2>$(TgsInteropVersion) <_Parameter3>$(TgsWebpanelVersion) <_Parameter4>$(TgsHostWatchdogVersion) <_Parameter5>$(TgsMariaDBRedistVersion) - + + + + + + - + + + + + + + + <_Parameter1>@(SerializedTelemetryKey) + + + + + + + diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs index e479388b79b..4cf31a5d289 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs @@ -1,9 +1,16 @@ using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + using Octokit; using Tgstation.Server.Host.Configuration; @@ -12,7 +19,7 @@ namespace Tgstation.Server.Host.Utils.GitHub { /// - sealed class GitHubClientFactory : IGitHubClientFactory + sealed class GitHubClientFactory : IGitHubClientFactory, IDisposable { /// /// Limit to the amount of days a can live in the . @@ -45,6 +52,11 @@ sealed class GitHubClientFactory : IGitHubClientFactory /// readonly Dictionary clientCache; + /// + /// The used to guard access to . + /// + readonly SemaphoreSlim clientCacheSemaphore; + /// /// Initializes a new instance of the class. /// @@ -61,50 +73,139 @@ public GitHubClientFactory( generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); clientCache = new Dictionary(); + clientCacheSemaphore = new SemaphoreSlim(1, 1); } /// - public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken); + public void Dispose() => clientCacheSemaphore.Dispose(); + + /// + public async ValueTask CreateClient(CancellationToken cancellationToken) + => (await GetOrCreateClient( + generalConfiguration.GitHubAccessToken, + null, + cancellationToken))!; /// - public IGitHubClient CreateClient(string accessToken) - => GetOrCreateClient( - accessToken ?? throw new ArgumentNullException(nameof(accessToken))); + public async ValueTask CreateClient(string accessToken, CancellationToken cancellationToken) + => (await GetOrCreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken)), + null, + cancellationToken))!; + + /// + public ValueTask CreateInstallationClient(string serializedPem, long repositoryId, CancellationToken cancellationToken) + => GetOrCreateClient(serializedPem, repositoryId, cancellationToken); /// - /// Retrieve a from the or add a new one based on a given . + /// Retrieve a from the or add a new one based on a given . /// - /// Optional access token to use as credentials. - /// The for the given . - GitHubClient GetOrCreateClient(string? accessToken) + /// Optional access token to use as credentials or GitHub App private key. If using a private key, must be set. + /// Setting this specifies is a private key and a GitHub App installation authenticated client will be returned. + /// The for the operation. + /// A resulting in the for the given or if authentication failed. +#pragma warning disable CA1506 // TODO: Decomplexify + async ValueTask GetOrCreateClient(string? accessTokenOrSerializedPem, long? installationRepositoryId, CancellationToken cancellationToken) +#pragma warning restore CA1506 { GitHubClient client; bool cacheHit; DateTimeOffset? lastUsed; - lock (clientCache) + using (await SemaphoreSlimContext.Lock(clientCacheSemaphore, cancellationToken)) { string cacheKey; - if (String.IsNullOrWhiteSpace(accessToken)) + if (String.IsNullOrWhiteSpace(accessTokenOrSerializedPem)) { - accessToken = null; + accessTokenOrSerializedPem = null; cacheKey = DefaultCacheKey; } else - cacheKey = accessToken; + cacheKey = accessTokenOrSerializedPem; cacheHit = clientCache.TryGetValue(cacheKey, out var tuple); var now = DateTimeOffset.UtcNow; if (!cacheHit) { + logger.LogTrace("Creating new GitHubClient..."); var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!; client = new GitHubClient( new ProductHeaderValue( product.Name, product.Version)); - if (accessToken != null) - client.Credentials = new Credentials(accessToken); + if (accessTokenOrSerializedPem != null) + { + if (installationRepositoryId.HasValue) + { + logger.LogTrace("Performing GitHub App authentication for installation on repository {installationRepositoryId}", installationRepositoryId.Value); + var splits = accessTokenOrSerializedPem.Split(':'); + if (splits.Length != 2) + { + logger.LogError("Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length); + return null; + } + + byte[] pemBytes; + try + { + pemBytes = Convert.FromBase64String(splits[1]); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to parse supposed base64 PEM!"); + return null; + } + + var pem = Encoding.UTF8.GetString(pemBytes); + + using var rsa = RSA.Create(); + rsa.ImportFromPem(pem); + + var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256); + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = false }; + + var nowDateTime = DateTime.UtcNow; + + var jwt = jwtSecurityTokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = splits[0], + Expires = nowDateTime.AddMinutes(10), + IssuedAt = nowDateTime, + SigningCredentials = signingCredentials, + }); + + var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt); + + client.Credentials = new Credentials(jwtStr, AuthenticationType.Bearer); + + Installation installation; + try + { + installation = await client.GitHubApps.GetRepositoryInstallationForCurrent(installationRepositoryId.Value); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to perform app authentication!"); + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + try + { + var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id); + + client.Credentials = new Credentials(installToken.Token); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to perform installation authentication!"); + return null; + } + } + else + client.Credentials = new Credentials(accessTokenOrSerializedPem); + } clientCache.Add(cacheKey, (Client: client, LastUsed: now)); lastUsed = null; diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs index efa08d13bba..a38b84b271d 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -44,13 +46,16 @@ public GitHubServiceFactory( } /// - public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient()); + public async ValueTask CreateService(CancellationToken cancellationToken) + => CreateServiceImpl( + await gitHubClientFactory.CreateClient(cancellationToken)); /// - public IAuthenticatedGitHubService CreateService(string accessToken) + public async ValueTask CreateService(string accessToken, CancellationToken cancellationToken) => CreateServiceImpl( - gitHubClientFactory.CreateClient( - accessToken ?? throw new ArgumentNullException(nameof(accessToken)))); + await gitHubClientFactory.CreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken)), + cancellationToken)); /// /// Create a . diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs index ec95fd16ab8..1808b4ca3c3 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs @@ -1,4 +1,7 @@ -using Octokit; +using System.Threading; +using System.Threading.Tasks; + +using Octokit; namespace Tgstation.Server.Host.Utils.GitHub { @@ -10,14 +13,25 @@ public interface IGitHubClientFactory /// /// Create a client. Low rate limit unless the server's GitHubAccessToken is set to bypass it. /// + /// The for the operation. /// A new . - IGitHubClient CreateClient(); + ValueTask CreateClient(CancellationToken cancellationToken); /// /// Create a client with authentication using a personal access token. /// /// The GitHub personal access token. + /// The for the operation. /// A new . - IGitHubClient CreateClient(string accessToken); + ValueTask CreateClient(string accessToken, CancellationToken cancellationToken); + + /// + /// Creates a GitHub App client for an installation. + /// + /// The private key . + /// The GitHub repository ID. + /// The for the operation. + /// A resulting in a new for the given or if authentication failed. + ValueTask CreateInstallationClient(string pem, long repositoryId, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs index f4e7be70f55..adb0ec88ebd 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs @@ -1,4 +1,7 @@ -namespace Tgstation.Server.Host.Utils.GitHub +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Utils.GitHub { /// /// Factory for s. @@ -8,14 +11,16 @@ public interface IGitHubServiceFactory /// /// Create a . /// - /// A new . - public IGitHubService CreateService(); + /// The for the operation. + /// A resulting in a new . + public ValueTask CreateService(CancellationToken cancellationToken); /// /// Create an . /// /// The access token to use for communication with GitHub. - /// A new . - public IAuthenticatedGitHubService CreateService(string accessToken); + /// The for the operation. + /// A resulting in a new . + public ValueTask CreateService(string accessToken, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index 732a61e54b2..353104e6de7 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -78,3 +78,7 @@ Swarm: # Should be left empty if using swarm mode is not desired # PublicAddress: # The public address of the swarm node # ControllerAddress: # Required on non-controller nodes. The internal address of the swarm controller's API'. Should be left empty on the controller itself # UpdateRequiredNodeCount: # The number of nodes expected to be in the swarm before initiating an update. This should count every server irrespective of whether or not they are the controller MINUS 1 +Telemetry: + DisableVersionReporting: false # Prevents you installation and the version you're using from being reported on the source repository's deployments list + ServerFriendlyName: null # Sets a friendly name for your server in reported telemetry. Must be unique. First come first serve + VersionReportingRepositoryId: 841149827 # GitHub repostiory ID where the tgs_version_telemetry workflow can be found diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs index 823036a10b3..cd6e5c86a02 100644 --- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs +++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs @@ -191,6 +191,8 @@ public async Task TestWithUserStupidity() "y", // swarm config "n", + // telemetry config + "n", //saved, now for second run //this time use defaults amap String.Empty, @@ -230,6 +232,9 @@ public async Task TestWithUserStupidity() "privatekey", "n", "http://controller.com", + // telemetry config + "y", + "telemetry name", //third run, we already hit all the code coverage so just get through it String.Empty, nameof(DatabaseType.MariaDB), @@ -267,7 +272,9 @@ public async Task TestWithUserStupidity() "https://controllerinternal.com", "https://controllerpublic.com", "privatekey", - "y" + "y", + // telemetry config + "n", }; var inputPos = 0; diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs index 6f4fac0ae26..33058be75f4 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -57,14 +58,14 @@ public async Task TestCreateBasicClient() mockOptions.SetupGet(x => x.Value).Returns(gc); var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object); - var client = factory.CreateClient(); + var client = await factory.CreateClient(CancellationToken.None); Assert.IsNotNull(client); var credentials = await client.Connection.CredentialStore.GetCredentials(); Assert.AreEqual(AuthenticationType.Anonymous, credentials.AuthenticationType); gc.GitHubAccessToken = "asdfasdfasdfasdfasdfasdf"; - client = factory.CreateClient(); + client = await factory.CreateClient(CancellationToken.None); Assert.IsNotNull(client); credentials = await client.Connection.CredentialStore.GetCredentials(); @@ -83,9 +84,9 @@ public async Task TestCreateTokenClient() mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object); - Assert.ThrowsException(() => factory.CreateClient(null)); + await Assert.ThrowsExceptionAsync(() => factory.CreateClient(null, CancellationToken.None).AsTask()); - var client = factory.CreateClient("asdf"); + var client = await factory.CreateClient("asdf", CancellationToken.None); Assert.IsNotNull(client); var credentials = await client.Connection.CredentialStore.GetCredentials(); @@ -96,7 +97,7 @@ public async Task TestCreateTokenClient() } [TestMethod] - public void TestClientCaching() + public async Task TestClientCaching() { var mockApp = new Mock(); mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable(); @@ -105,10 +106,10 @@ public void TestClientCaching() mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger(), mockOptions.Object); - var client1 = factory.CreateClient(); - var client2 = factory.CreateClient("asdf"); - var client3 = factory.CreateClient(); - var client4 = factory.CreateClient("asdf"); + var client1 = await factory.CreateClient(CancellationToken.None); + var client2 = await factory.CreateClient("asdf", CancellationToken.None); + var client3 = await factory.CreateClient(CancellationToken.None); + var client4 = await factory.CreateClient("asdf", CancellationToken.None); Assert.ReferenceEquals(client1, client3); Assert.ReferenceEquals(client2, client4); } diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs index daa9afdd4c2..24212eaa936 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -28,27 +30,29 @@ public void TestConstructor() } [TestMethod] - public void TestCreateService() + public async Task TestCreateService() { var mockFactory = new Mock(); - mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of()).Verifiable(); +#pragma warning disable CA2012 // Use ValueTasks correctly + mockFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable(); var mockToken = "asdf"; - mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of()).Verifiable(); + mockFactory.Setup(x => x.CreateClient(mockToken, It.IsAny())).Returns(ValueTask.FromResult(Mock.Of())).Verifiable(); +#pragma warning restore CA2012 // Use ValueTasks correctly var mockOptions = new Mock>(); mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of(), mockOptions.Object); - Assert.ThrowsException(() => factory.CreateService(null)); + await Assert.ThrowsExceptionAsync(() => factory.CreateService(null, CancellationToken.None).AsTask()); Assert.AreEqual(0, mockFactory.Invocations.Count); - var result1 = factory.CreateService(); + var result1 = await factory.CreateService(CancellationToken.None); Assert.IsNotNull(result1); - var result2 = factory.CreateService(mockToken); + var result2 = factory.CreateService(mockToken, CancellationToken.None); Assert.IsNotNull(result2); mockFactory.VerifyAll(); diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs index 2f657577820..d05faed9a65 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -18,13 +20,13 @@ public DummyGitHubServiceFactory(ICryptographySuite cryptographySuite, ILogger CreateDummyService(); + public ValueTask CreateService(CancellationToken cancellationToken) => ValueTask.FromResult(CreateDummyService()); - public IAuthenticatedGitHubService CreateService(string accessToken) + public ValueTask CreateService(string accessToken, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(accessToken); - return CreateDummyService(); + return ValueTask.FromResult(CreateDummyService()); } TestingGitHubService CreateDummyService() => new TestingGitHubService(cryptographySuite, logger); diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index 645edac147d..f9ed0b926ab 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -155,6 +155,7 @@ public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth $"General:OpenDreamGitUrl={OpenDreamUrl}", $"Security:TokenExpiryMinutes=120", // timeouts are useless for us $"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}", + "Telemetry:DisableVersionReporting=true", }; swarmArgs = new List(); diff --git a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs index d104dbd6d3a..585dd569997 100644 --- a/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs +++ b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs @@ -39,7 +39,7 @@ static TestingGitHubService() }); var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), Mock.Of>(), mockOptions.Object); - RealClient = gitHubClientFactory.CreateClient(); + RealClient = gitHubClientFactory.CreateClient(CancellationToken.None).GetAwaiter().GetResult(); } public static async Task InitializeAndInject(CancellationToken cancellationToken)