Add missing project creation support for replicas

Currently replication of project initialization is only possible
on master nodes.

Add the same functionality to the replicas.

Bug: Issue 15204
Change-Id: Ia3f15caf4ab1278ce97a21585b2da2f687ec1496
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
index 53b8e8a..8f5c9d0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
@@ -93,7 +93,7 @@
         "Cannot initialize project " + projectName);
   }
 
-  private boolean initProject(String projectName) {
+  protected boolean initProject(String projectName) {
     Optional<URIish> maybeUri = getGitRepositoryURI(projectName);
     if (!maybeUri.isPresent()) {
       logger.atSevere().log("Cannot initialize project '{}'", projectName);
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
index 65b8e1b..1425be6 100644
--- 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
@@ -15,10 +15,13 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
+import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
+import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse;
 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_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
@@ -28,10 +31,12 @@
 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.ResourceNotFoundException;
 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.extensions.restapi.Url;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.json.OutputFormat;
@@ -48,6 +53,7 @@
 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 com.googlesource.gerrit.plugins.replication.pull.api.exception.InitProjectException;
 import java.io.BufferedReader;
 import java.io.EOFException;
 import java.io.IOException;
@@ -67,6 +73,7 @@
 
   private FetchAction fetchAction;
   private ApplyObjectAction applyObjectAction;
+  private ProjectInitializationAction projectInitializationAction;
   private ProjectsCollection projectsCollection;
   private Gson gson;
   private Provider<CurrentUser> userProvider;
@@ -75,10 +82,12 @@
   public PullReplicationFilter(
       FetchAction fetchAction,
       ApplyObjectAction applyObjectAction,
+      ProjectInitializationAction projectInitializationAction,
       ProjectsCollection projectsCollection,
       Provider<CurrentUser> userProvider) {
     this.fetchAction = fetchAction;
     this.applyObjectAction = applyObjectAction;
+    this.projectInitializationAction = projectInitializationAction;
     this.projectsCollection = projectsCollection;
     this.userProvider = userProvider;
     this.gson = OutputFormat.JSON.newGsonBuilder().create();
@@ -107,6 +116,15 @@
         } else {
           httpResponse.sendError(SC_UNAUTHORIZED);
         }
+      } else if (isInitProjectAction(httpRequest)) {
+        if (userProvider.get().isIdentifiedUser()) {
+          if (!checkAcceptHeader(httpRequest, httpResponse)) {
+            return;
+          }
+          doInitProject(httpRequest, httpResponse);
+        } else {
+          httpResponse.sendError(SC_UNAUTHORIZED);
+        }
       } else {
         chain.doFilter(request, response);
       }
@@ -126,11 +144,27 @@
     } catch (ResourceConflictException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e);
+    } catch (InitProjectException | ResourceNotFoundException e) {
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e);
     } catch (Exception e) {
       throw new ServletException(e);
     }
   }
 
+  private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
+      throws RestApiException, IOException {
+
+    String path = httpRequest.getRequestURI();
+    String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
+    if (projectInitializationAction.initProject(projectName)) {
+      setResponse(
+          httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized");
+      return;
+    }
+    throw new InitProjectException(projectName);
+  }
+
   @SuppressWarnings("unchecked")
   private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest)
       throws RestApiException, IOException, PermissionBackendException {
@@ -222,4 +256,8 @@
   private boolean isFetchAction(HttpServletRequest httpRequest) {
     return httpRequest.getRequestURI().endsWith("pull-replication~fetch");
   }
+
+  private boolean isInitProjectAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().contains("pull-replication/init-project/");
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
new file mode 100644
index 0000000..85a7729
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
@@ -0,0 +1,25 @@
+// 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.exception;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public class InitProjectException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  public InitProjectException(String projectName) {
+    super("Cannot create project " + projectName);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
index 5fc22e4..919ac34 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
@@ -17,6 +17,7 @@
 import static com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction.getProjectInitializationUrl;
 
 import com.google.common.net.MediaType;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.extensions.restapi.Url;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -25,6 +26,8 @@
 import org.junit.Test;
 
 public class ProjectInitializationActionIT extends ActionITBase {
+  public static final String INVALID_TEST_PROJECT_NAME = "\0";
+  private String testProjectName = "new/Project";
 
   @Test
   public void shouldReturnUnauthorizedForUserWithoutPermissions() throws Exception {
@@ -63,11 +66,60 @@
             getNewProjectRequest, assertHttpResponseCode(HttpServletResponse.SC_OK), getContext());
   }
 
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldCreateRepositoryWhenNodeIsAReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_CREATED),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnInternalServerErrorIfProjectCannotBeCreatedWhenNodeIsAReplica()
+      throws Exception {
+    testProjectName = INVALID_TEST_PROJECT_NAME;
+    url = getURL();
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnBadRequestIfContentNotSetWhenNodeIsAReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithoutHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsWhenNodeIsAReplica()
+      throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED),
+            getAnonymousContext());
+  }
+
   @Override
   protected String getURL() {
     return userRestSession.url()
         + "/"
-        + getProjectInitializationUrl("pull-replication", Url.encode("new/Project"));
+        + getProjectInitializationUrl("pull-replication", Url.encode(testProjectName));
   }
 
   protected HttpPut createPutRequestWithHeaders() {