Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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,13 @@
package org.jenkinsci.test.acceptance.docker.fixtures;

import java.io.IOException;
import java.net.URI;
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,12 +18,13 @@
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 int READINESS_TIMEOUT_SECONDS = 600;
private static final int READINESS_POLL_INTERVAL_SECONDS = 10;
private static final int READINESS_REQUEST_TIMEOUT_SECONDS = 5;
private static final int READINESS_CONNECTION_TIMEOUT_MILLISECONDS = 500;

private static final ElasticTime time = new ElasticTime();

Expand All @@ -46,67 +44,54 @@ 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(Duration.ofMillis(time.milliseconds(READINESS_CONNECTION_TIMEOUT_MILLISECONDS)))
.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(Duration.ofSeconds(time.seconds(READINESS_TIMEOUT_SECONDS)))
.pollingEvery(Duration.ofSeconds(READINESS_POLL_INTERVAL_SECONDS))
.until(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(getHttpUrl().toURI())
var request = HttpRequest.newBuilder()
.uri(new URL(getHttpUrl()).toURI())
.GET()
.timeout(Duration.ofSeconds(1))
.timeout(Duration.ofSeconds(READINESS_REQUEST_TIMEOUT_SECONDS))
.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 | InterruptedException ignored) {
return false;
}
});
}

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 All @@ -119,4 +104,41 @@ public String createUserToken(String userName, String password, String email, St
.verifyOrDieWith("Unable to create user")
.trim();
}

// attempt to cleanup, no retry
public void cleanup(String token, String repoName, String groupName) {
try (var gitlabapi = new GitLabApi(getHttpUrl(), token)
.withRequestTimeout(GITLAB_API_CONNECT_TIMEOUT_MS, GITLAB_API_READ_TIMEOUT_MS)) {

try {
gitlabapi.getProjectApi().getProjects(repoName).stream()
.filter(proj -> repoName.equals(proj.getName()))
.findFirst()
.ifPresent(project -> {
try {
gitlabapi.getProjectApi().deleteProject(project);
} catch (GitLabApiException e) {
throw new RuntimeException("Failed to delete project: " + repoName, e);
}
});
} catch (Exception e) {
System.err.println("Failed to delete repo '" + repoName + "': " + e.getMessage());
}

try {
gitlabapi.getGroupApi().getGroups(groupName).stream()
.filter(g -> groupName.equals(g.getName()))
.findFirst()
.ifPresent(group -> {
try {
gitlabapi.getGroupApi().deleteGroup(group.getId());
} catch (GitLabApiException e) {
throw new RuntimeException("Failed to delete group: " + groupName, e);
}
});
} catch (Exception e) {
System.err.println("Failed to delete group '" + groupName + "': " + e.getMessage());
}
}
}
}
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 @@ -29,6 +28,7 @@ public String getCheckLog() {

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

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 @@ -38,8 +37,17 @@ public String getBranchIndexingLog() {
}
}

public String getBranchIndexingLogText() {
try {
return IOUtils.toString(url("indexing/consoleText").openStream(), 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,100 @@ FROM gitlab/gitlab-ce:18.6.1-ce.0

COPY create_user.rb /usr/bin/

# Optimize GitLab for testing: disable monitoring to avoid /dev/shm exhaustion,
# disable unused services, tune for 4GB memory (Puma 2 workers, Sidekiq 10 threads)
#
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