Add support for overriding replication configuration It is a next step in the direction of modifying the replicationn configuraiton from an external source. Where the external source could be a git repository or third party configuration managemnet system. This adds a `ReplicationConfigOverrides` interface and a default bindng for `DynamicItem<ReplicationConfigOverrides`. The added interface is just an extension of `ConfigResource` as Guice was complaining about duplicated binding for `ConfigResource` when it was used in `DynamicItem`. Additinally `RepilcationConfigOverrides` conveys better its purpose. For now, the default implementation provides no overrides. As the location of overrides files is still to be decided. Finally, the `MergedConfigResource` will take both, the base configuraiton for the file system, and configured overrides and merge both JGit `Config` objects togehter. With this approach, an implementation of `ReplicationConfigOverrides` can provide a single confiuration option or the whole configuration file. Later we may consider, discarding some of the overrides, like `gerrit.autoReload`, as disabling that option will effecively prevent users from updating configuation. Change-Id: I2e401c05571180719df33a4a094fbb4ccfb39f23
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java index b6532d9..9d0c978 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -42,7 +42,7 @@ public AutoReloadConfigDecorator( @PluginName String pluginName, WorkQueue workQueue, - FileReplicationConfig replicationConfig, + ReplicationConfigImpl replicationConfig, AutoReloadRunnable reloadRunner, EventBus eventBus) { this.currentConfig = replicationConfig;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java index 9ed43fe..ae07760 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnable.java
@@ -27,14 +27,14 @@ private final Provider<ObservableQueue> queueObserverProvider; private final ConfigParser configParser; private ReplicationConfig loadedConfig; - private Provider<FileReplicationConfig> replicationConfigProvider; + private Provider<ReplicationConfigImpl> replicationConfigProvider; private String loadedConfigVersion; private String lastFailedConfigVersion; @Inject public AutoReloadRunnable( ConfigParser configParser, - Provider<FileReplicationConfig> replicationConfigProvider, + Provider<ReplicationConfigImpl> replicationConfigProvider, EventBus eventBus, Provider<ObservableQueue> queueObserverProvider) { this.replicationConfigProvider = replicationConfigProvider;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java index 9c70aa6..008e50b 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -15,7 +15,7 @@ package com.googlesource.gerrit.plugins.replication; import static com.google.gerrit.server.project.ProjectCache.noSuchProject; -import static com.googlesource.gerrit.plugins.replication.FileReplicationConfig.replaceName; +import static com.googlesource.gerrit.plugins.replication.ReplicationConfigImpl.replaceName; import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.NON_EXISTING; import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java index 1d471fe..08f3477 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationsCollection.java
@@ -17,7 +17,7 @@ import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isGerrit; import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isGerritHttp; import static com.googlesource.gerrit.plugins.replication.AdminApiFactory.isSSH; -import static com.googlesource.gerrit.plugins.replication.FileReplicationConfig.replaceName; +import static com.googlesource.gerrit.plugins.replication.ReplicationConfigImpl.replaceName; import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog; import static java.util.stream.Collectors.toList;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/MergedConfigResource.java b/src/main/java/com/googlesource/gerrit/plugins/replication/MergedConfigResource.java new file mode 100644 index 0000000..4cd0af0 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/MergedConfigResource.java
@@ -0,0 +1,75 @@ +// Copyright (C) 2024 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; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.util.Providers; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; + +public class MergedConfigResource { + @VisibleForTesting + static MergedConfigResource withBaseOnly(ConfigResource base) { + return new MergedConfigResource(Providers.of(base), null); + } + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Provider<ConfigResource> base; + @Nullable private final DynamicItem<ReplicationConfigOverrides> overrides; + + @Inject + MergedConfigResource( + Provider<ConfigResource> base, @Nullable DynamicItem<ReplicationConfigOverrides> overrides) { + this.base = base; + this.overrides = overrides; + } + + public Config getConfig() { + Config config = base.get().getConfig(); + if (noOverrides()) { + return config; + } + + String overridesText = overrides.get().getConfig().toText(); + if (!overridesText.isEmpty()) { + try { + config.fromText(overridesText); + } catch (ConfigInvalidException e) { + logger.atWarning().withCause(e).log("Failed to merge replication config overrides"); + } + } + + return config; + } + + public String getVersion() { + String baseVersion = base.get().getVersion(); + if (noOverrides()) { + return baseVersion; + } + + return baseVersion + overrides.get().getVersion(); + } + + private boolean noOverrides() { + return overrides == null || overrides.get() == null; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/FileReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigImpl.java similarity index 89% rename from src/main/java/com/googlesource/gerrit/plugins/replication/FileReplicationConfig.java rename to src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigImpl.java index b34ea54..59c6bb7 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/FileReplicationConfig.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigImpl.java
@@ -13,7 +13,6 @@ // limitations under the License. package com.googlesource.gerrit.plugins.replication; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.annotations.PluginData; @@ -22,7 +21,7 @@ import java.nio.file.Path; import org.eclipse.jgit.lib.Config; -public class FileReplicationConfig implements ReplicationConfig { +public class ReplicationConfigImpl implements ReplicationConfig { private static final int DEFAULT_SSH_CONNECTION_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes private final SitePaths site; @@ -32,19 +31,18 @@ private final int maxRefsToShow; private int sshCommandTimeout; private int sshConnectionTimeout = DEFAULT_SSH_CONNECTION_TIMEOUT_MS; - private final ConfigResource configResource; + private final MergedConfigResource configResource; private final Path pluginDataDir; // TODO: remove in follow-up change, as this was added to reduce the diff size // for change Ic6a5c5b8ab5 - @VisibleForTesting - public FileReplicationConfig(SitePaths paths, @PluginData Path pluginDataDir) { - this(new FileConfigResource(paths), paths, pluginDataDir); + public ReplicationConfigImpl(SitePaths paths, @PluginData Path pluginDataDir) { + this(MergedConfigResource.withBaseOnly(new FileConfigResource(paths)), paths, pluginDataDir); } @Inject - public FileReplicationConfig( - ConfigResource configResource, SitePaths site, @PluginData Path pluginDataDir) { + public ReplicationConfigImpl( + MergedConfigResource configResource, SitePaths site, @PluginData Path pluginDataDir) { this.site = site; this.configResource = configResource; Config config = configResource.getConfig();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java index 220ec50..bcea543 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java
@@ -18,6 +18,7 @@ import static com.googlesource.gerrit.plugins.replication.FileConfigResource.CONFIG_NAME; import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.server.config.SitePaths; import com.google.inject.AbstractModule; import com.google.inject.Inject; @@ -46,6 +47,7 @@ @Override protected void configure() { bind(ConfigResource.class).to(getConfigResourceClass()); + DynamicItem.itemOf(binder(), ReplicationConfigOverrides.class); if (getReplicationConfig().getBoolean("gerrit", "autoReload", false)) { bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class).in(Scopes.SINGLETON); @@ -53,7 +55,7 @@ .annotatedWith(UniqueAnnotations.create()) .to(AutoReloadConfigDecorator.class); } else { - bind(ReplicationConfig.class).to(FileReplicationConfig.class).in(Scopes.SINGLETON); + bind(ReplicationConfig.class).to(ReplicationConfigImpl.class).in(Scopes.SINGLETON); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigOverrides.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigOverrides.java new file mode 100644 index 0000000..97a77bf --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigOverrides.java
@@ -0,0 +1,21 @@ +// Copyright (C) 2024 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; + +/** + * Provide a way to override or extend replication configuration from other sources, like git + * repository or external configuration management tool. + */ +public interface ReplicationConfigOverrides extends ConfigResource {}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java index 4c43cb0..342d394 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java
@@ -131,7 +131,8 @@ protected DestinationsCollection newDestinationsCollections(ConfigResource configResource) throws ConfigInvalidException { return newDestinationsCollections( - new FileReplicationConfig(configResource, sitePaths, pluginDataPath)); + new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(configResource), sitePaths, pluginDataPath)); } protected DestinationsCollection newDestinationsCollections(ReplicationConfig replicationConfig) @@ -144,7 +145,7 @@ eventBus); } - protected FileReplicationConfig newReplicationFileBasedConfig() { - return new FileReplicationConfig(sitePaths, pluginDataPath); + protected ReplicationConfigImpl newReplicationFileBasedConfig() { + return new ReplicationConfigImpl(sitePaths, pluginDataPath); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java index bb08830..aa0d5f3 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java
@@ -76,13 +76,18 @@ remoteConfig.save(); replicationConfig = - new FileReplicationConfig(new FanoutConfigResource(sitePaths), sitePaths, pluginDataPath); + new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(new FanoutConfigResource(sitePaths)), + sitePaths, + pluginDataPath); newAutoReloadConfig( () -> { try { - return new FileReplicationConfig( - new FanoutConfigResource(sitePaths), sitePaths, pluginDataPath); + return new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(new FanoutConfigResource(sitePaths)), + sitePaths, + pluginDataPath); } catch (IOException | ConfigInvalidException e) { throw new RuntimeException(e); } @@ -125,13 +130,18 @@ remoteConfig.save(); replicationConfig = - new FileReplicationConfig(new FanoutConfigResource(sitePaths), sitePaths, pluginDataPath); + new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(new FanoutConfigResource(sitePaths)), + sitePaths, + pluginDataPath); newAutoReloadConfig( () -> { try { - return new FileReplicationConfig( - new FanoutConfigResource(sitePaths), sitePaths, pluginDataPath); + return new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(new FanoutConfigResource(sitePaths)), + sitePaths, + pluginDataPath); } catch (IOException | ConfigInvalidException e) { throw new RuntimeException(e); } @@ -173,13 +183,18 @@ remoteConfig.save(); replicationConfig = - new FileReplicationConfig(new FanoutConfigResource(sitePaths), sitePaths, pluginDataPath); + new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(new FanoutConfigResource(sitePaths)), + sitePaths, + pluginDataPath); newAutoReloadConfig( () -> { try { - return new FileReplicationConfig( - new FanoutConfigResource(sitePaths), sitePaths, pluginDataPath); + return new ReplicationConfigImpl( + MergedConfigResource.withBaseOnly(new FanoutConfigResource(sitePaths)), + sitePaths, + pluginDataPath); } catch (IOException | ConfigInvalidException e) { throw new RuntimeException(e); } @@ -234,14 +249,14 @@ } private AutoReloadConfigDecorator newAutoReloadConfig( - Supplier<FileReplicationConfig> configSupplier) { + Supplier<ReplicationConfigImpl> configSupplier) { AutoReloadRunnable autoReloadRunnable = new AutoReloadRunnable( configParser, - new Provider<FileReplicationConfig>() { + new Provider<ReplicationConfigImpl>() { @Override - public FileReplicationConfig get() { + public ReplicationConfigImpl get() { return configSupplier.get(); } },
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnableTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnableTest.java index 14307f8..8a1b85d 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnableTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadRunnableTest.java
@@ -81,11 +81,11 @@ autoReloadRunnable.run(); } - private Provider<FileReplicationConfig> newVersionConfigProvider() { + private Provider<ReplicationConfigImpl> newVersionConfigProvider() { return new Provider<>() { @Override - public FileReplicationConfig get() { - return new FileReplicationConfig(sitePaths, sitePaths.data_dir) { + public ReplicationConfigImpl get() { + return new ReplicationConfigImpl(sitePaths, sitePaths.data_dir) { @Override public String getVersion() { return String.format("%s", System.nanoTime());
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java index 34712a7..9a5ece9 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java
@@ -266,7 +266,7 @@ FanoutConfigResource objectUnderTest = new FanoutConfigResource(sitePaths); String replicationConfigVersion = - new FileReplicationConfig(sitePaths, pluginDataPath).getVersion(); + new ReplicationConfigImpl(sitePaths, pluginDataPath).getVersion(); MoreFiles.deleteRecursively(sitePaths.etc_dir.resolve("replication"), ALLOW_INSECURE);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java new file mode 100644 index 0000000..80ad449 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java
@@ -0,0 +1,112 @@ +// Copyright (C) 2024 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import org.eclipse.jgit.lib.Config; +import org.junit.Test; + +public class MergedConfigResourceTest { + private static final int BASE_CONFIG_MAX_RETIRES = 10; + private static final int OVERRIDDEN_CONFIG_MAX_RETIRES = 5; + + @Test + public void onlyUseBaseConfig() { + final MergedConfigResource configResource = newMergedConfigResource(); + + assertThat(configResource.getVersion()).isEqualTo("base"); + assertThat(getMaxRetires(configResource)).isEqualTo(BASE_CONFIG_MAX_RETIRES); + } + + @Test + public void overrideBaseConfig() { + final MergedConfigResource configResource = + newMergedConfigResource(TestReplicationConfigOverrides.class); + + assertThat(configResource.getVersion()).isEqualTo("baseoverride"); + assertThat(getMaxRetires(configResource)).isEqualTo(OVERRIDDEN_CONFIG_MAX_RETIRES); + assertThat(getUseGcClient(configResource)).isTrue(); + } + + private MergedConfigResource newMergedConfigResource() { + return newMergedConfigResource(null); + } + + private MergedConfigResource newMergedConfigResource( + Class<? extends ReplicationConfigOverrides> overrides) { + return Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bind(ConfigResource.class).to(TestBaseConfigResource.class); + + DynamicItem.itemOf(binder(), ReplicationConfigOverrides.class); + if (overrides != null) { + DynamicItem.bind(binder(), ReplicationConfigOverrides.class).to(overrides); + } + } + }) + .getInstance(MergedConfigResource.class); + } + + private static class TestBaseConfigResource implements ConfigResource { + @Override + public Config getConfig() { + Config config = new Config(); + setMaxRetires(config, BASE_CONFIG_MAX_RETIRES); + return config; + } + + @Override + public String getVersion() { + return "base"; + } + } + + private static class TestReplicationConfigOverrides implements ReplicationConfigOverrides { + @Override + public Config getConfig() { + Config config = new Config(); + setMaxRetires(config, OVERRIDDEN_CONFIG_MAX_RETIRES); + setUseGcClient(config, true); + return config; + } + + @Override + public String getVersion() { + return "override"; + } + } + + private static void setMaxRetires(Config config, int value) { + config.setInt("replication", null, "maxRetries", value); + } + + private static void setUseGcClient(Config config, boolean value) { + config.setBoolean("replication", null, "useGcClient", value); + } + + private static int getMaxRetires(MergedConfigResource resource) { + return resource.getConfig().getInt("replication", null, "maxRetries", -1); + } + + private static boolean getUseGcClient(MergedConfigResource resource) { + return resource.getConfig().getBoolean("replication", null, "useGcClient", false); + } +}