Introduce repository size quota enforcer

This patch introduces "/repository:size" quota group that gets examined
when commits get pushed to the repository. It moves the responsibility of
quota check to Gerrit core so that implementation details don't have to
be exploited by quota plugin.
Pros:
* simplified implementation of quota plugin (only QuotaEnforcer needs
to be implemented)
* better compatibility in long term (quota plugin doesn't have to
recognise all the places when it is necessary to hook into in order
to deliver repository size quota functionality)

Change-Id: I481bec083be7f5b28bf69b9c8de119e74b32a095
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
This commit is contained in:
Jacek Centkowski
2019-05-16 17:10:26 +02:00
committed by David Pursehouse
parent adce023ba9
commit b8f7400d74
7 changed files with 300 additions and 11 deletions

View File

@@ -14,6 +14,10 @@
package com.google.gerrit.acceptance;
import static com.google.gerrit.server.git.receive.LazyPostReceiveHookChain.affectsSize;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.InProcessProtocol.Context;
import com.google.gerrit.common.data.Capable;
@@ -40,6 +44,9 @@ import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaException;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -263,6 +270,7 @@ class InProcessProtocol extends TestProtocol<Context> {
private final DynamicSet<PostReceiveHook> postReceiveHooks;
private final ThreadLocalRequestContext threadContext;
private final PermissionBackend permissionBackend;
private final QuotaBackend quotaBackend;
@Inject
Receive(
@@ -273,7 +281,8 @@ class InProcessProtocol extends TestProtocol<Context> {
PluginSetContext<ReceivePackInitializer> receivePackInitializers,
DynamicSet<PostReceiveHook> postReceiveHooks,
ThreadLocalRequestContext threadContext,
PermissionBackend permissionBackend) {
PermissionBackend permissionBackend,
QuotaBackend quotaBackend) {
this.userProvider = userProvider;
this.projectCache = projectCache;
this.factory = factory;
@@ -282,6 +291,7 @@ class InProcessProtocol extends TestProtocol<Context> {
this.postReceiveHooks = postReceiveHooks;
this.threadContext = threadContext;
this.permissionBackend = permissionBackend;
this.quotaBackend = quotaBackend;
}
@Override
@@ -321,10 +331,35 @@ class InProcessProtocol extends TestProtocol<Context> {
receivePackInitializers.runEach(
initializer -> initializer.init(projectState.getNameKey(), rp));
QuotaResponse.Aggregated availableTokens =
quotaBackend
.user(identifiedUser)
.project(req.project)
.availableTokens(REPOSITORY_SIZE_GROUP);
availableTokens.throwOnError();
availableTokens.availableTokens().ifPresent(v -> rp.setMaxObjectSizeLimit(v));
rp.setPostReceiveHook(PostReceiveHookChain.newChain(Lists.newArrayList(postReceiveHooks)));
ImmutableList<PostReceiveHook> hooks =
ImmutableList.<PostReceiveHook>builder()
.add(
(pack, commands) -> {
if (affectsSize(pack, commands)) {
try {
quotaBackend
.user(identifiedUser)
.project(req.project)
.requestTokens(REPOSITORY_SIZE_GROUP, pack.getPackSize())
.throwOnError();
} catch (QuotaException e) {
throw new RuntimeException(e);
}
}
})
.addAll(postReceiveHooks)
.build();
rp.setPostReceiveHook(PostReceiveHookChain.newChain(hooks));
return rp;
} catch (IOException | PermissionBackendException e) {
} catch (IOException | PermissionBackendException | QuotaException e) {
throw new RuntimeException(e);
}
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.git.receive;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import com.google.common.flogger.FluentLogger;
@@ -46,6 +47,9 @@ import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ContributorAgreementsChecker;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaException;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.inject.Inject;
@@ -96,6 +100,7 @@ public class AsyncReceiveCommits implements PreReceiveHook {
public static class Module extends PrivateModule {
@Override
public void configure() {
install(new FactoryModuleBuilder().build(LazyPostReceiveHookChain.Factory.class));
install(new FactoryModuleBuilder().build(AsyncReceiveCommits.Factory.class));
expose(AsyncReceiveCommits.Factory.class);
// Don't expose the binding for ReceiveCommits.Factory. All callers should
@@ -253,9 +258,10 @@ public class AsyncReceiveCommits implements PreReceiveHook {
RequestScopePropagator scopePropagator,
ReceiveConfig receiveConfig,
TransferConfig transferConfig,
Provider<LazyPostReceiveHookChain> lazyPostReceive,
LazyPostReceiveHookChain.Factory lazyPostReceive,
ContributorAgreementsChecker contributorAgreements,
Metrics metrics,
QuotaBackend quotaBackend,
@Named(TIMEOUT_NAME) long timeoutMillis,
@Assisted ProjectState projectState,
@Assisted IdentifiedUser user,
@@ -284,7 +290,7 @@ public class AsyncReceiveCommits implements PreReceiveHook {
receivePack.setRefFilter(new ReceiveRefFilter());
receivePack.setAllowPushOptions(true);
receivePack.setPreReceiveHook(this);
receivePack.setPostReceiveHook(lazyPostReceive.get());
receivePack.setPostReceiveHook(lazyPostReceive.create(user, projectName));
// If the user lacks READ permission, some references may be filtered and hidden from view.
// Check objects mentioned inside the incoming pack file are reachable from visible refs.
@@ -311,6 +317,17 @@ public class AsyncReceiveCommits implements PreReceiveHook {
factory.create(
projectState, user, receivePack, allRefsWatcher, messageSender, resultChangeIds);
receiveCommits.init();
QuotaResponse.Aggregated availableTokens =
quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
try {
availableTokens.throwOnError();
} catch (QuotaException e) {
logger.atWarning().withCause(e).log(
"Quota %s availableTokens request failed for project %s",
REPOSITORY_SIZE_GROUP, projectName);
throw new RuntimeException(e);
}
availableTokens.availableTokens().ifPresent(v -> receivePack.setMaxObjectSizeLimit(v));
}
/** Determine if the user can upload commits. */

View File

@@ -14,23 +14,78 @@
package com.google.gerrit.server.git.receive;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.util.Collection;
import org.eclipse.jgit.transport.PostReceiveHook;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
class LazyPostReceiveHookChain implements PostReceiveHook {
/**
* Class is responsible for calling all registered post-receive hooks. In addition, in case when
* repository size quota is defined, it requests tokens (pack size) that were received. This is the
* final step of enforcing repository size quota that deducts token from available tokens.
*/
public class LazyPostReceiveHookChain implements PostReceiveHook {
interface Factory {
LazyPostReceiveHookChain create(CurrentUser user, Project.NameKey project);
}
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final PluginSetContext<PostReceiveHook> hooks;
private final QuotaBackend quotaBackend;
private final CurrentUser user;
private final Project.NameKey project;
@Inject
LazyPostReceiveHookChain(PluginSetContext<PostReceiveHook> hooks) {
LazyPostReceiveHookChain(
PluginSetContext<PostReceiveHook> hooks,
QuotaBackend quotaBackend,
@Assisted CurrentUser user,
@Assisted Project.NameKey project) {
this.hooks = hooks;
this.quotaBackend = quotaBackend;
this.user = user;
this.project = project;
}
@Override
public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
hooks.runEach(h -> h.onPostReceive(rp, commands));
if (affectsSize(rp, commands)) {
QuotaResponse.Aggregated a =
quotaBackend
.user(user)
.project(project)
.requestTokens(REPOSITORY_SIZE_GROUP, rp.getPackSize());
if (a.hasError()) {
String msg =
String.format(
"%s request failed for project %s with [%s]",
REPOSITORY_SIZE_GROUP, project, a.errorMessage());
logger.atWarning().log(msg);
throw new RuntimeException(msg);
}
}
}
public static boolean affectsSize(ReceivePack rp, Collection<ReceiveCommand> commands) {
if (rp.getPackSize() > 0L) {
for (ReceiveCommand cmd : commands) {
if (cmd.getType() != ReceiveCommand.Type.DELETE) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,25 @@
// 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.server.quota;
public class QuotaGroupDefinitions {
/**
* Definition of repository size quota group. {@link QuotaEnforcer} implementations for repository
* size quota have to act on requests with this group name.
*/
public static final String REPOSITORY_SIZE_GROUP = "/repository:size";
private QuotaGroupDefinitions() {}
}