Merge branch 'stable-3.5' into stable-3.6 * stable-3.5: Add JGit client test for unset mirror Add mirror replication option for CGit client Change-Id: I2ee7ccd772ca50bd650329cb6da7a6973d3f3569
diff --git a/example-setup/broker/Dockerfile b/example-setup/broker/Dockerfile index 08eaba9..b79470c 100644 --- a/example-setup/broker/Dockerfile +++ b/example-setup/broker/Dockerfile
@@ -1,4 +1,4 @@ -FROM gerritcodereview/gerrit:3.5.5-almalinux8 +FROM gerritcodereview/gerrit:3.6.3-almalinux8 USER root @@ -12,7 +12,7 @@ # hence rename it with a 'z-' prefix because the Gerrit plugin loader starts the # plugins in filename alphabetical order. COPY --chown=gerrit:gerrit events-kafka.jar /var/gerrit/plugins/z-events-kafka.jar -COPY --chown=gerrit:gerrit libevents-broker.jar /var/gerrit/lib/libevents-broker.jar +COPY --chown=gerrit:gerrit events-broker.jar /var/gerrit/lib/events-broker.jar COPY --chown=gerrit:gerrit entrypoint.sh /tmp/ COPY --chown=gerrit:gerrit configs/replication.config.template /var/gerrit/etc/
diff --git a/example-setup/http/Dockerfile b/example-setup/http/Dockerfile index e9f8239..77fed72 100644 --- a/example-setup/http/Dockerfile +++ b/example-setup/http/Dockerfile
@@ -1,4 +1,4 @@ -FROM gerritcodereview/gerrit:3.5.5-almalinux8 +FROM gerritcodereview/gerrit:3.6.3-almalinux8 USER root
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java index 370f2fb..d02cb02 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
@@ -41,6 +41,7 @@ import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState; import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -336,23 +337,33 @@ try { long startedAt = context.getStartTime(); long delay = NANOSECONDS.toMillis(startedAt - createdAt); - metrics.record(config.getName(), delay, retryCount); git = gitManager.openRepository(projectName); - runImpl(); - long elapsed = NANOSECONDS.toMillis(context.stop()); - Optional<Long> elapsedEnd2End = - apiRequestMetrics - .flatMap(metrics -> metrics.stop(config.getName())) - .map(NANOSECONDS::toMillis); - repLog.info( - "[{}] {} replication from {} completed in {}ms, {}ms delay, {} retries{}", - taskIdHex, - replicationType, - uri, - elapsed, - delay, - retryCount, - elapsedEnd2End.map(el -> String.format(", E2E %dms", el)).orElse("")); + List<RefSpec> fetchRefSpecs = runImpl(); + + if (fetchRefSpecs.isEmpty()) { + repLog.info( + "[{}] {} replication from {} finished but no refs were replicated, {}ms delay, {} retries", + taskIdHex, + replicationType, + uri, + delay, + retryCount); + } else { + metrics.record(config.getName(), delay, retryCount); + long elapsed = NANOSECONDS.toMillis(context.stop()); + Optional<Long> elapsedEnd2End = + apiRequestMetrics + .flatMap(metrics -> metrics.stop(config.getName())) + .map(NANOSECONDS::toMillis); + repLog.info( + "[{}] Replication from {} completed in {}ms, {}ms delay, {} retries{}", + taskIdHex, + uri, + elapsed, + delay, + retryCount, + elapsedEnd2End.map(el -> String.format(", E2E %dms", el)).orElse("")); + } } catch (RepositoryNotFoundException e) { stateLog.error( "[" @@ -428,7 +439,7 @@ repLog.info("[{}] Cannot replicate from {}. It was canceled while running", taskIdHex, uri, e); } - private void runImpl() throws IOException { + private List<RefSpec> runImpl() throws IOException { Fetch fetch = fetchFactory.create(taskIdHex, uri, git); List<RefSpec> fetchRefSpecs = getFetchRefSpecs(); @@ -445,7 +456,7 @@ delta.remove(inexistentRef); if (delta.isEmpty()) { repLog.warn("[{}] Empty replication task, skipping.", taskIdHex); - return; + return Collections.emptyList(); } runImpl(); @@ -453,6 +464,7 @@ notifyRefReplicatedIOException(); throw e; } + return fetchRefSpecs; } public List<RefSpec> getFetchRefSpecs() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java index d8c4a8d..6457f8a 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java
@@ -18,6 +18,7 @@ import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.systemstatus.ServerInformation; +import com.google.gerrit.server.config.GerritIsReplica; import com.google.gerrit.server.events.EventDispatcher; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; @@ -36,6 +37,7 @@ private final ReplicationState.Factory replicationStateFactory; private final SourcesCollection sourcesCollection; private final WorkQueue workQueue; + private boolean isReplica; @Inject protected OnStartStop( @@ -45,7 +47,8 @@ DynamicItem<EventDispatcher> eventDispatcher, ReplicationState.Factory replicationStateFactory, SourcesCollection sourcesCollection, - WorkQueue workQueue) { + WorkQueue workQueue, + @GerritIsReplica Boolean isReplica) { this.srvInfo = srvInfo; this.fetchAll = fetchAll; this.config = config; @@ -54,11 +57,13 @@ this.fetchAllFuture = Atomics.newReference(); this.sourcesCollection = sourcesCollection; this.workQueue = workQueue; + this.isReplica = isReplica; } @Override public void start() { - if (srvInfo.getState() == ServerInformation.State.STARTUP + if (isReplica + && srvInfo.getState() == ServerInformation.State.STARTUP && config.isReplicateAllOnPluginStart()) { ReplicationState state = replicationStateFactory.create(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java index 72a6b74..005d383 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -15,6 +15,7 @@ package com.googlesource.gerrit.plugins.replication.pull; import static com.googlesource.gerrit.plugins.replication.StartReplicationCapability.START_REPLICATION; +import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION; import com.google.common.eventbus.EventBus; import com.google.gerrit.extensions.annotations.Exports; @@ -44,8 +45,8 @@ import com.googlesource.gerrit.plugins.replication.ReplicationConfig; import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig; import com.googlesource.gerrit.plugins.replication.StartReplicationCapability; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability; import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob; -import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule; import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule; import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient; import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient; @@ -82,13 +83,16 @@ .annotatedWith(Names.named(ReplicationQueueMetrics.REPLICATION_QUEUE_METRICS)) .toInstance(pluginMetricMaker); + bind(CapabilityDefinition.class) + .annotatedWith(Exports.named(CALL_FETCH_ACTION)) + .to(FetchApiCapability.class); + install(new PullReplicationGroupModule()); bind(BearerTokenProvider.class).in(Scopes.SINGLETON); bind(RevisionReader.class).in(Scopes.SINGLETON); bind(ApplyObject.class); install(new FactoryModuleBuilder().build(FetchJob.Factory.class)); install(new ApplyObjectCacheModule()); - install(new PullReplicationApiModule()); install(new FetchRefReplicatedEventModule());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java index 6bafaa4..aafe12f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
@@ -181,12 +181,7 @@ event.getRefName(), event.refUpdate.get().oldRev, event.refUpdate.get().newRev); - fire( - event.refUpdate.get().project, - ObjectId.fromString(event.refUpdate.get().newRev), - event.getRefName(), - event.eventCreatedOn, - ZEROS_OBJECTID.equals(event.refUpdate.get().newRev)); + fire(ReferenceUpdatedEvent.from(event)); } } } @@ -215,42 +210,45 @@ return !refsFilter.match(refName); } - private void fire( - String projectName, - ObjectId objectId, - String refName, - long eventCreatedOn, - boolean isDelete) { + private void fire(ReferenceUpdatedEvent event) { ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher.get())); - fire(Project.nameKey(projectName), objectId, refName, eventCreatedOn, isDelete, state); + fire(event, state); state.markAllFetchTasksScheduled(); } - private void fire( - NameKey project, - ObjectId objectId, - String refName, - long eventCreatedOn, - boolean isDelete, - ReplicationState state) { + private void fire(ReferenceUpdatedEvent event, ReplicationState state) { if (!running) { stateLog.warn( - "Replication plugin did not finish startup before event, event replication is postponed", + String.format( + "Replication plugin did not finish startup before event, event replication is postponed" + + " for event %s", + event), state); - beforeStartupEventsQueue.add( - ReferenceUpdatedEvent.create(project.get(), refName, objectId, eventCreatedOn, isDelete)); + beforeStartupEventsQueue.add(event); queueMetrics.incrementQueuedBeforStartup(); return; } ForkJoinPool fetchCallsPool = null; try { - fetchCallsPool = new ForkJoinPool(sources.get().getAll().size()); + List<Source> allSources = sources.get().getAll(); + int numSources = allSources.size(); + if (numSources == 0) { + repLog.debug("No replication sources configured -> skipping fetch"); + return; + } + fetchCallsPool = new ForkJoinPool(numSources); final Consumer<Source> callFunction = - callFunction(project, objectId, refName, eventCreatedOn, isDelete, state); + callFunction( + Project.nameKey(event.projectName()), + event.objectId(), + event.refName(), + event.eventCreatedOn(), + event.isDelete(), + state); fetchCallsPool - .submit(() -> sources.get().getAll().parallelStream().forEach(callFunction)) + .submit(() -> allSources.parallelStream().forEach(callFunction)) .get(fetchCallsTimeout, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { stateLog.error( @@ -523,7 +521,20 @@ private HttpResult initProject( Project.NameKey project, URIish uri, FetchApiClient fetchClient, HttpResult result) throws IOException, ClientProtocolException { - HttpResult initProjectResult = fetchClient.initProject(project, uri); + RevisionData refsMetaConfigRevisionData = + revReaderProvider + .get() + .read(project, null, RefNames.REFS_CONFIG, 0) + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Project %s does not have %s", project, RefNames.REFS_CONFIG))); + + List<RevisionData> refsMetaConfigDataList = + fetchWholeMetaHistory(project, RefNames.REFS_CONFIG, refsMetaConfigRevisionData); + HttpResult initProjectResult = + fetchClient.initProject(project, uri, System.currentTimeMillis(), refsMetaConfigDataList); if (initProjectResult.isSuccessful()) { result = fetchClient.callFetch(project, FetchOne.ALL_REFS, uri); } else { @@ -539,12 +550,7 @@ String eventKey = String.format("%s:%s", event.projectName(), event.refName()); if (!eventsReplayed.contains(eventKey)) { repLog.info("Firing pending task {}", event); - fire( - event.projectName(), - event.objectId(), - event.refName(), - event.eventCreatedOn(), - event.isDelete()); + fire(event); eventsReplayed.add(eventKey); } } @@ -578,6 +584,15 @@ projectName, refName, objectId, eventCreatedOn, isDelete); } + static ReferenceUpdatedEvent from(RefUpdatedEvent event) { + return ReferenceUpdatedEvent.create( + event.refUpdate.get().project, + event.getRefName(), + ObjectId.fromString(event.refUpdate.get().newRev), + event.eventCreatedOn, + ZEROS_OBJECTID.equals(event.refUpdate.get().newRev)); + } + public abstract String projectName(); public abstract String refName();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java index a8799c2..d7ae063 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
@@ -17,6 +17,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.flogger.FluentLogger; +import com.google.gerrit.server.config.GerritIsReplica; +import com.google.inject.Inject; import com.googlesource.gerrit.plugins.replication.ConfigParser; import com.googlesource.gerrit.plugins.replication.RemoteConfiguration; import java.net.URISyntaxException; @@ -32,6 +34,13 @@ private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private boolean isReplica; + + @Inject + SourceConfigParser(@GerritIsReplica Boolean isReplica) { + this.isReplica = isReplica; + } + /* (non-Javadoc) * @see com.googlesource.gerrit.plugins.replication.ConfigParser#parseRemotes(org.eclipse.jgit.lib.Config) */ @@ -45,7 +54,7 @@ ImmutableList.Builder<RemoteConfiguration> sourceConfigs = ImmutableList.builder(); for (RemoteConfig c : allFetchRemotes(config)) { - if (c.getURIs().isEmpty()) { + if (isReplica && c.getURIs().isEmpty()) { continue; }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java index 72f0266..d535ac9 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
@@ -26,6 +26,7 @@ import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; +import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput; import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException; import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException; @@ -33,6 +34,7 @@ import java.util.Objects; import javax.servlet.http.HttpServletResponse; +@Singleton public class ApplyObjectAction implements RestModifyView<ProjectResource, RevisionInput> { private final ApplyObjectCommand applyObjectCommand;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java index f897012..e49c8b6 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -35,7 +35,6 @@ import com.googlesource.gerrit.plugins.replication.pull.ReplicationState; import com.googlesource.gerrit.plugins.replication.pull.Source; import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection; -import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject; import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState; import java.io.IOException; import java.util.Optional; @@ -49,7 +48,6 @@ private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final PullReplicationStateLogger fetchStateLog; - private final ApplyObject applyObject; private final DynamicItem<EventDispatcher> eventDispatcher; private final ProjectCache projectCache; private final SourcesCollection sourcesCollection; @@ -61,13 +59,11 @@ PullReplicationStateLogger fetchStateLog, ProjectCache projectCache, SourcesCollection sourcesCollection, - ApplyObject applyObject, PermissionBackend permissionBackend, DynamicItem<EventDispatcher> eventDispatcher, LocalGitRepositoryManagerProvider gitManagerProvider) { this.fetchStateLog = fetchStateLog; this.projectCache = projectCache; - this.applyObject = applyObject; this.eventDispatcher = eventDispatcher; this.sourcesCollection = sourcesCollection; this.permissionBackend = permissionBackend;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java index 04797bd..9e69f8d 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -30,6 +30,7 @@ import com.google.gerrit.server.ioutil.HexFormat; import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; +import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input; import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory; import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException; @@ -38,6 +39,7 @@ import java.util.concurrent.TimeoutException; import org.eclipse.jgit.errors.TransportException; +@Singleton public class FetchAction implements RestModifyView<ProjectResource, Input> { private final FetchCommand command; private final WorkQueue workQueue;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java index 73a4ac5..27afcfd 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
@@ -17,7 +17,7 @@ import com.google.gerrit.extensions.config.CapabilityDefinition; public class FetchApiCapability extends CapabilityDefinition { - static final String CALL_FETCH_ACTION = "callFetchAction"; + public static final String CALL_FETCH_ACTION = "callFetchAction"; @Override public String getDescription() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java index b8249ae..5323425 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
@@ -97,7 +97,12 @@ if (fetchType == ReplicationType.ASYNC) { state.markAllFetchTasksScheduled(); Future<?> future = source.get().schedule(name, refName, state, apiRequestMetrics); - future.get(source.get().getTimeout(), TimeUnit.SECONDS); + int timeout = source.get().getTimeout(); + if (timeout == 0) { + future.get(); + } else { + future.get(timeout, TimeUnit.SECONDS); + } } else { Optional<FetchOne> maybeFetch = source
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java index 77d0e0b..7ca8805 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
@@ -26,8 +26,10 @@ import com.google.gerrit.server.permissions.RefPermission; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException; +@Singleton public class FetchPreconditions { private final String pluginName; private final PermissionBackend permissionBackend;
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 index 0f3e1e8..95082b8 100644 --- 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
@@ -16,7 +16,6 @@ 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.name.Names; @@ -24,12 +23,10 @@ import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider; public class HttpModule extends ServletModule { - private boolean isReplica; private final BearerTokenProvider bearerTokenProvider; @Inject - public HttpModule(@GerritIsReplica Boolean isReplica, BearerTokenProvider bearerTokenProvider) { - this.isReplica = isReplica; + public HttpModule(BearerTokenProvider bearerTokenProvider) { this.bearerTokenProvider = bearerTokenProvider; } @@ -49,12 +46,8 @@ .in(Scopes.SINGLETON); }); - if (isReplica) { - DynamicSet.bind(binder(), AllRequestFilter.class) - .to(PullReplicationFilter.class) - .in(Scopes.SINGLETON); - } else { - serveRegex("/init-project/.*$").with(ProjectInitializationAction.class); - } + DynamicSet.bind(binder(), AllRequestFilter.class) + .to(PullReplicationFilter.class) + .in(Scopes.SINGLETON); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java index 2e1c5d4..adb333c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -27,11 +27,13 @@ import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.replication.LocalFS; import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps; import java.util.Optional; import org.eclipse.jgit.transport.URIish; +@Singleton class ProjectDeletionAction implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> { private static final PluginPermission DELETE_PROJECT =
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 8711379..e7289ac 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
@@ -14,25 +14,39 @@ package com.googlesource.gerrit.plugins.replication.pull.api; +import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog; 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_FORBIDDEN; +import com.google.common.base.Strings; import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.index.project.ProjectIndexer; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.replication.LocalFS; import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps; +import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput; +import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException; +import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException; +import com.googlesource.gerrit.plugins.replication.pull.api.util.PayloadSerDes; import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; import java.util.Optional; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -51,17 +65,23 @@ private final Provider<CurrentUser> userProvider; private final PermissionBackend permissionBackend; private final ProjectIndexer projectIndexer; + private final ApplyObjectCommand applyObjectCommand; + private final ProjectCache projectCache; @Inject ProjectInitializationAction( GerritConfigOps gerritConfigOps, Provider<CurrentUser> userProvider, PermissionBackend permissionBackend, - ProjectIndexer projectIndexer) { + ProjectIndexer projectIndexer, + ApplyObjectCommand applyObjectCommand, + ProjectCache projectCache) { this.gerritConfigOps = gerritConfigOps; this.userProvider = userProvider; this.permissionBackend = permissionBackend; this.projectIndexer = projectIndexer; + this.applyObjectCommand = applyObjectCommand; + this.projectCache = projectCache; } @Override @@ -73,47 +93,140 @@ return; } - String path = httpServletRequest.getRequestURI(); - String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1)); + String gitRepositoryName = getGitRepositoryName(httpServletRequest); try { - if (initProject(projectName)) { + boolean initProjectStatus; + String contentType = httpServletRequest.getContentType(); + if (checkContentType(contentType, MediaType.JSON_UTF_8)) { + // init project request includes project configuration in JSON format. + initProjectStatus = initProjectWithConfiguration(httpServletRequest, gitRepositoryName); + } else if (checkContentType(contentType, MediaType.PLAIN_TEXT_UTF_8)) { + // init project request does not include project configuration. + initProjectStatus = initProject(gitRepositoryName); + } else { + setResponse( + httpServletResponse, + SC_BAD_REQUEST, + String.format( + "Invalid Content Type. Only %s or %s is supported.", + MediaType.JSON_UTF_8.toString(), MediaType.PLAIN_TEXT_UTF_8.toString())); + return; + } + + if (initProjectStatus) { setResponse( httpServletResponse, HttpServletResponse.SC_CREATED, - "Project " + projectName + " initialized"); + "Project " + gitRepositoryName + " initialized"); return; } - } catch (AuthException | PermissionBackendException e) { + setResponse( httpServletResponse, - HttpServletResponse.SC_FORBIDDEN, - "User not authorized to create project " + projectName); - return; + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Cannot initialize project " + gitRepositoryName); + } catch (BadRequestException | IllegalArgumentException e) { + logExceptionAndUpdateResponse(httpServletResponse, e, SC_BAD_REQUEST, gitRepositoryName); + } catch (RefUpdateException | MissingParentObjectException e) { + logExceptionAndUpdateResponse(httpServletResponse, e, SC_CONFLICT, gitRepositoryName); + } catch (AuthException | PermissionBackendException e) { + logExceptionAndUpdateResponse(httpServletResponse, e, SC_FORBIDDEN, gitRepositoryName); } - - setResponse( - httpServletResponse, - HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - "Cannot initialize project " + projectName); } - public boolean initProject(String projectName) throws AuthException, PermissionBackendException { + public boolean initProject(String gitRepositoryName) + throws AuthException, PermissionBackendException { + if (initProject(gitRepositoryName, true)) { + repLog.info("Init project API from {}", gitRepositoryName); + return true; + } + return false; + } + + private boolean initProjectWithConfiguration( + HttpServletRequest httpServletRequest, String gitRepositoryName) + throws AuthException, PermissionBackendException, IOException, BadRequestException, + MissingParentObjectException, RefUpdateException { + + RevisionsInput input = PayloadSerDes.parseRevisionsInput(httpServletRequest); + validateInput(input); + if (!initProject(gitRepositoryName, false)) { + return false; + } + + String projectName = gitRepositoryName.replace(".git", ""); + applyObjectCommand.applyObjects( + Project.nameKey(projectName), + input.getRefName(), + input.getRevisionsData(), + input.getLabel(), + input.getEventCreatedOn()); + projectCache.onCreateProject(Project.nameKey(projectName)); + repLog.info( + "Init project API from {} for {}:{} - {}", + input.getLabel(), + projectName, + input.getRefName(), + Arrays.toString(input.getRevisionsData())); + return true; + } + + private boolean initProject(String gitRepositoryName, boolean needsProjectReindexing) + throws AuthException, PermissionBackendException { // When triggered internally(for example by consuming stream events) user is not provided // and internal user is returned. Project creation should be always allowed for internal user. if (!userProvider.get().isInternalUser()) { permissionBackend.user(userProvider.get()).check(GlobalPermission.CREATE_PROJECT); } - Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(projectName); + Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(gitRepositoryName); if (!maybeUri.isPresent()) { - logger.atSevere().log("Cannot initialize project '%s'", projectName); + logger.atSevere().log("Cannot initialize project '%s'", gitRepositoryName); return false; } LocalFS localFS = new LocalFS(maybeUri.get()); - Project.NameKey projectNameKey = Project.NameKey.parse(projectName); + Project.NameKey projectNameKey = Project.NameKey.parse(gitRepositoryName); if (localFS.createProject(projectNameKey, RefNames.HEAD)) { - projectIndexer.index(projectNameKey); + if (needsProjectReindexing) { + projectIndexer.index(projectNameKey); + } return true; } return false; } + + private void validateInput(RevisionsInput input) { + + if (Strings.isNullOrEmpty(input.getLabel())) { + throw new IllegalArgumentException("Source label cannot be null or empty"); + } + + if (!Objects.equals(input.getRefName(), RefNames.REFS_CONFIG)) { + throw new IllegalArgumentException( + String.format("Ref-update refname should be %s", RefNames.REFS_CONFIG)); + } + input.validate(); + } + + private String getGitRepositoryName(HttpServletRequest httpServletRequest) { + String path = httpServletRequest.getRequestURI(); + return Url.decode(path.substring(path.lastIndexOf('/') + 1)); + } + + private void logExceptionAndUpdateResponse( + HttpServletResponse httpServletResponse, + Exception e, + int statusCode, + String gitRepositoryName) + throws IOException { + repLog.error("Init Project API FAILED for {}", gitRepositoryName, e); + setResponse(httpServletResponse, statusCode, e.getMessage()); + } + + private boolean checkContentType(String contentType, MediaType mediaType) { + try { + return MediaType.parse(contentType).is(mediaType); + } catch (Exception e) { + return false; + } + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java deleted file mode 100644 index d1d28a6..0000000 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java +++ /dev/null
@@ -1,43 +0,0 @@ -// Copyright (C) 2020 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.server.project.ProjectResource.PROJECT_KIND; -import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION; - -import com.google.gerrit.extensions.annotations.Exports; -import com.google.gerrit.extensions.config.CapabilityDefinition; -import com.google.gerrit.extensions.restapi.RestApiModule; -import com.google.inject.Scopes; - -public class PullReplicationApiModule extends RestApiModule { - @Override - protected void configure() { - bind(FetchAction.class).in(Scopes.SINGLETON); - bind(ApplyObjectAction.class).in(Scopes.SINGLETON); - bind(ProjectDeletionAction.class).in(Scopes.SINGLETON); - bind(UpdateHeadAction.class).in(Scopes.SINGLETON); - post(PROJECT_KIND, "fetch").to(FetchAction.class); - post(PROJECT_KIND, "apply-object").to(ApplyObjectAction.class); - post(PROJECT_KIND, "apply-objects").to(ApplyObjectsAction.class); - delete(PROJECT_KIND, "delete-project").to(ProjectDeletionAction.class); - put(PROJECT_KIND, "HEAD").to(UpdateHeadAction.class); - - bind(FetchPreconditions.class).in(Scopes.SINGLETON); - bind(CapabilityDefinition.class) - .annotatedWith(Exports.named(CALL_FETCH_ACTION)) - .to(FetchApiCapability.class); - } -}
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 e54d408..d0e5c10 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
@@ -16,16 +16,14 @@ 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; import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.api.projects.HeadInput; import com.google.gerrit.extensions.restapi.AuthException; @@ -35,29 +33,25 @@ 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.httpd.AllRequestFilter; import com.google.gerrit.httpd.restapi.RestApiServlet; -import com.google.gerrit.json.OutputFormat; +import com.google.gerrit.server.AnonymousUser; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectResource; -import com.google.gerrit.server.restapi.project.ProjectsCollection; -import com.google.gson.Gson; +import com.google.gerrit.server.project.ProjectState; 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.TypeLiteral; +import com.google.inject.Provider; 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.data.RevisionsInput; -import com.googlesource.gerrit.plugins.replication.pull.api.exception.InitProjectException; import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException; -import java.io.BufferedReader; -import java.io.EOFException; +import com.googlesource.gerrit.plugins.replication.pull.api.util.PayloadSerDes; import java.io.IOException; -import java.io.PrintWriter; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -83,9 +77,9 @@ private ProjectInitializationAction projectInitializationAction; private UpdateHeadAction updateHEADAction; private ProjectDeletionAction projectDeletionAction; - private ProjectsCollection projectsCollection; - private Gson gson; + private ProjectCache projectCache; private String pluginName; + private final Provider<CurrentUser> currentUserProvider; @Inject public PullReplicationFilter( @@ -95,17 +89,18 @@ ProjectInitializationAction projectInitializationAction, UpdateHeadAction updateHEADAction, ProjectDeletionAction projectDeletionAction, - ProjectsCollection projectsCollection, - @PluginName String pluginName) { + ProjectCache projectCache, + @PluginName String pluginName, + Provider<CurrentUser> currentUserProvider) { this.fetchAction = fetchAction; this.applyObjectAction = applyObjectAction; this.applyObjectsAction = applyObjectsAction; this.projectInitializationAction = projectInitializationAction; this.updateHEADAction = updateHEADAction; this.projectDeletionAction = projectDeletionAction; - this.projectsCollection = projectsCollection; + this.projectCache = projectCache; this.pluginName = pluginName; - this.gson = OutputFormat.JSON.newGsonBuilder().create(); + this.currentUserProvider = currentUserProvider; } @Override @@ -120,20 +115,26 @@ HttpServletRequest httpRequest = (HttpServletRequest) request; try { if (isFetchAction(httpRequest)) { - writeResponse(httpResponse, doFetch(httpRequest)); + failIfcurrentUserIsAnonymous(); + PayloadSerDes.writeResponse(httpResponse, doFetch(httpRequest)); } else if (isApplyObjectAction(httpRequest)) { - writeResponse(httpResponse, doApplyObject(httpRequest)); + failIfcurrentUserIsAnonymous(); + PayloadSerDes.writeResponse(httpResponse, doApplyObject(httpRequest)); } else if (isApplyObjectsAction(httpRequest)) { - writeResponse(httpResponse, doApplyObjects(httpRequest)); + failIfcurrentUserIsAnonymous(); + PayloadSerDes.writeResponse(httpResponse, doApplyObjects(httpRequest)); } else if (isInitProjectAction(httpRequest)) { + failIfcurrentUserIsAnonymous(); if (!checkAcceptHeader(httpRequest, httpResponse)) { return; } doInitProject(httpRequest, httpResponse); } else if (isUpdateHEADAction(httpRequest)) { - writeResponse(httpResponse, doUpdateHEAD(httpRequest)); + failIfcurrentUserIsAnonymous(); + PayloadSerDes.writeResponse(httpResponse, doUpdateHEAD(httpRequest)); } else if (isDeleteProjectAction(httpRequest)) { - writeResponse(httpResponse, doDeleteProject(httpRequest)); + failIfcurrentUserIsAnonymous(); + PayloadSerDes.writeResponse(httpResponse, doDeleteProject(httpRequest)); } else { chain.doFilter(request, response); } @@ -156,7 +157,7 @@ } catch (ResourceConflictException e) { RestApiServlet.replyError( httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e); - } catch (InitProjectException | ResourceNotFoundException e) { + } catch (ResourceNotFoundException e) { RestApiServlet.replyError( httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e); } catch (NoSuchElementException e) { @@ -173,108 +174,67 @@ } } - private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse) - throws RestApiException, IOException, PermissionBackendException { - - IdString id = getInitProjectName(httpRequest).get(); - String projectName = id.get(); - if (projectInitializationAction.initProject(projectName)) { - setResponse( - httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized"); - return; + private void failIfcurrentUserIsAnonymous() throws UnauthorizedAuthException { + CurrentUser currentUser = currentUserProvider.get(); + if (currentUser instanceof AnonymousUser) { + throw new UnauthorizedAuthException(); } - throw new InitProjectException(projectName); + } + + private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse) + throws IOException, ServletException { + projectInitializationAction.service(httpRequest, httpResponse); } @SuppressWarnings("unchecked") private Response<String> doApplyObject(HttpServletRequest httpRequest) throws RestApiException, IOException, PermissionBackendException { - RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class)); + RevisionInput input = PayloadSerDes.parseRevisionInput(httpRequest); IdString id = getProjectName(httpRequest).get(); - ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); - return (Response<String>) applyObjectAction.apply(projectResource, input); + return (Response<String>) applyObjectAction.apply(parseProjectResource(id), input); } @SuppressWarnings("unchecked") private Response<String> doApplyObjects(HttpServletRequest httpRequest) throws RestApiException, IOException, PermissionBackendException { - RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class)); + RevisionsInput input = PayloadSerDes.parseRevisionsInput(httpRequest); IdString id = getProjectName(httpRequest).get(); - ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); - return (Response<String>) applyObjectsAction.apply(projectResource, input); + return (Response<String>) applyObjectsAction.apply(parseProjectResource(id), input); } @SuppressWarnings("unchecked") private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception { - HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class)); + HeadInput input = PayloadSerDes.parseHeadInput(httpRequest); IdString id = getProjectName(httpRequest).get(); - ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); - return (Response<String>) updateHEADAction.apply(projectResource, input); + return (Response<String>) updateHEADAction.apply(parseProjectResource(id), input); } @SuppressWarnings("unchecked") private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception { IdString id = getProjectName(httpRequest).get(); - ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<String>) - projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput()); + projectDeletionAction.apply( + parseProjectResource(id), new ProjectDeletionAction.DeleteInput()); } @SuppressWarnings("unchecked") private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest) throws IOException, RestApiException, PermissionBackendException { - Input input = readJson(httpRequest, TypeLiteral.get(Input.class)); + Input input = PayloadSerDes.parseInput(httpRequest); IdString id = getProjectName(httpRequest).get(); - ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); - return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input); + return (Response<Map<String, Object>>) fetchAction.apply(parseProjectResource(id), input); } - private <T> void writeResponse(HttpServletResponse httpResponse, Response<T> 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 ProjectResource parseProjectResource(IdString id) throws ResourceNotFoundException { + Optional<ProjectState> project = projectCache.get(Project.nameKey(id.get())); + if (project.isEmpty()) { + throw new ResourceNotFoundException(id); } - } - - 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 new ProjectResource(project.get(), currentUserProvider.get()); } /**
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 deleted file mode 100644 index 85a7729..0000000 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java +++ /dev/null
@@ -1,25 +0,0 @@ -// 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/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java new file mode 100644 index 0000000..414af37 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
@@ -0,0 +1,106 @@ +// Copyright (C) 2023 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.util; + +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.api.projects.HeadInput; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.httpd.restapi.RestApiServlet; +import com.google.gerrit.json.OutputFormat; +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; +import com.google.inject.TypeLiteral; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction; +import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput; +import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput; +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class PayloadSerDes { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final Gson gson = OutputFormat.JSON.newGsonBuilder().create(); + + public static RevisionInput parseRevisionInput(HttpServletRequest httpRequest) + throws BadRequestException, IOException { + return parse(httpRequest, TypeLiteral.get(RevisionInput.class)); + } + + public static RevisionsInput parseRevisionsInput(HttpServletRequest httpRequest) + throws BadRequestException, IOException { + return parse(httpRequest, TypeLiteral.get(RevisionsInput.class)); + } + + public static HeadInput parseHeadInput(HttpServletRequest httpRequest) + throws BadRequestException, IOException { + return parse(httpRequest, TypeLiteral.get(HeadInput.class)); + } + + public static FetchAction.Input parseInput(HttpServletRequest httpRequest) + throws BadRequestException, IOException { + return parse(httpRequest, TypeLiteral.get(FetchAction.Input.class)); + } + + public static <T> void writeResponse(HttpServletResponse httpResponse, Response<T> 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 static <T> T parse(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"); + } + } + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java index 1991260..f7ed4cb 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
@@ -40,7 +40,22 @@ return callFetch(project, refName, targetUri, MILLISECONDS.toNanos(System.currentTimeMillis())); } - HttpResult initProject(Project.NameKey project, URIish uri) throws IOException; + /** + * Replicates the creation of a project, including the configuration stored in refs/meta/config. + * + * @param project The unique name of the project. + * @param uri The destination URI where the project and its configuration should be replicated to. + * @param eventCreatedOn The timestamp indicating when the init project event occurred. + * @param refsMetaConfigRevisionData A history of revisions for the refs/meta/config ref. + * @return An HTTP result object providing information about the replication process. + * @throws IOException If an I/O error occurs during the replication. + */ + HttpResult initProject( + Project.NameKey project, + URIish uri, + long eventCreatedOn, + List<RevisionData> refsMetaConfigRevisionData) + throws IOException; HttpResult deleteProject(Project.NameKey project, URIish apiUri) throws IOException;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java index b606ba8..7607e4b 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -23,6 +23,7 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.server.config.GerritInstanceId; @@ -130,14 +131,29 @@ } /* (non-Javadoc) - * @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#initProject(com.google.gerrit.entities.Project.NameKey, org.eclipse.jgit.transport.URIish) + * @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#initProject(com.google.gerrit.entities.Project.NameKey, org.eclipse.jgit.transport.URIish, long, java.util.List<com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData>) */ @Override - public HttpResult initProject(Project.NameKey project, URIish uri) throws IOException { + public HttpResult initProject( + NameKey project, + URIish uri, + long eventCreatedOn, + List<RevisionData> refsMetaConfigRevisionData) + throws IOException { String url = formatInitProjectUrl(uri.toString(), project); + + RevisionData[] inputData = new RevisionData[refsMetaConfigRevisionData.size()]; + RevisionsInput input = + new RevisionsInput( + instanceId, + RefNames.REFS_CONFIG, + eventCreatedOn, + refsMetaConfigRevisionData.toArray(inputData)); + HttpPut put = new HttpPut(url); + put.setEntity(new StringEntity(GSON.toJson(input))); put.addHeader(new BasicHeader("Accept", MediaType.ANY_TEXT_TYPE.toString())); - put.addHeader(new BasicHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString())); + put.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString())); return executeRequest(put, bearerTokenProvider.get(), uri); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java index 36356e9..05033b4 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
@@ -61,10 +61,6 @@ throw new MissingParentObjectException(name, refSpec.getSource(), parent.getId()); } } - refHead = newObjectID = oi.insert(commitObject.getType(), commitObject.getContent()); - - RevisionObjectData treeObject = revisionData.getTreeObject(); - oi.insert(treeObject.getType(), treeObject.getContent()); } for (RevisionObjectData rev : revisionData.getBlobs()) { @@ -75,6 +71,13 @@ refHead = newObjectID; } + if (commitObject != null) { + RevisionObjectData treeObject = revisionData.getTreeObject(); + oi.insert(treeObject.getType(), treeObject.getContent()); + + refHead = oi.insert(commitObject.getType(), commitObject.getContent()); + } + oi.flush(); if (commitObject == null) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java index 8fac9ed..43ec9bf 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
@@ -28,6 +28,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; @@ -107,7 +109,9 @@ if (credentialsProvider.supports(user, pass) && credentialsProvider.get(uri, user, pass) && uri.getScheme() != null - && !"ssh".equalsIgnoreCase(uri.getScheme())) { + && !"ssh".equalsIgnoreCase(uri.getScheme()) + && StringUtils.isNotEmpty(user.getValue()) + && ArrayUtils.isNotEmpty(pass.getValue())) { return uri.setUser(user.getValue()).setPass(String.valueOf(pass.getValue())); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java index 9a10898..434c0f0 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
@@ -35,7 +35,7 @@ @Override public Void visit(AssistedInjectBinding<? extends FetchFactory> binding) { - TypeLiteral<CGitFetch> nativeGitFetchType = new TypeLiteral<CGitFetch>() {}; + TypeLiteral<CGitFetch> nativeGitFetchType = new TypeLiteral<>() {}; for (AssistedMethod method : binding.getAssistedMethods()) { if (method.getImplementationType().equals(nativeGitFetchType)) { String[] command = new String[] {"git", "--version"};
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java index acb68cf..0fa89b5 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
@@ -14,7 +14,7 @@ package com.googlesource.gerrit.plugins.replication.pull.fetch; -import com.jcraft.jsch.JSchException; +import org.apache.sshd.common.SshException; import org.eclipse.jgit.errors.TransportException; public class PermanentTransportException extends TransportException { @@ -26,7 +26,8 @@ public static TransportException wrapIfPermanentTransportException(TransportException e) { Throwable cause = e.getCause(); - if (cause instanceof JSchException && cause.getMessage().startsWith("UnknownHostKey:")) { + if (cause instanceof SshException + && cause.getMessage().startsWith("Failed (UnsupportedCredentialItem) to execute:")) { return new PermanentTransportException("Terminal fetch failure", e); }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md index 73c5c05..22b52e7 100644 --- a/src/main/resources/Documentation/about.md +++ b/src/main/resources/Documentation/about.md
@@ -16,10 +16,6 @@ group that is granted the 'Pull Replication' capability (provided by this plugin) or the 'Administrate Server' capability. -When replicating hidden projects, the pull replication user needs to have -the 'Administrate Server' capability or being added as the owner of each -individual project that is supposed to be replicated. - Change Indexing --------
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java index ee286bb..dd9f98e 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
@@ -18,9 +18,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import com.google.gerrit.acceptance.TestMetricMaker; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.registration.DynamicItem; -import com.google.gerrit.metrics.DisabledMetricMaker; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.PerThreadRequestScope; import com.google.gerrit.server.util.IdGenerator; @@ -54,6 +54,7 @@ private final Project.NameKey PROJECT_NAME = Project.NameKey.parse(TEST_PROJECT_NAME); private final String TEST_REF = "refs/heads/refForReplicationTask"; private final String URI_PATTERN = "http://test.com/" + TEST_PROJECT_NAME + ".git"; + private final TestMetricMaker testMetricMaker = new TestMetricMaker(); @Mock private GitRepositoryManager grm; @Mock private Repository repository; @@ -72,8 +73,9 @@ @Before public void setup() throws Exception { + testMetricMaker.reset(); FetchReplicationMetrics fetchReplicationMetrics = - new FetchReplicationMetrics("pull-replication", new DisabledMetricMaker()); + new FetchReplicationMetrics("pull-replication", testMetricMaker); urIish = new URIish(URI_PATTERN); grm = mock(GitRepositoryManager.class); @@ -704,6 +706,51 @@ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null); } + @Test + public void shouldNotRecordReplicationLatencyMetricIfAllRefsAreExcluded() throws Exception { + setupMocks(true); + String filteredRef = "refs/heads/filteredRef"; + Set<String> refSpecs = Set.of(TEST_REF, filteredRef); + createTestStates(TEST_REF, 1); + createTestStates(filteredRef, 1); + setupFetchFactoryMock( + List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()), + Optional.of(List.of(TEST_REF))); + objectUnderTest.addRefs(refSpecs); + objectUnderTest.setReplicationFetchFilter(replicationFilter); + ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class); + when(replicationFilter.get()).thenReturn(mockFilter); + when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Collections.emptySet()); + + objectUnderTest.run(); + + verify(pullReplicationApiRequestMetrics, never()).stop(any()); + assertThat(testMetricMaker.getTimer("replication_latency")).isEqualTo(0); + } + + @Test + public void shouldRecordReplicationLatencyMetricWhenAtLeastOneRefWasReplicated() + throws Exception { + setupMocks(true); + String filteredRef = "refs/heads/filteredRef"; + Set<String> refSpecs = Set.of(TEST_REF, filteredRef); + createTestStates(TEST_REF, 1); + createTestStates(filteredRef, 1); + setupFetchFactoryMock( + List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()), + Optional.of(List.of(TEST_REF))); + objectUnderTest.addRefs(refSpecs); + objectUnderTest.setReplicationFetchFilter(replicationFilter); + ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class); + when(replicationFilter.get()).thenReturn(mockFilter); + when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Set.of(TEST_REF)); + + objectUnderTest.run(); + + verify(pullReplicationApiRequestMetrics).stop(any()); + assertThat(testMetricMaker.getTimer("replication_latency")).isGreaterThan(0); + } + private void setupRequestScopeMock() { when(scoper.scope(any())) .thenAnswer(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java index 09a465c..fcb0702 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
@@ -18,7 +18,7 @@ import com.googlesource.gerrit.plugins.replication.pull.fetch.InexistentRefTransportException; import com.googlesource.gerrit.plugins.replication.pull.fetch.PermanentTransportException; -import com.jcraft.jsch.JSchException; +import org.apache.sshd.common.SshException; import org.eclipse.jgit.errors.TransportException; import org.junit.Test; @@ -29,7 +29,9 @@ assertThat( PermanentTransportException.wrapIfPermanentTransportException( new TransportException( - "SSH error", new JSchException("UnknownHostKey: some.place")))) + "SSH error", + new SshException( + "Failed (UnsupportedCredentialItem) to execute: some.commands")))) .isInstanceOf(PermanentTransportException.class); }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java index 5cf01b1..0721dd2 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
@@ -49,7 +49,8 @@ @UseLocalDisk @TestPlugin( name = "pull-replication", - sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule") + sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule", + httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule") public class PullReplicationFanoutConfigIT extends LightweightPluginDaemonTest { private static final Optional<String> ALL_PROJECTS = Optional.empty(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java index 18f7a0c..5717a8b 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2022 The Android Open Source Project +// Copyright (C) 2020 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. @@ -14,9 +14,45 @@ package com.googlesource.gerrit.plugins.replication.pull; +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.GitUtil.fetch; +import static com.google.gerrit.acceptance.GitUtil.pushOne; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +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.config.GerritConfig; +import com.google.gerrit.entities.Permission; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.events.HeadUpdatedListener; +import com.google.gerrit.extensions.events.ProjectDeletedListener; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator; +import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; +import org.eclipse.jgit.transport.URIish; +import org.junit.Ignore; +import org.junit.Test; @SkipProjectClone @UseLocalDisk @@ -25,4 +61,350 @@ sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationITAbstract$PullReplicationTestModule", httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule") -public class PullReplicationIT extends PullReplicationITAbstract {} +public class PullReplicationIT extends PullReplicationSetupBase { + + @Override + protected void setReplicationSource( + String remoteName, List<String> replicaSuffixes, Optional<String> project) + throws IOException { + List<String> fetchUrls = + buildReplicaURLs(replicaSuffixes, s -> gitPath.resolve("${name}" + s + ".git").toString()); + config.setStringList("remote", remoteName, "url", fetchUrls); + config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); + config.setString("remote", remoteName, "fetch", "+refs/*:refs/*"); + 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(); + } + + @Override + public void setUpTestPlugin() throws Exception { + setUpTestPlugin(false); + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateNewChangeRef() throws Exception { + testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); + + Result pushResult = createChange(); + RevCommit sourceCommit = pushResult.getCommit(); + String sourceRef = pushResult.getPatchSet().refName(); + + ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class); + FakeGitReferenceUpdatedEvent event = + new FakeGitReferenceUpdatedEvent( + project, + sourceRef, + ObjectId.zeroId().getName(), + sourceCommit.getId().getName(), + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(event); + + try (Repository repo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, sourceRef) != null); + + Ref targetBranchRef = getRef(repo, sourceRef); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId()); + } + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateNewBranch() throws Exception { + String testProjectName = project + TEST_REPLICATION_SUFFIX; + createTestProject(testProjectName); + + String newBranch = "refs/heads/mybranch"; + String master = "refs/heads/master"; + BranchInput input = new BranchInput(); + input.revision = master; + gApi.projects().name(testProjectName).branch(newBranch).create(input); + String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision; + + ReplicationQueue pullReplicationQueue = + plugin.getSysInjector().getInstance(ReplicationQueue.class); + FakeGitReferenceUpdatedEvent event = + new FakeGitReferenceUpdatedEvent( + project, + newBranch, + ObjectId.zeroId().getName(), + branchRevision, + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(event); + + try (Repository repo = repoManager.openRepository(project); + Repository sourceRepo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, newBranch) != null); + + Ref targetBranchRef = getRef(repo, newBranch); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision); + } + } + + @Test + @UseLocalDisk + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateForceUpdatedBranch() throws Exception { + boolean forcedPush = true; + String testProjectName = project + TEST_REPLICATION_SUFFIX; + NameKey testProjectNameKey = createTestProject(testProjectName); + + String newBranch = "refs/heads/mybranch"; + String master = "refs/heads/master"; + BranchInput input = new BranchInput(); + input.revision = master; + gApi.projects().name(testProjectName).branch(newBranch).create(input); + + projectOperations + .project(testProjectNameKey) + .forUpdate() + .add(allow(Permission.PUSH).ref(newBranch).group(REGISTERED_USERS).force(true)) + .update(); + + String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision; + + ReplicationQueue pullReplicationQueue = + plugin.getSysInjector().getInstance(ReplicationQueue.class); + FakeGitReferenceUpdatedEvent event = + new FakeGitReferenceUpdatedEvent( + project, + newBranch, + ObjectId.zeroId().getName(), + branchRevision, + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(event); + + try (Repository repo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, newBranch) != null); + + Ref targetBranchRef = getRef(repo, newBranch); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision); + } + + TestRepository<InMemoryRepository> testProject = cloneProject(testProjectNameKey); + fetch(testProject, RefNames.REFS_HEADS + "*:" + RefNames.REFS_HEADS + "*"); + RevCommit amendedCommit = testProject.amendRef(newBranch).message("Amended commit").create(); + PushResult pushResult = + pushOne(testProject, newBranch, newBranch, false, forcedPush, Collections.emptyList()); + Collection<RemoteRefUpdate> pushedRefs = pushResult.getRemoteUpdates(); + assertThat(pushedRefs).hasSize(1); + assertThat(pushedRefs.iterator().next().getStatus()).isEqualTo(Status.OK); + + FakeGitReferenceUpdatedEvent forcedPushEvent = + new FakeGitReferenceUpdatedEvent( + project, + newBranch, + branchRevision, + amendedCommit.getId().getName(), + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(forcedPushEvent); + + try (Repository repo = repoManager.openRepository(project); + Repository sourceRepo = repoManager.openRepository(project)) { + waitUntil( + () -> + checkedGetRef(repo, newBranch) != null + && checkedGetRef(repo, newBranch) + .getObjectId() + .getName() + .equals(amendedCommit.getId().getName())); + } + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateNewChangeRefCGitClient() throws Exception { + AutoReloadConfigDecorator autoReloadConfigDecorator = + getInstance(AutoReloadConfigDecorator.class); + + config.setBoolean("replication", null, "useCGitClient", true); + config.save(); + + autoReloadConfigDecorator.reload(); + + testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); + + Result pushResult = createChange(); + RevCommit sourceCommit = pushResult.getCommit(); + String sourceRef = pushResult.getPatchSet().refName(); + + ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class); + FakeGitReferenceUpdatedEvent event = + new FakeGitReferenceUpdatedEvent( + project, + sourceRef, + ObjectId.zeroId().getName(), + sourceCommit.getId().getName(), + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(event); + + try (Repository repo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, sourceRef) != null); + + Ref targetBranchRef = getRef(repo, sourceRef); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId()); + } + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateNewBranchCGitClient() throws Exception { + AutoReloadConfigDecorator autoReloadConfigDecorator = + getInstance(AutoReloadConfigDecorator.class); + + config.setBoolean("replication", null, "useCGitClient", true); + config.save(); + + autoReloadConfigDecorator.reload(); + + String testProjectName = project + TEST_REPLICATION_SUFFIX; + createTestProject(testProjectName); + + String newBranch = "refs/heads/mybranch"; + String master = "refs/heads/master"; + BranchInput input = new BranchInput(); + input.revision = master; + gApi.projects().name(testProjectName).branch(newBranch).create(input); + String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision; + + ReplicationQueue pullReplicationQueue = + plugin.getSysInjector().getInstance(ReplicationQueue.class); + FakeGitReferenceUpdatedEvent event = + new FakeGitReferenceUpdatedEvent( + project, + newBranch, + ObjectId.zeroId().getName(), + branchRevision, + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(event); + + try (Repository repo = repoManager.openRepository(project); + Repository sourceRepo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, newBranch) != null); + + Ref targetBranchRef = getRef(repo, newBranch); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision); + } + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldCreateNewProject() throws Exception { + NameKey projectToCreate = Project.nameKey(project.get() + "_created"); + + setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToCreate.get())); + config.save(); + AutoReloadConfigDecorator autoReloadConfigDecorator = + getInstance(AutoReloadConfigDecorator.class); + autoReloadConfigDecorator.reload(); + Source source = + getInstance(SourcesCollection.class).getByRemoteName(TEST_REPLICATION_REMOTE).get(); + + FetchApiClient client = getInstance(FetchApiClient.Factory.class).create(source); + client.initProject( + projectToCreate, + new URIish(source.getApis().get(0)), + System.currentTimeMillis(), + Collections.emptyList()); + + waitUntil(() -> repoManager.list().contains(projectToCreate)); + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateProjectDeletion() throws Exception { + String projectToDelete = project.get(); + setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToDelete)); + config.save(); + AutoReloadConfigDecorator autoReloadConfigDecorator = + getInstance(AutoReloadConfigDecorator.class); + autoReloadConfigDecorator.reload(); + + ProjectDeletedListener.Event event = + new ProjectDeletedListener.Event() { + @Override + public String getProjectName() { + return projectToDelete; + } + + @Override + public NotifyHandling getNotify() { + return NotifyHandling.NONE; + } + }; + for (ProjectDeletedListener l : deletedListeners) { + l.onProjectDeleted(event); + } + + waitUntil(() -> !repoManager.list().contains(project)); + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateHeadUpdate() throws Exception { + String testProjectName = project.get(); + setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(testProjectName)); + config.save(); + AutoReloadConfigDecorator autoReloadConfigDecorator = + getInstance(AutoReloadConfigDecorator.class); + autoReloadConfigDecorator.reload(); + + String newBranch = "refs/heads/mybranch"; + String master = "refs/heads/master"; + BranchInput input = new BranchInput(); + input.revision = master; + gApi.projects().name(testProjectName).branch(newBranch).create(input); + + ReplicationQueue pullReplicationQueue = + plugin.getSysInjector().getInstance(ReplicationQueue.class); + + HeadUpdatedListener.Event event = new FakeHeadUpdateEvent(master, newBranch, testProjectName); + pullReplicationQueue.onHeadUpdated(event); + + waitUntil( + () -> { + try { + return gApi.projects().name(testProjectName).head().equals(newBranch); + } catch (RestApiException e) { + return false; + } + }); + } + + @Ignore + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + @GerritConfig(name = "container.replica", value = "true") + public void shouldReplicateNewChangeRefToReplica() throws Exception { + testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); + + Result pushResult = createChange(); + RevCommit sourceCommit = pushResult.getCommit(); + String sourceRef = pushResult.getPatchSet().refName(); + + ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class); + FakeGitReferenceUpdatedEvent event = + new FakeGitReferenceUpdatedEvent( + project, + sourceRef, + ObjectId.zeroId().getName(), + sourceCommit.getId().getName(), + TEST_REPLICATION_REMOTE); + pullReplicationQueue.onEvent(event); + + try (Repository repo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, sourceRef) != null); + + Ref targetBranchRef = getRef(repo, sourceRef); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId()); + } + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java index 1cffec7..a07aa55 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.events.ProjectDeletedListener; @@ -147,7 +148,8 @@ .when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any())) .thenReturn(httpResult); when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(fetchHttpResult); - when(fetchRestApiClient.initProject(any(), any())).thenReturn(successfulHttpResult); + when(fetchRestApiClient.initProject(any(), any(), anyLong(), any())) + .thenReturn(successfulHttpResult); when(successfulHttpResult.isSuccessful()).thenReturn(true); when(httpResult.isSuccessful()).thenReturn(true); when(fetchHttpResult.isSuccessful()).thenReturn(true); @@ -206,7 +208,7 @@ objectUnderTest.start(); objectUnderTest.onEvent(event); - verify(fetchRestApiClient).initProject(any(), any()); + verify(fetchRestApiClient).initProject(any(), any(), anyLong(), any()); } @Test @@ -219,7 +221,22 @@ objectUnderTest.start(); objectUnderTest.onEvent(event); - verify(fetchRestApiClient, never()).initProject(any(), any()); + verify(fetchRestApiClient, never()).initProject(any(), any(), anyLong(), any()); + } + + @Test + public void shouldNotCallInitProjectWhenProjectWithoutConfiguration() throws Exception { + Event event = new TestEvent("refs/changes/01/1/meta"); + when(httpResult.isSuccessful()).thenReturn(false); + when(httpResult.isProjectMissing(any())).thenReturn(true); + when(source.isCreateMissingRepositories()).thenReturn(true); + when(revReader.read(any(), any(), eq(RefNames.REFS_CONFIG), anyInt())) + .thenReturn(Optional.empty()); + + objectUnderTest.start(); + objectUnderTest.onEvent(event); + + verify(fetchRestApiClient, never()).initProject(any(), any(), anyLong(), any()); } @Test @@ -369,7 +386,7 @@ } @Test - public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() throws Exception { + public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() { when(source.wouldDeleteProject(any())).thenReturn(true); String projectName = "testProject"; @@ -385,7 +402,7 @@ } @Test - public void shouldNotCallDeleteWhenProjectNotToDelete() throws Exception { + public void shouldNotCallDeleteWhenProjectNotToDelete() { when(source.wouldDeleteProject(any())).thenReturn(false); FakeProjectDeletedEvent event = new FakeProjectDeletedEvent("testProject"); @@ -397,7 +414,7 @@ } @Test - public void shouldScheduleUpdateHeadWhenWouldFetchProject() throws Exception { + public void shouldScheduleUpdateHeadWhenWouldFetchProject() { when(source.wouldFetchProject(any())).thenReturn(true); String projectName = "aProject"; @@ -413,7 +430,7 @@ } @Test - public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() throws Exception { + public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() { when(source.wouldFetchProject(any())).thenReturn(false); String projectName = "aProject";
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 index a46846e..4a3fa55 100644 --- 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
@@ -162,7 +162,7 @@ } public ResponseHandler<Object> assertHttpResponseCode(int responseCode) { - return new ResponseHandler<Object>() { + return new ResponseHandler<>() { @Override public Object handleResponse(HttpResponse response)
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java index f1f9e44..fc1b02c 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
@@ -102,7 +102,6 @@ fetchStateLog, projectCache, sourceCollection, - applyObject, permissionBackend, eventDispatcherDataItem, new LocalGitRepositoryManagerProvider(gitManager));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java index 10415e4..cf7515e 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -94,10 +94,11 @@ @Test @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId") @GerritConfig(name = "container.replica", value = "true") - public void shouldReturnForbiddenForUserWithoutPermissionsOnReplica() throws Exception { + public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception { httpClientFactory .create(source) - .execute(createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); + .execute( + createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED)); } @Test
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 c543969..0f13881 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
@@ -184,12 +184,13 @@ @Test @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId") @GerritConfig(name = "container.replica", value = "true") - public void shouldReturnForbiddenForUserWithoutPermissionsWhenNodeIsAReplica() throws Exception { + public void shouldReturnUnauthorizedForUserWithoutPermissionsWhenNodeIsAReplica() + throws Exception { httpClientFactory .create(source) .execute( createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); + assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED)); } @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java index 9e5ca8d..9492d49 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -4,18 +4,25 @@ import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY; import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.internal.verification.VerificationModeFactory.atLeastOnce; import static org.mockito.internal.verification.VerificationModeFactory.times; import com.google.common.net.MediaType; +import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.restapi.*; +import com.google.gerrit.server.AnonymousUser; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectResource; -import com.google.gerrit.server.restapi.project.ProjectsCollection; +import com.google.gerrit.server.project.ProjectState; +import com.google.inject.util.Providers; import java.io.*; import java.nio.charset.StandardCharsets; +import java.util.Optional; import javax.servlet.FilterChain; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; @@ -37,10 +44,12 @@ @Mock private ProjectInitializationAction projectInitializationAction; @Mock private UpdateHeadAction updateHEADAction; @Mock private ProjectDeletionAction projectDeletionAction; - @Mock private ProjectsCollection projectsCollection; - @Mock private ProjectResource projectResource; + @Mock private ProjectCache projectCache; + @Mock private ProjectState projectState; @Mock private ServletOutputStream outputStream; @Mock private PrintWriter printWriter; + @Mock private IdentifiedUser identifiedUserMock; + @Mock private AnonymousUser anonymousUserMock; private final String PLUGIN_NAME = "pull-replication"; private final String PROJECT_NAME = "some-project"; private final String PROJECT_NAME_GIT = "some-project.git"; @@ -60,6 +69,10 @@ private final Response OK_RESPONSE = Response.ok(); private PullReplicationFilter createPullReplicationFilter() { + return createPullReplicationFilter(identifiedUserMock); + } + + private PullReplicationFilter createPullReplicationFilter(CurrentUser currentUser) { return new PullReplicationFilter( fetchAction, applyObjectAction, @@ -67,8 +80,9 @@ projectInitializationAction, updateHEADAction, projectDeletionAction, - projectsCollection, - PLUGIN_NAME); + projectCache, + PLUGIN_NAME, + Providers.of(currentUser)); } private void defineBehaviours(byte[] payload, String uri) throws Exception { @@ -76,15 +90,14 @@ InputStream is = new ByteArrayInputStream(payload); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); when(request.getReader()).thenReturn(bufferedReader); - when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) - .thenReturn(projectResource); + when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState)); when(response.getWriter()).thenReturn(printWriter); } private void verifyBehaviours() throws Exception { verify(request, atLeastOnce()).getRequestURI(); verify(request).getReader(); - verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)); + verify(projectCache).get(Project.nameKey(PROJECT_NAME)); verify(response).getWriter(); verify(response).setContentType("application/json"); verify(response).setStatus(HttpServletResponse.SC_OK); @@ -107,7 +120,7 @@ pullReplicationFilter.doFilter(request, response, filterChain); verifyBehaviours(); - verify(fetchAction).apply(eq(projectResource), any()); + verify(fetchAction).apply(any(ProjectResource.class), any()); } @Test @@ -130,7 +143,7 @@ pullReplicationFilter.doFilter(request, response, filterChain); verifyBehaviours(); - verify(applyObjectAction).apply(eq(projectResource), any()); + verify(applyObjectAction).apply(any(ProjectResource.class), any()); } @Test @@ -152,7 +165,7 @@ pullReplicationFilter.doFilter(request, response, filterChain); verifyBehaviours(); - verify(applyObjectsAction).apply(eq(projectResource), any()); + verify(applyObjectsAction).apply(any(ProjectResource.class), any()); } @Test @@ -160,15 +173,11 @@ when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI); when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString()); - when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(true); - when(response.getWriter()).thenReturn(printWriter); final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); pullReplicationFilter.doFilter(request, response, filterChain); - verify(request, times(5)).getRequestURI(); - verify(projectInitializationAction).initProject(eq(PROJECT_NAME_GIT)); - verify(response).getWriter(); + verify(projectInitializationAction).service(request, response); } @Test @@ -183,15 +192,14 @@ pullReplicationFilter.doFilter(request, response, filterChain); verifyBehaviours(); - verify(updateHEADAction).apply(eq(projectResource), any()); + verify(updateHEADAction).apply(any(ProjectResource.class), any()); } @Test public void shouldFilterProjectDeletionAction() throws Exception { when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); when(request.getMethod()).thenReturn("DELETE"); - when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) - .thenReturn(projectResource); + when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState)); when(projectDeletionAction.apply(any(), any())).thenReturn(OK_RESPONSE); when(response.getWriter()).thenReturn(printWriter); @@ -199,8 +207,8 @@ pullReplicationFilter.doFilter(request, response, filterChain); verify(request, times(7)).getRequestURI(); - verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)); - verify(projectDeletionAction).apply(eq(projectResource), any()); + verify(projectCache).get(Project.nameKey(PROJECT_NAME)); + verify(projectDeletionAction).apply(any(ProjectResource.class), any()); verify(response).getWriter(); verify(response).setContentType("application/json"); verify(response).setStatus(OK_RESPONSE.statusCode()); @@ -215,6 +223,17 @@ } @Test + public void shouldGoNextInChainWhenAnonymousRequestUriDoesNotMatch() throws Exception { + when(request.getRequestURI()).thenReturn("any-url"); + lenient().when(response.getOutputStream()).thenReturn(outputStream); + + final PullReplicationFilter pullReplicationFilter = + createPullReplicationFilter(anonymousUserMock); + pullReplicationFilter.doFilter(request, response, filterChain); + verify(filterChain).doFilter(request, response); + } + + @Test public void shouldBe404WhenJsonIsMalformed() throws Exception { byte[] payloadMalformedJson = "some-json-malformed".getBytes(StandardCharsets.UTF_8); InputStream is = new ByteArrayInputStream(payloadMalformedJson); @@ -230,24 +249,10 @@ } @Test - public void shouldBe500WhenProjectCannotBeInitiated() throws Exception { - when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI); - when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString()); - when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(false); - when(response.getOutputStream()).thenReturn(outputStream); - - final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); - pullReplicationFilter.doFilter(request, response, filterChain); - - verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - - @Test public void shouldBe500WhenResourceNotFound() throws Exception { when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); when(request.getMethod()).thenReturn("DELETE"); - when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) - .thenReturn(projectResource); + when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState)); when(projectDeletionAction.apply(any(), any())) .thenThrow(new ResourceNotFoundException("resource not found")); when(response.getOutputStream()).thenReturn(outputStream); @@ -280,6 +285,19 @@ } @Test + public void shouldBe401WhenUserIsAnonymous() throws Exception { + byte[] payloadFetchAction = "{}".getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetchAction, FETCH_URI); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(anonymousUserMock); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test public void shouldBe422WhenEntityCannotBeProcessed() throws Exception { byte[] payloadFetchAction = ("{" @@ -304,8 +322,7 @@ public void shouldBe409WhenThereIsResourceConflict() throws Exception { when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); when(request.getMethod()).thenReturn("DELETE"); - when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) - .thenReturn(projectResource); + when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState)); when(projectDeletionAction.apply(any(), any())) .thenThrow(new ResourceConflictException("Resource conflict"));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java index 25aa2a7..65ccdfb 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
@@ -24,6 +24,7 @@ import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.io.CharStreams; +import com.google.common.net.MediaType; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.RefNames; import com.googlesource.gerrit.plugins.replication.CredentialsFactory; @@ -37,6 +38,7 @@ import java.nio.ByteBuffer; import java.util.Collections; import org.apache.http.Header; +import org.apache.http.HttpHeaders; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; @@ -94,6 +96,17 @@ + "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\"" + blobObjectId + "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}}"; + + String expectedInitProjectWithConfigPayload = + "{\"label\":\"Replication\",\"ref_name\":\"refs/meta/config\",\"event_created_on\":" + + eventCreatedOn + + ",\"revisions_data\":[{\"commit_object\":{\"sha1\":\"" + + commitObjectId + + "\",\"type\":1,\"content\":\"dHJlZSA3NzgxNGQyMTZhNmNhYjJkZGI5ZjI4NzdmYmJkMGZlYmRjMGZhNjA4CnBhcmVudCA5ODNmZjFhM2NmNzQ3MjVhNTNhNWRlYzhkMGMwNjEyMjEyOGY1YThkCmF1dGhvciBHZXJyaXQgVXNlciAxMDAwMDAwIDwxMDAwMDAwQDY5ZWMzOGYwLTM1MGUtNGQ5Yy05NmQ0LWJjOTU2ZjJmYWFhYz4gMTYxMDU3ODY0OCArMDEwMApjb21taXR0ZXIgR2Vycml0IENvZGUgUmV2aWV3IDxyb290QG1hY3plY2gtWFBTLTE1PiAxNjEwNTc4NjQ4ICswMTAwCgpVcGRhdGUgcGF0Y2ggc2V0IDEKClBhdGNoIFNldCAxOgoKKDEgY29tbWVudCkKClBhdGNoLXNldDogMQo\\u003d\"},\"tree_object\":{\"sha1\":\"" + + treeObjectId + + "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\"" + + blobObjectId + + "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}]}"; String commitObject = "tree " + treeObjectId @@ -374,18 +387,26 @@ @Test public void shouldCallInitProjectEndpoint() throws Exception { - - objectUnderTest.initProject(Project.nameKey("test_repo"), new URIish(api)); + objectUnderTest.initProject( + Project.nameKey("test_repo"), + new URIish(api), + eventCreatedOn, + Collections.singletonList(createSampleRevisionData())); verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any()); - HttpPut httpPut = httpPutCaptor.getValue(); + String payload = + CharStreams.toString( + new InputStreamReader(httpPut.getEntity().getContent(), Charsets.UTF_8)); assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host"); + assertThat(httpPut.getHeaders(HttpHeaders.CONTENT_TYPE)[0].getValue()) + .isEqualTo(MediaType.JSON_UTF_8.toString()); assertThat(httpPut.getURI().getPath()) .isEqualTo( String.format( "%s/plugins/pull-replication/init-project/test_repo.git", urlAuthenticationPrefix())); + assertThat(payload).isEqualTo(expectedInitProjectWithConfigPayload); assertAuthentication(httpPut); }