| // 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. |
| |
| import com.google.gerrit.common.Nullable |
| import com.google.gerrit.server.update.context.RefUpdateContext |
| import com.googlesource.gerrit.plugins.replication.* |
| |
| import com.google.common.collect.* |
| import com.google.common.flogger.* |
| import com.google.common.io.* |
| 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 org.eclipse.jgit.errors.* |
| import org.eclipse.jgit.dircache.* |
| import org.eclipse.jgit.lib.* |
| |
| import org.eclipse.jgit.revwalk.* |
| import org.eclipse.jgit.treewalk.* |
| |
| 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" |
| |
| @Inject |
| AllProjectsName allProjectsName |
| |
| @Inject |
| GitRepositoryManager repoManager |
| |
| @Inject |
| Provider<AllProjectsName> allProjectsNameProvider |
| |
| @Inject |
| @GerritPersonIdent |
| PersonIdent gerritPersonIdent |
| |
| @Override |
| Config getConfig() { |
| Config config = EMPTY_CONFIG |
| |
| Repository repo = repoManager.openRepository(allProjectsName) |
| RevWalk rw = new RevWalk(repo) |
| try { |
| Ref ref = repo.exactRef(REF_NAME) |
| if (ref) { |
| RevTree tree = rw.parseTree(ref.objectId) |
| config = addFanoutRemotes(repo, tree, getBaseConfig(repo, tree)) |
| } |
| } catch (IOException e) { |
| logger.atWarning().withCause(e).log("Cannot read replication config from git repository") |
| } catch (ConfigInvalidException e) { |
| logger.atWarning().withCause(e).log("Cannot parse replication config from git repository") |
| } finally { |
| rw.close() |
| repo.close() |
| } |
| |
| config |
| } |
| |
| Config getBaseConfig(Repository repo, RevTree tree) { |
| TreeWalk tw = TreeWalk.forPath(repo, FileConfigResource.CONFIG_NAME, tree) |
| return tw ? new BlobBasedConfig(new Config(), repo, tw.getObjectId(0)) : EMPTY_CONFIG |
| } |
| |
| Config addFanoutRemotes(Repository repo, RevTree tree, Config destination) |
| throws IOException, ConfigInvalidException { |
| TreeWalk tw = TreeWalk.forPath(repo, FanoutConfigResource.CONFIG_DIR, tree) |
| if (tw) { |
| removeRemotes(destination) |
| |
| tw.enterSubtree() |
| while (tw.next()) { |
| if (tw.fileMode == REGULAR_FILE && tw.nameString.endsWith(".config")) { |
| Config remoteConfig = new BlobBasedConfig(new Config(), repo, tw.getObjectId(0)) |
| addRemoteConfig(tw.nameString, remoteConfig, destination) |
| } |
| } |
| } |
| |
| destination |
| } |
| |
| static def removeRemotes(Config config) { |
| Set < String > remoteNames = config.getSubsections("remote") |
| if (!remoteNames) { |
| logger.atSevere().log( |
| "When replication directory is present replication.config file cannot contain remote configuration. Ignoring: %s", |
| remoteNames.join(",")) |
| |
| for (String name: remoteNames) { |
| config.unsetSection("remote", name) |
| } |
| } |
| } |
| |
| static def addRemoteConfig(String fileName, Config source, Config destination) { |
| String remoteName = Files.getNameWithoutExtension(fileName) |
| source.getNames("remote").each { |
| name -> |
| destination.setStringList( |
| "remote", |
| remoteName, |
| name, |
| Lists.newArrayList(source.getStringList("remote", null, name))) |
| } |
| } |
| |
| @Override |
| String getVersion() { |
| Repository repo = repoManager.openRepository(allProjectsName) |
| try { |
| ObjectId configHead = repo.resolve(REF_NAME) |
| return configHead ? configHead.name() : "" |
| } catch (IOException e) { |
| throw new IllegalStateException("Could not open replication configuration repository", e) |
| } finally { |
| 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 { |
| |
| @Override |
| void configure(Binder binder) { |
| DynamicItem.bind(binder, ReplicationConfigOverrides.class) |
| .to(GitReplicationConfigOverrides.class) |
| } |
| } |
| |
| modules = [GitReplicationConfigModule] |