Implement ReplicationConfigOverrides.update() The newly added `update(Config)` method to `ReplicationConfigOverrides` allows us to update the replication configuration programmatically. Added implementation stores remotes definitions in the "fanout" fashion in the `refs/meta/replication` branch of `All-Projects` repository. Other configuration options will be stored in the `replication.config` file in the root of the branch. If no configuration or branch is present they will be created by the script. Change-Id: Ifa744342536fc029a220833f8f59c9a15954ac65
diff --git a/replication/replication-config-from-git.groovy b/replication/replication-config-from-git.groovy index efcb32f..dfeb812 100644 --- a/replication/replication-config-from-git.groovy +++ b/replication/replication-config-from-git.groovy
@@ -12,33 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. +import com.google.gerrit.common.Nullable +import com.google.gerrit.server.update.context.RefUpdateContext import com.googlesource.gerrit.plugins.replication.* - import com.googlesource.gerrit.plugins.replication.FanoutConfigResource.* - import com.googlesource.gerrit.plugins.replication.FileConfigResource.* - import com.google.inject.* import com.google.common.collect.* import com.google.common.flogger.* import com.google.common.io.* - import com.google.common.io.Files.* import com.google.gerrit.entities.* import com.google.gerrit.extensions.registration.* import com.google.gerrit.server.config.* import com.google.gerrit.server.git.* + import com.google.gerrit.server.* import com.google.inject.* - import java.io.* - import java.util.* - import org.eclipse.jgit.errors.* + import org.eclipse.jgit.dircache.* import org.eclipse.jgit.lib.* - import org.eclipse.jgit.lib.FileMode.* import org.eclipse.jgit.revwalk.* import org.eclipse.jgit.treewalk.* - class GitReplicationConfigOverrides implements ReplicationConfigOverrides { - FluentLogger logger = FluentLogger.forEnclosingClass() +import static java.nio.charset.StandardCharsets.* +import static org.eclipse.jgit.dircache.DirCacheEntry.* +import static org.eclipse.jgit.lib.FileMode.* +import static org.eclipse.jgit.lib.RefUpdate.Result.* + +class GitReplicationConfigOverrides implements ReplicationConfigOverrides { + static final FluentLogger logger = FluentLogger.forEnclosingClass() Config EMPTY_CONFIG = new Config() def REF_NAME = RefNames.REFS_META + "replication" @@ -49,6 +50,13 @@ @Inject GitRepositoryManager repoManager + @Inject + Provider<AllProjectsName> allProjectsNameProvider + + @Inject + @GerritPersonIdent + PersonIdent gerritPersonIdent + @Override Config getConfig() { Config config = EMPTY_CONFIG @@ -86,7 +94,7 @@ tw.enterSubtree() while (tw.next()) { - if (tw.fileMode == FileMode.REGULAR_FILE && tw.nameString.endsWith(".config")) { + if (tw.fileMode == REGULAR_FILE && tw.nameString.endsWith(".config")) { Config remoteConfig = new BlobBasedConfig(new Config(), repo, tw.getObjectId(0)) addRemoteConfig(tw.nameString, remoteConfig, destination) } @@ -96,7 +104,7 @@ destination } - def removeRemotes(Config config) { + static def removeRemotes(Config config) { Set < String > remoteNames = config.getSubsections("remote") if (!remoteNames) { logger.atSevere().log( @@ -109,7 +117,7 @@ } } - def addRemoteConfig(String fileName, Config source, Config destination) { + static def addRemoteConfig(String fileName, Config source, Config destination) { String remoteName = Files.getNameWithoutExtension(fileName) source.getNames("remote").each { name -> @@ -133,6 +141,142 @@ repo.close() } } + + @Override + void update(Config config) throws IOException { + Repository repo = repoManager.openRepository(allProjectsNameProvider.get()) + RefUpdateContext ctx = RefUpdateContext.open(RefUpdateContext.RefUpdateType.PLUGIN) + RevWalk rw = new RevWalk(repo) + ObjectReader reader = repo.newObjectReader() + ObjectInserter inserter = repo.newObjectInserter() + try { + ObjectId configHead = repo.resolve(REF_NAME) + DirCache dirCache = readTree(repo, reader, configHead) + DirCacheEditor editor = dirCache.editor() + Config rootConfig = readConfig(FileConfigResource.CONFIG_NAME, repo, rw, configHead) + + for (String section : config.getSections()) { + if ("remote".equals(section)) { + updateRemoteConfig(config, repo, rw, configHead, editor, inserter) + } else { + updateRootConfig(config, section, rootConfig) + } + } + insertConfig(FileConfigResource.CONFIG_NAME, rootConfig, editor, inserter) + editor.finish() + + CommitBuilder cb = new CommitBuilder() + ObjectId newTreeId = dirCache.writeTree(inserter) + if (configHead != null) { + ObjectId oldTreeId = repo.parseCommit(configHead).tree + if (oldTreeId == newTreeId) { + logger.atInfo().log("No configuration changes were applied, ignoring") + return; + } + cb.setParentId(configHead) + } + cb.setAuthor(gerritPersonIdent) + cb.setCommitter(gerritPersonIdent) + cb.setTreeId(newTreeId); + cb.setMessage("Update configuration") + ObjectId newConfigHead = inserter.insert(cb) + inserter.flush() + RefUpdate refUpdate = repo.getRefDatabase().newUpdate(REF_NAME, false) + refUpdate.setNewObjectId(newConfigHead) + RefUpdate.Result result = refUpdate.update() + if (result != FAST_FORWARD && result != NEW) { + throw new IOException("Updating replication config failed: " + result) + } + } finally { + inserter.close() + reader.close() + rw.close() + ctx.close() + repo.close() + } + } + + Config readConfig( + String configPath, Repository repo, RevWalk rw, @Nullable ObjectId treeId) { + if (treeId != null) { + try { + RevTree tree = rw.parseTree(treeId) + TreeWalk tw = TreeWalk.forPath(repo, configPath, tree) + if (tw != null) { + return new BlobBasedConfig(new Config(), repo, tw.getObjectId(0)) + } + } catch (ConfigInvalidException | IOException e) { + logger.atWarning().withCause(e).log( + "failed to load replication configuration from branch %s of %s, path %s", + REF_NAME, allProjectsName.get(), configPath) + } + } + + return new Config(); + } + + static DirCache readTree(Repository repo, ObjectReader reader, ObjectId configHead) + throws IOException { + DirCache dc = DirCache.newInCore() + if (configHead != null) { + RevTree tree = repo.parseCommit(configHead).getTree() + DirCacheBuilder b = dc.builder() + b.addTree(new byte[0], STAGE_0, reader, tree) + b.finish() + } + return dc; + } + + void updateRemoteConfig( + Config config, + Repository repo, + RevWalk rw, + @Nullable ObjectId refId, + DirCacheEditor editor, + ObjectInserter inserter) + throws IOException { + for (String remoteName : config.getSubsections("remote")) { + String configPath = String.format("%s/%s.config", FanoutConfigResource.CONFIG_DIR, remoteName) + Config baseConfig = readConfig(configPath, repo, rw, refId) + + updateConfigSubSections(config, "remote", remoteName, baseConfig) + insertConfig(configPath, baseConfig, editor, inserter) + } + } + + static void updateRootConfig(Config config, String section, Config rootConfig) { + for (String subsection : config.getSubsections(section)) { + updateConfigSubSections(config, section, subsection, rootConfig) + } + + for (String name : config.getNames(section, true)) { + List<String> values = Lists.newArrayList(config.getStringList(section, null, name)) + rootConfig.setStringList(section, null, name, values) + } + } + + static void updateConfigSubSections( + Config source, String section, String subsection, Config destination) { + for (String name : source.getNames(section, subsection, true)) { + List<String> values = Lists.newArrayList(source.getStringList(section, subsection, name)) + destination.setStringList(section, subsection, name, values) + } + } + + static void insertConfig( + String configPath, Config config, DirCacheEditor editor, ObjectInserter inserter) + throws IOException { + String configText = config.toText() + ObjectId configId = inserter.insert(Constants.OBJ_BLOB, configText.getBytes(UTF_8)) + editor.add( + new DirCacheEditor.PathEdit(configPath) { + @Override + void apply(DirCacheEntry ent) { + ent.setFileMode(REGULAR_FILE) + ent.setObjectId(configId) + } + }); + } } class GitReplicationConfigModule implements Module {