Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package org.jenkinsci.test.acceptance.docker.fixtures;

import java.io.IOException;
import java.net.URI;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import org.gitlab4j.api.GitLabApi;
import org.gitlab4j.api.GitLabApiException;
import org.gitlab4j.api.ProjectApi;
import org.gitlab4j.api.models.Project;
import org.jenkinsci.test.acceptance.docker.Docker;
import org.jenkinsci.test.acceptance.docker.DockerContainer;
import org.jenkinsci.test.acceptance.docker.DockerFixture;
Expand All @@ -21,15 +17,20 @@
id = "gitlab-plugin",
ports = {80, 443, 22})
public class GitLabContainer extends DockerContainer {
protected static final String REPO_DIR = "/home/gitlab/gitlabRepo";
public static final int GITLAB_API_CONNECT_TIMEOUT_MS = 30_000;
public static final int GITLAB_API_READ_TIMEOUT_MS = 120_000;

private static final HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofMillis(200))
.build();
private static final Duration READINESS_TIMEOUT = Duration.ofMinutes(10);
private static final Duration READINESS_POLL_INTERVAL = Duration.ofSeconds(10);
private static final Duration READINESS_REQUEST_TIMEOUT = Duration.ofSeconds(1);
private static final Duration READINESS_CONNECTION_TIMEOUT = Duration.ofMillis(500);

private static final ElasticTime time = new ElasticTime();

private static Duration time(Duration duration) {
return Duration.ofMillis(time.milliseconds(duration.toMillis()));
}

public String host() {
return ipBound(22);
}
Expand All @@ -46,67 +47,55 @@ public String httpHost() {
return ipBound(80);
}

public URL getURL() throws IOException {
return new URL("http://" + getIpAddress() + sshPort());
public String getHttpUrl() {
return "http://" + httpHost() + ":" + httpPort();
}

public URL getHttpUrl() throws IOException {
return new URL("http", httpHost(), httpPort(), "");
/**
* @return Authenticated Git URL ("http://username:token@host:port/username/repo.git")
*/
public String repoUrl(String projectPath, String token) {
String username = projectPath.split("/")[0];
return getHttpUrl().replace("://", "://" + username + ":" + token + "@") + "/" + projectPath + ".git";
}

/** URL visible from the host. */
public String getRepoUrl() {
return "ssh://git@" + host() + ":" + sshPort() + REPO_DIR;
/**
* Extracts the GitLab project path from an authenticated Git repository URL.
*
* @param repoUrl see {@link GitLabContainer#repoUrl(String, String)}
* @return Project path ("username/repo")
*/
public String extractProjectPath(String repoUrl) {
String afterAuth = repoUrl.split("@")[1];
return afterAuth.split("/", 2)[1].replace(".git", "");
}

public void waitForReady(CapybaraPortingLayer p) {
var client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(time(READINESS_CONNECTION_TIMEOUT))
.build();

p.waitFor()
.withMessage("Waiting for GitLab to come up")
.withTimeout(Duration.ofSeconds(200)) // GitLab starts in about 2 minutes add some headway
.pollingEvery(Duration.ofSeconds(2))
.withTimeout(READINESS_TIMEOUT)
.pollingEvery(READINESS_POLL_INTERVAL)
.ignoring(UncheckedIOException.class)
.until(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(getHttpUrl().toURI())
var request = HttpRequest.newBuilder()
.uri(new URL(getHttpUrl()).toURI())
.GET()
.timeout(Duration.ofSeconds(1))
.timeout(time(READINESS_REQUEST_TIMEOUT))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body().contains("GitLab Community Edition");
} catch (IOException ignored) {
// we can not use .ignoring as this is a checked exception (even though a callable can throw
// this!)
return Boolean.FALSE;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}

public HttpResponse<String> createRepo(String repoName, String token) throws RuntimeException {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(getHttpUrl() + "/api/v4/projects"))
.header("Content-Type", "application/json")
.header("PRIVATE-TOKEN", token)
.POST(HttpRequest.BodyPublishers.ofString("{ \"name\": \"" + repoName + "\" }"))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void deleteRepo(String token, String repoName) throws IOException, GitLabApiException {
// get the project and delete the project
GitLabApi gitlabapi = new GitLabApi(getHttpUrl().toString(), token);
ProjectApi projApi = new ProjectApi(gitlabapi);

Project project = projApi.getProjects().stream()
.filter((proj -> repoName.equals(proj.getName())))
.findAny()
.orElse(null);
projApi.deleteProject(project);
}

public String createUserToken(String userName, String password, String email, String isAdmin)
throws IOException, InterruptedException {
return Docker.cmd("exec", getCid())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public void setOwner(String owner) {
find(by.path("/sources/source/projectOwner")).sendKeys(owner);
}

public void setProject(String owner, String project) {
find(by.path("/sources/source/projectPath")).click();
waitFor(by.option(owner + "/" + project)).click();
public void enableTagDiscovery() {
control("/hetero-list-add[traits]").selectDropdownMenu("Discover tags");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ public GitLabOrganizationFolder(Injector injector, URL url, String name) {
}

public void create(String owner) {
control(by.path("/hetero-list-add[navigators]")).click();
find(by.partialLinkText("GitLab Group")).click();
control(by.path("/hetero-list-add[navigators]")).selectDropdownMenu("GitLab Group");
find(by.path("/navigators/projectOwner")).sendKeys(owner);
}

Expand All @@ -27,9 +26,10 @@ public String getCheckLog() {
return driver.getPageSource();
}

public GitLabOrganizationFolder waitForCheckFinished(final int timeout) {
public GitLabOrganizationFolder waitForCheckFinished(Duration timeout) {
waitFor()
.withTimeout(Duration.ofSeconds(timeout))
.withMessage("Waiting for GitLab group scan to finish in %s", this.name)
.withTimeout(timeout)
.until(() -> GitLabOrganizationFolder.this.getCheckLog().contains("Finished: "));

return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

/**
* A pipeline multi-branch job (requires installation of multi-branch-project-plugin).
*
*/
@Describable("org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject")
public class WorkflowMultiBranchJob extends Folder {
Expand All @@ -31,15 +30,24 @@ public <T extends BranchSource> T addBranchSource(final Class<T> type) {
}

public String getBranchIndexingLog() {
try {
return IOUtils.toString(url("indexing/console").openStream(), StandardCharsets.UTF_8);
try (var in = url("indexing/console").openStream()) {
return IOUtils.toString(in, StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new AssertionError(ex);
}
}

public String getBranchIndexingLogText() {
try (var in = url("indexing/consoleText").openStream()) {
return IOUtils.toString(in, StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new AssertionError(ex);
}
}

public WorkflowMultiBranchJob waitForBranchIndexingFinished(final int timeout) {
waitFor()
.withMessage("Waiting for branch indexing to finish in %s", this.name)
.withTimeout(Duration.ofMillis(super.time.seconds(timeout)))
.until(() -> WorkflowMultiBranchJob.this.getBranchIndexingLog().contains("Finished: "));

Expand All @@ -50,6 +58,7 @@ public WorkflowJob getJob(final String name) {
return this.getJobs().get(WorkflowJob.class, name);
}

// NOTE: GitLab uses a different selector see GitLabPluginTest#reIndex
public void reIndex() {
final List<WebElement> scanRepoNow =
driver.findElements(by.xpath("//div[@class=\"task\"]//*[text()=\"Scan Repository Now\"]"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,118 @@ FROM gitlab/gitlab-ce:18.6.1-ce.0

COPY create_user.rb /usr/bin/

# ╔══════════════════════════════════════════════════════════════════════════╗
# ║ BEHOLD! THE GITLAB TEST CONTAINER OF MINIMALISM! ║
# ║ ║
# ║ None of these are required for tests to pass - feel free to remove them! ║
# ║ These optimizations exist solely to reduce CI resource usage. ║
# ║ ║
# ║ "With great performance comes great configuration complexity" ║
# ║ - Ancient Proverb ║
# ╚══════════════════════════════════════════════════════════════════════════╝
#
# https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
# https://docs.gitlab.com/omnibus/settings/memory_constrained_envs
# https://docs.gitlab.com/omnibus/settings/redis
# https://docs.gitlab.com/omnibus/settings/nginx
# https://docs.gitlab.com/omnibus/settings/database
# https://docs.gitlab.com/administration/environment_variables
# https://docs.gitlab.com/ee/administration/operations/puma
# https://docs.gitlab.com/ee/administration/sidekiq
# https://docs.gitlab.com/ee/administration/gitaly
# https://docs.gitlab.com/ee/administration/monitoring/prometheus

ENV GITLAB_SKIP_PG_UPGRADE=true \
GITLAB_SKIP_TAIL_LOGS=true \
GITLAB_POST_RECONFIGURE_SCRIPT="" \
GITLAB_ROOT_PASSWORD=testpassword123 \
GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN="" \
GITLAB_SKIP_UNMIGRATED_DATA_CHECK=true \
GITLAB_SKIP_RECONFIGURE=false

ENV GITLAB_OMNIBUS_CONFIG="prometheus_monitoring['enable'] = false; \
node_exporter['enable'] = false; \
redis_exporter['enable'] = false; \
postgres_exporter['enable'] = false; \
gitlab_exporter['enable'] = false; \
alertmanager['enable'] = false; \
registry['enable'] = false; \
gitlab_pages['enable'] = false; \
gitlab_kas['enable'] = false; \
sentinel['enable'] = false; \
mattermost['enable'] = false; \
storage_check['enable'] = false; \
gitlab_sshd['enable'] = false; \
logrotate['enable'] = false; \
gitlab_rails['gitlab_default_projects_features_builds'] = false; \
gitlab_rails['gitlab_default_projects_features_container_registry'] = false; \
gitlab_rails['gitlab_default_projects_features_issues'] = false; \
gitlab_rails['gitlab_default_projects_features_wiki'] = false; \
gitlab_rails['gitlab_default_projects_features_snippets'] = false; \
gitlab_rails['gitlab_email_enabled'] = false; \
gitlab_rails['incoming_email_enabled'] = false; \
gitlab_rails['terraform_state_enabled'] = false; \
gitlab_rails['packages_enabled'] = false; \
gitlab_rails['dependency_proxy_enabled'] = false; \
gitlab_rails['actioncable_enabled'] = false; \
gitlab_rails['omniauth_enabled'] = false; \
gitlab_rails['gravatar_enabled'] = false; \
gitlab_rails['automatic_issue_creation_enabled'] = false; \
gitlab_rails['gitlab_shell_ssh_port'] = 0; \
gitlab_rails['gitaly_timeout'] = 30; \
gitlab_rails['backup_keep_time'] = 0; \
gitlab_rails['usage_ping_enabled'] = false; \
gitlab_rails['sentry_enabled'] = false; \
gitlab_rails['db_pool'] = 10; \
gitlab_rails['cron_jobs'] = {}; \
gitlab_rails['auto_migrate'] = true; \
gitlab_rails['initial_root_password'] = 'testpassword123'; \
gitlab_rails['monitoring_whitelist'] = []; \
gitlab_rails['pipeline_schedule_worker_cron'] = ''; \
gitlab_rails['stuck_ci_jobs_worker_cron'] = ''; \
gitlab_rails['analytics_usage_trends_cron_worker_cron'] = ''; \
gitlab_rails['analytics_devops_adoption_cron_worker_cron'] = ''; \
gitlab_rails['env'] = { 'MALLOC_ARENA_MAX' => '2', 'MALLOC_CONF' => 'dirty_decay_ms:1000,muzzy_decay_ms:1000' }; \
gitlab_rails['migrate_timeout'] = 60; \
gitlab_rails['db_statement_timeout'] = 15000; \
gitlab_rails['rake_cache_clear'] = false; \
gitlab_rails['db_load_balancing'] = { 'hosts' => [] }; \
puma['worker_processes'] = 1; \
puma['min_threads'] = 1; \
puma['max_threads'] = 2; \
puma['worker_timeout'] = 30; \
puma['per_worker_max_memory_mb'] = 512; \
sidekiq['concurrency'] = 5; \
sidekiq['max_retries'] = 1; \
sidekiq['queue_groups'] = ['*']; \
gitaly['env'] = { 'GITALY_COMMAND_SPAWN_MAX_PARALLEL' => '2' }; \
postgresql['max_connections'] = 50; \
postgresql['work_mem'] = '4MB'; \
postgresql['shared_buffers'] = '128MB'; \
postgresql['checkpoint_completion_target'] = 0.7; \
postgresql['checkpoint_timeout'] = '15min'; \
postgresql['wal_buffers'] = '8MB'; \
postgresql['fsync'] = 'off'; \
postgresql['synchronous_commit'] = 'off'; \
postgresql['full_page_writes'] = 'off'; \
postgresql['autovacuum'] = 'off'; \
postgresql['track_activities'] = 'off'; \
postgresql['track_counts'] = 'off'; \
postgresql['track_io_timing'] = 'off'; \
postgresql['log_statement'] = 'none'; \
postgresql['log_duration'] = 'off'; \
postgresql['log_min_duration_statement'] = 1000; \
gitaly['configuration'] = { concurrency: [{ rpc: '/gitaly.SmartHTTPService/PostReceivePack', max_per_repo: 2 }, { rpc: '/gitaly.SSHService/SSHUploadPack', max_per_repo: 2 }], git: { catfile_cache_size: 5 } }; \
nginx['worker_processes'] = 1; \
nginx['worker_connections'] = 256; \
nginx['gzip_enabled'] = false; \
nginx['keepalive_timeout'] = 10; \
nginx['status'] = { 'enable' => false }; \
redis['maxmemory'] = '256mb'; \
redis['maxmemory_policy'] = 'allkeys-lru'; \
redis['save'] = []; \
redis['appendonly'] = 'no'; \
redis['stop_writes_on_bgsave_error'] = 'no';"

# Expose the required ports
EXPOSE 80 443 22
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
input_array = ARGV

user = User.create();
user.name = input_array[0];
user.username = input_array[0];
user.password = input_array[1];
user.confirmed_at = '01/01/1990';
user.admin = input_array[3];
user.email = input_array[2];
user.save!;

token = user.personal_access_tokens.create(scopes: [:api], name: 'MyToken');
token.expires_at='01/01/2024';
token.save!;
puts token.token;
# frozen_string_literal: true

# Script to create GitLab user with personal access token
# Usage: gitlab-rails runner create_user.rb <username> <password> <email> <is_admin>

username, password, email, is_admin = ARGV

# Ensure default organization exists
default_org = Organizations::Organization.find_or_create_by!(name: 'Default', path: 'default') do |org|
org.description = 'Default organization for test users'
end

# Create user with namespace and organization
user_params = {
name: username,
username: username,
password: password,
password_confirmation: password,
email: email,
admin: is_admin == 'true',
skip_confirmation: true,
organization_id: default_org&.id
}.compact

result = Users::CreateService.new(nil, user_params).execute

raise "Failed to create user: #{result.message}" unless result.success?

user = result.payload[:user]

raise "Failed to create user. Result: #{result.inspect}" unless user&.persisted?

# Create personal access token with 1-month expiration
token = user.personal_access_tokens.create!(
scopes: [:api, :read_user, :read_api, :read_repository, :write_repository],
name: 'MyToken',
expires_at: 30.days.from_now
)

puts token.token
Loading