Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/Octoshift/Services/GitlabApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,41 @@ public virtual async Task<string> StartExport(string groupPath, string projectPa
);
}

public virtual async Task<GitlabExportDetails> GetExportDetails(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}/export";

var exportResponse = await _client.GetAsync(url);
var exportData = JObject.Parse(exportResponse);

return new GitlabExportDetails(
(long?)exportData["id"],
(string)exportData["export_status"],
(string)exportData["_links"]?["api_url"],
exportData.ToString());
}

public virtual async Task<GitlabProjectDetails> GetProjectDetails(string groupPath, string projectPath)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
var url = $"{_gitlabBaseUrl}/api/v4/projects/{encodedProjectPath}?statistics=true";

var projectResponse = await _client.GetAsync(url);
var projectData = JObject.Parse(projectResponse);
var projectStatistics = (JObject)projectData["statistics"];

return new GitlabProjectDetails(
(long?)projectData["id"],
(string)projectData["path_with_namespace"],
(string)projectData["web_url"],
(bool?)projectData["archived"],
(string)projectData["visibility"],
(long?)projectStatistics?["repository_size"],
(long?)projectStatistics?["uploads_size"],
(long?)projectStatistics?["job_artifacts_size"]);
}

public virtual async Task DownloadExportArchive(string groupPath, string projectPath, string file)
{
var encodedProjectPath = GetEncodedProjectPath(groupPath, projectPath);
Expand Down Expand Up @@ -168,3 +203,15 @@ private static string GetEncodedProjectPath(string groupPath, string projectPath
return pathWithNamespace.EscapeDataString();
}
}

public record GitlabExportDetails(long? Id, string ExportStatus, string DownloadUrl, string RawJson);

public record GitlabProjectDetails(
long? Id,
string PathWithNamespace,
string WebUrl,
bool? Archived,
string Visibility,
long? RepositorySize,
long? UploadsSize,
long? JobArtifactsSize);
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using FluentAssertions;
using OctoshiftCLI.GitlabToGithub.Commands.DiagnoseGitlabExport;
using OctoshiftCLI.Services;
using Xunit;

namespace OctoshiftCLI.Tests.GitlabToGithub.Commands.DiagnoseGitlabExport;

public class DiagnoseGitlabExportCommandArgsTests
{
private readonly OctoLogger _log = new();

[Theory]
[InlineData(null, "group", "project", "--gitlab-server-url must be provided.")]
[InlineData("https://gitlab.contoso.com", null, "project", "--gitlab-group must be provided.")]
[InlineData("https://gitlab.contoso.com", "group", null, "--gitlab-project must be provided.")]
public void Validate_Requires_Gitlab_Project_Inputs(string gitlabServerUrl, string gitlabGroup, string gitlabProject, string expectedMessage)
{
var args = new DiagnoseGitlabExportCommandArgs
{
GitlabServerUrl = gitlabServerUrl,
GitlabGroup = gitlabGroup,
GitlabProject = gitlabProject
};

var ex = Assert.Throws<OctoshiftCliException>(() => args.Validate(_log));
ex.Message.Should().Be(expectedMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Threading.Tasks;
using Moq;
using OctoshiftCLI.GitlabToGithub.Commands.DiagnoseGitlabExport;
using OctoshiftCLI.Services;
using Xunit;

namespace OctoshiftCLI.Tests.GitlabToGithub.Commands.DiagnoseGitlabExport;

public class DiagnoseGitlabExportCommandHandlerTests
{
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
private readonly Mock<GitlabApi> _mockGitlabApi = TestHelpers.CreateMock<GitlabApi>();
private readonly Mock<FileSystemProvider> _mockFileSystemProvider = TestHelpers.CreateMock<FileSystemProvider>();

private readonly DiagnoseGitlabExportCommandHandler _handler;

public DiagnoseGitlabExportCommandHandlerTests()
{
_handler = new DiagnoseGitlabExportCommandHandler(_mockOctoLogger.Object, _mockGitlabApi.Object, _mockFileSystemProvider.Object);
}

[Fact]
public async Task Handle_Writes_Report_With_Export_Status_And_Gitlab_Admin_Commands()
{
var args = new DiagnoseGitlabExportCommandArgs
{
GitlabServerUrl = "https://gitlab.contoso.com",
GitlabGroup = "parent/group",
GitlabProject = "project",
Output = "diagnostics.md"
};
string report = null;

_mockGitlabApi.Setup(m => m.GetServerVersion()).ReturnsAsync(("18.11.0-ee", true));
_mockGitlabApi.Setup(m => m.GetProjectDetails("parent/group", "project"))
.ReturnsAsync(new GitlabProjectDetails(123, "parent/group/project", "https://gitlab.contoso.com/parent/group/project", false, "private", 42, 43, 44));
_mockGitlabApi.Setup(m => m.GetExportDetails("parent/group", "project"))
.ReturnsAsync(new GitlabExportDetails(123, "failed", null, "{\"export_status\":\"failed\"}"));
_mockFileSystemProvider.Setup(m => m.WriteAllTextAsync("diagnostics.md", It.IsAny<string>()))
.Callback<string, string>((_, contents) => report = contents)
.Returns(Task.CompletedTask);

await _handler.Handle(args);

Assert.Contains("Export status: failed", report);
Assert.Contains("Project.find_by_full_path('parent/group/project')", report);
Assert.Contains("/var/log/gitlab/sidekiq/current", report);
Assert.Contains("/var/log/gitlab/gitlab-rails/importer.log", report);
_mockOctoLogger.Verify(m => m.LogWarning(It.Is<string>(s => s.Contains("GitLab reported the project export as failed"))), Times.Once);
_mockOctoLogger.Verify(m => m.LogSuccess("Wrote GitLab export diagnostics to diagnostics.md."), Times.Once);
}

[Fact]
public async Task Handle_Throws_When_Output_Exists_Without_Overwrite()
{
var args = new DiagnoseGitlabExportCommandArgs
{
GitlabServerUrl = "https://gitlab.contoso.com",
GitlabGroup = "parent/group",
GitlabProject = "project",
Output = "diagnostics.md"
};

_mockFileSystemProvider.Setup(m => m.FileExists("diagnostics.md")).Returns(true);

var ex = await Assert.ThrowsAsync<OctoshiftCliException>(() => _handler.Handle(args));

Assert.Equal("File diagnostics.md already exists! Use --overwrite to overwrite this file.", ex.Message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using FluentAssertions;
using Moq;
using OctoshiftCLI.GitlabToGithub.Commands.DiagnoseGitlabExport;
using OctoshiftCLI.GitlabToGithub.Factories;
using OctoshiftCLI.Services;
using Xunit;

namespace OctoshiftCLI.Tests.GitlabToGithub.Commands.DiagnoseGitlabExport;

public class DiagnoseGitlabExportCommandTests
{
private const string GITLAB_SERVER_URL = "https://gitlab.contoso.com";
private const string GITLAB_PAT = "gitlab-pat";

private readonly Mock<IServiceProvider> _mockServiceProvider = new();
private readonly Mock<GitlabApiFactory> _mockGitlabApiFactory = TestHelpers.CreateMock<GitlabApiFactory>();
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
private readonly Mock<FileSystemProvider> _mockFileSystemProvider = TestHelpers.CreateMock<FileSystemProvider>();

private readonly DiagnoseGitlabExportCommand _command = [];

public DiagnoseGitlabExportCommandTests()
{
_mockServiceProvider.Setup(m => m.GetService(typeof(OctoLogger))).Returns(_mockOctoLogger.Object);
_mockServiceProvider.Setup(m => m.GetService(typeof(GitlabApiFactory))).Returns(_mockGitlabApiFactory.Object);
_mockServiceProvider.Setup(m => m.GetService(typeof(FileSystemProvider))).Returns(_mockFileSystemProvider.Object);
}

[Fact]
public void Should_Have_Options()
{
_command.Should().NotBeNull();
_command.Name.Should().Be("diagnose-gitlab-export");
_command.Options.Count.Should().Be(8);

TestHelpers.VerifyCommandOption(_command.Options, "gitlab-server-url", false);
TestHelpers.VerifyCommandOption(_command.Options, "gitlab-group", false);
TestHelpers.VerifyCommandOption(_command.Options, "gitlab-project", false);
TestHelpers.VerifyCommandOption(_command.Options, "gitlab-pat", false);
TestHelpers.VerifyCommandOption(_command.Options, "output", false);
TestHelpers.VerifyCommandOption(_command.Options, "overwrite", false);
TestHelpers.VerifyCommandOption(_command.Options, "no-ssl-verify", false);
TestHelpers.VerifyCommandOption(_command.Options, "verbose", false);
}

[Fact]
public void It_Creates_The_GitlabApi_With_The_Provided_Server_Url_And_Pat()
{
var args = new DiagnoseGitlabExportCommandArgs
{
GitlabServerUrl = GITLAB_SERVER_URL,
GitlabPat = GITLAB_PAT,
NoSslVerify = true
};

_command.BuildHandler(args, _mockServiceProvider.Object);

_mockGitlabApiFactory.Verify(m => m.Create(GITLAB_SERVER_URL, GITLAB_PAT, true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using OctoshiftCLI.Commands;
using OctoshiftCLI.GitlabToGithub.Factories;
using OctoshiftCLI.Services;

namespace OctoshiftCLI.GitlabToGithub.Commands.DiagnoseGitlabExport;

public class DiagnoseGitlabExportCommand : CommandBase<DiagnoseGitlabExportCommandArgs, DiagnoseGitlabExportCommandHandler>
{
public DiagnoseGitlabExportCommand() : base(
name: "diagnose-gitlab-export",
description: "Collects GitLab project export diagnostics and writes a report with GitLab admin log commands.")
Comment on lines +13 to +14
{
AddOption(GitlabServerUrl);
AddOption(GitlabGroup);
AddOption(GitlabProject);
AddOption(GitlabPat);
AddOption(Output);
AddOption(Overwrite);
AddOption(NoSslVerify);
AddOption(Verbose);
}

public Option<string> GitlabServerUrl { get; } = new(
name: "--gitlab-server-url",
description: "The full URL of the GitLab server, e.g. https://gitlab.mycompany.com");

public Option<string> GitlabGroup { get; } = new(
name: "--gitlab-group",
description: "The GitLab group (full namespace path) that contains the project.");

public Option<string> GitlabProject { get; } = new(
name: "--gitlab-project",
description: "The GitLab project to diagnose.");

public Option<string> GitlabPat { get; } = new(
name: "--gitlab-pat",
description: "The GitLab PAT. If not passed, it will read the PAT from the GITLAB_PAT environment variable.");

public Option<string> Output { get; } = new(
name: "--output",
description: "Local Markdown file to write diagnostics to.");

public Option<bool> Overwrite { get; } = new(
name: "--overwrite",
description: "Overwrite the output file if it exists.");

public Option<bool> NoSslVerify { get; } = new(
name: "--no-ssl-verify",
description: "Disables SSL verification when communicating with your GitLab instance.");

public Option<bool> Verbose { get; } = new("--verbose");

public override DiagnoseGitlabExportCommandHandler BuildHandler(DiagnoseGitlabExportCommandArgs args, IServiceProvider sp)
{
if (args is null)
{
throw new ArgumentNullException(nameof(args));
}

if (sp is null)
{
throw new ArgumentNullException(nameof(sp));
}

var log = sp.GetRequiredService<OctoLogger>();
var gitlabApiFactory = sp.GetRequiredService<GitlabApiFactory>();
var gitlabApi = gitlabApiFactory.Create(args.GitlabServerUrl, args.GitlabPat, args.NoSslVerify);
var fileSystemProvider = sp.GetRequiredService<FileSystemProvider>();

return new DiagnoseGitlabExportCommandHandler(log, gitlabApi, fileSystemProvider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.IO;
using OctoshiftCLI.Commands;
using OctoshiftCLI.Extensions;
using OctoshiftCLI.Services;

namespace OctoshiftCLI.GitlabToGithub.Commands.DiagnoseGitlabExport;

public class DiagnoseGitlabExportCommandArgs : CommandArgs
{
public string GitlabServerUrl { get; set; }
public string GitlabGroup { get; set; }
public string GitlabProject { get; set; }
[Secret]
public string GitlabPat { get; set; }
public string Output { get; set; }
public bool Overwrite { get; set; }
public bool NoSslVerify { get; set; }

public override void Validate(OctoLogger log)
{
if (GitlabServerUrl.IsNullOrWhiteSpace())
{
throw new OctoshiftCliException("--gitlab-server-url must be provided.");
}

if (GitlabGroup.IsNullOrWhiteSpace())
{
throw new OctoshiftCliException("--gitlab-group must be provided.");
}

if (GitlabProject.IsNullOrWhiteSpace())
{
throw new OctoshiftCliException("--gitlab-project must be provided.");
}

if (Output.HasValue() && Directory.Exists(Output))
{
throw new OctoshiftCliException("--output must be a file path, not a directory.");
}
}
}
Loading
Loading