blob: 1b931bae5284f48adfe89722dda42287729c6c1a [file] [log] [blame]
// Copyright (C) 2020 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.codeowners.restapi;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.plugins.codeowners.api.CheckCodeOwnerConfigFilesInput;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigScanner;
import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.plugins.codeowners.validation.CodeOwnerConfigValidator;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.ListBranches;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* REST endpoint that checks/validates the code owner config files in a project.
*
* <p>This REST endpoint handles {@code POST
* /projects/<project-name>/branches/<branch-name>/code_owners.check_config} requests.
*
* <p>The implementation of this REST endpoint iterates over all code owner config files in the
* project. This means the expected performance of this REST endpoint is rather low and it should
* not be used in any critical path where performance matters.
*/
@Singleton
public class CheckCodeOwnerConfigFiles
implements RestModifyView<ProjectResource, CheckCodeOwnerConfigFilesInput> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Provider<CurrentUser> currentUser;
private final PermissionBackend permissionBackend;
private final Provider<ListBranches> listBranches;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
private final CodeOwnerConfigValidator codeOwnerConfigValidator;
@Inject
public CheckCodeOwnerConfigFiles(
Provider<CurrentUser> currentUser,
PermissionBackend permissionBackend,
Provider<ListBranches> listBranches,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory,
CodeOwnerConfigValidator codeOwnerConfigValidator) {
this.currentUser = currentUser;
this.permissionBackend = permissionBackend;
this.listBranches = listBranches;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory;
this.codeOwnerConfigValidator = codeOwnerConfigValidator;
}
@Override
public Response<Map<String, Map<String, List<ConsistencyProblemInfo>>>> apply(
ProjectResource projectResource, CheckCodeOwnerConfigFilesInput input)
throws RestApiException, PermissionBackendException, IOException {
if (!currentUser.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
// This REST endpoint requires the caller to be a project owner.
permissionBackend
.currentUser()
.project(projectResource.getNameKey())
.check(ProjectPermission.WRITE_CONFIG);
logger.atFine().log(
"checking code owner config files for project %s"
+ " (validateDisabledBranches = %s, branches = %s, path = %s, verbosity = %s)",
projectResource.getNameKey(),
input.validateDisabledBranches,
input.branches,
input.path,
input.verbosity);
ImmutableSet<BranchNameKey> branches = branches(projectResource);
validateInput(projectResource.getNameKey(), branches, input);
ImmutableMap.Builder<String, Map<String, List<ConsistencyProblemInfo>>> resultsByBranchBuilder =
ImmutableMap.builder();
branches.stream()
.filter(branchNameKey -> shouldValidateBranch(input, branchNameKey))
.filter(
branchNameKey ->
validateDisabledBranches(input)
|| !codeOwnersPluginConfiguration
.getProjectConfig(branchNameKey.project())
.isDisabled(branchNameKey.branch()))
.forEach(
branchNameKey ->
resultsByBranchBuilder.put(
branchNameKey.branch(),
checkBranch(input.path, branchNameKey, input.verbosity)));
return Response.ok(resultsByBranchBuilder.build());
}
private ImmutableSet<BranchNameKey> branches(ProjectResource projectResource)
throws RestApiException, IOException, PermissionBackendException {
return listBranches.get().apply(projectResource).value().stream()
.filter(branchInfo -> !"HEAD".equals(branchInfo.ref))
.map(branchInfo -> BranchNameKey.create(projectResource.getNameKey(), branchInfo.ref))
.collect(toImmutableSet());
}
private Map<String, List<ConsistencyProblemInfo>> checkBranch(
String pathGlob,
BranchNameKey branchNameKey,
@Nullable ConsistencyProblemInfo.Status verbosity) {
ListMultimap<String, ConsistencyProblemInfo> problemsByPath = LinkedListMultimap.create();
CodeOwnerBackend codeOwnerBackend =
codeOwnersPluginConfiguration
.getProjectConfig(branchNameKey.project())
.getBackend(branchNameKey.branch());
codeOwnerConfigScannerFactory
.create()
// Do not check the default code owner config file in refs/meta/config, as this config is
// stored in another branch. If it should be checked users must check the code owner config
// files in refs/meta/config explicitly.
.includeDefaultCodeOwnerConfig(false)
.visit(
branchNameKey,
codeOwnerConfig -> {
problemsByPath.putAll(
codeOwnerBackend.getFilePath(codeOwnerConfig.key()).toString(),
checkCodeOwnerConfig(
branchNameKey, codeOwnerBackend, codeOwnerConfig, verbosity));
return true;
},
(codeOwnerConfigFilePath, configInvalidException) -> {
problemsByPath.put(
codeOwnerConfigFilePath.toString(),
new ConsistencyProblemInfo(
ConsistencyProblemInfo.Status.FATAL, configInvalidException.getMessage()));
},
pathGlob);
return Multimaps.asMap(problemsByPath);
}
private ImmutableList<ConsistencyProblemInfo> checkCodeOwnerConfig(
BranchNameKey branchNameKey,
CodeOwnerBackend codeOwnerBackend,
CodeOwnerConfig codeOwnerConfig,
@Nullable ConsistencyProblemInfo.Status verbosity) {
return codeOwnerConfigValidator
.validateCodeOwnerConfig(
branchNameKey, currentUser.get().asIdentifiedUser(), codeOwnerBackend, codeOwnerConfig)
.map(
commitValidationMessage ->
createConsistencyProblemInfo(commitValidationMessage, verbosity))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableList());
}
public static Optional<ConsistencyProblemInfo> createConsistencyProblemInfo(
CommitValidationMessage commitValidationMessage,
@Nullable ConsistencyProblemInfo.Status verbosity) {
switch (commitValidationMessage.getType()) {
case FATAL:
return Optional.of(
new ConsistencyProblemInfo(
ConsistencyProblemInfo.Status.FATAL, commitValidationMessage.getMessage()));
case ERROR:
if (ConsistencyProblemInfo.Status.FATAL.equals(verbosity)) {
// errors should not be reported
return Optional.empty();
}
return Optional.of(
new ConsistencyProblemInfo(
ConsistencyProblemInfo.Status.ERROR, commitValidationMessage.getMessage()));
case WARNING:
if (ConsistencyProblemInfo.Status.FATAL.equals(verbosity)
|| ConsistencyProblemInfo.Status.ERROR.equals(verbosity)) {
// warnings should not be reported
return Optional.empty();
}
return Optional.of(
new ConsistencyProblemInfo(
ConsistencyProblemInfo.Status.WARNING, commitValidationMessage.getMessage()));
case HINT:
case OTHER:
return Optional.empty();
}
throw new IllegalStateException(
String.format(
"unknown message type %s for message %s",
commitValidationMessage.getType(), commitValidationMessage.getMessage()));
}
private void validateInput(
Project.NameKey projectName,
ImmutableSet<BranchNameKey> branches,
CheckCodeOwnerConfigFilesInput input)
throws RestApiException {
if (input.branches != null) {
for (String branchName : input.branches) {
BranchNameKey branchNameKey = BranchNameKey.create(projectName, branchName);
if (!branches.contains(branchNameKey)) {
throw new UnprocessableEntityException(String.format("branch %s not found", branchName));
}
if ((input.validateDisabledBranches == null || !input.validateDisabledBranches)
&& codeOwnersPluginConfiguration.getProjectConfig(projectName).isDisabled(branchName)) {
throw new BadRequestException(
String.format(
"code owners functionality for branch %s is disabled,"
+ " set 'validate_disabled_braches' in the input to 'true' if code owner"
+ " config files in this branch should be validated",
branchName));
}
}
}
}
private static boolean shouldValidateBranch(
CheckCodeOwnerConfigFilesInput input, BranchNameKey branchNameKey) {
boolean shouldValidateBranch =
input.branches == null
|| input.branches.contains(branchNameKey.branch())
|| input.branches.contains(branchNameKey.shortName());
if (!shouldValidateBranch) {
logger.atFine().log("skip validation for branch %s", branchNameKey.branch());
}
return shouldValidateBranch;
}
private static boolean validateDisabledBranches(CheckCodeOwnerConfigFilesInput input) {
return input.validateDisabledBranches != null && input.validateDisabledBranches;
}
}