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()));
+ }
+}