Merge branch 'stable-3.4' into master * stable-3.4: Allow using pull-replication on replica nodes Change-Id: Ic051cddcc98e0d10a1ba926cd5a3a1246e05b4fd
diff --git a/BUILD b/BUILD index 705c332..82112d1 100644 --- a/BUILD +++ b/BUILD
@@ -10,6 +10,7 @@ "Gerrit-PluginName: pull-replication", "Gerrit-Module: com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule", "Gerrit-SshModule: com.googlesource.gerrit.plugins.replication.pull.SshModule", + "Gerrit-HttpModule: com.googlesource.gerrit.plugins.replication.pull.api.HttpModule" ], resources = glob(["src/main/resources/**/*"]), deps = [ @@ -45,5 +46,6 @@ ), deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [ ":pull-replication__plugin", + "//plugins/replication:replication", ], )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java new file mode 100644 index 0000000..7ae7d54 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
@@ -0,0 +1,40 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication.pull.api; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.httpd.AllRequestFilter; +import com.google.gerrit.server.config.GerritIsReplica; +import com.google.inject.Inject; +import com.google.inject.Scopes; +import com.google.inject.servlet.ServletModule; + +public class HttpModule extends ServletModule { + private boolean isReplica; + + @Inject + public HttpModule(@GerritIsReplica Boolean isReplica) { + this.isReplica = isReplica; + } + + @Override + protected void configureServlets() { + if (isReplica) { + DynamicSet.bind(binder(), AllRequestFilter.class) + .to(PullReplicationFilter.class) + .in(Scopes.SINGLETON); + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java new file mode 100644 index 0000000..65b8e1b --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -0,0 +1,225 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication.pull.api; + +import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import com.google.common.base.Splitter; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.httpd.AllRequestFilter; +import com.google.gerrit.httpd.restapi.RestApiServlet; +import com.google.gerrit.json.OutputFormat; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.restapi.project.ProjectsCollection; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.MalformedJsonException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input; +import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput; +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class PullReplicationFilter extends AllRequestFilter { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private FetchAction fetchAction; + private ApplyObjectAction applyObjectAction; + private ProjectsCollection projectsCollection; + private Gson gson; + private Provider<CurrentUser> userProvider; + + @Inject + public PullReplicationFilter( + FetchAction fetchAction, + ApplyObjectAction applyObjectAction, + ProjectsCollection projectsCollection, + Provider<CurrentUser> userProvider) { + this.fetchAction = fetchAction; + this.applyObjectAction = applyObjectAction; + this.projectsCollection = projectsCollection; + this.userProvider = userProvider; + this.gson = OutputFormat.JSON.newGsonBuilder().create(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + chain.doFilter(request, response); + return; + } + + HttpServletResponse httpResponse = (HttpServletResponse) response; + HttpServletRequest httpRequest = (HttpServletRequest) request; + try { + if (isFetchAction(httpRequest)) { + if (userProvider.get().isIdentifiedUser()) { + writeResponse(httpResponse, doFetch(httpRequest)); + } else { + httpResponse.sendError(SC_UNAUTHORIZED); + } + } else if (isApplyObjectAction(httpRequest)) { + if (userProvider.get().isIdentifiedUser()) { + writeResponse(httpResponse, doApplyObject(httpRequest)); + } else { + httpResponse.sendError(SC_UNAUTHORIZED); + } + } else { + chain.doFilter(request, response); + } + + } catch (AuthException e) { + RestApiServlet.replyError( + httpRequest, httpResponse, SC_FORBIDDEN, e.getMessage(), e.caching(), e); + } catch (MalformedJsonException | JsonParseException e) { + logger.atFine().withCause(e).log("REST call failed on JSON parsing"); + RestApiServlet.replyError( + httpRequest, httpResponse, SC_BAD_REQUEST, "Invalid json in request", e); + } catch (BadRequestException e) { + RestApiServlet.replyError(httpRequest, httpResponse, SC_BAD_REQUEST, e.getMessage(), e); + } catch (UnprocessableEntityException e) { + RestApiServlet.replyError( + httpRequest, httpResponse, SC_UNPROCESSABLE_ENTITY, e.getMessage(), e.caching(), e); + } catch (ResourceConflictException e) { + RestApiServlet.replyError( + httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e); + } catch (Exception e) { + throw new ServletException(e); + } + } + + @SuppressWarnings("unchecked") + private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest) + throws RestApiException, IOException, PermissionBackendException { + RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class)); + IdString id = getProjectName(httpRequest); + ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); + + return (Response<Map<String, Object>>) applyObjectAction.apply(projectResource, input); + } + + @SuppressWarnings("unchecked") + private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest) + throws IOException, RestApiException, PermissionBackendException { + Input input = readJson(httpRequest, TypeLiteral.get(Input.class)); + IdString id = getProjectName(httpRequest); + ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); + + return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input); + } + + private void writeResponse( + HttpServletResponse httpResponse, Response<Map<String, Object>> response) throws IOException { + String responseJson = gson.toJson(response); + if (response.statusCode() == SC_OK || response.statusCode() == SC_CREATED) { + + httpResponse.setContentType("application/json"); + httpResponse.setStatus(response.statusCode()); + PrintWriter writer = httpResponse.getWriter(); + writer.print(new String(RestApiServlet.JSON_MAGIC)); + writer.print(responseJson); + } else { + httpResponse.sendError(response.statusCode(), responseJson); + } + } + + private <T> T readJson(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral) + throws IOException, BadRequestException { + + try (BufferedReader br = httpRequest.getReader(); + JsonReader json = new JsonReader(br)) { + try { + json.setLenient(true); + + try { + json.peek(); + } catch (EOFException e) { + throw new BadRequestException("Expected JSON object", e); + } + + return gson.fromJson(json, typeLiteral.getType()); + } finally { + try { + // Reader.close won't consume the rest of the input. Explicitly consume the request + // body. + br.skip(Long.MAX_VALUE); + } catch (Exception e) { + // ignore, e.g. trying to consume the rest of the input may fail if the request was + // cancelled + logger.atFine().withCause(e).log("Exception during the parsing of the request json"); + } + } + } + } + + /** + * Return project name from request URI. Request URI format: + * /a/projects/<project_name>/pull-replication~apply-object + * + * @param req + * @return project name + */ + private IdString getProjectName(HttpServletRequest req) { + String path = req.getRequestURI(); + + List<IdString> out = new ArrayList<>(); + for (String p : Splitter.on('/').split(path)) { + out.add(IdString.fromUrl(p)); + } + if (!out.isEmpty() && out.get(out.size() - 1).isEmpty()) { + out.remove(out.size() - 1); + } + return out.get(3); + } + + private boolean isApplyObjectAction(HttpServletRequest httpRequest) { + return httpRequest.getRequestURI().endsWith("pull-replication~apply-object"); + } + + private boolean isFetchAction(HttpServletRequest httpRequest) { + return httpRequest.getRequestURI().endsWith("pull-replication~fetch"); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java new file mode 100644 index 0000000..49cf608 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -0,0 +1,202 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication.pull.api; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.stream.Collectors.toList; + +import com.google.gerrit.acceptance.LightweightPluginDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.acceptance.SkipProjectClone; +import com.google.gerrit.acceptance.TestPlugin; +import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Inject; +import com.googlesource.gerrit.plugins.replication.CredentialsFactory; +import com.googlesource.gerrit.plugins.replication.pull.RevisionReader; +import com.googlesource.gerrit.plugins.replication.pull.Source; +import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection; +import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData; +import com.googlesource.gerrit.plugins.replication.pull.client.SourceHttpClient; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.message.BasicHeader; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; + +@SkipProjectClone +@UseLocalDisk +@TestPlugin( + name = "pull-replication", + sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule", + httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule") +public abstract class ActionITBase extends LightweightPluginDaemonTest { + protected static final Optional<String> ALL_PROJECTS = Optional.empty(); + protected static final int TEST_REPLICATION_DELAY = 60; + protected static final String TEST_REPLICATION_SUFFIX = "suffix1"; + protected static final String TEST_REPLICATION_REMOTE = "remote1"; + + protected Path gitPath; + protected FileBasedConfig config; + protected FileBasedConfig secureConfig; + protected RevisionReader revisionReader; + + @Inject private SitePaths sitePaths; + @Inject private ProjectOperations projectOperations; + CredentialsFactory credentials; + Source source; + SourceHttpClient.Factory httpClientFactory; + String url; + + protected abstract String getURL(); + + @Override + public void setUpTestPlugin() throws Exception { + gitPath = sitePaths.site_path.resolve("git"); + + config = + new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED); + setReplicationSource( + TEST_REPLICATION_REMOTE, + TEST_REPLICATION_SUFFIX, + ALL_PROJECTS); // Simulates a full replication.config initialization + config.save(); + + secureConfig = + new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED); + setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword()); + secureConfig.save(); + + super.setUpTestPlugin(); + + httpClientFactory = plugin.getSysInjector().getInstance(SourceHttpClient.Factory.class); + credentials = plugin.getSysInjector().getInstance(CredentialsFactory.class); + revisionReader = plugin.getSysInjector().getInstance(RevisionReader.class); + source = plugin.getSysInjector().getInstance(SourcesCollection.class).getAll().get(0); + + url = getURL(); + } + + protected HttpPost createRequest(String sendObjectPayload) { + HttpPost post = new HttpPost(url); + post.setEntity(new StringEntity(sendObjectPayload, StandardCharsets.UTF_8)); + post.addHeader(new BasicHeader("Content-Type", "application/json")); + return post; + } + + protected String createRef() throws Exception { + return createRef(Project.nameKey(project + TEST_REPLICATION_SUFFIX)); + } + + protected String createRef(NameKey projectName) throws Exception { + testRepo = cloneProject(createTestProject(projectName.get())); + + Result pushResult = createChange("topic", "test.txt", "test_content"); + return RefNames.changeMetaRef(pushResult.getChange().getId()); + } + + protected Optional<RevisionData> createRevisionData(String refName) throws Exception { + return createRevisionData(Project.nameKey(project + TEST_REPLICATION_SUFFIX), refName); + } + + protected Optional<RevisionData> createRevisionData(NameKey projectName, String refName) + throws Exception { + return revisionReader.read(projectName, refName); + } + + protected Object encode(byte[] content) { + return Base64.getEncoder().encodeToString(content); + } + + public ResponseHandler<Object> assertHttpResponseCode(int responseCode) { + return new ResponseHandler<Object>() { + + @Override + public Object handleResponse(HttpResponse response) + throws ClientProtocolException, IOException { + assertThat(response.getStatusLine().getStatusCode()).isEqualTo(responseCode); + return null; + } + }; + } + + protected HttpClientContext getContext() { + HttpClientContext ctx = HttpClientContext.create(); + CredentialsProvider adapted = new BasicCredentialsProvider(); + adapted.setCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(admin.username(), admin.httpPassword())); + ctx.setCredentialsProvider(adapted); + return ctx; + } + + protected HttpClientContext getAnonymousContext() { + HttpClientContext ctx = HttpClientContext.create(); + return ctx; + } + + private Project.NameKey createTestProject(String name) throws Exception { + return projectOperations.newProject().name(name).parent(project).create(); + } + + private void setReplicationSource( + String remoteName, String replicaSuffix, Optional<String> project) throws IOException { + setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project); + } + + private void setReplicationSource( + String remoteName, List<String> replicaSuffixes, Optional<String> project) + throws IOException { + + List<String> replicaUrls = + replicaSuffixes.stream() + .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString()) + .collect(toList()); + config.setString("replication", null, "instanceLabel", remoteName); + config.setStringList("remote", remoteName, "url", replicaUrls); + config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); + config.setString("remote", remoteName, "fetch", "+refs/tags/*:refs/tags/*"); + config.setInt("remote", remoteName, "timeout", 600); + config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY); + project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj)); + config.setBoolean("gerrit", null, "autoReload", true); + config.save(); + } + + private void setReplicationCredentials(String remoteName, String username, String password) + throws IOException { + secureConfig.setString("remote", remoteName, "username", username); + secureConfig.setString("remote", remoteName, "password", password); + secureConfig.save(); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java index 75c31de..ec172ad 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
@@ -15,99 +15,17 @@ package com.googlesource.gerrit.plugins.replication.pull.api; import static com.google.common.truth.Truth.assertThat; -import static java.util.stream.Collectors.toList; -import com.google.gerrit.acceptance.LightweightPluginDaemonTest; -import com.google.gerrit.acceptance.PushOneCommit.Result; -import com.google.gerrit.acceptance.SkipProjectClone; -import com.google.gerrit.acceptance.TestPlugin; -import com.google.gerrit.acceptance.UseLocalDisk; -import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.entities.Project; -import com.google.gerrit.entities.RefNames; +import com.google.gerrit.entities.Project.NameKey; import com.google.gerrit.extensions.restapi.Url; -import com.google.gerrit.server.config.SitePaths; -import com.google.inject.Inject; -import com.googlesource.gerrit.plugins.replication.CredentialsFactory; -import com.googlesource.gerrit.plugins.replication.pull.RevisionReader; -import com.googlesource.gerrit.plugins.replication.pull.Source; -import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData; -import com.googlesource.gerrit.plugins.replication.pull.client.SourceHttpClient; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; import java.util.Optional; -import org.apache.http.HttpResponse; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.message.BasicHeader; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; import org.junit.Test; -@SkipProjectClone -@UseLocalDisk -@TestPlugin( - name = "pull-replication", - sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule") -public class ApplyObjectActionIT extends LightweightPluginDaemonTest { - private static final Optional<String> ALL_PROJECTS = Optional.empty(); - private static final int TEST_REPLICATION_DELAY = 60; - private static final String TEST_REPLICATION_SUFFIX = "suffix1"; - private static final String TEST_REPLICATION_REMOTE = "remote1"; - - private Path gitPath; - private FileBasedConfig config; - private FileBasedConfig secureConfig; - private RevisionReader revisionReader; - - @Inject private SitePaths sitePaths; - @Inject private ProjectOperations projectOperations; - CredentialsFactory credentials; - Source source; - SourceHttpClient.Factory httpClientFactory; - String url; - - @Override - public void setUpTestPlugin() throws Exception { - gitPath = sitePaths.site_path.resolve("git"); - - config = - new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED); - setReplicationSource( - TEST_REPLICATION_REMOTE, - TEST_REPLICATION_SUFFIX, - ALL_PROJECTS); // Simulates a full replication.config initialization - config.save(); - - secureConfig = - new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED); - setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword()); - secureConfig.save(); - - super.setUpTestPlugin(); - - httpClientFactory = plugin.getSysInjector().getInstance(SourceHttpClient.Factory.class); - credentials = plugin.getSysInjector().getInstance(CredentialsFactory.class); - revisionReader = plugin.getSysInjector().getInstance(RevisionReader.class); - source = plugin.getSysInjector().getInstance(SourcesCollection.class).getAll().get(0); - - url = - String.format( - "%s/a/projects/%s/pull-replication~apply-object", - adminRestSession.url(), Url.encode(project.get())); - } +public class ApplyObjectActionIT extends ActionITBase { @Test public void shouldAcceptPayloadWithAsyncField() throws Exception { @@ -147,6 +65,71 @@ } @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldAcceptPayloadWhenNodeIsAReplica() throws Exception { + String payloadWithoutAsyncFieldTemplate = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}"; + + String refName = createRef(); + Optional<RevisionData> revisionDataOption = createRevisionData(refName); + assertThat(revisionDataOption.isPresent()).isTrue(); + + RevisionData revisionData = revisionDataOption.get(); + String sendObjectPayload = + createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); + + HttpPost post = createRequest(sendObjectPayload); + httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldAcceptPayloadWhenNodeIsAReplicaAndProjectNameContainsSlash() throws Exception { + String payloadWithoutAsyncFieldTemplate = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}"; + NameKey projectName = Project.nameKey("test/repo"); + String refName = createRef(projectName); + Optional<RevisionData> revisionDataOption = createRevisionData(projectName, refName); + assertThat(revisionDataOption.isPresent()).isTrue(); + + RevisionData revisionData = revisionDataOption.get(); + String sendObjectPayload = + createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); + url = + String.format( + "%s/a/projects/%s/pull-replication~apply-object", + adminRestSession.url(), Url.encode(projectName.get())); + HttpPost post = createRequest(sendObjectPayload); + httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldReturnUnauthorizedWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception { + String payloadWithoutAsyncFieldTemplate = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}"; + + String refName = createRef(); + Optional<RevisionData> revisionDataOption = createRevisionData(refName); + assertThat(revisionDataOption.isPresent()).isTrue(); + + RevisionData revisionData = revisionDataOption.get(); + String sendObjectPayload = + createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); + + HttpPost post = createRequest(sendObjectPayload); + httpClientFactory + .create(source) + .execute(post, assertHttpResponseCode(401), getAnonymousContext()); + } + + @Test public void shouldReturnBadRequestCodeWhenMandatoryFieldLabelIsMissing() throws Exception { String payloadWithoutLabelFieldTemplate = "{\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}, \"async\":true}"; @@ -192,81 +175,10 @@ return sendObjectPayload; } - private HttpPost createRequest(String sendObjectPayload) { - HttpPost post = new HttpPost(url); - post.setEntity(new StringEntity(sendObjectPayload, StandardCharsets.UTF_8)); - post.addHeader(new BasicHeader("Content-Type", "application/json")); - return post; - } - - private String createRef() throws Exception { - testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); - - Result pushResult = createChange("topic", "test.txt", "test_content"); - return RefNames.changeMetaRef(pushResult.getChange().getId()); - } - - private Optional<RevisionData> createRevisionData(String refName) throws Exception { - return revisionReader.read(Project.nameKey(project + TEST_REPLICATION_SUFFIX), refName); - } - - private Object encode(byte[] content) { - return Base64.getEncoder().encodeToString(content); - } - - public ResponseHandler<Object> assertHttpResponseCode(int responseCode) { - return new ResponseHandler<Object>() { - - @Override - public Object handleResponse(HttpResponse response) - throws ClientProtocolException, IOException { - assertThat(response.getStatusLine().getStatusCode()).isEqualTo(responseCode); - return null; - } - }; - } - - private HttpClientContext getContext() { - HttpClientContext ctx = HttpClientContext.create(); - CredentialsProvider adapted = new BasicCredentialsProvider(); - adapted.setCredentials( - AuthScope.ANY, new UsernamePasswordCredentials(admin.username(), admin.httpPassword())); - ctx.setCredentialsProvider(adapted); - return ctx; - } - - private Project.NameKey createTestProject(String name) throws Exception { - return projectOperations.newProject().name(name).parent(project).create(); - } - - private void setReplicationSource( - String remoteName, String replicaSuffix, Optional<String> project) throws IOException { - setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project); - } - - private void setReplicationSource( - String remoteName, List<String> replicaSuffixes, Optional<String> project) - throws IOException { - - List<String> replicaUrls = - replicaSuffixes.stream() - .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString()) - .collect(toList()); - config.setString("replication", null, "instanceLabel", remoteName); - config.setStringList("remote", remoteName, "url", replicaUrls); - config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); - config.setString("remote", remoteName, "fetch", "+refs/tags/*:refs/tags/*"); - config.setInt("remote", remoteName, "timeout", 600); - config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY); - project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj)); - config.setBoolean("gerrit", null, "autoReload", true); - config.save(); - } - - private void setReplicationCredentials(String remoteName, String username, String password) - throws IOException { - secureConfig.setString("remote", remoteName, "username", username); - secureConfig.setString("remote", remoteName, "password", password); - secureConfig.save(); + @Override + protected String getURL() { + return String.format( + "%s/a/projects/%s/pull-replication~apply-object", + adminRestSession.url(), Url.encode(project.get())); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java new file mode 100644 index 0000000..7b73df7 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
@@ -0,0 +1,84 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.replication.pull.api; + +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.extensions.restapi.Url; +import org.junit.Test; + +public class FetchActionIT extends ActionITBase { + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldFetchRefWhenNodeIsAReplica() throws Exception { + String refName = createRef(); + String sendObjectPayload = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\", \"ref_name\": \"" + + refName + + "\", \"async\":false}"; + + httpClientFactory + .create(source) + .execute(createRequest(sendObjectPayload), assertHttpResponseCode(201), getContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldFetchRefWhenNodeIsAReplicaAndProjectNameContainsSlash() throws Exception { + NameKey projectName = Project.nameKey("test/repo"); + String refName = createRef(projectName); + String sendObjectPayload = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\", \"ref_name\": \"" + + refName + + "\", \"async\":false}"; + url = + String.format( + "%s/a/projects/%s/pull-replication~fetch", + adminRestSession.url(), Url.encode(projectName.get())); + httpClientFactory + .create(source) + .execute(createRequest(sendObjectPayload), assertHttpResponseCode(201), getContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldReturnUnauthorizedWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception { + String refName = createRef(); + String sendObjectPayload = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\", \"ref_name\": \"" + + refName + + "\", \"async\":false}"; + + httpClientFactory + .create(source) + .execute( + createRequest(sendObjectPayload), assertHttpResponseCode(401), getAnonymousContext()); + } + + @Override + protected String getURL() { + return String.format( + "%s/a/projects/%s/pull-replication~fetch", + adminRestSession.url(), Url.encode(project.get())); + } +}