Files
gerrit/java/com/google/gerrit/server/restapi/project/CreateProject.java
Edwin Kempin 5eb0219eb3 Set logging tag with plugin name on invocation of extension point
With this logging tag one can easily see in the log file which logs have
been triggered by the plugin. For traces this is interesting because
then for actions such as loading files, doing index queries etc. one can
see if they have been triggered by a plugin. E.g. if there is an
extensive number of index queries that are triggered by a plugin we want
to see this in the trace.

Such logging tags are mostly useful for extension points that are likely
to have complex or expensive implementations. We may not want to set
such a logging tag for extension points which only have trivial
implementations.

Setting the logging tag with the plugin name when an extension point is
invoked is done by opening a trace context. To invoke extension points
with such a trace context they should be invoked through a plugin
context. The plugin context can be directly injected. Instead of
injecting DynamicItem/DynamicSet/DynamicMap you can now inject
PluginItemContext/PluginSetContext/PluginMapContext.

The plugin context classes offer methods to invoke the extension
points that automatically set the trace context. In addition they
provide functionality for catching and logging exceptions from plugins.
This makes the logging and exception handling for plugin invocations
more consistent across the code base and removes the need to have code
for this in multiple places.

Since exception handling with plugin contexts is easy now more plugin
invocations handle exceptions now which makes Gerrit more resilient
against plugin failures.

This change adapts calls to most important extensions points, such as
validators and listeners.

Change-Id: Iab41d0059049f06ca41b697e93aa6a1f9668de5b
Signed-off-by: Edwin Kempin <ekempin@google.com>
2018-09-27 17:05:53 +02:00

423 lines
18 KiB
Java

// Copyright (C) 2013 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.server.restapi.project;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.ProjectUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
import com.google.gerrit.server.config.RepositoryConfig;
import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.RepositoryCaseMismatchException;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.plugincontext.PluginItemContext;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.CreateProjectArgs;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectJson;
import com.google.gerrit.server.project.ProjectNameLockManager;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceiveCommand;
@RequiresCapability(GlobalCapability.CREATE_PROJECT)
@Singleton
public class CreateProject
implements RestCollectionCreateView<TopLevelResource, ProjectResource, ProjectInput> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Provider<ProjectsCollection> projectsCollection;
private final Provider<GroupsCollection> groupsCollection;
private final PluginSetContext<ProjectCreationValidationListener>
projectCreationValidationListeners;
private final ProjectJson json;
private final GitRepositoryManager repoManager;
private final PluginSetContext<NewProjectCreatedListener> createdListeners;
private final ProjectCache projectCache;
private final GroupBackend groupBackend;
private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
private final MetaDataUpdate.User metaDataUpdateFactory;
private final GitReferenceUpdated referenceUpdated;
private final RepositoryConfig repositoryCfg;
private final PersonIdent serverIdent;
private final Provider<IdentifiedUser> identifiedUser;
private final Provider<PutConfig> putConfig;
private final AllProjectsName allProjects;
private final AllUsersName allUsers;
private final PluginItemContext<ProjectNameLockManager> lockManager;
@Inject
CreateProject(
Provider<ProjectsCollection> projectsCollection,
Provider<GroupsCollection> groupsCollection,
ProjectJson json,
PluginSetContext<ProjectCreationValidationListener> projectCreationValidationListeners,
GitRepositoryManager repoManager,
PluginSetContext<NewProjectCreatedListener> createdListeners,
ProjectCache projectCache,
GroupBackend groupBackend,
ProjectOwnerGroupsProvider.Factory projectOwnerGroups,
MetaDataUpdate.User metaDataUpdateFactory,
GitReferenceUpdated referenceUpdated,
RepositoryConfig repositoryCfg,
@GerritPersonIdent PersonIdent serverIdent,
Provider<IdentifiedUser> identifiedUser,
Provider<PutConfig> putConfig,
AllProjectsName allProjects,
AllUsersName allUsers,
PluginItemContext<ProjectNameLockManager> lockManager) {
this.projectsCollection = projectsCollection;
this.groupsCollection = groupsCollection;
this.projectCreationValidationListeners = projectCreationValidationListeners;
this.json = json;
this.repoManager = repoManager;
this.createdListeners = createdListeners;
this.projectCache = projectCache;
this.groupBackend = groupBackend;
this.projectOwnerGroups = projectOwnerGroups;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.referenceUpdated = referenceUpdated;
this.repositoryCfg = repositoryCfg;
this.serverIdent = serverIdent;
this.identifiedUser = identifiedUser;
this.putConfig = putConfig;
this.allProjects = allProjects;
this.allUsers = allUsers;
this.lockManager = lockManager;
}
@Override
public Response<ProjectInfo> apply(TopLevelResource resource, IdString id, ProjectInput input)
throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
String name = id.get();
if (input == null) {
input = new ProjectInput();
}
if (input.name != null && !name.equals(input.name)) {
throw new BadRequestException("name must match URL");
}
CreateProjectArgs args = new CreateProjectArgs();
args.setProjectName(ProjectUtil.stripGitSuffix(name));
String parentName =
MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
args.newParent = projectsCollection.get().parse(parentName, false).getNameKey();
if (args.newParent.equals(allUsers)) {
throw new ResourceConflictException(
String.format("Cannot inherit from '%s' project", allUsers.get()));
}
args.createEmptyCommit = input.createEmptyCommit;
args.permissionsOnly = input.permissionsOnly;
args.projectDescription = Strings.emptyToNull(input.description);
args.submitType = input.submitType;
args.branch = normalizeBranchNames(input.branches);
if (input.owners == null || input.owners.isEmpty()) {
args.ownerIds = new ArrayList<>(projectOwnerGroups.create(args.getProject()).get());
} else {
args.ownerIds = Lists.newArrayListWithCapacity(input.owners.size());
for (String owner : input.owners) {
args.ownerIds.add(groupsCollection.get().parse(owner).getGroupUUID());
}
}
args.contributorAgreements =
MoreObjects.firstNonNull(input.useContributorAgreements, InheritableBoolean.INHERIT);
args.signedOffBy = MoreObjects.firstNonNull(input.useSignedOffBy, InheritableBoolean.INHERIT);
args.contentMerge =
input.submitType == SubmitType.FAST_FORWARD_ONLY
? InheritableBoolean.FALSE
: MoreObjects.firstNonNull(input.useContentMerge, InheritableBoolean.INHERIT);
args.newChangeForAllNotInTarget =
MoreObjects.firstNonNull(
input.createNewChangeForAllNotInTarget, InheritableBoolean.INHERIT);
args.changeIdRequired =
MoreObjects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
args.rejectEmptyCommit =
MoreObjects.firstNonNull(input.rejectEmptyCommit, InheritableBoolean.INHERIT);
args.enableSignedPush =
MoreObjects.firstNonNull(input.enableSignedPush, InheritableBoolean.INHERIT);
args.requireSignedPush =
MoreObjects.firstNonNull(input.requireSignedPush, InheritableBoolean.INHERIT);
try {
args.maxObjectSizeLimit = ProjectConfig.validMaxObjectSizeLimit(input.maxObjectSizeLimit);
} catch (ConfigInvalidException e) {
throw new BadRequestException(e.getMessage());
}
Lock nameLock = lockManager.call(lockManager -> lockManager.getLock(args.getProject()));
nameLock.lock();
try {
try {
projectCreationValidationListeners.runEach(
l -> l.validateNewProject(args), ValidationException.class);
} catch (ValidationException e) {
throw new ResourceConflictException(e.getMessage(), e);
}
ProjectState projectState = createProject(args);
checkNotNull(projectState, "failed to create project " + args.getProject().get());
if (input.pluginConfigValues != null) {
ConfigInput in = new ConfigInput();
in.pluginConfigValues = input.pluginConfigValues;
putConfig.get().apply(projectState, in);
}
return Response.created(json.format(projectState));
} finally {
nameLock.unlock();
}
}
private ProjectState createProject(CreateProjectArgs args)
throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
final Project.NameKey nameKey = args.getProject();
try {
final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
try (Repository repo = repoManager.openRepository(nameKey)) {
if (repo.getObjectDatabase().exists()) {
throw new ResourceConflictException("project \"" + nameKey + "\" exists");
}
} catch (RepositoryNotFoundException e) {
// It does not exist, safe to ignore.
}
try (Repository repo = repoManager.createRepository(nameKey)) {
RefUpdate u = repo.updateRef(Constants.HEAD);
u.disableRefLog();
u.link(head);
createProjectConfig(args);
if (!args.permissionsOnly && args.createEmptyCommit) {
createEmptyCommits(repo, nameKey, args.branch);
}
fire(nameKey, head);
return projectCache.get(nameKey);
}
} catch (RepositoryCaseMismatchException e) {
throw new ResourceConflictException(
"Cannot create "
+ nameKey.get()
+ " because the name is already occupied by another project."
+ " The other project has the same name, only spelled in a"
+ " different case.");
} catch (RepositoryNotFoundException badName) {
throw new BadRequestException("invalid project name: " + nameKey);
} catch (ConfigInvalidException e) {
String msg = "Cannot create " + nameKey;
logger.atSevere().withCause(e).log(msg);
throw e;
}
}
private void createProjectConfig(CreateProjectArgs args)
throws IOException, ConfigInvalidException {
try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) {
ProjectConfig config = ProjectConfig.read(md);
Project newProject = config.getProject();
newProject.setDescription(args.projectDescription);
newProject.setSubmitType(
MoreObjects.firstNonNull(
args.submitType, repositoryCfg.getDefaultSubmitType(args.getProject())));
newProject.setBooleanConfig(
BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, args.contributorAgreements);
newProject.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, args.signedOffBy);
newProject.setBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE, args.contentMerge);
newProject.setBooleanConfig(
BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
args.newChangeForAllNotInTarget);
newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit);
newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
newProject.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, args.enableSignedPush);
newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_SIGNED_PUSH, args.requireSignedPush);
if (args.newParent != null) {
newProject.setParentName(args.newParent);
}
if (!args.ownerIds.isEmpty()) {
AccessSection all = config.getAccessSection(AccessSection.ALL, true);
for (AccountGroup.UUID ownerId : args.ownerIds) {
GroupDescription.Basic g = groupBackend.get(ownerId);
if (g != null) {
GroupReference group = config.resolve(GroupReference.forGroup(g));
all.getPermission(Permission.OWNER, true).add(new PermissionRule(group));
}
}
}
md.setMessage("Created project\n");
config.commit(md);
md.getRepository().setGitwebDescription(args.projectDescription);
}
projectCache.onCreateProject(args.getProject());
}
private List<String> normalizeBranchNames(List<String> branches) throws BadRequestException {
if (branches == null || branches.isEmpty()) {
return Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
}
List<String> normalizedBranches = new ArrayList<>();
for (String branch : branches) {
while (branch.startsWith("/")) {
branch = branch.substring(1);
}
branch = RefNames.fullName(branch);
if (!Repository.isValidRefName(branch)) {
throw new BadRequestException(String.format("Branch \"%s\" is not a valid name.", branch));
}
if (!normalizedBranches.contains(branch)) {
normalizedBranches.add(branch);
}
}
return normalizedBranches;
}
private void createEmptyCommits(Repository repo, Project.NameKey project, List<String> refs)
throws IOException {
try (ObjectInserter oi = repo.newObjectInserter()) {
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
cb.setCommitter(serverIdent);
cb.setMessage("Initial empty repository\n");
ObjectId id = oi.insert(cb);
oi.flush();
for (String ref : refs) {
RefUpdate ru = repo.updateRef(ref);
ru.setNewObjectId(id);
Result result = ru.update();
switch (result) {
case NEW:
referenceUpdated.fire(
project, ru, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
break;
case FAST_FORWARD:
case FORCED:
case IO_FAILURE:
case LOCK_FAILURE:
case NOT_ATTEMPTED:
case NO_CHANGE:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
{
throw new IOException(
String.format("Failed to create ref \"%s\": %s", ref, result.name()));
}
}
}
} catch (IOException e) {
logger.atSevere().withCause(e).log("Cannot create empty commit for %s", project.get());
throw e;
}
}
private void fire(Project.NameKey name, String head) {
if (createdListeners.isEmpty()) {
return;
}
Event event = new Event(name, head);
createdListeners.runEach(l -> l.onNewProjectCreated(event));
}
static class Event extends AbstractNoNotifyEvent implements NewProjectCreatedListener.Event {
private final Project.NameKey name;
private final String head;
Event(Project.NameKey name, String head) {
this.name = name;
this.head = head;
}
@Override
public String getProjectName() {
return name.get();
}
@Override
public String getHeadName() {
return head;
}
}
}