blob: 0e1d875734ceddcf78762af9f2966b9485ca7f5a [file] [log] [blame]
// 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.entities.Project;
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.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;
}
}
}