Merge branch 'stable-3.1'
* stable-3.1:
Jenkinsfile: use gerrit-ci-library pipeline for plugin validation
Mute logging for missing fetch param in remote section
Fix NPE issue from pull replication after configuration auto reload
Make HTTP connection parameters source-aware
Fix issue with auto reload of sources
Add 'Pull Replication' capability
Adopt new replication configuration structure
Change-Id: I5d1cfdba092afc8ba7abd2f52561f1ee575fdd90
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..448692b
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,2 @@
+pluginPipeline(formatCheckId: 'gerritforge:pull-replication-format-3852e64366bb37d13b8baf8af9b15cfd38eb9227',
+ buildCheckId: 'gerritforge:pull-replication-3852e64366bb37d13b8baf8af9b15cfd38eb9227')
\ No newline at end of file
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 1b079dd..9add07c 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
@@ -32,28 +32,33 @@
import com.google.inject.internal.UniqueAnnotations;
import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
import com.googlesource.gerrit.plugins.replication.AutoReloadSecureCredentialsFactoryDecorator;
+import com.googlesource.gerrit.plugins.replication.ConfigParser;
import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.FanoutReplicationConfig;
+import com.googlesource.gerrit.plugins.replication.MainReplicationConfig;
import com.googlesource.gerrit.plugins.replication.ObservableQueue;
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
-import com.googlesource.gerrit.plugins.replication.ReplicationConfigValidator;
import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
import com.googlesource.gerrit.plugins.replication.StartReplicationCapability;
import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule;
import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
-import com.googlesource.gerrit.plugins.replication.pull.client.HttpClientProvider;
+import com.googlesource.gerrit.plugins.replication.pull.client.HttpClient;
+import com.googlesource.gerrit.plugins.replication.pull.client.SourceHttpClient;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
-import org.apache.http.impl.client.CloseableHttpClient;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
class PullReplicationModule extends AbstractModule {
+ private final SitePaths site;
private final Path cfgPath;
@Inject
public PullReplicationModule(SitePaths site) {
+ this.site = site;
cfgPath = site.etc_dir.resolve("replication.config");
}
@@ -62,7 +67,11 @@
install(new PullReplicationApiModule());
- bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class).in(Scopes.SINGLETON);
+ install(
+ new FactoryModuleBuilder()
+ .implement(HttpClient.class, SourceHttpClient.class)
+ .build(SourceHttpClient.Factory.class));
+
install(new FactoryModuleBuilder().build(Source.Factory.class));
install(new FactoryModuleBuilder().build(FetchRestApiClient.Factory.class));
@@ -94,15 +103,18 @@
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReplicationQueue.class);
- bind(ReplicationConfigValidator.class).to(SourcesCollection.class);
+ bind(ConfigParser.class).to(SourceConfigParser.class).in(Scopes.SINGLETON);
if (getReplicationConfig().getBoolean("gerrit", "autoReload", false)) {
- bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class);
+ bind(ReplicationConfig.class)
+ .annotatedWith(MainReplicationConfig.class)
+ .to(getReplicationConfigClass());
+ bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class).in(Scopes.SINGLETON);
bind(LifecycleListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(AutoReloadConfigDecorator.class);
} else {
- bind(ReplicationConfig.class).to(ReplicationFileBasedConfig.class);
+ bind(ReplicationConfig.class).to(getReplicationConfigClass()).in(Scopes.SINGLETON);
}
DynamicSet.setOf(binder(), ReplicationStateListener.class);
@@ -122,4 +134,11 @@
}
return config;
}
+
+ private Class<? extends ReplicationConfig> getReplicationConfigClass() {
+ if (Files.exists(site.etc_dir.resolve("replication"))) {
+ return FanoutReplicationConfig.class;
+ }
+ return ReplicationFileBasedConfig.class;
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
index 0f03929..3a4d4a7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
@@ -73,7 +74,9 @@
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
import org.apache.commons.io.FilenameUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
@@ -85,6 +88,7 @@
public class Source {
private static final Logger repLog = PullReplicationLogger.repLog;
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public interface Factory {
Source create(SourceConfiguration config);
@@ -103,6 +107,7 @@
private final PerThreadRequestScope.Scoper threadScoper;
private final SourceConfiguration config;
private final DynamicItem<EventDispatcher> eventDispatcher;
+ private CloseableHttpClient httpClient;
protected enum RetryReason {
TRANSPORT_ERROR,
@@ -194,6 +199,14 @@
threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
}
+ public synchronized CloseableHttpClient memoize(
+ Supplier<CloseableHttpClient> httpClientSupplier) {
+ if (httpClient == null) {
+ httpClient = httpClientSupplier.get();
+ }
+ return httpClient;
+ }
+
private void addRecursiveParents(
AccountGroup.UUID g,
Builder<AccountGroup.UUID> builder,
@@ -218,12 +231,21 @@
pool = workQueue.createQueue(config.getPoolThreads(), poolName);
}
- public int shutdown() {
+ public synchronized int shutdown() {
int cnt = 0;
if (pool != null) {
cnt = pool.shutdownNow().size();
pool = null;
}
+ if (httpClient != null) {
+ try {
+ httpClient.close();
+ httpClient = null;
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log("Error occurred while closing HTTP client connections");
+ }
+ }
+
return cnt;
}
@@ -647,6 +669,22 @@
return config.getApis();
}
+ public int getConnectionTimeout() {
+ return config.getConnectionTimeout();
+ }
+
+ public int getIdleTimeout() {
+ return config.getIdleTimeout();
+ }
+
+ public int getMaxConnectionsPerRoute() {
+ return config.getMaxConnectionsPerRoute();
+ }
+
+ public int getMaxConnections() {
+ return config.getMaxConnections();
+ }
+
public int getMaxRetries() {
return config.getMaxRetries();
}
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
new file mode 100644
index 0000000..6dfed44
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
@@ -0,0 +1,93 @@
+// 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
+import com.googlesource.gerrit.plugins.replication.ConfigParser;
+import com.googlesource.gerrit.plugins.replication.RemoteConfiguration;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+
+public class SourceConfigParser implements ConfigParser {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ /* (non-Javadoc)
+ * @see com.googlesource.gerrit.plugins.replication.ConfigParser#parseRemotes(org.eclipse.jgit.lib.Config)
+ */
+ public List<RemoteConfiguration> parseRemotes(Config config) throws ConfigInvalidException {
+
+ if (config.getSections().isEmpty()) {
+ logger.atWarning().log("Replication config does not exist or it's empty; not replicating");
+ return Collections.emptyList();
+ }
+
+ ImmutableList.Builder<RemoteConfiguration> sourceConfigs = ImmutableList.builder();
+ for (RemoteConfig c : allFetchRemotes(config)) {
+ if (c.getURIs().isEmpty()) {
+ continue;
+ }
+
+ // fetch source has to be specified.
+ if (c.getFetchRefSpecs().isEmpty()) {
+ throw new ConfigInvalidException(
+ String.format("You must specify a valid refSpec for this remote"));
+ }
+
+ SourceConfiguration sourceConfig = new SourceConfiguration(c, config);
+
+ if (!sourceConfig.isSingleProjectMatch()) {
+ for (URIish u : c.getURIs()) {
+ if (u.getPath() == null || !u.getPath().contains("${name}")) {
+ throw new ConfigInvalidException(
+ String.format("remote.%s.url \"%s\" lacks ${name} placeholder", c.getName(), u));
+ }
+ }
+ }
+ sourceConfigs.add(sourceConfig);
+ }
+ return sourceConfigs.build();
+ }
+
+ private static List<RemoteConfig> allFetchRemotes(Config cfg) throws ConfigInvalidException {
+
+ Set<String> names = cfg.getSubsections("remote");
+ List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
+ for (String name : names) {
+ try {
+ final RemoteConfig remoteConfig = new RemoteConfig(cfg, name);
+ if (!remoteConfig.getFetchRefSpecs().isEmpty()) {
+ result.add(remoteConfig);
+ } else {
+ logger.atFine().log(
+ "Skip loading of remote [remote \"%s\"], since it has no 'fetch' configuration",
+ name);
+ }
+ } catch (URISyntaxException e) {
+ throw new ConfigInvalidException(
+ String.format("remote %s has invalid URL in %s", name, cfg));
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
index 009d952..04fae31 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
@@ -26,6 +26,9 @@
static final int DEFAULT_REPLICATION_DELAY = 15;
static final int DEFAULT_RESCHEDULE_DELAY = 3;
static final int DEFAULT_SLOW_LATENCY_THRESHOLD_SECS = 900;
+ static final int DEFAULT_MAX_CONNECTION_INACTIVITY_MS = 10000;
+ static final int DEFAULT_CONNECTION_TIMEOUT_MS = 5000;
+ static final int DEFAULT_CONNECTIONS_PER_ROUTE = 100;
private final int delay;
private final int rescheduleDelay;
@@ -41,6 +44,10 @@
private final ImmutableList<String> authGroupNames;
private final RemoteConfig remoteConfig;
private final ImmutableList<String> apis;
+ private final int connectionTimeout;
+ private final int idleTimeout;
+ private final int maxConnectionsPerRoute;
+ private final int maxConnections;
private final int maxRetries;
private int slowLatencyThreshold;
@@ -49,6 +56,12 @@
String name = remoteConfig.getName();
urls = ImmutableList.copyOf(cfg.getStringList("remote", name, "url"));
apis = ImmutableList.copyOf(cfg.getStringList("remote", name, "apiUrl"));
+ connectionTimeout =
+ cfg.getInt("remote", name, "connectionTimeout", DEFAULT_CONNECTION_TIMEOUT_MS);
+ idleTimeout = cfg.getInt("remote", name, "idleTimeout", DEFAULT_MAX_CONNECTION_INACTIVITY_MS);
+ maxConnectionsPerRoute =
+ cfg.getInt("replication", "maxConnectionsPerRoute", DEFAULT_CONNECTIONS_PER_ROUTE);
+ maxConnections = cfg.getInt("replication", "maxConnections", 2 * maxConnectionsPerRoute);
delay = Math.max(0, getInt(remoteConfig, cfg, "replicationdelay", DEFAULT_REPLICATION_DELAY));
rescheduleDelay =
Math.max(3, getInt(remoteConfig, cfg, "rescheduledelay", DEFAULT_RESCHEDULE_DELAY));
@@ -109,6 +122,22 @@
return apis;
}
+ public int getConnectionTimeout() {
+ return connectionTimeout;
+ }
+
+ public int getIdleTimeout() {
+ return idleTimeout;
+ }
+
+ public int getMaxConnectionsPerRoute() {
+ return maxConnectionsPerRoute;
+ }
+
+ public int getMaxConnections() {
+ return maxConnections;
+ }
+
@Override
public ImmutableList<String> getAdminUrls() {
return adminUrls;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourcesCollection.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourcesCollection.java
index f24d25e..7be4971 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourcesCollection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourcesCollection.java
@@ -16,41 +16,41 @@
import static java.util.stream.Collectors.toList;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.git.WorkQueue;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.ConfigParser;
import com.googlesource.gerrit.plugins.replication.RemoteConfiguration;
-import com.googlesource.gerrit.plugins.replication.ReplicationConfigValidator;
-import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
-import java.io.IOException;
-import java.net.URISyntaxException;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.URIish;
@Singleton
-public class SourcesCollection implements ReplicationSources, ReplicationConfigValidator {
+public class SourcesCollection implements ReplicationSources {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Source.Factory sourceFactory;
private volatile List<Source> sources;
private boolean shuttingDown;
+ private final Provider<ReplicationQueue> replicationQueue;
@Inject
public SourcesCollection(
- ReplicationFileBasedConfig replicationConfig, Source.Factory sourceFactory, EventBus eventBus)
+ ReplicationConfig replicationConfig,
+ ConfigParser configParser,
+ Source.Factory sourceFactory,
+ EventBus eventBus,
+ Provider<ReplicationQueue> replicationQueue)
throws ConfigInvalidException {
this.sourceFactory = sourceFactory;
- this.sources = allSources(sourceFactory, validateConfig(replicationConfig));
+ this.sources =
+ allSources(sourceFactory, configParser.parseRemotes(replicationConfig.getConfig()));
+ this.replicationQueue = replicationQueue;
eventBus.register(this);
}
@@ -113,69 +113,12 @@
logger.atWarning().log("Shutting down: configuration reload ignored");
return;
}
-
- sources = allSources(sourceFactory, sourceConfigurations);
- logger.atInfo().log("Configuration reloaded: %d sources", getAll().size());
- }
-
- @Override
- public List<RemoteConfiguration> validateConfig(ReplicationFileBasedConfig newConfig)
- throws ConfigInvalidException {
-
try {
- newConfig.getConfig().load();
- } catch (IOException e) {
- throw new ConfigInvalidException(
- String.format("Cannot read %s: %s", newConfig.getConfig().getFile(), e.getMessage()), e);
+ replicationQueue.get().stop();
+ sources = allSources(sourceFactory, sourceConfigurations);
+ logger.atInfo().log("Configuration reloaded: %d sources", getAll().size());
+ } finally {
+ replicationQueue.get().start();
}
-
- ImmutableList.Builder<RemoteConfiguration> sourceConfigs = ImmutableList.builder();
- for (RemoteConfig c : allFetchRemotes(newConfig.getConfig())) {
- if (c.getURIs().isEmpty()) {
- continue;
- }
-
- // fetch source has to be specified.
- if (c.getFetchRefSpecs().isEmpty()) {
- throw new ConfigInvalidException(
- String.format("You must specify a valid refSpec for this remote"));
- }
-
- SourceConfiguration sourceConfig = new SourceConfiguration(c, newConfig.getConfig());
-
- if (!sourceConfig.isSingleProjectMatch()) {
- for (URIish u : c.getURIs()) {
- if (u.getPath() == null || !u.getPath().contains("${name}")) {
- throw new ConfigInvalidException(
- String.format("remote.%s.url \"%s\" lacks ${name} placeholder", c.getName(), u));
- }
- }
- }
- sourceConfigs.add(sourceConfig);
- }
- return sourceConfigs.build();
- }
-
- private static List<RemoteConfig> allFetchRemotes(FileBasedConfig cfg)
- throws ConfigInvalidException {
-
- Set<String> names = cfg.getSubsections("remote");
- List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
- for (String name : names) {
- try {
- final RemoteConfig remoteConfig = new RemoteConfig(cfg, name);
- if (!remoteConfig.getFetchRefSpecs().isEmpty()) {
- result.add(remoteConfig);
- } else {
- logger.atWarning().log(
- "Skip loading of remote [remote \"%s\"], since it has no 'fetch' configuration",
- name);
- }
- } catch (URISyntaxException e) {
- throw new ConfigInvalidException(
- String.format("remote %s has invalid URL in %s", name, cfg.getFile()));
- }
- }
- return result;
}
}
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 d51590d..c44bcbc 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
@@ -20,6 +20,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -40,13 +41,18 @@
private final FetchCommand command;
private final WorkQueue workQueue;
private final DynamicItem<UrlFormatter> urlFormatter;
+ private final FetchPreconditions preConditions;
@Inject
public FetchAction(
- FetchCommand command, WorkQueue workQueue, DynamicItem<UrlFormatter> urlFormatter) {
+ FetchCommand command,
+ WorkQueue workQueue,
+ DynamicItem<UrlFormatter> urlFormatter,
+ FetchPreconditions preConditions) {
this.command = command;
this.workQueue = workQueue;
this.urlFormatter = urlFormatter;
+ this.preConditions = preConditions;
}
public static class Input {
@@ -57,6 +63,10 @@
@Override
public Response<?> apply(ProjectResource resource, Input input) throws RestApiException {
+
+ if (!preConditions.canCallFetchApi()) {
+ throw new AuthException("not allowed to call fetch command");
+ }
try {
if (Strings.isNullOrEmpty(input.label)) {
throw new BadRequestException("Source label cannot be null or empty");
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
new file mode 100644
index 0000000..73a4ac5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
@@ -0,0 +1,26 @@
+// 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 com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class FetchApiCapability extends CapabilityDefinition {
+ static final String CALL_FETCH_ACTION = "callFetchAction";
+
+ @Override
+ public String getDescription() {
+ return "Pull Replication";
+ }
+}
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
new file mode 100644
index 0000000..ca1557a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
@@ -0,0 +1,47 @@
+// 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.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class FetchPreconditions {
+ private final String pluginName;
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> userProvider;
+
+ @Inject
+ public FetchPreconditions(
+ @PluginName String pluginName,
+ Provider<CurrentUser> userProvider,
+ PermissionBackend permissionBackend) {
+ this.pluginName = pluginName;
+ this.userProvider = userProvider;
+ this.permissionBackend = permissionBackend;
+ }
+
+ public Boolean canCallFetchApi() {
+ PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get());
+ return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
+ || userPermission.testOrFalse(new PluginPermission(pluginName, CALL_FETCH_ACTION));
+ }
+}
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
index 470c985..1663ad2 100644
--- 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
@@ -15,7 +15,10 @@
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;
@@ -24,5 +27,10 @@
protected void configure() {
bind(FetchAction.class).in(Scopes.SINGLETON);
post(PROJECT_KIND, "fetch").to(FetchAction.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/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
index 4296f67..502d3b7 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
@@ -20,7 +20,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
-import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
import com.googlesource.gerrit.plugins.replication.pull.Source;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -36,7 +36,6 @@
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
-import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.eclipse.jgit.transport.CredentialItem;
@@ -51,18 +50,18 @@
}
private final CredentialsFactory credentials;
- private final CloseableHttpClient httpClient;
+ private final SourceHttpClient.Factory httpClientFactory;
private final Source source;
private final String instanceLabel;
@Inject
FetchRestApiClient(
CredentialsFactory credentials,
- CloseableHttpClient httpClient,
- ReplicationFileBasedConfig replicationConfig,
+ SourceHttpClient.Factory httpClientFactory,
+ ReplicationConfig replicationConfig,
@Assisted Source source) {
this.credentials = credentials;
- this.httpClient = httpClient;
+ this.httpClientFactory = httpClientFactory;
this.source = source;
this.instanceLabel =
replicationConfig.getConfig().getString("replication", null, "instanceLabel");
@@ -81,7 +80,7 @@
String.format("{\"label\":\"%s\", \"ref_name\": \"%s\"}", instanceLabel, refName),
StandardCharsets.UTF_8));
post.addHeader(new BasicHeader("Content-Type", "application/json"));
- return httpClient.execute(post, this, getContext(targetUri));
+ return httpClientFactory.create(source).execute(post, this, getContext(targetUri));
}
@Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java
new file mode 100644
index 0000000..7bfc7d1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java
@@ -0,0 +1,31 @@
+// 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.client;
+
+import java.io.IOException;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.protocol.HttpContext;
+
+/** HTTP client for executing URI requests to a remote site */
+public interface HttpClient {
+
+ public <T> T execute(
+ final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler,
+ final HttpContext context)
+ throws ClientProtocolException, IOException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClientProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClientProvider.java
deleted file mode 100644
index 6d6e803..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClientProvider.java
+++ /dev/null
@@ -1,62 +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.client;
-
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.conn.HttpClientConnectionManager;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
-
-/** Provides an HTTP client with SSL capabilities. */
-public class HttpClientProvider implements Provider<CloseableHttpClient> {
- private static final int CONNECTIONS_PER_ROUTE = 100;
-
- // Up to 2 target instances with the max number of connections per host:
- private static final int MAX_CONNECTIONS = 2 * CONNECTIONS_PER_ROUTE;
-
- private static final int MAX_CONNECTION_INACTIVITY_MS = 10000;
- private static final int DEFAULT_TIMEOUT_MS = 5000;
-
- @Override
- public CloseableHttpClient get() {
- try {
- return HttpClients.custom()
- .setConnectionManager(customConnectionManager())
- .setDefaultRequestConfig(customRequestConfig())
- .build();
- } catch (Exception e) {
- throw new ProvisionException("Couldn't create CloseableHttpClient", e);
- }
- }
-
- private RequestConfig customRequestConfig() {
- return RequestConfig.custom()
- .setConnectTimeout(DEFAULT_TIMEOUT_MS)
- .setSocketTimeout(DEFAULT_TIMEOUT_MS)
- .setConnectionRequestTimeout(DEFAULT_TIMEOUT_MS)
- .build();
- }
-
- private HttpClientConnectionManager customConnectionManager() throws Exception {
- PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
- connManager.setDefaultMaxPerRoute(CONNECTIONS_PER_ROUTE);
- connManager.setMaxTotal(MAX_CONNECTIONS);
- connManager.setValidateAfterInactivity(MAX_CONNECTION_INACTIVITY_MS);
- return connManager;
- }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java
new file mode 100644
index 0000000..ee0fe79
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java
@@ -0,0 +1,74 @@
+// 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.client;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.Source;
+import java.io.IOException;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.HttpContext;
+
+/** Apache HTTP client implementation based on Source-specific parameters */
+public class SourceHttpClient implements HttpClient {
+ private final Source source;
+
+ public interface Factory {
+ public HttpClient create(Source source);
+ }
+
+ @Inject
+ public SourceHttpClient(@Assisted Source source) {
+ this.source = source;
+ }
+
+ @Override
+ public <T> T execute(
+ HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws ClientProtocolException, IOException {
+ return source
+ .memoize(
+ () ->
+ HttpClients.custom()
+ .setConnectionManager(customConnectionManager(source))
+ .setDefaultRequestConfig(customRequestConfig(source))
+ .build())
+ .execute(request, responseHandler, context);
+ }
+
+ private static RequestConfig customRequestConfig(Source source) {
+ int connectionTimeout = source.getConnectionTimeout();
+ return RequestConfig.custom()
+ .setConnectTimeout(connectionTimeout)
+ .setSocketTimeout(connectionTimeout)
+ .setConnectionRequestTimeout(connectionTimeout)
+ .build();
+ }
+
+ private static HttpClientConnectionManager customConnectionManager(Source source) {
+ PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
+
+ connManager.setDefaultMaxPerRoute(source.getMaxConnectionsPerRoute());
+ connManager.setMaxTotal(source.getMaxConnections());
+ connManager.setValidateAfterInactivity(source.getIdleTimeout());
+ return connManager;
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 46ec740..d921e0b 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -8,3 +8,10 @@
configuration is not recommended. It is also possible to specify a
local path as replication source. This makes e.g. sense if a network
share is mounted to which the repositories should be replicated from.
+
+Access
+------
+
+To be allowed to trigger pull replication a user must be a member of a
+group that is granted the 'Pull Replication' capability (provided
+by this plugin) or the 'Administrate Server' capability.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index c8c53df..a6ea668 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -121,6 +121,16 @@
provided in the remote configuration section which name is equal
to instanceLabel.
+replication.maxConnectionsPerRoute
+: Maximum number of HTTP connections per one HTTP route.
+
+ Default: 100
+
+replication.maxConnections
+: Total number of HTTP connections pool.
+
+ Default: 2 * replication.maxConnectionsPerRoute
+
remote.NAME.url
: Address of the remote server to fetch from. Single URL can be
specified within a single remote block. A remote node can request
@@ -148,6 +158,21 @@
different destinations which share the same settings. Gerrit calls
all URLs in sequence.
+remote.NAME.connectionTimeout
+: Defines the socket timeout ({@code SO_TIMEOUT}) in milliseconds,
+ which is the timeout for waiting for data or, put differently,
+ a maximum period inactivity between two consecutive data packets.
+
+ Default: 5000
+
+remote.NAME.idleTimeout
+: Defines period of inactivity in milliseconds after which persistent connections must
+ be re-validated prior to being leased to the consumer. Non-positive value disables
+ connection validation. This check helps detect connections that have become stale
+ (half-closed) while kept inactive in the pool.
+
+ Default: 10000
+
remote.NAME.uploadpack
: Path of the `git-upload-pack` executable on the remote system,
if using the SSH transport.
@@ -291,6 +316,82 @@
By default, replicates without matching, i.e. replicates
everything from all remotes.
+Directory `replication`
+--------------------
+The optional directory `$site_path/etc/replication` contains Git-style
+config files that controls the replication settings for the pull replication
+plugin. When present all `remote` sections from `replication.config` file are
+ignored.
+
+Files are composed of one `remote` section. Multiple `remote` sections or any
+other section makes the file invalid and skipped by the pull replication plugin.
+File name defines remote section name. Each section provides common configuration
+settings for one or more destination URLs. For more details how to setup `remote`
+sections please refer to the `replication.config` section.
+
+### Configuration example:
+
+Static configuration in `$site_path/etc/replication.config`:
+
+```
+[gerrit]
+ autoReload = true
+ replicateOnStartup = false
+[replication]
+ instanceLabel = host-one
+ lockErrorMaxRetries = 5
+ maxRetries = 5
+```
+
+Remote sections in `$site_path/etc/replication` directory:
+
+* File `$site_path/etc/replication/host-two.config`
+
+ ```
+ [remote]
+ url = gerrit2@host-two.example.com:/some/path/${name}.git
+ apiUrl = http://host-two
+ fetch = +refs/*:refs/*
+ ```
+
+
+* File `$site_path/etc/replication/host-three.config`
+
+ ```
+ [remote]
+ url = mirror1.host-three:/pub/git/${name}.git
+ url = mirror2.host-three:/pub/git/${name}.git
+ url = mirror3.host-three:/pub/git/${name}.git
+ apiUrl = http://host-three
+ fetch = +refs/heads/*:refs/heads/*
+ fetch = +refs/tags/*:refs/tags/*
+ ```
+
+Pull replication plugin resolves config files to the following configuration:
+
+```
+[gerrit]
+ autoReload = true
+ replicateOnStartup = false
+[replication]
+ instanceLabel = host-one
+ lockErrorMaxRetries = 5
+ maxRetries = 5
+
+[remote "host-two"]
+ url = gerrit2@host-two.example.com:/some/path/${name}.git
+ apiUrl = http://host-two
+ fetch = +refs/*:refs/*
+
+[remote "host-three"]
+ url = mirror1.host-three:/pub/git/${name}.git
+ url = mirror2.host-three:/pub/git/${name}.git
+ url = mirror3.host-three:/pub/git/${name}.git
+ apiUrl = http://host-three
+ fetch = +refs/heads/*:refs/heads/*
+ fetch = +refs/tags/*:refs/tags/*
+```
+
File `secure.config`
--------------------
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
new file mode 100644
index 0000000..cb5af42
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
@@ -0,0 +1,311 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+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.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.FS;
+import org.junit.Test;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+ name = "pull-replication",
+ sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule")
+public class PullReplicationFanoutConfigIT extends LightweightPluginDaemonTest {
+ private static final Optional<String> ALL_PROJECTS = Optional.empty();
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final int TEST_REPLICATION_DELAY = 60;
+ private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2);
+ private static final String TEST_REPLICATION_SUFFIX = "suffix1";
+ private static final String TEST_REPLICATION_REMOTE = "remote1";
+
+ @Inject private SitePaths sitePaths;
+ @Inject private ProjectOperations projectOperations;
+ private Path gitPath;
+ private FileBasedConfig config;
+ private FileBasedConfig remoteConfig;
+ private FileBasedConfig secureConfig;
+
+ @Override
+ public void setUpTestPlugin() throws Exception {
+ gitPath = sitePaths.site_path.resolve("git");
+
+ config =
+ new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
+ remoteConfig =
+ new FileBasedConfig(
+ sitePaths
+ .etc_dir
+ .resolve("replication/" + TEST_REPLICATION_REMOTE + ".config")
+ .toFile(),
+ FS.DETECTED);
+
+ setReplicationSource(
+ TEST_REPLICATION_REMOTE); // Simulates a full replication.config initialization
+
+ setRemoteConfig(TEST_REPLICATION_SUFFIX, ALL_PROJECTS);
+
+ secureConfig =
+ new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
+ setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
+ secureConfig.save();
+
+ super.setUpTestPlugin();
+ }
+
+ @Test
+ 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);
+ GitReferenceUpdatedListener.Event event =
+ new FakeGitReferenceUpdatedEvent(
+ project,
+ sourceRef,
+ ObjectId.zeroId().getName(),
+ sourceCommit.getId().getName(),
+ ReceiveCommand.Type.CREATE);
+ pullReplicationQueue.onGitReferenceUpdated(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
+ public void shouldReplicateNewChangeRefAfterConfigReloaded() throws Exception {
+ testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+ // Trigger configuration autoreload
+ AutoReloadConfigDecorator autoReloadConfigDecorator =
+ getInstance(AutoReloadConfigDecorator.class);
+ SourcesCollection sources = getInstance(SourcesCollection.class);
+ remoteConfig.setInt("remote", null, "timeout", 1000);
+ remoteConfig.save();
+ autoReloadConfigDecorator.reload();
+ waitUntil(() -> !sources.getAll().isEmpty() && sources.getAll().get(0).getTimeout() == 1000);
+
+ Result pushResult = createChange();
+ RevCommit sourceCommit = pushResult.getCommit();
+ final String sourceRef = pushResult.getPatchSet().refName();
+ ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+ GitReferenceUpdatedListener.Event event =
+ new FakeGitReferenceUpdatedEvent(
+ project,
+ sourceRef,
+ ObjectId.zeroId().getName(),
+ sourceCommit.getId().getName(),
+ ReceiveCommand.Type.CREATE);
+ pullReplicationQueue.onGitReferenceUpdated(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
+ 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);
+ GitReferenceUpdatedListener.Event event =
+ new FakeGitReferenceUpdatedEvent(
+ project,
+ newBranch,
+ ObjectId.zeroId().getName(),
+ branchRevision,
+ ReceiveCommand.Type.CREATE);
+ pullReplicationQueue.onGitReferenceUpdated(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
+ public void shouldAutoReloadConfiguration() throws Exception {
+ SourcesCollection sources = getInstance(SourcesCollection.class);
+ AutoReloadConfigDecorator autoReloadConfigDecorator =
+ getInstance(AutoReloadConfigDecorator.class);
+ assertThat(sources.getAll().get(0).getTimeout()).isEqualTo(600);
+ remoteConfig.setInt("remote", null, "timeout", 1000);
+ remoteConfig.save();
+ autoReloadConfigDecorator.reload();
+ waitUntil(() -> !sources.getAll().isEmpty() && sources.getAll().get(0).getTimeout() == 1000);
+ }
+
+ @Test
+ public void shouldAutoReloadConfigurationWhenRemoteConfigAdded() throws Exception {
+ FileBasedConfig newRemoteConfig =
+ new FileBasedConfig(
+ sitePaths.etc_dir.resolve("replication/new-remote-config.config").toFile(),
+ FS.DETECTED);
+ try {
+ SourcesCollection sources = getInstance(SourcesCollection.class);
+ AutoReloadConfigDecorator autoReloadConfigDecorator =
+ getInstance(AutoReloadConfigDecorator.class);
+ assertThat(sources.getAll().size()).isEqualTo(1);
+
+ setRemoteConfig(newRemoteConfig, TEST_REPLICATION_SUFFIX, ALL_PROJECTS);
+ autoReloadConfigDecorator.reload();
+ waitUntil(() -> sources.getAll().size() == 2);
+
+ } finally {
+ // clean up
+ Files.delete(newRemoteConfig.getFile().toPath());
+ }
+ }
+
+ @Test
+ public void shouldAutoReloadConfigurationWhenRemoteConfigDeleted() throws Exception {
+ SourcesCollection sources = getInstance(SourcesCollection.class);
+ AutoReloadConfigDecorator autoReloadConfigDecorator =
+ getInstance(AutoReloadConfigDecorator.class);
+ assertThat(sources.getAll().size()).isEqualTo(1);
+
+ FileBasedConfig newRemoteConfig =
+ new FileBasedConfig(
+ sitePaths.etc_dir.resolve("replication/new-remote-config.config").toFile(),
+ FS.DETECTED);
+ setRemoteConfig(newRemoteConfig, TEST_REPLICATION_SUFFIX, ALL_PROJECTS);
+ autoReloadConfigDecorator.reload();
+ waitUntil(() -> sources.getAll().size() == 2);
+
+ Files.delete(newRemoteConfig.getFile().toPath());
+
+ autoReloadConfigDecorator.reload();
+
+ waitUntil(() -> sources.getAll().size() == 1);
+ }
+
+ private Ref getRef(Repository repo, String branchName) throws IOException {
+ return repo.getRefDatabase().exactRef(branchName);
+ }
+
+ private Ref checkedGetRef(Repository repo, String branchName) {
+ try {
+ return repo.getRefDatabase().exactRef(branchName);
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo);
+ return null;
+ }
+ }
+
+ private void setReplicationSource(String remoteName) throws IOException {
+ config.setString("replication", null, "instanceLabel", remoteName);
+ config.setBoolean("gerrit", null, "autoReload", true);
+ config.save();
+ }
+
+ private void setRemoteConfig(String replicaSuffix, Optional<String> project) throws IOException {
+ setRemoteConfig(remoteConfig, replicaSuffix, project);
+ }
+
+ private void setRemoteConfig(
+ FileBasedConfig remoteConfig, String replicaSuffix, Optional<String> project)
+ throws IOException {
+ setRemoteConfig(remoteConfig, Arrays.asList(replicaSuffix), project);
+ }
+
+ private void setRemoteConfig(
+ FileBasedConfig remoteConfig, List<String> replicaSuffixes, Optional<String> project)
+ throws IOException {
+ List<String> replicaUrls =
+ replicaSuffixes.stream()
+ .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString())
+ .collect(toList());
+ remoteConfig.setStringList("remote", null, "url", replicaUrls);
+ remoteConfig.setString("remote", null, "apiUrl", adminRestSession.url());
+ remoteConfig.setString("remote", null, "fetch", "+refs/*:refs/*");
+ remoteConfig.setInt("remote", null, "timeout", 600);
+ remoteConfig.setInt("remote", null, "replicationDelay", TEST_REPLICATION_DELAY);
+ project.ifPresent(prj -> remoteConfig.setString("remote", null, "projects", prj));
+ remoteConfig.save();
+ }
+
+ private void setReplicationCredentials(String remoteName, String username, String password)
+ throws IOException {
+ secureConfig.setString("remote", remoteName, "username", username);
+ secureConfig.setString("remote", remoteName, "password", password);
+ secureConfig.save();
+ }
+
+ private void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
+ WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT);
+ }
+
+ private <T> T getInstance(Class<T> classObj) {
+ return plugin.getSysInjector().getInstance(classObj);
+ }
+
+ private Project.NameKey createTestProject(String name) throws Exception {
+ return projectOperations.newProject().name(name).create();
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
index 39c6717..2c888c8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
@@ -18,11 +18,12 @@
import static org.apache.http.HttpStatus.SC_ACCEPTED;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -60,6 +61,7 @@
@Mock DynamicItem<UrlFormatter> urlFormatterDynamicItem;
@Mock UrlFormatter urlFormatter;
@Mock WorkQueue.Task<Void> task;
+ @Mock FetchPreconditions preConditions;
@Before
public void setup() {
@@ -75,8 +77,9 @@
});
when(urlFormatterDynamicItem.get()).thenReturn(urlFormatter);
when(task.getTaskId()).thenReturn(taskId);
+ when(preConditions.canCallFetchApi()).thenReturn(true);
- fetchAction = new FetchAction(fetchCommand, workQueue, urlFormatterDynamicItem);
+ fetchAction = new FetchAction(fetchCommand, workQueue, urlFormatterDynamicItem, preConditions);
}
@Test
@@ -205,6 +208,18 @@
fetchAction.apply(projectResource, inputParams);
}
+ @Test(expected = AuthException.class)
+ public void shouldThrowAuthExceptionWhenCallFetchActionCapabilityNotAssigned()
+ throws RestApiException {
+ FetchAction.Input inputParams = new FetchAction.Input();
+ inputParams.label = label;
+ inputParams.refName = refName;
+
+ when(preConditions.canCallFetchApi()).thenReturn(false);
+
+ fetchAction.apply(projectResource, inputParams);
+ }
+
@Test
public void shouldReturnScheduledTaskForAsyncCall() throws RestApiException {
FetchAction.Input inputParams = new FetchAction.Input();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
index 22dd0e9..669161f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
@@ -34,7 +34,6 @@
import org.apache.http.Header;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.transport.CredentialItem;
@@ -56,7 +55,8 @@
public class FetchRestApiClientTest {
@Mock CredentialsProvider credentialProvider;
@Mock CredentialsFactory credentials;
- @Mock CloseableHttpClient httpClient;
+ @Mock HttpClient httpClient;
+ @Mock SourceHttpClient.Factory httpClientFactory;
@Mock FileBasedConfig config;
@Mock ReplicationFileBasedConfig replicationConfig;
@Mock Source source;
@@ -96,8 +96,9 @@
HttpResult httpResult = new HttpResult(SC_CREATED, Optional.of("result message"));
when(httpClient.execute(any(HttpPost.class), any(), any())).thenReturn(httpResult);
-
- objectUnderTest = new FetchRestApiClient(credentials, httpClient, replicationConfig, source);
+ when(httpClientFactory.create(any())).thenReturn(httpClient);
+ objectUnderTest =
+ new FetchRestApiClient(credentials, httpClientFactory, replicationConfig, source);
}
@Test