Add project custom enforcement policy
Adds the ability of project or ref fine-grained policy
enforcement. This is typically needed whenever some of the projects
and refs requires different level of "consistency" across sites.
For example, the draft comments in All-Users are changed very often
across nodes and thus very likely to be misaligned. The loss of a draft
comment isn't nice, but is not so severe as the loss of a committed
code change.
Bug: Issue 10711
Change-Id: Ib477439b9c90af6e5c1719854e778c4b0aa6855e
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
index 3756097..72fec47 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -22,12 +22,16 @@
import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.spi.Message;
import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
@@ -517,6 +521,8 @@
public static final String KEY_MIGRATE = "migrate";
public final String TRANSACTION_LOCK_TIMEOUT_KEY = "transactionLockTimeoutMs";
+ public static final String SUBSECTION_ENFORCEMENT_RULES = "enforcementRules";
+
private final String connectionString;
private final String root;
private final int sessionTimeoutMs;
@@ -529,6 +535,8 @@
private final int casMaxRetries;
private final boolean enabled;
+ private final Multimap<EnforcePolicy, String> enforcementRules;
+
private final Long transactionLockTimeOut;
private CuratorFramework build;
@@ -600,7 +608,14 @@
checkArgument(StringUtils.isNotEmpty(connectionString), "zookeeper.%s contains no servers");
- enabled = Configuration.getBoolean(cfg, SECTION, SUBSECTION, ENABLE_KEY, true);
+ enabled = Configuration.getBoolean(cfg, SECTION, null, ENABLE_KEY, true);
+
+ enforcementRules = MultimapBuilder.hashKeys().arrayListValues().build();
+ for (EnforcePolicy policy : EnforcePolicy.values()) {
+ enforcementRules.putAll(
+ policy,
+ Configuration.getList(cfg, SECTION, SUBSECTION_ENFORCEMENT_RULES, policy.name()));
+ }
}
public CuratorFramework buildCurator() {
@@ -632,6 +647,15 @@
public boolean isEnabled() {
return enabled;
}
+
+ public Multimap<EnforcePolicy, String> getEnforcementRules() {
+ return enforcementRules;
+ }
+ }
+
+ static List<String> getList(
+ Supplier<Config> cfg, String section, String subsection, String name) {
+ return ImmutableList.copyOf(cfg.get().getStringList(section, subsection, name));
}
static boolean getBoolean(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
index 8a652a9..7d8df66 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
@@ -18,6 +18,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Scopes;
import com.googlesource.gerrit.plugins.multisite.Configuration;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.CustomSharedRefEnforcementByProject;
import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement;
import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.ZkValidationModule;
@@ -43,8 +44,14 @@
if (!disableGitRepositoryValidation) {
bind(GitRepositoryManager.class).to(MultiSiteGitRepositoryManager.class);
}
+ if (cfg.getZookeeperConfig().getEnforcementRules().isEmpty()) {
+ bind(SharedRefEnforcement.class).to(DefaultSharedRefEnforcement.class).in(Scopes.SINGLETON);
+ } else {
+ bind(SharedRefEnforcement.class)
+ .to(CustomSharedRefEnforcementByProject.class)
+ .in(Scopes.SINGLETON);
+ }
- bind(SharedRefEnforcement.class).to(DefaultSharedRefEnforcement.class).in(Scopes.SINGLETON);
install(new ZkValidationModule(cfg));
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java
new file mode 100644
index 0000000..7a806a8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2019 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.multisite.validation.dfsrefdb;
+
+import static com.google.common.base.Suppliers.memoize;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class CustomSharedRefEnforcementByProject implements SharedRefEnforcement {
+ private static final String ALL = ".*";
+
+ private final Supplier<Map<String, Map<String, EnforcePolicy>>> predefEnforcements;
+
+ @Inject
+ public CustomSharedRefEnforcementByProject(Configuration config) {
+ this.predefEnforcements = memoize(() -> parseDryRunEnforcementsToMap(config));
+ }
+
+ private static Map<String, Map<String, EnforcePolicy>> parseDryRunEnforcementsToMap(
+ Configuration config) {
+ Map<String, Map<String, EnforcePolicy>> enforcementMap = new HashMap<>();
+
+ for (Map.Entry<EnforcePolicy, String> enforcementEntry :
+ config.getZookeeperConfig().getEnforcementRules().entries()) {
+ parseEnforcementEntry(enforcementMap, enforcementEntry);
+ }
+
+ return enforcementMap;
+ }
+
+ private static void parseEnforcementEntry(
+ Map<String, Map<String, EnforcePolicy>> enforcementMap,
+ Map.Entry<EnforcePolicy, String> enforcementEntry) {
+ Iterator<String> projectAndRef = Splitter.on(':').split(enforcementEntry.getValue()).iterator();
+ EnforcePolicy enforcementPolicy = enforcementEntry.getKey();
+
+ if (projectAndRef.hasNext()) {
+ String projectName = emptyToAll(projectAndRef.next());
+ String refName = emptyToAll(projectAndRef.hasNext() ? projectAndRef.next() : ALL);
+
+ Map<String, EnforcePolicy> existingOrDefaultRef =
+ enforcementMap.getOrDefault(projectName, new HashMap<>());
+
+ existingOrDefaultRef.put(refName, enforcementPolicy);
+
+ enforcementMap.put(projectName, existingOrDefaultRef);
+ }
+ }
+
+ private static String emptyToAll(String value) {
+ return value.trim().isEmpty() ? ALL : value;
+ }
+
+ @Override
+ public EnforcePolicy getPolicy(String projectName, String refName) {
+ if (isRefToBeIgnoredBySharedRefDb(refName)) {
+ return EnforcePolicy.IGNORED;
+ }
+
+ return getRefEnforcePolicy(projectName, refName);
+ }
+
+ private EnforcePolicy getRefEnforcePolicy(String projectName, String refName) {
+ Map<String, EnforcePolicy> orDefault =
+ predefEnforcements
+ .get()
+ .getOrDefault(
+ projectName, predefEnforcements.get().getOrDefault(ALL, ImmutableMap.of()));
+
+ return MoreObjects.firstNonNull(
+ orDefault.getOrDefault(refName, orDefault.get(ALL)), EnforcePolicy.REQUIRED);
+ }
+
+ @Override
+ public EnforcePolicy getPolicy(String projectName) {
+ Map<String, EnforcePolicy> policiesForProject =
+ predefEnforcements
+ .get()
+ .getOrDefault(
+ projectName, predefEnforcements.get().getOrDefault(ALL, ImmutableMap.of()));
+ return policiesForProject.getOrDefault(ALL, EnforcePolicy.REQUIRED);
+ }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e3abe59..be32c27 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -187,6 +187,34 @@
: Enable the use of a shared ref-database
Defaults: true
+```ref-database.enforcementRules.<policy>```
+: Level of consistency enforcement across sites on a project:refs basis.
+ Supports multiple values for enforcing the policy on multiple projects or refs.
+ If the project or ref is omitted, apply the policy to all projects or all refs.
+
+ The <policy> can be one of the following values:
+
+ 1. REQUIRED - Throw an exception if a git ref-update is processed again
+ a local ref not yet in sync with the shared ref-database.
+ The user transaction is cancelled. The Gerrit GUI (or the Git client)
+ receives an HTTP 500 - Internal Server Error.
+
+ 2. DESIRED - Validate the git ref-update against the shared ref-database.
+ Any misaligned is logged in errors_log file but the user operation is allowed
+ to continue successfully.
+
+ 3. IGNORED - Ignore any validation against the shared ref-database.
+
+ *Example:*
+ ```
+ [ref-database "enforcementRules"]
+ DESIRED = AProject:/refs/heads/feature
+ ```
+
+ Relax the alignment with the shared ref-database for AProject on refs/heads/feature.
+
+ Defaults: No rules = All projects are REQUIRED to be consistent on all refs.
+
```ref-database.zookeeper.connectString```
: Connection string to Zookeeper
@@ -230,14 +258,14 @@
operations on Zookeeper
Defaults: 1000
-
+
```ref-database.zookeeper.casRetryPolicyMaxSleepTimeMs```
: Configuration for the maximum sleep timeout in milliseconds of the
BoundedExponentialBackoffRetry policy used for the Compare and Swap
operations on Zookeeper
Defaults: 3000
-
+
```ref-database.zookeeper.casRetryPolicyMaxRetries```
: Configuration for the maximum number of retries of the
BoundedExponentialBackoffRetry policy used for the Compare and Swap
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/CustomSharedRefEnforcementByProjectTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/CustomSharedRefEnforcementByProjectTest.java
new file mode 100644
index 0000000..e4d7861
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/CustomSharedRefEnforcementByProjectTest.java
@@ -0,0 +1,159 @@
+// Copyright (C) 2019 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.multisite.validation.dfsrefdb.zookeeper;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase.newRef;
+
+import com.googlesource.gerrit.plugins.multisite.Configuration;
+import com.googlesource.gerrit.plugins.multisite.Configuration.ZookeeperConfig;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.CustomSharedRefEnforcementByProject;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
+import java.util.Arrays;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CustomSharedRefEnforcementByProjectTest implements RefFixture {
+
+ SharedRefEnforcement refEnforcement;
+
+ @Before
+ public void setUp() {
+ Config multiSiteConfig = new Config();
+ multiSiteConfig.setStringList(
+ ZookeeperConfig.SECTION,
+ ZookeeperConfig.SUBSECTION_ENFORCEMENT_RULES,
+ EnforcePolicy.DESIRED.name(),
+ Arrays.asList(
+ "ProjectOne",
+ "ProjectTwo:refs/heads/master/test",
+ "ProjectTwo:refs/heads/master/test2"));
+ multiSiteConfig.setString(
+ ZookeeperConfig.SECTION,
+ ZookeeperConfig.SUBSECTION_ENFORCEMENT_RULES,
+ EnforcePolicy.IGNORED.name(),
+ ":refs/heads/master/test");
+
+ refEnforcement = newCustomRefEnforcement(multiSiteConfig);
+ }
+
+ @Test
+ public void projectOneShouldReturnDesiredForAllRefs() {
+ Ref aRef = newRef("refs/heads/master/2", AN_OBJECT_ID_1);
+ assertThat(refEnforcement.getPolicy("ProjectOne", aRef.getName()))
+ .isEqualTo(EnforcePolicy.DESIRED);
+ }
+
+ @Test
+ public void projectOneEnforcementShouldAlwaysPrevail() {
+ Ref aRef = newRef("refs/heads/master/test", AN_OBJECT_ID_1);
+ assertThat(refEnforcement.getPolicy("ProjectOne", aRef.getName()))
+ .isEqualTo(EnforcePolicy.DESIRED);
+ }
+
+ @Test
+ public void aNonListedProjectShouldIgnoreRefForMasterTest() {
+ Ref aRef = newRef("refs/heads/master/test", AN_OBJECT_ID_1);
+ assertThat(refEnforcement.getPolicy("NonListedProject", aRef.getName()))
+ .isEqualTo(EnforcePolicy.IGNORED);
+ }
+
+ @Test
+ public void projectTwoSpecificRefShouldReturnDesiredPolicy() {
+ Ref refOne = newRef("refs/heads/master/test", AN_OBJECT_ID_1);
+ Ref refTwo = newRef("refs/heads/master/test2", AN_OBJECT_ID_1);
+
+ assertThat(refEnforcement.getPolicy("ProjectTwo", refOne.getName()))
+ .isEqualTo(EnforcePolicy.DESIRED);
+ assertThat(refEnforcement.getPolicy("ProjectTwo", refTwo.getName()))
+ .isEqualTo(EnforcePolicy.DESIRED);
+ }
+
+ @Test
+ public void aNonListedProjectShouldReturnRequired() {
+ Ref refOne = newRef("refs/heads/master/newChange", AN_OBJECT_ID_1);
+ assertThat(refEnforcement.getPolicy("NonListedProject", refOne.getName()))
+ .isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ @Test
+ public void aNonListedRefInProjectShouldReturnRequired() {
+ Ref refOne = newRef("refs/heads/master/test3", AN_OBJECT_ID_1);
+ assertThat(refEnforcement.getPolicy("ProjectTwo", refOne.getName()))
+ .isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ @Test
+ public void aNonListedProjectAndRefShouldReturnRequired() {
+ Ref refOne = newRef("refs/heads/master/test3", AN_OBJECT_ID_1);
+ assertThat(refEnforcement.getPolicy("NonListedProject", refOne.getName()))
+ .isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ @Test
+ public void getProjectPolicyForProjectOneShouldRetrunDesired() {
+ assertThat(refEnforcement.getPolicy("ProjectOne")).isEqualTo(EnforcePolicy.DESIRED);
+ }
+
+ @Test
+ public void getProjectPolicyForProjectTwoShouldReturnRequired() {
+ assertThat(refEnforcement.getPolicy("ProjectTwo")).isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ @Test
+ public void getProjectPolicyForNonListedProjectShouldReturnRequired() {
+ assertThat(refEnforcement.getPolicy("NonListedProject")).isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ @Test
+ public void getProjectPolicyForNonListedProjectWhenSingleProject() {
+ SharedRefEnforcement customEnforcement =
+ newCustomRefEnforcementWithValue(EnforcePolicy.DESIRED, ":refs/heads/master");
+
+ assertThat(customEnforcement.getPolicy("NonListedProject")).isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ @Test
+ public void getANonListedProjectWhenOnlyOneProjectIsListedShouldReturnRequired() {
+ SharedRefEnforcement customEnforcement =
+ newCustomRefEnforcementWithValue(EnforcePolicy.DESIRED, "AProject:");
+ assertThat(customEnforcement.getPolicy("NonListedProject", "refs/heads/master"))
+ .isEqualTo(EnforcePolicy.REQUIRED);
+ }
+
+ private SharedRefEnforcement newCustomRefEnforcementWithValue(
+ EnforcePolicy policy, String... projectAndRefs) {
+ Config multiSiteConfig = new Config();
+ multiSiteConfig.setStringList(
+ ZookeeperConfig.SECTION,
+ ZookeeperConfig.SUBSECTION_ENFORCEMENT_RULES,
+ policy.name(),
+ Arrays.asList(projectAndRefs));
+ return newCustomRefEnforcement(multiSiteConfig);
+ }
+
+ private SharedRefEnforcement newCustomRefEnforcement(Config multiSiteConfig) {
+ return new CustomSharedRefEnforcementByProject(
+ new Configuration(multiSiteConfig, new Config()));
+ }
+
+ @Override
+ public String testBranch() {
+ return "fooBranch";
+ }
+}