Improved gitlab api autodetection (#650)

* API autodetection without restart is working
* changed the order in which the plugin tries the api. it's now v4 then v3
This commit is contained in:
Karsten Kraus 2017-10-31 18:26:34 +01:00 committed by Owen Mehegan
parent 3887c52c9e
commit 9428abd4b3
13 changed files with 403 additions and 75 deletions

View File

@ -1,12 +1,17 @@
package com.dabsquared.gitlabjenkins.gitlab.api;
import com.dabsquared.gitlabjenkins.gitlab.api.model.*;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Branch;
import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Label;
import com.dabsquared.gitlabjenkins.gitlab.api.model.MergeRequest;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Pipeline;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Project;
import com.dabsquared.gitlabjenkins.gitlab.api.model.User;
import com.dabsquared.gitlabjenkins.gitlab.hook.model.State;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.ws.rs.PathParam;
import java.util.List;
@ -50,5 +55,5 @@ public interface GitLabApi {
List<Label> getLabels(String projectId);
List<Pipeline> getPipelines(@PathParam("projectId") String projectName);
List<Pipeline> getPipelines(String projectName);
}

View File

@ -33,9 +33,11 @@ public abstract class GitLabClientBuilder implements Comparable<GitLabClientBuil
}
private final String id;
private final int ordinal;
protected GitLabClientBuilder(String id) {
protected GitLabClientBuilder(String id, int ordinal) {
this.id = id;
this.ordinal = ordinal;
}
@Nonnull
@ -48,6 +50,7 @@ public abstract class GitLabClientBuilder implements Comparable<GitLabClientBuil
@Override
public final int compareTo(@Nonnull GitLabClientBuilder other) {
return id().compareTo(other.id());
int o = ordinal - other.ordinal;
return o != 0 ? o : id().compareTo(other.id());
}
}

View File

@ -8,46 +8,22 @@ import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.annotation.Nonnull;
import javax.ws.rs.NotFoundException;
import java.util.NoSuchElementException;
import java.util.ArrayList;
import java.util.Collection;
@Extension
@Restricted(NoExternalUse.class)
public final class AutodetectGitLabClientBuilder extends GitLabClientBuilder {
public AutodetectGitLabClientBuilder() {
super("autodetect");
super("autodetect", 0);
}
@Override
@Nonnull
public GitLabClient buildClient(String url, String token, boolean ignoreCertificateErrors, int connectionTimeout, int readTimeout) {
return autodetectOrDie(url, token, ignoreCertificateErrors, connectionTimeout, readTimeout);
}
private GitLabClient autodetectOrDie(String url, String token, boolean ignoreCertificateErrors, int connectionTimeout, int readTimeout) {
GitLabClient client = autodetect(url, token, ignoreCertificateErrors, connectionTimeout, readTimeout);
if (client != null) {
return client;
}
throw new NoSuchElementException("no client-builder found that supports server at " + url);
}
private GitLabClient autodetect(String url, String token, boolean ignoreCertificateErrors, int connectionTimeout, int readTimeout) {
for (GitLabClientBuilder candidate : getAllGitLabClientBuilders()) {
if (candidate == this) {
continue; // ignore ourself...
}
GitLabClient client = candidate.buildClient(url, token, ignoreCertificateErrors, connectionTimeout, readTimeout);
try {
client.headCurrentUser();
return client;
} catch (NotFoundException ignored) {
// api-endpoint not found (== api-level not supported by this client)
}
}
return null;
Collection<GitLabClientBuilder> candidates = new ArrayList<>(getAllGitLabClientBuilders());
candidates.remove(this);
return new GitLabClient(url, new AutodetectingGitlabApi(candidates, url, token, ignoreCertificateErrors, connectionTimeout, readTimeout));
}
}

View File

@ -0,0 +1,321 @@
package com.dabsquared.gitlabjenkins.gitlab.api.impl;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabApi;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabClient;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabClientBuilder;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Branch;
import com.dabsquared.gitlabjenkins.gitlab.api.model.BuildState;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Label;
import com.dabsquared.gitlabjenkins.gitlab.api.model.MergeRequest;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Pipeline;
import com.dabsquared.gitlabjenkins.gitlab.api.model.Project;
import com.dabsquared.gitlabjenkins.gitlab.api.model.User;
import com.dabsquared.gitlabjenkins.gitlab.hook.model.State;
import javax.ws.rs.NotFoundException;
import java.util.List;
import java.util.NoSuchElementException;
final class AutodetectingGitlabApi implements GitLabApi {
private final Iterable<GitLabClientBuilder> builders;
private final String url;
private final String token;
private final boolean ignoreCertificateErrors;
private final int connectionTimeout;
private final int readTimeout;
private GitLabApi delegate;
AutodetectingGitlabApi(Iterable<GitLabClientBuilder> builders, String url, String token, boolean ignoreCertificateErrors, int connectionTimeout, int readTimeout) {
this.builders = builders;
this.url = url;
this.token = token;
this.ignoreCertificateErrors = ignoreCertificateErrors;
this.connectionTimeout = connectionTimeout;
this.readTimeout = readTimeout;
}
@Override
public Project createProject(final String projectName) {
return execute(
new GitLabOperation<Project>() {
@Override
Project execute(GitLabApi api) {
return api.createProject(projectName);
}
});
}
@Override
public MergeRequest createMergeRequest(final Integer projectId, final String sourceBranch, final String targetBranch, final String title) {
return execute(
new GitLabOperation<MergeRequest>() {
@Override
MergeRequest execute(GitLabApi api) {
return api.createMergeRequest(projectId, sourceBranch, targetBranch, title);
}
});
}
@Override
public Project getProject(final String projectName) {
return execute(
new GitLabOperation<Project>() {
@Override
Project execute(GitLabApi api) {
return api.getProject(projectName);
}
});
}
@Override
public Project updateProject(final String projectId, final String name, final String path) {
return execute(
new GitLabOperation<Project>() {
@Override
Project execute(GitLabApi api) {
return api.updateProject(projectId, name, path);
}
});
}
@Override
public void deleteProject(final String projectId) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.deleteProject(projectId);
return null;
}
});
}
@Override
public void addProjectHook(final String projectId, final String url, final Boolean pushEvents, final Boolean mergeRequestEvents, final Boolean noteEvents) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.addProjectHook(projectId, url, pushEvents, mergeRequestEvents, noteEvents);
return null;
}
});
}
@Override
public void changeBuildStatus(final String projectId, final String sha, final BuildState state, final String ref, final String context, final String targetUrl, final String description) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.changeBuildStatus(projectId, sha, state, ref, context, targetUrl, description);
return null;
}
});
}
@Override
public void changeBuildStatus(final Integer projectId, final String sha, final BuildState state, final String ref, final String context, final String targetUrl, final String description) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.changeBuildStatus(projectId, sha, state, ref, context, targetUrl, description);
return null;
}
});
}
@Override
public void getCommit(final String projectId, final String sha) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.getCommit(projectId, sha);
return null;
}
});
}
@Override
public void acceptMergeRequest(final Integer projectId, final Integer mergeRequestId, final String mergeCommitMessage, final boolean shouldRemoveSourceBranch) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.acceptMergeRequest(projectId, mergeRequestId, mergeCommitMessage, shouldRemoveSourceBranch);
return null;
}
});
}
@Override
public void createMergeRequestNote(final Integer projectId, final Integer mergeRequestId, final String body) {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.createMergeRequestNote(projectId, mergeRequestId, body);
return null;
}
});
}
@Override
public List<MergeRequest> getMergeRequests(final String projectId, final State state, final int page, final int perPage) {
return execute(
new GitLabOperation<List<MergeRequest>>() {
@Override
List<MergeRequest> execute(GitLabApi api) {
return api.getMergeRequests(projectId, state, page, perPage);
}
});
}
@Override
public List<Branch> getBranches(final String projectId) {
return execute(
new GitLabOperation<List<Branch>>() {
@Override
List<Branch> execute(GitLabApi api) {
return api.getBranches(projectId);
}
});
}
@Override
public Branch getBranch(final String projectId, final String branch) {
return execute(
new GitLabOperation<Branch>() {
@Override
Branch execute(GitLabApi api) {
return api.getBranch(projectId, branch);
}
});
}
@Override
public void headCurrentUser() {
execute(
new GitLabOperation<Void>() {
@Override
Void execute(GitLabApi api) {
api.headCurrentUser();
return null;
}
});
}
@Override
public User getCurrentUser() {
return execute(
new GitLabOperation<User>() {
@Override
User execute(GitLabApi api) {
return api.getCurrentUser();
}
});
}
@Override
public User addUser(final String email, final String username, final String name, final String password) {
return execute(
new GitLabOperation<User>() {
@Override
User execute(GitLabApi api) {
return api.addUser(email, username, name, password);
}
});
}
@Override
public User updateUser(final String userId, final String email, final String username, final String name, final String password) {
return execute(
new GitLabOperation<User>() {
@Override
User execute(GitLabApi api) {
return api.updateUser(userId, email, username, name, password);
}
});
}
@Override
public List<Label> getLabels(final String projectId) {
return execute(
new GitLabOperation<List<Label>>() {
@Override
List<Label> execute(GitLabApi api) {
return api.getLabels(projectId);
}
});
}
@Override
public List<Pipeline> getPipelines(final String projectName) {
return execute(
new GitLabOperation<List<Pipeline>>() {
@Override
List<Pipeline> execute(GitLabApi api) {
return api.getPipelines(projectName);
}
});
}
private GitLabApi delegate(boolean reset) {
if (reset || delegate == null) {
delegate = autodetectOrDie();
}
return delegate;
}
private GitLabClient autodetectOrDie() {
GitLabClient client = autodetect();
if (client != null) {
return client;
}
throw new NoSuchElementException("no client-builder found that supports server at " + url);
}
private GitLabClient autodetect() {
for (GitLabClientBuilder candidate : builders) {
GitLabClient client = candidate.buildClient(url, token, ignoreCertificateErrors, connectionTimeout, readTimeout);
try {
client.headCurrentUser();
return client;
} catch (NotFoundException ignored) {
// api-endpoint not found (== api-level not supported by this client)
}
}
return null;
}
private <R> R execute(GitLabOperation<R> operation) {
return operation.execute(false);
}
private abstract class GitLabOperation<R> {
private R execute(boolean reset) {
try {
return execute(delegate(reset));
} catch (NotFoundException e) {
if (reset) {
throw e;
}
return execute(true);
}
}
abstract R execute(GitLabApi api);
}
}

View File

@ -68,8 +68,8 @@ public class ResteasyGitLabClientBuilder extends GitLabClientBuilder {
private final Class<? extends GitLabApi> apiProxyClass;
ResteasyGitLabClientBuilder(String id, Class<? extends GitLabApi> apiProxyClass) {
super(id);
ResteasyGitLabClientBuilder(String id, int ordinal, Class<? extends GitLabApi> apiProxyClass) {
super(id, ordinal);
this.apiProxyClass = apiProxyClass;
}

View File

@ -19,7 +19,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.List;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.V3GitLabClientBuilder.ID;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.V3GitLabApiProxy.ID;
/**
@ -27,6 +27,8 @@ import static com.dabsquared.gitlabjenkins.gitlab.api.impl.V3GitLabClientBuilder
*/
@Path("/api/" + ID)
interface V3GitLabApiProxy extends GitLabApi {
String ID = "v3";
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)

View File

@ -9,9 +9,9 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
@Extension
@Restricted(NoExternalUse.class)
public final class V3GitLabClientBuilder extends ResteasyGitLabClientBuilder {
static final String ID = "v3";
private static final int ORDINAL = 2;
public V3GitLabClientBuilder() {
super(ID, V3GitLabApiProxy.class);
super(V3GitLabApiProxy.ID, ORDINAL, V3GitLabApiProxy.class);
}
}

View File

@ -19,7 +19,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.List;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.V4GitLabClientBuilder.ID;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.V4GitLabApiProxy.ID;
/**
@ -27,6 +27,8 @@ import static com.dabsquared.gitlabjenkins.gitlab.api.impl.V4GitLabClientBuilder
*/
@Path("/api/" + ID)
interface V4GitLabApiProxy extends GitLabApi {
String ID = "v4";
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)

View File

@ -9,9 +9,9 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
@Extension
@Restricted(NoExternalUse.class)
public final class V4GitLabClientBuilder extends ResteasyGitLabClientBuilder {
static final String ID = "v4";
private static final int ORDINAL = 1;
public V4GitLabClientBuilder() {
super(ID, V4GitLabApiProxy.class);
super(V4GitLabApiProxy.ID, ORDINAL, V4GitLabApiProxy.class);
}
}

View File

@ -22,11 +22,11 @@ public class GitLabClientBuilderTest {
public JenkinsRule jenkins = new JenkinsRule();
@Test
public void getAllGitLabClientBuilders_list_is_sorted_by_id() {
public void getAllGitLabClientBuilders_list_is_sorted_by_ordinal() {
List<GitLabClientBuilder> builders = getAllGitLabClientBuilders();
assertThat(builders.get(0), instanceOf(AutodetectGitLabClientBuilder.class));
assertThat(builders.get(1), instanceOf(V3GitLabClientBuilder.class));
assertThat(builders.get(2), instanceOf(V4GitLabClientBuilder.class));
assertThat(builders.get(1), instanceOf(V4GitLabClientBuilder.class));
assertThat(builders.get(2), instanceOf(V3GitLabClientBuilder.class));
}
@Test

View File

@ -1,28 +1,27 @@
package com.dabsquared.gitlabjenkins.gitlab.api.impl;
import com.dabsquared.gitlabjenkins.gitlab.api.GitLabClientBuilder;
import com.trilead.ssh2.util.TimeoutService;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.mockserver.client.server.MockServerClient;
import org.mockserver.junit.MockServerRule;
import org.mockserver.matchers.Times;
import org.mockserver.model.HttpRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.API_TOKEN;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.addGitLabApiToken;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.buildClientWithDefaults;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.responseNotFound;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.responseOk;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.versionRequest;
import static com.dabsquared.gitlabjenkins.gitlab.api.impl.TestUtility.*;
import static org.junit.Assert.fail;
import static org.mockserver.matchers.Times.exactly;
import static org.mockserver.matchers.Times.once;
public class AutodetectGitLabClientBuilderTest {
public class AutodetectingGitLabApiTest {
@Rule
public MockServerRule mockServer = new MockServerRule(this);
@Rule
@ -30,6 +29,7 @@ public class AutodetectGitLabClientBuilderTest {
private MockServerClient mockServerClient;
private String gitLabUrl;
private GitLabClientBuilder clientBuilder;
private AutodetectingGitlabApi api;
private HttpRequest v3Request;
private HttpRequest v4Request;
@ -38,25 +38,43 @@ public class AutodetectGitLabClientBuilderTest {
gitLabUrl = "http://localhost:" + mockServer.getPort() + "/gitlab";
addGitLabApiToken();
clientBuilder = new AutodetectGitLabClientBuilder();
List<GitLabClientBuilder> builders = Arrays.<GitLabClientBuilder>asList(new V3GitLabClientBuilder(), new V4GitLabClientBuilder());
api = new AutodetectingGitlabApi(builders, gitLabUrl, API_TOKEN, true, 10, 10);
v3Request = versionRequest(V3GitLabClientBuilder.ID);
v4Request = versionRequest(V4GitLabClientBuilder.ID);
v3Request = versionRequest(V3GitLabApiProxy.ID);
v4Request = versionRequest(V4GitLabApiProxy.ID);
}
@Test
public void buildClient_success_v3() throws Exception {
mockServerClient.when(v3Request).respond(responseOk());
TestUtility.assertApiImpl(buildClientWithDefaults(clientBuilder, gitLabUrl),V3GitLabApiProxy.class);
mockServerClient.verify(v3Request);
api.headCurrentUser();
assertApiImpl(api, V3GitLabApiProxy.class);
mockServerClient.verify(v3Request, v3Request);
}
@Test
public void buildClient_success_v4() throws Exception {
mockServerClient.when(v3Request).respond(responseNotFound());
mockServerClient.when(v4Request).respond(responseOk());
TestUtility.assertApiImpl(buildClientWithDefaults(clientBuilder, gitLabUrl),V4GitLabApiProxy.class);
mockServerClient.verify(v3Request, v4Request);
api.headCurrentUser();
assertApiImpl(api, V4GitLabApiProxy.class);
mockServerClient.verify(v3Request, v4Request, v4Request);
}
@Test
public void buildClient_success_switching_apis() throws Exception {
mockServerClient.when(v3Request, once()).respond(responseNotFound());
mockServerClient.when(v4Request, exactly(2)).respond(responseOk());
api.headCurrentUser();
assertApiImpl(api, V4GitLabApiProxy.class);
mockServerClient.when(v4Request, once()).respond(responseNotFound());
mockServerClient.when(v3Request, exactly(2)).respond(responseOk());
api.headCurrentUser();
assertApiImpl(api, V3GitLabApiProxy.class);
mockServerClient.verify(v3Request, v4Request, v4Request, v3Request, v3Request);
}
@Test
@ -64,11 +82,10 @@ public class AutodetectGitLabClientBuilderTest {
mockServerClient.when(v3Request).respond(responseNotFound());
mockServerClient.when(v4Request).respond(responseNotFound());
try {
clientBuilder.buildClient(gitLabUrl, API_TOKEN, true, 10, 10);
fail("buildClient should throw exception when no matching candidate is found");
api.headCurrentUser();
fail("endpoint should throw exception when no matching delegate is found");
} catch (NoSuchElementException e) {
mockServerClient.verify(v3Request, v4Request);
}
}
}

View File

@ -21,7 +21,7 @@ public class ResteasyGitLabClientBuilderTest {
@Test
public void buildClient() throws Exception {
GitLabClientBuilder clientBuilder = new ResteasyGitLabClientBuilder("test", V3GitLabApiProxy.class);
GitLabClientBuilder clientBuilder = new ResteasyGitLabClientBuilder("test", 0, V3GitLabApiProxy.class);
assertApiImpl(buildClientWithDefaults(clientBuilder, "http://localhost/"), V3GitLabApiProxy.class);
}

View File

@ -32,10 +32,10 @@ import static org.mockserver.model.HttpResponse.response;
class TestUtility {
static final String API_TOKEN = "secret";
static final String API_TOKEN_ID = "apiTokenId";
static final boolean IGNORE_CERTIFICATE_ERRORS = true;
static final int CONNECTION_TIMEOUT = 10;
static final int READ_TIMEOUT = 10;
private static final String API_TOKEN_ID = "apiTokenId";
private static final boolean IGNORE_CERTIFICATE_ERRORS = true;
private static final int CONNECTION_TIMEOUT = 10;
private static final int READ_TIMEOUT = 10;
static void addGitLabApiToken() throws IOException {
for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(Jenkins.getInstance())) {
@ -59,11 +59,7 @@ class TestUtility {
return responseWithStatus(NOT_FOUND);
}
static HttpResponse responseUnauthorized() {
return responseWithStatus(UNAUTHORIZED);
}
static HttpResponse responseWithStatus(Status status) {
private static HttpResponse responseWithStatus(Status status) {
return response().withStatusCode(status.getStatusCode());
}
@ -77,5 +73,11 @@ class TestUtility {
assertThat(apiField.get(client), instanceOf(apiImplClass));
}
static void assertApiImpl(AutodetectingGitlabApi api, Class<? extends GitLabApi> apiImplClass) throws Exception {
Field delegate = api.getClass().getDeclaredField("delegate");
delegate.setAccessible(true);
assertApiImpl((GitLabClient) delegate.get(api), apiImplClass);
}
private TestUtility() { /* utility class */ }
}