Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Gitlab CE module #1135

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 15 additions & 1 deletion Testcontainers.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Expand Down Expand Up @@ -195,6 +195,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Gitlab", "src\Testcontainers.Gitlab\Testcontainers.Gitlab.csproj", "{B3857615-7DD1-41D2-BA74-938DA4469E5E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Gitlab.Tests", "tests\Testcontainers.Gitlab.Tests\Testcontainers.Gitlab.Tests.csproj", "{185C65BB-0F79-40D0-A940-7CBE7C8E6A30}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -568,6 +572,14 @@ Global
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU
{B3857615-7DD1-41D2-BA74-938DA4469E5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3857615-7DD1-41D2-BA74-938DA4469E5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3857615-7DD1-41D2-BA74-938DA4469E5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3857615-7DD1-41D2-BA74-938DA4469E5E}.Release|Any CPU.Build.0 = Release|Any CPU
{185C65BB-0F79-40D0-A940-7CBE7C8E6A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{185C65BB-0F79-40D0-A940-7CBE7C8E6A30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{185C65BB-0F79-40D0-A940-7CBE7C8E6A30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{185C65BB-0F79-40D0-A940-7CBE7C8E6A30}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -661,5 +673,7 @@ Global
{1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{B3857615-7DD1-41D2-BA74-938DA4469E5E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{185C65BB-0F79-40D0-A940-7CBE7C8E6A30} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions src/Testcontainers.Gitlab/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
89 changes: 89 additions & 0 deletions src/Testcontainers.Gitlab/GitlabBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
namespace Testcontainers.Gitlab;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class GitlabBuilder : ContainerBuilder<GitlabBuilder, GitlabContainer, GitlabConfiguration>
{
/// <summary>
/// This is the default image for gitlab community edition in version 16.11.1 .
/// </summary>
public const string GitlabImage = "gitlab/gitlab-ce:16.11.1-ce.0";
/// <summary>
/// This port is used for http communication to gitlab instance.
/// </summary>

public const ushort GitlabHttpPort = 80;
/// <summary>
/// This port is used for ssh communication to gitlab instance.
/// </summary>
public const ushort GitlabSshPort = 22;

/// <summary>
/// Initializes a new instance of the <see cref="GitlabBuilder" /> class.
/// </summary>
public GitlabBuilder()
: this(new GitlabConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="GitlabBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private GitlabBuilder(GitlabConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override GitlabConfiguration DockerResourceConfiguration { get; }

/// <inheritdoc />
public override GitlabContainer Build()
{
Validate();
return new GitlabContainer(DockerResourceConfiguration);
}

/// <inheritdoc />
protected override GitlabBuilder Init()
{
return base.Init()
.WithImage(GitlabImage)
.WithPortBinding(GitlabHttpPort, true)
.WithPortBinding(GitlabSshPort, true)
.WithEnvironment("GITLAB_ROOT_PASSWORD", DockerResourceConfiguration.Password)
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(80)
.UntilPortIsAvailable(22)
.UntilContainerIsHealthy()
.UntilHttpRequestIsSucceeded(request => request.ForPath("/users/sign_in").ForStatusCode(HttpStatusCode.OK)));
}

/// <summary>
/// Sets the Gitlab password.
/// </summary>
/// <param name="password">The Gitlab password.</param>
/// <returns>A configured instance of <see cref="GitlabBuilder" />.</returns>
public GitlabBuilder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new GitlabConfiguration(password: password));
}

/// <inheritdoc />
protected override void Validate() => base.Validate();

/// <inheritdoc />
protected override GitlabBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
=> Merge(DockerResourceConfiguration, new GitlabConfiguration(resourceConfiguration));

/// <inheritdoc />
protected override GitlabBuilder Clone(IContainerConfiguration resourceConfiguration)
=> Merge(DockerResourceConfiguration, new GitlabConfiguration(resourceConfiguration));

/// <inheritdoc />
protected override GitlabBuilder Merge(GitlabConfiguration oldValue, GitlabConfiguration newValue)
=> new(new GitlabConfiguration(oldValue, newValue));
}
71 changes: 71 additions & 0 deletions src/Testcontainers.Gitlab/GitlabConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Testcontainers.Gitlab;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class GitlabConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class.
/// </summary>
/// <param name="password">The Gitlab admin password.</param>
public GitlabConfiguration(string password)
{
_password = password;
}

/// <summary>
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class.
/// </summary>
public GitlabConfiguration()
{
_password = Guid.NewGuid().ToString();
}

/// <summary>
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public GitlabConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public GitlabConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public GitlabConfiguration(GitlabConfiguration resourceConfiguration)
: this(new GitlabConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public GitlabConfiguration(GitlabConfiguration oldValue, GitlabConfiguration newValue)
: base(oldValue, newValue)
{
_password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
}

/// <summary>
/// Gets the Gitlab admin password.
/// </summary>
public string Password => _password;

private readonly string _password;
}
73 changes: 73 additions & 0 deletions src/Testcontainers.Gitlab/GitlabContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Testcontainers.Gitlab.Models;
using Testcontainers.Gitlab.RegexPatterns;

namespace Testcontainers.Gitlab;

/// <inheritdoc cref="DockerContainer" />
/// <summary>
/// Initializes a new instance of the <see cref="GitlabContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
[PublicAPI]
public sealed class GitlabContainer(GitlabConfiguration configuration) : DockerContainer(configuration)
{
/// <summary>
/// Gets the root password.
/// </summary>
public string Password => configuration.Password;


/// <summary>
/// Generate a personal access token.
/// </summary>
/// <param name="pat">The personal access token to create.</param>
/// <returns></returns>
/// <exception cref="DataMisalignedException"></exception>
public async Task<PersonalAccessToken> GenerateAccessToken(PersonalAccessToken pat)
{
var scope = "[" + '\'' + pat.Scope.ToString().Replace(", ", "\', \'") + '\'' + "]";

var command = $"token = User.find_by_username('{pat.User}')" +
$".personal_access_tokens" +
$".create(name: '{pat.Name}', scopes: {scope}, expires_at: {pat.ExpirationInDays}.days.from_now); " +
$"puts token.cleartext_tokens";

var tokenCommand = new List<string>{
{ "gitlab-rails" },
{ "runner" },
{ command }
};

ExecResult tokenResult = await ExecAsync(tokenCommand);

string token;
if (tokenResult.ExitCode == 0)
{
var match = GitlabRegex.GitlabPersonalAccessToken.Match(tokenResult.Stdout);
token = match.Value;
}
else
{
throw new DataMisalignedException("Stderr: " + tokenResult.Stderr + "|" + "Stdout: " + tokenResult.Stdout);
}
pat.TokenInternal = token;
return pat;
}

/// <summary>
/// Generate a personal access token.
/// </summary>
/// <param name="name">Name of the personal access token. If left empty a GUID will be used.</param>
/// <param name="user">The name of the user that owns this personal access token.</param>
/// <param name="scope">The scope that will be given to the token.</param>
/// <param name="expirationInDays">Days until the tokens expires.</param>
/// <returns></returns>
public async Task<PersonalAccessToken> GenerateAccessToken(string user, PersonalAccessTokenScopes scope, string name = "", int expirationInDays = 365)
=> await GenerateAccessToken(new PersonalAccessToken
{
Name = string.IsNullOrWhiteSpace(name) ? Guid.NewGuid().ToString() : name,
User = user,
Scope = scope,
ExpirationInDays = expirationInDays
});
}
36 changes: 36 additions & 0 deletions src/Testcontainers.Gitlab/Models/PersonalAccessToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Testcontainers.Gitlab.Models;

/// <summary>
/// The personal access token that is used to authenticate against the API from gitlab.
/// </summary>
public record PersonalAccessToken
{
/// <param name="name">Name of the personal access token. If left empty a GUID will be used.</param>
/// <param name="user">The name of the user that owns this personal access token.</param>
/// <param name="scope">The scope that will be given to the token.</param>
/// <param name="expirationInDays">Days until the tokens expires.</param>
/// <summary>
/// Name of the personal access token. If left empty a GUID will be used.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The name of the user that owns this personal access token.
/// </summary>
public string User { get; set; } = string.Empty;
/// <summary>
/// The scope that will be given to the token.
/// </summary>
public PersonalAccessTokenScopes Scope { get; set; } = PersonalAccessTokenScopes.None;
/// <summary>
/// Days until the tokens expires.
/// </summary>
public int ExpirationInDays { get; set; } = 365;
/// <summary>
/// Internal token that is used to set the token publically.
/// </summary>
internal string TokenInternal { get; set; } = string.Empty;
/// <summary>
/// The token that will be generated.
/// </summary>
public string Token => TokenInternal;
}
18 changes: 18 additions & 0 deletions src/Testcontainers.Gitlab/Models/PersonalAccessTokenScopes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Testcontainers.Gitlab.Models
{
[Flags]
public enum PersonalAccessTokenScopes
{
None = 0,
api = 2,
read_api = 4,
read_user = 8,
read_repository = 16,
write_repository = 32,
read_registry = 64,
write_registry = 128,
create_runner = 256,
ai_features = 512,
k8s_proxy = 1024,
}
}
18 changes: 18 additions & 0 deletions src/Testcontainers.Gitlab/RegexPatterns/GitlabRegex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.RegularExpressions;

namespace Testcontainers.Gitlab.RegexPatterns;

/// <summary>
/// This class contains regex patterns that are used in gitlab.
/// </summary>
public static partial class GitlabRegex
{
/// <summary>
/// GitLab Personal Access Token
/// </summary>
public static Regex GitlabPersonalAccessToken => new(@"glpat-[0-9a-zA-Z_\-]{20}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// Regex Pattern to find the gitlab root password
/// </summary>
public static Regex GitlabRootPassword => new(@"Password: .*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
12 changes: 12 additions & 0 deletions src/Testcontainers.Gitlab/Testcontainers.Gitlab.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
15 changes: 15 additions & 0 deletions src/Testcontainers.Gitlab/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Net;
global using System.Linq;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers;
global using DotNet.Testcontainers.Builders;
global using DotNet.Testcontainers.Configurations;
global using DotNet.Testcontainers.Containers;
global using JetBrains.Annotations;
global using Microsoft.Extensions.Logging;
1 change: 1 addition & 0 deletions tests/Testcontainers.Gitlab.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true