diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9a03a40d5..c32b7d2c6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -169,7 +169,7 @@ jobs: fail-fast: false matrix: runner-os: [windows-latest, ubuntu-latest, macos-latest] - source-vcs: [AdoBasic, AdoCsv, Bbs, Ghes, Github, Gitlab] + source-vcs: [AdoBasic, AdoCsv, Bbs, Ghes, Github, Gitlab, GithubDR] runs-on: ${{ matrix.runner-os }} concurrency: integration-test-${{ matrix.source-vcs }}-${{ matrix.runner-os }} steps: @@ -284,6 +284,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }} GITLAB_PAT: ${{ secrets.GITLAB_PAT }} + E2E_SOURCE_PROXIMA_PAT: ${{ secrets.E2E_SOURCE_PROXIMA_PAT }} LD_LIBRARY_PATH: "$LD_LIBRARY_PATH:${{ github.workspace }}/src/OctoshiftCLI.IntegrationTests/bin/Debug/net8.0/runtimes/ubuntu.18.04-x64/native" run: dotnet test src/OctoshiftCLI.IntegrationTests/OctoshiftCLI.IntegrationTests.csproj --filter "${{ matrix.source-vcs }}ToGithub" --logger:"junit;LogFilePath=integration-tests.xml" --logger "console;verbosity=normal" /p:VersionPrefix=9.9 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 64308fda4..465ac7b47 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -94,7 +94,7 @@ jobs: fail-fast: false matrix: runner-os: [windows-latest, ubuntu-latest, macos-latest] - source-vcs: [AdoBasic, AdoCsv, Bbs, Ghes, Github, Gitlab] + source-vcs: [AdoBasic, AdoCsv, Bbs, Ghes, Github, Gitlab, GithubDR] runs-on: ${{ matrix.runner-os }} concurrency: integration-test-${{ matrix.source-vcs }}-${{ matrix.runner-os }} steps: @@ -217,6 +217,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }} GITLAB_PAT: ${{ secrets.GITLAB_PAT }} + E2E_SOURCE_PROXIMA_PAT: ${{ secrets.E2E_SOURCE_PROXIMA_PAT }} GEI_DEBUG_MODE: 'true' LD_LIBRARY_PATH: '$LD_LIBRARY_PATH:${{ github.workspace }}/src/OctoshiftCLI.IntegrationTests/bin/Debug/net8.0/runtimes/ubuntu.18.04-x64/native' run: dotnet test src/OctoshiftCLI.IntegrationTests/OctoshiftCLI.IntegrationTests.csproj --filter "${{ matrix.source-vcs }}ToGithub" --logger:"junit;LogFilePath=integration-tests.xml" --logger "console;verbosity=normal" /p:VersionPrefix=9.9 diff --git a/README.md b/README.md index 4aed61e42..fe0dfe7cf 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ To see the available commands and options run: Refer to the [official documentation](https://docs.github.com/en/migrations/using-github-enterprise-importer) for more details, including differences when migrating from GitHub Enterprise Server. +#### Migrating from GitHub Enterprise Cloud with data residency (ghe.com) + +`gh gei` also supports migrating from a GitHub Enterprise Cloud with data residency tenant. Pass `--github-source-api-url` with the API endpoint of your source tenant (or set the `GH_SOURCE_API_URL` environment variable): + +>`gh gei migrate-repo --github-source-org SOURCE_ORG --source-repo SOURCE_REPO --github-source-api-url https://api.SUBDOMAIN.ghe.com --github-target-org TARGET_ORG --target-repo TARGET_REPO` + +If the target is also a data residency tenant, add `--target-api-url` and `--target-uploads-url` as usual. `--github-source-api-url` and `--ghes-api-url` cannot be used together. + ### Azure DevOps to GitHub Usage 1. Create Personal Access Tokens with access to the Azure DevOps org, and the GitHub org (for more details on scopes needed refer to our [official documentation](https://docs.github.com/en/migrations/using-github-enterprise-importer/preparing-to-migrate-with-github-enterprise-importer/managing-access-for-github-enterprise-importer)). diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b1378917..d287078bb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1 +1 @@ - +- Added support for migrating from GitHub Enterprise Cloud with data residency (ghe.com) as a source in `gh gei migrate-repo`, `generate-script`, `migrate-secret-alerts`, and `migrate-code-scanning-alerts`. Configure via `--github-source-api-url` (e.g. `https://api.tenant.ghe.com`) or the `GH_SOURCE_API_URL` environment variable. `--github-source-api-url` and `--ghes-api-url` cannot be used together. diff --git a/src/Octoshift/Extensions/StringExtensions.cs b/src/Octoshift/Extensions/StringExtensions.cs index b2beb5870..118a04147 100644 --- a/src/Octoshift/Extensions/StringExtensions.cs +++ b/src/Octoshift/Extensions/StringExtensions.cs @@ -33,5 +33,8 @@ public static bool IsUrl(this string s) && Uri.TryCreate(s, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); } + + public static bool IsProximaApiUrl(this string s) => + !s.IsNullOrWhiteSpace() && Regex.IsMatch(s.Trim(), @"^https?://api\.[^/]+\.ghe\.com/?$", RegexOptions.IgnoreCase); } } diff --git a/src/Octoshift/Services/EnvironmentVariableProvider.cs b/src/Octoshift/Services/EnvironmentVariableProvider.cs index 878d92734..9b2bed26b 100644 --- a/src/Octoshift/Services/EnvironmentVariableProvider.cs +++ b/src/Octoshift/Services/EnvironmentVariableProvider.cs @@ -7,6 +7,7 @@ public class EnvironmentVariableProvider { private const string SOURCE_GH_PAT = "GH_SOURCE_PAT"; private const string TARGET_GH_PAT = "GH_PAT"; + private const string SOURCE_GH_API_URL = "GH_SOURCE_API_URL"; private const string ADO_PAT = "ADO_PAT"; private const string AZURE_STORAGE_CONNECTION_STRING = "AZURE_STORAGE_CONNECTION_STRING"; private const string AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; @@ -31,6 +32,8 @@ public EnvironmentVariableProvider(OctoLogger logger) public virtual string SourceGithubPersonalAccessToken(bool throwIfNotFound = true) => GetSecret(SOURCE_GH_PAT, false) ?? TargetGithubPersonalAccessToken(throwIfNotFound); + public virtual string SourceGithubApiUrl() => GetValue(SOURCE_GH_API_URL, false); + public virtual string TargetGithubPersonalAccessToken(bool throwIfNotFound = true) => GetSecret(TARGET_GH_PAT, throwIfNotFound); diff --git a/src/OctoshiftCLI.IntegrationTests/GithubDRToGithub.cs b/src/OctoshiftCLI.IntegrationTests/GithubDRToGithub.cs new file mode 100644 index 000000000..105fe94ec --- /dev/null +++ b/src/OctoshiftCLI.IntegrationTests/GithubDRToGithub.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using OctoshiftCLI.Services; +using Xunit; +using Xunit.Abstractions; + +namespace OctoshiftCLI.IntegrationTests; + +// Integration test for GitHub Enterprise Cloud with data residency (GithubDR) source migrations. +// Requires E2E_SOURCE_PROXIMA_PAT and GHEC_PAT secrets to be set. The source repo is treated as +// read-only (tenant-managed); only the target org is reset each run. +[Collection("Integration Tests")] +public sealed class GithubDRToGithub : IDisposable +{ + private const string GITHUBDR_API_URL = "https://api.migration-tools-staffwus201.ghe.com"; + private const string GITHUBDR_SOURCE_ORG = "octoshift"; + private const string GITHUBDR_SOURCE_REPO = "tiny"; + private const string UPLOADS_URL = "https://uploads.github.com"; + + private readonly ITestOutputHelper _output; + private readonly TestHelper _targetHelper; + private readonly HttpClient _versionClient; + private readonly HttpClient _targetGithubHttpClient; + private readonly GithubClient _targetGithubClient; + private readonly GithubApi _targetGithubApi; + private readonly HttpClient _sourceGithubHttpClient; + private readonly GithubClient _sourceGithubClient; + private readonly GithubApi _sourceGithubApi; + private readonly ArchiveUploader _archiveUploader; + private readonly Dictionary _tokens; + private readonly DateTime _startTime; + + public GithubDRToGithub(ITestOutputHelper output) + { + _startTime = DateTime.Now; + _output = output; + + TestHelper.AssertCredentialsPresent( + ("E2E_SOURCE_PROXIMA_PAT", "GitHub Enterprise Cloud with data residency (source) personal access token"), + ("GHEC_PAT", "GitHub Enterprise Cloud (target) personal access token")); + + var logger = new OctoLogger(_ => { }, x => _output.WriteLine(x), _ => { }, _ => { }); + + var sourceGithubToken = Environment.GetEnvironmentVariable("E2E_SOURCE_PROXIMA_PAT"); + var targetGithubToken = Environment.GetEnvironmentVariable("GHEC_PAT"); + + _tokens = new Dictionary + { + ["GH_SOURCE_PAT"] = sourceGithubToken, + ["GH_PAT"] = targetGithubToken, + }; + + _versionClient = new HttpClient(); + var retryPolicy = new RetryPolicy(logger, "GithubDR (E2E_SOURCE_PROXIMA_PAT)"); + var environmentVariableProvider = new EnvironmentVariableProvider(logger); + + _sourceGithubHttpClient = new HttpClient(); + _sourceGithubClient = new GithubClient(logger, _sourceGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger, "GithubDR (E2E_SOURCE_PROXIMA_PAT)"), new DateTimeProvider(), sourceGithubToken); + _archiveUploader = new ArchiveUploader(_targetGithubClient, UPLOADS_URL, logger, retryPolicy, environmentVariableProvider); + _sourceGithubApi = new GithubApi(_sourceGithubClient, GITHUBDR_API_URL, new RetryPolicy(logger, "GithubDR (E2E_SOURCE_PROXIMA_PAT)"), _archiveUploader); + + _targetGithubHttpClient = new HttpClient(); + _targetGithubClient = new GithubClient(logger, _targetGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger, "GitHub (GHEC_PAT)"), new DateTimeProvider(), targetGithubToken); + _targetGithubApi = new GithubApi(_targetGithubClient, "https://api.github.com", new RetryPolicy(logger, "GitHub (GHEC_PAT)"), _archiveUploader); + + _targetHelper = new TestHelper(_output, _targetGithubApi, _targetGithubClient); + } + + [Fact] + public async Task Basic() + { + var githubTargetOrg = $"octoshift-e2e-githubdr-{TestHelper.GetOsName()}"; + + var retryPolicy = new RetryPolicy(null); + + // Source repo is tenant-managed (read-only); only reset the target. + await retryPolicy.Retry(async () => await _targetHelper.ResetGithubTestEnvironment(githubTargetOrg)); + + var command = $"gei migrate-repo --github-source-org {GITHUBDR_SOURCE_ORG} --source-repo {GITHUBDR_SOURCE_REPO} --github-source-api-url {GITHUBDR_API_URL} --github-target-org {githubTargetOrg} --target-repo {GITHUBDR_SOURCE_REPO} --target-repo-visibility private --use-github-storage"; + + await _targetHelper.RunCliCommand(command, "gh", _tokens); + + _targetHelper.AssertNoErrorInLogs(_startTime); + + await _targetHelper.AssertGithubRepoExists(githubTargetOrg, GITHUBDR_SOURCE_REPO); + await _targetHelper.AssertGithubRepoInitialized(githubTargetOrg, GITHUBDR_SOURCE_REPO); + } + + public void Dispose() + { + _sourceGithubHttpClient?.Dispose(); + _targetGithubHttpClient?.Dispose(); + _versionClient?.Dispose(); + } +} diff --git a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs index 12e3207af..886ffd6e7 100644 --- a/src/OctoshiftCLI.Tests/StringExtensionsTests.cs +++ b/src/OctoshiftCLI.Tests/StringExtensionsTests.cs @@ -39,5 +39,27 @@ public void IsUrl_Detects_URLs_Correctly(string value, bool expectedResult) result.Should().Be(expectedResult); } + + [Theory] + [InlineData("https://api.tenant.ghe.com", true)] + [InlineData("http://api.tenant.ghe.com", true)] + [InlineData("https://api.TENANT.ghe.com", true)] + [InlineData("https://api.tenant.ghe.com/", true)] + [InlineData(" https://api.tenant.ghe.com ", true)] + [InlineData("https://api.foo.bar.ghe.com", true)] + [InlineData("https://tenant.ghe.com", false)] + [InlineData("https://api.github.com", false)] + [InlineData("https://api.tenant.ghe.com/foo", false)] + [InlineData("https://ghes.contoso.com/api/v3", false)] + [InlineData("api.tenant.ghe.com", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData(" ", false)] + public void IsProximaApiUrl_Detects_Proxima_Api_URLs_Correctly(string value, bool expectedResult) + { + var result = value.IsProximaApiUrl(); + + result.Should().Be(expectedResult); + } } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandHandlerTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandHandlerTests.cs index a34666231..fa80ba05a 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandHandlerTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandHandlerTests.cs @@ -244,6 +244,36 @@ public async Task Sequential_Github_Ghes_Repo() _script.Should().Be(expected); } + [Fact] + public async Task Sequential_Github_Proxima_Source_Emits_GithubSourceApiUrl_Flag() + { + // Arrange + const string proximaApiUrl = "https://api.tenant.ghe.com"; + + _mockGithubApi + .Setup(m => m.GetRepos(SOURCE_ORG)) + .ReturnsAsync(new[] { (REPO, "private") }); + + var expected = $"Exec {{ gh gei migrate-repo --github-source-org \"{SOURCE_ORG}\" --source-repo \"{REPO}\" --github-target-org \"{TARGET_ORG}\" --target-repo \"{REPO}\" --github-source-api-url \"{proximaApiUrl}\" --target-repo-visibility private }}"; + + // Act + var args = new GenerateScriptCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + GithubTargetOrg = TARGET_ORG, + Output = new FileInfo("unit-test-output"), + GithubSourceApiUrl = proximaApiUrl, + NoSslVerify = true, // should be ignored for Proxima + Sequential = true + }; + await _handler.Handle(args); + + _script = TrimNonExecutableLines(_script); + + // Assert + _script.Should().Be(expected); + } + [Fact] public async Task Parallel_Github_Multiple_Repos() { diff --git a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandTests.cs index dd8a482f4..0c80bc0dd 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/GenerateScript/GenerateScriptCommandTests.cs @@ -39,11 +39,12 @@ public void Should_Have_Options() var command = new GenerateScriptCommand(); command.Should().NotBeNull(); command.Name.Should().Be("generate-script"); - command.Options.Count.Should().Be(17); + command.Options.Count.Should().Be(18); TestHelpers.VerifyCommandOption(command.Options, "github-source-org", true); TestHelpers.VerifyCommandOption(command.Options, "github-target-org", true); TestHelpers.VerifyCommandOption(command.Options, "ghes-api-url", false); + TestHelpers.VerifyCommandOption(command.Options, "github-source-api-url", false); TestHelpers.VerifyCommandOption(command.Options, "no-ssl-verify", false); TestHelpers.VerifyCommandOption(command.Options, "skip-releases", false); TestHelpers.VerifyCommandOption(command.Options, "lock-source-repo", false); diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandTests.cs index e4e036733..df025d51f 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandTests.cs @@ -33,7 +33,7 @@ public void Should_Have_Options() { _command.Should().NotBeNull(); _command.Name.Should().Be("migrate-code-scanning-alerts"); - _command.Options.Count.Should().Be(11); + _command.Options.Count.Should().Be(12); TestHelpers.VerifyCommandOption(_command.Options, "source-org", true); TestHelpers.VerifyCommandOption(_command.Options, "source-repo", true); @@ -41,6 +41,7 @@ public void Should_Have_Options() TestHelpers.VerifyCommandOption(_command.Options, "target-repo", false); TestHelpers.VerifyCommandOption(_command.Options, "target-api-url", false); TestHelpers.VerifyCommandOption(_command.Options, "ghes-api-url", false); + TestHelpers.VerifyCommandOption(_command.Options, "github-source-api-url", false); TestHelpers.VerifyCommandOption(_command.Options, "no-ssl-verify", false); TestHelpers.VerifyCommandOption(_command.Options, "github-source-pat", false); TestHelpers.VerifyCommandOption(_command.Options, "github-target-pat", false); diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs index 2cdbcab6a..7b03e790e 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandArgsTests.cs @@ -376,5 +376,63 @@ public void Validate_Throws_When_TargetRepo_Is_Url() .ThrowExactly() .WithMessage("The --target-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo')."); } + + [Fact] + public void GithubSourceApiUrl_And_GhesApiUrl_Both_Set_Throws() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + GithubTargetOrg = TARGET_ORG, + GhesApiUrl = GHES_API_URL, + GithubSourceApiUrl = "https://api.tenant.ghe.com" + }; + + FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object)) + .Should() + .ThrowExactly() + .WithMessage("*Only one of --github-source-api-url or --ghes-api-url*"); + } + + [Fact] + public void UseGithubStorage_Validates_With_GithubSourceApiUrl() + { + var args = new MigrateRepoCommandArgs + { + GithubSourceOrg = SOURCE_ORG, + SourceRepo = SOURCE_REPO, + GithubTargetOrg = TARGET_ORG, + GithubSourceApiUrl = "https://api.tenant.ghe.com", + UseGithubStorage = true + }; + + args.Validate(_mockOctoLogger.Object); + + args.TargetRepo.Should().Be(SOURCE_REPO); + } + + [Fact] + public void GetSourceApiUrl_Prefers_GithubSourceApiUrl() + { + var args = new MigrateRepoCommandArgs + { + GhesApiUrl = GHES_API_URL, + GithubSourceApiUrl = "https://api.tenant.ghe.com" + }; + + args.GetSourceApiUrl().Should().Be("https://api.tenant.ghe.com"); + } + + [Fact] + public void GetSourceApiUrl_Falls_Back_To_GhesApiUrl() + { + var args = new MigrateRepoCommandArgs + { + GhesApiUrl = GHES_API_URL + }; + + args.GetSourceApiUrl().Should().Be(GHES_API_URL); + } } } diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandTests.cs index e728de3e4..de439835d 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandTests.cs @@ -13,7 +13,7 @@ public void Should_Have_Options() command.Should().NotBeNull(); command.Name.Should().Be("migrate-repo"); - command.Options.Count.Should().Be(27); + command.Options.Count.Should().Be(28); TestHelpers.VerifyCommandOption(command.Options, "github-source-org", true); TestHelpers.VerifyCommandOption(command.Options, "source-repo", true); @@ -22,6 +22,7 @@ public void Should_Have_Options() TestHelpers.VerifyCommandOption(command.Options, "target-api-url", false); TestHelpers.VerifyCommandOption(command.Options, "target-uploads-url", false); TestHelpers.VerifyCommandOption(command.Options, "ghes-api-url", false); + TestHelpers.VerifyCommandOption(command.Options, "github-source-api-url", false); TestHelpers.VerifyCommandOption(command.Options, "azure-storage-connection-string", false); TestHelpers.VerifyCommandOption(command.Options, "aws-bucket-name", false); TestHelpers.VerifyCommandOption(command.Options, "aws-access-key", false); diff --git a/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandTests.cs b/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandTests.cs index d1b7914a1..2e00deb8e 100644 --- a/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandTests.cs @@ -33,7 +33,7 @@ public void Should_Have_Options() { _command.Should().NotBeNull(); _command.Name.Should().Be("migrate-secret-alerts"); - _command.Options.Count.Should().Be(11); + _command.Options.Count.Should().Be(12); TestHelpers.VerifyCommandOption(_command.Options, "source-org", true); TestHelpers.VerifyCommandOption(_command.Options, "source-repo", true); @@ -41,6 +41,7 @@ public void Should_Have_Options() TestHelpers.VerifyCommandOption(_command.Options, "target-repo", false); TestHelpers.VerifyCommandOption(_command.Options, "target-api-url", false); TestHelpers.VerifyCommandOption(_command.Options, "ghes-api-url", false); + TestHelpers.VerifyCommandOption(_command.Options, "github-source-api-url", false); TestHelpers.VerifyCommandOption(_command.Options, "no-ssl-verify", false); TestHelpers.VerifyCommandOption(_command.Options, "github-source-pat", false); TestHelpers.VerifyCommandOption(_command.Options, "github-target-pat", false); diff --git a/src/OctoshiftCLI.Tests/gei/Services/GhesVersionCheckerTests.cs b/src/OctoshiftCLI.Tests/gei/Services/GhesVersionCheckerTests.cs index 7ce3d7d47..1407a5111 100644 --- a/src/OctoshiftCLI.Tests/gei/Services/GhesVersionCheckerTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Services/GhesVersionCheckerTests.cs @@ -51,4 +51,12 @@ public async Task Empty_Ghes_Url_Returns_False() var result = await _service.AreBlobCredentialsRequired(""); result.Should().Be(false); } + + [Fact] + public async Task Proxima_Api_Url_Returns_False_Without_Calling_Source() + { + var result = await _service.AreBlobCredentialsRequired("https://api.tenant.ghe.com"); + result.Should().Be(false); + _mockGithubApi.Verify(m => m.GetEnterpriseServerVersion(), Times.Never); + } } diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommand.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommand.cs index 3021f17b4..4fb133aee 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommand.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommand.cs @@ -24,6 +24,7 @@ public GenerateScriptCommand() : base( AddOption(TargetApiUrl); AddOption(TargetUploadsUrl); AddOption(GhesApiUrl); + AddOption(GithubSourceApiUrl); AddOption(AwsBucketName); AddOption(AwsRegion); AddOption(NoSslVerify); @@ -54,6 +55,10 @@ public GenerateScriptCommand() : base( { Description = "Required if migrating from GHES. The api endpoint for the hostname of your GHES instance. For example: http(s)://myghes.com/api/v3" }; + public Option GithubSourceApiUrl { get; } = new("--github-source-api-url") + { + Description = "Required if migrating from GitHub Enterprise Cloud with data residency (ghe.com). The API endpoint for the source data residency tenant. For example: https://api.tenant.ghe.com. Uses GH_SOURCE_API_URL environment variable if not set. May not be used together with --ghes-api-url." + }; public Option NoSslVerify { get; } = new("--no-ssl-verify") { Description = "Only effective if migrating from GHES. Disables SSL verification when communicating with your GHES instance. All other migration steps will continue to verify SSL. If your GHES instance has a self-signed SSL certificate then setting this flag will allow data to be extracted." @@ -128,9 +133,11 @@ public override GenerateScriptCommandHandler BuildHandler(GenerateScriptCommandA var sourceGithubApiFactory = sp.GetRequiredService(); - var sourceGithubApi = args.GhesApiUrl.HasValue() && args.NoSslVerify ? - sourceGithubApiFactory.CreateClientNoSsl(args.GhesApiUrl, args.TargetUploadsUrl, args.GithubSourcePat) : - sourceGithubApiFactory.Create(args.GhesApiUrl, args.TargetUploadsUrl, args.GithubSourcePat); + var sourceApiUrl = args.GetSourceApiUrl(); + + var sourceGithubApi = sourceApiUrl.HasValue() && args.NoSslVerify ? + sourceGithubApiFactory.CreateClientNoSsl(sourceApiUrl, args.TargetUploadsUrl, args.GithubSourcePat) : + sourceGithubApiFactory.Create(sourceApiUrl, args.TargetUploadsUrl, args.GithubSourcePat); var ghesVersionChecker = ghesVersionCheckerFactory.Create(sourceGithubApi); diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs index 26aa90416..335f945d5 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommandArgs.cs @@ -12,6 +12,7 @@ public class GenerateScriptCommandArgs : CommandArgs public string GithubTargetOrg { get; set; } public FileInfo Output { get; set; } public string GhesApiUrl { get; set; } + public string GithubSourceApiUrl { get; set; } public string AwsBucketName { get; set; } public string AwsRegion { get; set; } public bool NoSslVerify { get; set; } @@ -26,6 +27,8 @@ public class GenerateScriptCommandArgs : CommandArgs public string TargetUploadsUrl { get; set; } public bool UseGithubStorage { get; set; } + public string GetSourceApiUrl() => GithubSourceApiUrl.HasValue() ? GithubSourceApiUrl : GhesApiUrl; + public override void Validate(OctoLogger log) { if (GithubSourceOrg.IsUrl()) @@ -38,11 +41,23 @@ public override void Validate(OctoLogger log) throw new OctoshiftCliException($"The --github-target-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org')."); } + if (GithubSourceApiUrl.IsNullOrWhiteSpace()) + { + GithubSourceApiUrl = System.Environment.GetEnvironmentVariable("GH_SOURCE_API_URL"); + } + + if (GhesApiUrl.HasValue() && GithubSourceApiUrl.HasValue()) + { + throw new OctoshiftCliException("Only one of --github-source-api-url or --ghes-api-url may be specified."); + } + + var sourceApiUrl = GetSourceApiUrl(); + if (AwsBucketName.HasValue()) { - if (GhesApiUrl.IsNullOrWhiteSpace()) + if (sourceApiUrl.IsNullOrWhiteSpace()) { - throw new OctoshiftCliException("--ghes-api-url must be specified when --aws-bucket-name is specified."); + throw new OctoshiftCliException("--ghes-api-url or --github-source-api-url must be specified when --aws-bucket-name is specified."); } if (UseGithubStorage) @@ -56,19 +71,21 @@ public override void Validate(OctoLogger log) throw new OctoshiftCliException("--ghes-api-url must be specified when --no-ssl-verify is specified."); } - if (GhesApiUrl.IsNullOrWhiteSpace() && UseGithubStorage) + if (sourceApiUrl.IsNullOrWhiteSpace() && UseGithubStorage) { - throw new OctoshiftCliException("--ghes-api-url must be specified when --use-github-storage is specified."); + throw new OctoshiftCliException("--ghes-api-url or --github-source-api-url must be specified when --use-github-storage is specified."); } - if (GhesApiUrl.HasValue()) + if (sourceApiUrl.HasValue()) { - var result = Uri.TryCreate(GhesApiUrl, UriKind.Absolute, out var uriResult) + var result = Uri.TryCreate(sourceApiUrl, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); if (!result) { - throw new OctoshiftCliException("--ghes-api-url is invalid. Please check URL before trying again."); + throw new OctoshiftCliException(GithubSourceApiUrl.HasValue() + ? "--github-source-api-url is invalid. Please check URL before trying again." + : "--ghes-api-url is invalid. Please check URL before trying again."); } } } diff --git a/src/gei/Commands/GenerateScript/GenerateScriptCommandHandler.cs b/src/gei/Commands/GenerateScript/GenerateScriptCommandHandler.cs index 2760bfe0e..f9240de43 100644 --- a/src/gei/Commands/GenerateScript/GenerateScriptCommandHandler.cs +++ b/src/gei/Commands/GenerateScript/GenerateScriptCommandHandler.cs @@ -44,7 +44,7 @@ public async Task Handle(GenerateScriptCommandArgs args) _log.LogInformation("Generating Script..."); - var script = await GenerateScript(args.GithubSourceOrg, args.GithubTargetOrg, args.GhesApiUrl, args.AwsBucketName, args.AwsRegion, args.NoSslVerify, args.Sequential, args.SkipReleases, args.LockSourceRepo, args.DownloadMigrationLogs, args.KeepArchive, args.TargetApiUrl, args.TargetUploadsUrl, args.UseGithubStorage); + var script = await GenerateScript(args.GithubSourceOrg, args.GithubTargetOrg, args.GetSourceApiUrl(), args.AwsBucketName, args.AwsRegion, args.NoSslVerify, args.Sequential, args.SkipReleases, args.LockSourceRepo, args.DownloadMigrationLogs, args.KeepArchive, args.TargetApiUrl, args.TargetUploadsUrl, args.UseGithubStorage); if (script.HasValue() && args.Output.HasValue()) { @@ -209,7 +209,10 @@ private string MigrateGithubRepoScript(string githubSourceOrg, string githubTarg private string GetGhesRepoOptions(string ghesApiUrl, string awsBucketName, string awsRegion, bool noSslVerify, bool keepArchive, bool useGithubStorage) { - return $"--ghes-api-url \"{ghesApiUrl}\"{(awsBucketName.HasValue() ? $" --aws-bucket-name \"{awsBucketName}\"" : "")}{(awsRegion.HasValue() ? $" --aws-region \"{awsRegion}\"" : "")}{(noSslVerify ? " --no-ssl-verify" : string.Empty)}{(keepArchive ? " --keep-archive" : string.Empty)}{(useGithubStorage ? " --use-github-storage" : string.Empty)}"; + var sourceUrlFlag = ghesApiUrl.IsProximaApiUrl() ? "--github-source-api-url" : "--ghes-api-url"; + var emitNoSslVerify = noSslVerify && !ghesApiUrl.IsProximaApiUrl(); + + return $"{sourceUrlFlag} \"{ghesApiUrl}\"{(awsBucketName.HasValue() ? $" --aws-bucket-name \"{awsBucketName}\"" : "")}{(awsRegion.HasValue() ? $" --aws-region \"{awsRegion}\"" : "")}{(emitNoSslVerify ? " --no-ssl-verify" : string.Empty)}{(keepArchive ? " --keep-archive" : string.Empty)}{(useGithubStorage ? " --use-github-storage" : string.Empty)}"; } private string WaitForMigrationScript(string targetApiUrl, string repoMigrationKey = null) => $"gh gei wait-for-migration{(targetApiUrl.HasValue() ? $" --target-api-url \"{targetApiUrl}\"" : string.Empty)} --migration-id $RepoMigrations[\"{repoMigrationKey}\"]"; diff --git a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommand.cs b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommand.cs index 17cd7d455..70ab0c42c 100644 --- a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommand.cs +++ b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommand.cs @@ -21,6 +21,7 @@ public MigrateCodeScanningAlertsCommand() : base( AddOption(TargetApiUrl); AddOption(GhesApiUrl); + AddOption(GithubSourceApiUrl); AddOption(NoSslVerify); AddOption(GithubSourcePat); @@ -46,6 +47,11 @@ public MigrateCodeScanningAlertsCommand() : base( Description = "Required if migrating from GHES. The API endpoint for your GHES instance. For example: http(s)://ghes.contoso.com/api/v3" }; + public Option GithubSourceApiUrl { get; } = new("--github-source-api-url") + { + Description = + "Required if migrating from GitHub Enterprise Cloud with data residency (ghe.com). The API endpoint for the source data residency tenant. For example: https://api.tenant.ghe.com. Uses GH_SOURCE_API_URL environment variable if not set. May not be used together with --ghes-api-url." + }; public Option NoSslVerify { get; } = new("--no-ssl-verify") { Description = @@ -86,7 +92,7 @@ public override MigrateCodeScanningAlertsCommandHandler BuildHandler(MigrateCode var log = sp.GetRequiredService(); var codeScanningAlertServiceFactory = sp.GetRequiredService(); - var codeScanningAlertService = codeScanningAlertServiceFactory.Create(args.GhesApiUrl, args.GithubSourcePat, args.TargetApiUrl, args.GithubTargetPat, args.NoSslVerify); + var codeScanningAlertService = codeScanningAlertServiceFactory.Create(args.GetSourceApiUrl(), args.GithubSourcePat, args.TargetApiUrl, args.GithubTargetPat, args.NoSslVerify); return new MigrateCodeScanningAlertsCommandHandler(log, codeScanningAlertService); } diff --git a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs index a3215a8e9..865ddc313 100644 --- a/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateCodeScanningAlerts/MigrateCodeScanningAlertsCommandArgs.cs @@ -12,6 +12,7 @@ public class MigrateCodeScanningAlertsCommandArgs : CommandArgs public string TargetRepo { get; set; } public string TargetApiUrl { get; set; } public string GhesApiUrl { get; set; } + public string GithubSourceApiUrl { get; set; } public bool NoSslVerify { get; set; } public bool DryRun { get; set; } [Secret] @@ -19,6 +20,8 @@ public class MigrateCodeScanningAlertsCommandArgs : CommandArgs [Secret] public string GithubTargetPat { get; set; } + public string GetSourceApiUrl() => GithubSourceApiUrl.HasValue() ? GithubSourceApiUrl : GhesApiUrl; + public override void Validate(OctoLogger log) { if (SourceOrg.IsUrl()) @@ -46,5 +49,20 @@ public override void Validate(OctoLogger log) TargetRepo = SourceRepo; log?.LogInformation("Since target-repo is not provided, source-repo value will be used for target-repo."); } + + if (GithubSourceApiUrl.IsNullOrWhiteSpace()) + { + GithubSourceApiUrl = System.Environment.GetEnvironmentVariable("GH_SOURCE_API_URL"); + } + + if (GhesApiUrl.HasValue() && GithubSourceApiUrl.HasValue()) + { + throw new OctoshiftCliException("Only one of --github-source-api-url or --ghes-api-url may be specified."); + } + + if (NoSslVerify && GhesApiUrl.IsNullOrWhiteSpace()) + { + throw new OctoshiftCliException("--ghes-api-url must be specified when --no-ssl-verify is specified."); + } } } diff --git a/src/gei/Commands/MigrateRepo/MigrateRepoCommand.cs b/src/gei/Commands/MigrateRepo/MigrateRepoCommand.cs index 1fbdd004e..975498a5f 100644 --- a/src/gei/Commands/MigrateRepo/MigrateRepoCommand.cs +++ b/src/gei/Commands/MigrateRepo/MigrateRepoCommand.cs @@ -23,6 +23,7 @@ public MigrateRepoCommand() : base( AddOption(TargetApiUrl); AddOption(TargetUploadsUrl); AddOption(GhesApiUrl); + AddOption(GithubSourceApiUrl); AddOption(AzureStorageConnectionString); AddOption(AwsBucketName); AddOption(AwsAccessKey); @@ -76,6 +77,10 @@ public MigrateRepoCommand() : base( { Description = "Required if migrating from GHES. The API endpoint for your GHES instance. For example: http(s)://ghes.contoso.com/api/v3" }; + public Option GithubSourceApiUrl { get; } = new("--github-source-api-url") + { + Description = "Required if migrating from GitHub Enterprise Cloud with data residency (ghe.com). The API endpoint for the source data residency tenant. For example: https://api.tenant.ghe.com. Uses GH_SOURCE_API_URL environment variable if not set. May not be used together with --ghes-api-url." + }; public Option AzureStorageConnectionString { get; } = new("--azure-storage-connection-string") { Description = "Required if migrating from GHES (Not required if migrating from GitHub Enterprise Server 3.8.0 or later). The connection string for the Azure storage account, used to upload data archives pre-migration. For example: \"DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net\"" @@ -183,13 +188,15 @@ public override MigrateRepoCommandHandler BuildHandler(MigrateRepoCommandArgs ar AwsApi awsApi = null; HttpDownloadService httpDownloadService = null; - if (args.GhesApiUrl.HasValue() || (args.GitArchivePath.HasValue() && args.MetadataArchivePath.HasValue())) + var sourceApiUrl = args.GetSourceApiUrl(); + + if (sourceApiUrl.HasValue() || (args.GitArchivePath.HasValue() && args.MetadataArchivePath.HasValue())) { var sourceGithubApiFactory = sp.GetRequiredService(); var awsApiFactory = sp.GetRequiredService(); var azureApiFactory = sp.GetRequiredService(); var httpDownloadServiceFactory = sp.GetRequiredService(); - ghesApi = args.NoSslVerify ? sourceGithubApiFactory.CreateClientNoSsl(args.GhesApiUrl, null, args.GithubSourcePat) : sourceGithubApiFactory.Create(args.GhesApiUrl, null, args.GithubSourcePat); + ghesApi = args.NoSslVerify ? sourceGithubApiFactory.CreateClientNoSsl(sourceApiUrl, null, args.GithubSourcePat) : sourceGithubApiFactory.Create(sourceApiUrl, null, args.GithubSourcePat); httpDownloadService = args.NoSslVerify ? httpDownloadServiceFactory.CreateClientNoSsl() : httpDownloadServiceFactory.CreateDefault(); if (args.AzureStorageConnectionString.HasValue() || environmentVariableProvider.AzureStorageConnectionString(false).HasValue()) diff --git a/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs b/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs index 076979476..bc79d7425 100644 --- a/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs +++ b/src/gei/Commands/MigrateRepo/MigrateRepoCommandArgs.cs @@ -13,6 +13,7 @@ public class MigrateRepoCommandArgs : CommandArgs public string TargetApiUrl { get; set; } public string TargetUploadsUrl { get; set; } public string GhesApiUrl { get; set; } + public string GithubSourceApiUrl { get; set; } [Secret] public string AzureStorageConnectionString { get; set; } public string AwsBucketName { get; set; } @@ -39,6 +40,8 @@ public class MigrateRepoCommandArgs : CommandArgs public bool KeepArchive { get; set; } public bool UseGithubStorage { get; set; } + public string GetSourceApiUrl() => GithubSourceApiUrl.HasValue() ? GithubSourceApiUrl : GhesApiUrl; + public override void Validate(OctoLogger log) { if (GithubSourceOrg.IsUrl()) @@ -63,6 +66,7 @@ public override void Validate(OctoLogger log) DefaultSourcePat(log); DefaultTargetRepo(log); + DefaultGithubSourceApiUrl(); if (GitArchiveUrl.HasValue() && GitArchivePath.HasValue()) { @@ -84,26 +88,31 @@ public override void Validate(OctoLogger log) throw new OctoshiftCliException("When using archive files, you must provide both --git-archive-path --metadata-archive-path"); } - if (GhesApiUrl.IsNullOrWhiteSpace()) + if (GhesApiUrl.HasValue() && GithubSourceApiUrl.HasValue()) + { + throw new OctoshiftCliException("Only one of --github-source-api-url or --ghes-api-url may be specified."); + } + + if (NoSslVerify && GhesApiUrl.IsNullOrWhiteSpace()) + { + throw new OctoshiftCliException("--ghes-api-url must be specified when --no-ssl-verify is specified."); + } + + if (GetSourceApiUrl().IsNullOrWhiteSpace()) { if (AwsBucketName.HasValue() && GitArchivePath.IsNullOrWhiteSpace()) { - throw new OctoshiftCliException("When using --aws-bucket-name, you must provide --ghes-api-url, or --git-archive-path and --metadata-archive-path"); + throw new OctoshiftCliException("When using --aws-bucket-name, you must provide --ghes-api-url, --github-source-api-url, or --git-archive-path and --metadata-archive-path"); } if (UseGithubStorage && GitArchivePath.IsNullOrWhiteSpace()) { - throw new OctoshiftCliException("When using --use-github-storage, you must provide --ghes-api-url, or --git-archive-path and --metadata-archive-path"); - } - - if (NoSslVerify) - { - throw new OctoshiftCliException("--ghes-api-url must be specified when --no-ssl-verify is specified."); + throw new OctoshiftCliException("When using --use-github-storage, you must provide --ghes-api-url, --github-source-api-url, or --git-archive-path and --metadata-archive-path"); } if (KeepArchive) { - throw new OctoshiftCliException("--ghes-api-url must be specified when --keep-archive is specified."); + throw new OctoshiftCliException("--ghes-api-url or --github-source-api-url must be specified when --keep-archive is specified."); } } @@ -135,5 +144,13 @@ private void DefaultSourcePat(OctoLogger log) log?.LogInformation("Since github-target-pat is provided, github-source-pat will also use its value."); } } + + private void DefaultGithubSourceApiUrl() + { + if (GithubSourceApiUrl.IsNullOrWhiteSpace()) + { + GithubSourceApiUrl = System.Environment.GetEnvironmentVariable("GH_SOURCE_API_URL"); + } + } } } diff --git a/src/gei/Commands/MigrateRepo/MigrateRepoCommandHandler.cs b/src/gei/Commands/MigrateRepo/MigrateRepoCommandHandler.cs index 138fc0c99..48aeb2ccc 100644 --- a/src/gei/Commands/MigrateRepo/MigrateRepoCommandHandler.cs +++ b/src/gei/Commands/MigrateRepo/MigrateRepoCommandHandler.cs @@ -67,14 +67,15 @@ public async Task Handle(MigrateRepoCommandArgs args) _log.LogInformation("Migrating Repo..."); - var blobCredentialsRequired = args.GitArchivePath.HasValue() || await _ghesVersionChecker.AreBlobCredentialsRequired(args.GhesApiUrl); + var sourceApiUrl = args.GetSourceApiUrl(); + var blobCredentialsRequired = args.GitArchivePath.HasValue() || await _ghesVersionChecker.AreBlobCredentialsRequired(sourceApiUrl); - if (args.GhesApiUrl.HasValue() || args.GitArchivePath.HasValue()) + if (sourceApiUrl.HasValue() || args.GitArchivePath.HasValue()) { ValidateUploadOptions(args, blobCredentialsRequired); } - if (args.GhesApiUrl.HasValue()) + if (sourceApiUrl.HasValue()) { var targetRepoExists = await _targetGithubApi.DoesRepoExist(args.GithubTargetOrg, args.TargetRepo); var targetOrgExists = await _targetGithubApi.DoesOrgExist(args.GithubTargetOrg); @@ -105,7 +106,7 @@ public async Task Handle(MigrateRepoCommandArgs args) throw new OctoshiftCliException(message, ex); } - if (args.GhesApiUrl.HasValue()) + if (sourceApiUrl.HasValue()) { (args.GitArchiveUrl, args.MetadataArchiveUrl) = await GenerateAndUploadArchive( args.GithubSourceOrg, @@ -170,7 +171,7 @@ public async Task Handle(MigrateRepoCommandArgs args) args.MetadataArchiveUrl, args.SkipReleases, args.TargetRepoVisibility, - args.GhesApiUrl.IsNullOrWhiteSpace() && args.LockSourceRepo); + sourceApiUrl.IsNullOrWhiteSpace() && args.LockSourceRepo); } catch (OctoshiftCliException ex) { @@ -216,7 +217,7 @@ public async Task Handle(MigrateRepoCommandArgs args) private string GetSourceToken(MigrateRepoCommandArgs args) => args.GithubSourcePat ?? _environmentVariableProvider.SourceGithubPersonalAccessToken(); - private string GetSourceRepoUrl(MigrateRepoCommandArgs args) => GetGithubRepoUrl(args.GithubSourceOrg, args.SourceRepo, args.GhesApiUrl.HasValue() ? ExtractGhesBaseUrl(args.GhesApiUrl) : null); + private string GetSourceRepoUrl(MigrateRepoCommandArgs args) => GetGithubRepoUrl(args.GithubSourceOrg, args.SourceRepo, args.GetSourceApiUrl().HasValue() ? ExtractGhesBaseUrl(args.GetSourceApiUrl()) : null); private string ExtractGhesBaseUrl(string ghesApiUrl) { diff --git a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommand.cs b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommand.cs index c1998ecb9..bbb796b18 100644 --- a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommand.cs +++ b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommand.cs @@ -21,6 +21,7 @@ public MigrateSecretAlertsCommand() : base( AddOption(TargetApiUrl); AddOption(GhesApiUrl); + AddOption(GithubSourceApiUrl); AddOption(NoSslVerify); AddOption(GithubSourcePat); @@ -46,6 +47,11 @@ public MigrateSecretAlertsCommand() : base( Description = "Required if migrating from GHES. The API endpoint for your GHES instance. For example: http(s)://ghes.contoso.com/api/v3" }; + public Option GithubSourceApiUrl { get; } = new("--github-source-api-url") + { + Description = + "Required if migrating from GitHub Enterprise Cloud with data residency (ghe.com). The API endpoint for the source data residency tenant. For example: https://api.tenant.ghe.com. Uses GH_SOURCE_API_URL environment variable if not set. May not be used together with --ghes-api-url." + }; public Option NoSslVerify { get; } = new("--no-ssl-verify") { Description = @@ -86,7 +92,7 @@ public override MigrateSecretAlertsCommandHandler BuildHandler(MigrateSecretAler var log = sp.GetRequiredService(); var secretScanningAlertServiceFactory = sp.GetRequiredService(); - var secretScanningAlertService = secretScanningAlertServiceFactory.Create(args.GhesApiUrl, args.GithubSourcePat, args.TargetApiUrl, args.GithubTargetPat, args.NoSslVerify); + var secretScanningAlertService = secretScanningAlertServiceFactory.Create(args.GetSourceApiUrl(), args.GithubSourcePat, args.TargetApiUrl, args.GithubTargetPat, args.NoSslVerify); return new MigrateSecretAlertsCommandHandler(log, secretScanningAlertService); } diff --git a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs index 23a1fbf00..fc39a0c7d 100644 --- a/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs +++ b/src/gei/Commands/MigrateSecretAlerts/MigrateSecretAlertsCommandArgs.cs @@ -12,6 +12,7 @@ public class MigrateSecretAlertsCommandArgs : CommandArgs public string TargetRepo { get; set; } public string TargetApiUrl { get; set; } public string GhesApiUrl { get; set; } + public string GithubSourceApiUrl { get; set; } public bool NoSslVerify { get; set; } public bool DryRun { get; set; } [Secret] @@ -19,6 +20,8 @@ public class MigrateSecretAlertsCommandArgs : CommandArgs [Secret] public string GithubTargetPat { get; set; } + public string GetSourceApiUrl() => GithubSourceApiUrl.HasValue() ? GithubSourceApiUrl : GhesApiUrl; + public override void Validate(OctoLogger log) { if (SourceOrg.IsUrl()) @@ -46,5 +49,20 @@ public override void Validate(OctoLogger log) TargetRepo = SourceRepo; log?.LogInformation("Since target-repo is not provided, source-repo value will be used for target-repo."); } + + if (GithubSourceApiUrl.IsNullOrWhiteSpace()) + { + GithubSourceApiUrl = System.Environment.GetEnvironmentVariable("GH_SOURCE_API_URL"); + } + + if (GhesApiUrl.HasValue() && GithubSourceApiUrl.HasValue()) + { + throw new OctoshiftCliException("Only one of --github-source-api-url or --ghes-api-url may be specified."); + } + + if (NoSslVerify && GhesApiUrl.IsNullOrWhiteSpace()) + { + throw new OctoshiftCliException("--ghes-api-url must be specified when --no-ssl-verify is specified."); + } } } diff --git a/src/gei/Services/GhesVersionChecker.cs b/src/gei/Services/GhesVersionChecker.cs index 61550913a..edb4078ea 100644 --- a/src/gei/Services/GhesVersionChecker.cs +++ b/src/gei/Services/GhesVersionChecker.cs @@ -23,6 +23,13 @@ public virtual async Task AreBlobCredentialsRequired(string ghesApiUrl) if (ghesApiUrl.HasValue()) { + // GitHub Enterprise Cloud with data residency (Proxima) has GHOS available and never requires + // customer-provided blob storage credentials. + if (ghesApiUrl.IsProximaApiUrl()) + { + return false; + } + blobCredentialsRequired = true; _log.LogInformation("Using GitHub Enterprise Server - verifying server version");