| // 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.google.gerrit.plugins.checks.db; |
| |
| import com.google.common.base.Throwables; |
| import com.google.gerrit.exceptions.DuplicateKeyException; |
| import com.google.gerrit.git.LockFailureException; |
| import com.google.gerrit.git.RefUpdateUtil; |
| import com.google.gerrit.plugins.checks.Checker; |
| import com.google.gerrit.plugins.checks.CheckerCreation; |
| import com.google.gerrit.plugins.checks.CheckerUpdate; |
| import com.google.gerrit.plugins.checks.CheckerUuid; |
| import com.google.gerrit.plugins.checks.CheckersUpdate; |
| import com.google.gerrit.plugins.checks.NoSuchCheckerException; |
| import com.google.gerrit.plugins.checks.api.CheckerStatus; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.update.RetryHelper; |
| import com.google.gerrit.server.update.RetryHelper.ActionType; |
| import com.google.inject.assistedinject.Assisted; |
| import com.google.inject.assistedinject.AssistedInject; |
| import java.io.IOException; |
| import java.util.Optional; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.BatchRefUpdate; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** Class to write checkers to NoteDb. */ |
| class NoteDbCheckersUpdate implements CheckersUpdate { |
| interface Factory { |
| /** |
| * Creates a {@code CheckersUpdate} which uses the identity of the specified user to mark |
| * database modifications executed by it. For NoteDb, this identity is used as author and |
| * committer for all related commits. |
| * |
| * <p><strong>Note</strong>: Please use this method with care and consider using the {@link |
| * com.google.gerrit.server.UserInitiated} annotation on the provider of a {@code |
| * CheckersUpdate} instead. |
| * |
| * @param currentUser the user to which modifications should be attributed |
| */ |
| NoteDbCheckersUpdate create(IdentifiedUser currentUser); |
| |
| /** |
| * Creates a {@code CheckersUpdate} which uses the server identity to mark database |
| * modifications executed by it. For NoteDb, this identity is used as author and committer for |
| * all related commits. |
| * |
| * <p><strong>Note</strong>: Please use this method with care and consider using the {@link |
| * com.google.gerrit.server.ServerInitiated} annotation on the provider of a {@code |
| * CheckersUpdate} instead. |
| */ |
| NoteDbCheckersUpdate createWithServerIdent(); |
| } |
| |
| private final GitRepositoryManager repoManager; |
| private final AllProjectsName allProjectsName; |
| private final MetaDataUpdateFactory metaDataUpdateFactory; |
| private final GitReferenceUpdated gitRefUpdated; |
| private final RetryHelper retryHelper; |
| private final Optional<IdentifiedUser> currentUser; |
| |
| @AssistedInject |
| NoteDbCheckersUpdate( |
| GitRepositoryManager repoManager, |
| AllProjectsName allProjectsName, |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| GitReferenceUpdated gitRefUpdated, |
| RetryHelper retryHelper, |
| @GerritPersonIdent PersonIdent serverIdent) { |
| this( |
| repoManager, |
| allProjectsName, |
| metaDataUpdateInternalFactory, |
| gitRefUpdated, |
| retryHelper, |
| serverIdent, |
| Optional.empty()); |
| } |
| |
| @AssistedInject |
| NoteDbCheckersUpdate( |
| GitRepositoryManager repoManager, |
| AllProjectsName allProjectsName, |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| GitReferenceUpdated gitRefUpdated, |
| RetryHelper retryHelper, |
| @GerritPersonIdent PersonIdent serverIdent, |
| @Assisted IdentifiedUser currentUser) { |
| this( |
| repoManager, |
| allProjectsName, |
| metaDataUpdateInternalFactory, |
| gitRefUpdated, |
| retryHelper, |
| serverIdent, |
| Optional.of(currentUser)); |
| } |
| |
| private NoteDbCheckersUpdate( |
| GitRepositoryManager repoManager, |
| AllProjectsName allProjectsName, |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| GitReferenceUpdated gitRefUpdated, |
| RetryHelper retryHelper, |
| @GerritPersonIdent PersonIdent serverIdent, |
| Optional<IdentifiedUser> currentUser) { |
| this.repoManager = repoManager; |
| this.allProjectsName = allProjectsName; |
| this.gitRefUpdated = gitRefUpdated; |
| this.retryHelper = retryHelper; |
| this.currentUser = currentUser; |
| |
| metaDataUpdateFactory = |
| getMetaDataUpdateFactory(metaDataUpdateInternalFactory, currentUser, serverIdent); |
| } |
| |
| @Override |
| public Checker createChecker(CheckerCreation checkerCreation, CheckerUpdate checkerUpdate) |
| throws DuplicateKeyException, IOException, ConfigInvalidException { |
| try { |
| return retryHelper.execute( |
| RetryHelper.ActionType.PLUGIN_UPDATE, |
| () -> createCheckerInNoteDb(checkerCreation, checkerUpdate), |
| LockFailureException.class::isInstance); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| Throwables.throwIfInstanceOf(e, DuplicateKeyException.class); |
| Throwables.throwIfInstanceOf(e, IOException.class); |
| Throwables.throwIfInstanceOf(e, ConfigInvalidException.class); |
| throw new IOException(e); |
| } |
| } |
| |
| private Checker createCheckerInNoteDb( |
| CheckerCreation checkerCreation, CheckerUpdate checkerUpdate) |
| throws DuplicateKeyException, IOException, ConfigInvalidException { |
| try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) { |
| CheckerConfig checkerConfig = |
| CheckerConfig.createForNewChecker(allProjectsName, allProjectsRepo, checkerCreation); |
| checkerConfig.setCheckerUpdate(checkerUpdate); |
| |
| CheckersByRepositoryNotes checkersByRepositoryNotes = |
| CheckersByRepositoryNotes.load(allProjectsName, allProjectsRepo); |
| if (!checkerUpdate.getStatus().isPresent() |
| || checkerUpdate.getStatus().get() == CheckerStatus.ENABLED) { |
| // Only inserts to the notes if the status is not set or set as "ENABLED". Does not insert |
| // if the checker is DISABLED. |
| checkersByRepositoryNotes.insert( |
| checkerCreation.getCheckerUuid(), checkerCreation.getRepository()); |
| } |
| |
| commit(allProjectsRepo, checkerConfig, checkersByRepositoryNotes); |
| |
| return checkerConfig |
| .getLoadedChecker() |
| .orElseThrow( |
| () -> new IllegalStateException("Created checker wasn't automatically loaded")); |
| } |
| } |
| |
| private void commit( |
| Repository allProjectsRepo, |
| CheckerConfig checkerConfig, |
| CheckersByRepositoryNotes checkersByRepositoryNotes) |
| throws IOException { |
| BatchRefUpdate batchRefUpdate = allProjectsRepo.getRefDatabase().newBatchUpdate(); |
| try (MetaDataUpdate metaDataUpdate = |
| metaDataUpdateFactory.create(allProjectsName, allProjectsRepo, batchRefUpdate)) { |
| checkerConfig.commit(metaDataUpdate); |
| checkersByRepositoryNotes.commit(metaDataUpdate); |
| } |
| RefUpdateUtil.executeChecked(batchRefUpdate, allProjectsRepo); |
| |
| gitRefUpdated.fire( |
| allProjectsName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null)); |
| } |
| |
| private static MetaDataUpdateFactory getMetaDataUpdateFactory( |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| Optional<IdentifiedUser> currentUser, |
| PersonIdent serverIdent) { |
| return (projectName, repository, batchRefUpdate) -> { |
| MetaDataUpdate metaDataUpdate = |
| metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate); |
| metaDataUpdate.getCommitBuilder().setCommitter(serverIdent); |
| PersonIdent authorIdent; |
| if (currentUser.isPresent()) { |
| metaDataUpdate.setAuthor(currentUser.get()); |
| authorIdent = |
| currentUser.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone()); |
| } else { |
| authorIdent = serverIdent; |
| } |
| metaDataUpdate.getCommitBuilder().setAuthor(authorIdent); |
| return metaDataUpdate; |
| }; |
| } |
| |
| @FunctionalInterface |
| private interface MetaDataUpdateFactory { |
| MetaDataUpdate create( |
| Project.NameKey projectName, Repository repository, BatchRefUpdate batchRefUpdate) |
| throws IOException; |
| } |
| |
| @Override |
| public Checker updateChecker(CheckerUuid checkerUuid, CheckerUpdate checkerUpdate) |
| throws NoSuchCheckerException, IOException, ConfigInvalidException { |
| return updateCheckerWithRetry(checkerUuid, checkerUpdate); |
| } |
| |
| private Checker updateCheckerWithRetry(CheckerUuid checkerUuid, CheckerUpdate checkerUpdate) |
| throws NoSuchCheckerException, IOException, ConfigInvalidException { |
| try { |
| return retryHelper.execute( |
| ActionType.PLUGIN_UPDATE, |
| () -> updateCheckerInNoteDb(checkerUuid, checkerUpdate), |
| LockFailureException.class::isInstance); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| Throwables.throwIfInstanceOf(e, IOException.class); |
| Throwables.throwIfInstanceOf(e, ConfigInvalidException.class); |
| Throwables.throwIfInstanceOf(e, NoSuchCheckerException.class); |
| throw new IOException(e); |
| } |
| } |
| |
| private Checker updateCheckerInNoteDb(CheckerUuid checkerUuid, CheckerUpdate checkerUpdate) |
| throws IOException, ConfigInvalidException, NoSuchCheckerException { |
| try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) { |
| CheckerConfig checkerConfig = |
| CheckerConfig.loadForChecker(allProjectsName, allProjectsRepo, checkerUuid); |
| checkerConfig.setCheckerUpdate(checkerUpdate); |
| if (!checkerConfig.getLoadedChecker().isPresent()) { |
| throw new NoSuchCheckerException(checkerUuid); |
| } |
| |
| CheckersByRepositoryNotes checkersByRepositoryNotes = |
| CheckersByRepositoryNotes.load(allProjectsName, allProjectsRepo); |
| |
| Checker checker = checkerConfig.getLoadedChecker().get(); |
| Project.NameKey oldRepositoryName = checker.getRepository(); |
| Project.NameKey newRepositoryName = checkerUpdate.getRepository().orElse(oldRepositoryName); |
| |
| CheckerStatus newStatus = checkerUpdate.getStatus().orElse(checker.getStatus()); |
| switch (newStatus) { |
| // May produce some redundant notes updates, but CheckersByRepositoryNotes knows how to |
| // short-circuit on no-ops, and the logic in this method is simple. |
| case DISABLED: |
| checkersByRepositoryNotes.remove(checkerUuid, oldRepositoryName); |
| checkersByRepositoryNotes.remove(checkerUuid, newRepositoryName); |
| break; |
| case ENABLED: |
| if (oldRepositoryName.equals(newRepositoryName)) { |
| checkersByRepositoryNotes.insert(checkerUuid, newRepositoryName); |
| } else { |
| checkersByRepositoryNotes.update(checkerUuid, oldRepositoryName, newRepositoryName); |
| } |
| break; |
| default: |
| throw new IllegalStateException("invalid checker status: " + newStatus); |
| } |
| |
| commit(allProjectsRepo, checkerConfig, checkersByRepositoryNotes); |
| |
| Checker updatedChecker = |
| checkerConfig |
| .getLoadedChecker() |
| .orElseThrow( |
| () -> new IllegalStateException("Updated checker wasn't automatically loaded")); |
| return updatedChecker; |
| } |
| } |
| } |