Add option to preserve migration mode during online external-ids migration

Introduce a `--preserve-migration-mode` flag to the SSH command
`migrate-externalids-to-insensitive`. When enabled, this option skips
setting `auth.userNameCaseInsensitiveMigrationMode` to `false` after the
migration completes.

This change allows admins of multi-primary and/or IaC environments to
update the configuration across all primaries and/or with tooling like
Chef/Ansible after migration, rather than having it modified
automatically by the command on one primary.

Release-Notes: Add --preserve-migration-mode option to online external-ids migration
Change-Id: I3d6915e36b878b5c353d78045a99459b669c3814
diff --git a/Documentation/cmd-migrate-externalids-to-insensitive.txt b/Documentation/cmd-migrate-externalids-to-insensitive.txt
index b023089..6458303 100644
--- a/Documentation/cmd-migrate-externalids-to-insensitive.txt
+++ b/Documentation/cmd-migrate-externalids-to-insensitive.txt
@@ -7,6 +7,7 @@
 [verse]
 --
 _ssh_ -p <port> <host> _gerrit migrate-externalids-to-insensitive_
+  [--preserve-migration-mode]
 --
 
 == DESCRIPTION
@@ -29,6 +30,13 @@
 == SCRIPTING
 This command is intended to be used in scripts.
 
+== OPTIONS
+--preserve-migration-mode::
+	Skip setting `auth.userNameCaseInsensitiveMigrationMode` to `false` after the
+	migration completes successfully. This option is useful in multi-primary and
+	IaC setups where admins may prefer to update the migration mode config on all
+	primaries and/or using IaC tooling after the migration completes.
+
 == EXAMPLES
 Start the online external ids migration:
 
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensitivityMigrator.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensitivityMigrator.java
index f72cfa9..e441681 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensitivityMigrator.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensitivityMigrator.java
@@ -70,7 +70,7 @@
         globalConfig.getBoolean("auth", "userNameCaseInsensitive", false);
   }
 
-  public void migrate() {
+  public void migrate(boolean preserveMigrationMode) {
     if (!isUserNameCaseInsensitive || !isUserNameCaseInsensitiveMigrationMode) {
       logger.atSevere().log(
           "External IDs online migration requires auth.userNameCaseInsensitive and"
@@ -91,7 +91,9 @@
               monitor.endTask();
             }
             try {
-              updateGerritConfig();
+              if (!preserveMigrationMode) {
+                updateGerritConfig();
+              }
               monitor.beginTask("Reindex accounts", ProgressMonitor.UNKNOWN);
 
               @SuppressWarnings("unused")
diff --git a/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java b/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
index 5c38dd7..0fded1b 100644
--- a/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
@@ -22,12 +22,19 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(
     name = "migrate-externalids-to-insensitive",
     description = "Migrate external-ids to case insensitive")
 public class ExternalIdCaseSensitivityMigrationCommand extends SshCommand {
+  @Option(
+      name = "--preserve-migration-mode",
+      usage =
+          "Skip setting auth.userNameCaseInsensitiveMigrationMode "
+              + "to false after the migration finishes successfully.")
+  private boolean preserveMigrationMode;
 
   @Inject OnlineExternalIdCaseSensitivityMigrator onlineExternalIdCaseSensitivityMigrator;
   @Inject @GerritServerConfig private Config globalConfig;
@@ -45,7 +52,7 @@
               + " auth.userNameCaseInsensitiveMigrationMode to be set to true. Cannot start"
               + " migration!");
     }
-    onlineExternalIdCaseSensitivityMigrator.migrate();
+    onlineExternalIdCaseSensitivityMigrator.migrate(preserveMigrationMode);
     stdout.println(
         "External ids case insensitivity migration started. To check if it's completed look for"
             + " \"External IDs migration completed!\" message in the Gerrit server logs");
diff --git a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java
index 9879657..3fb9d4e 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCaseSensitivityMigrator;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.storage.notedb.OnlineExternalIdCaseSensitivityMigrator;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -42,6 +43,8 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
 public class OnlineExternalIdCaseSensitivityMigratorIT extends AbstractDaemonTest {
@@ -52,6 +55,7 @@
   @Inject private ExternalIdKeyFactory externalIdKeyFactory;
   @Inject private ExternalIdFactory externalIdFactory;
   @Inject private OnlineExternalIdCaseSensitivityMigrator objectUnderTest;
+  @Inject private SitePaths sitePaths;
 
   @Override
   public Module createModule() {
@@ -85,7 +89,7 @@
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
 
-      objectUnderTest.migrate();
+      objectUnderTest.migrate(false);
 
       extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isFalse();
@@ -93,6 +97,47 @@
 
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isFalse();
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
+
+      FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+      cfg.load();
+      assertThat(cfg.getBoolean("auth", null, "userNameCaseInsensitiveMigrationMode", true))
+          .isFalse();
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+  @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "true")
+  public void shouldMigrateExternalIdWithPreserveMigrationMode()
+      throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+      createExternalId(
+          md, extIdNotes, SCHEME_GERRIT, "JonDoe", accountId, isUserNameCaseInsensitive);
+      createExternalId(
+          md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+      assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isTrue();
+      assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "jondoe").isPresent()).isFalse();
+
+      assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+      assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+
+      objectUnderTest.migrate(true);
+
+      extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isFalse();
+      assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "jondoe").isPresent()).isTrue();
+
+      assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isFalse();
+      assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
+
+      FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
+      cfg.load();
+      assertThat(cfg.getBoolean("auth", null, "userNameCaseInsensitiveMigrationMode", true))
+          .isTrue();
     }
   }
 
@@ -118,7 +163,7 @@
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isFalse();
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
 
-      objectUnderTest.migrate();
+      objectUnderTest.migrate(false);
     }
   }
 
@@ -157,7 +202,7 @@
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").get().email())
           .isEqualTo("test@email.com");
 
-      objectUnderTest.migrate();
+      objectUnderTest.migrate(false);
 
       extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isFalse();
@@ -187,7 +232,7 @@
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
 
-      objectUnderTest.migrate();
+      objectUnderTest.migrate(false);
 
       extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       assertThat(
@@ -215,7 +260,7 @@
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
 
-      assertThrows(DuplicateExternalIdKeyException.class, () -> objectUnderTest.migrate());
+      assertThrows(DuplicateExternalIdKeyException.class, () -> objectUnderTest.migrate(false));
     }
   }
 
@@ -235,7 +280,7 @@
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
 
-      objectUnderTest.migrate();
+      objectUnderTest.migrate(false);
 
       extIdNotes = externalIdNotesFactory.load(allUsersRepo);
 
@@ -257,7 +302,7 @@
       createExternalId(
           md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
 
-      objectUnderTest.migrate();
+      objectUnderTest.migrate(false);
 
       extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();