-
-
Notifications
You must be signed in to change notification settings - Fork 289
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
base: develop
Are you sure you want to change the base?
Changes from 14 commits
9937337
ef08260
5c4e69d
70a3dd4
2a84111
4b66ecf
f312ba4
e24c659
2fe34db
ee97b6d
2bcc74f
ddb7d74
656a9a5
04a30fc
aaeca1f
321477f
b8db1fb
24f1ca5
754260a
b9aab60
5acf244
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
root = true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
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. | ||
/// </summary> | ||
public const string GitlabImage = "gitlab/gitlab-ce"; | ||
/// <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) | ||
.WithWaitStrategy(Wait.ForUnixContainer().UntilFileExists("/etc/gitlab/initial_root_password")) | ||
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(80)) | ||
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(22)) | ||
.WithWaitStrategy(Wait.ForUnixContainer().UntilContainerIsHealthy()) | ||
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPath("/users/sign_in").ForStatusCode(HttpStatusCode.OK))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This overrides the previous wait strategies. You need to add the wait strategy once (you can chain the until members though). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've fixed the WaitStrategy so that the previous strategy will not be overwritten. |
||
} | ||
|
||
/// <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)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
namespace Testcontainers.Gitlab; | ||
|
||
/// <inheritdoc cref="ContainerConfiguration" /> | ||
[PublicAPI] | ||
public sealed class GitlabConfiguration : ContainerConfiguration | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="GitlabConfiguration" /> class. | ||
/// </summary> | ||
public GitlabConfiguration() | ||
{ | ||
} | ||
|
||
/// <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) | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
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> | ||
/// The password for the root user | ||
/// </summary> | ||
public string RootPassword { get; private set; } | ||
|
||
public override async Task StartAsync(CancellationToken ct = default) | ||
{ | ||
await base.StartAsync(ct); | ||
RootPassword = await GetInitialRootPassword(); | ||
} | ||
|
||
/// <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 | ||
}); | ||
|
||
|
||
/// <summary> | ||
/// This method returns the initial root password for the gitlab root user. | ||
/// </summary> | ||
/// <returns>Returns the initial root password generated by gitlab.</returns> | ||
private async Task<string> GetInitialRootPassword() | ||
{ | ||
var byteArray = await ReadFileAsync("/etc/gitlab/initial_root_password"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testcontainers allows uploading files before the container starts. Can we upload the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I checked last night if there is some easier method to provide a root password and there a sereval ways. You can set the root password with an environment variable named I would strongly prefer to set the root password via an environment variable, would either way would be valid. I try to find some time this week to resolve this. If you have an preferred implementation than just let me know. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be fixed now. I've selected the approach via an environment variable just to keep it simple. |
||
|
||
string fileContent = Encoding.UTF8.GetString(byteArray); | ||
var match = GitlabRegex.GitlabRootPassword.Match(fileContent); | ||
string[] splits = match.Value.Split(' '); | ||
return splits[1]; | ||
} | ||
} |
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; | ||
} |
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, | ||
} | ||
} |
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); | ||
} |
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> |
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
root = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please pin the version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing. I tested with the latest image, with is version 16.11.1 .