Add REST Interface to list and replenish limits
Implement a REST API that is the equivalent of the ssh "list" and
"replenish" commands detailed in the existing rate-limiter plugin
documentation. To mimic the functionality of the ssh command, keywords
are used in the endpoints and options are provided as parameters.
Feature: Issue 10304
Change-Id: I16753b26b994109b25134605f577fc688bb46060
diff --git a/BUILD b/BUILD
index 354abff..b3af8df 100644
--- a/BUILD
+++ b/BUILD
@@ -14,6 +14,7 @@
"Gerrit-PluginName: rate-limiter",
"Gerrit-Module: com.googlesource.gerrit.plugins.ratelimiter.Module",
"Gerrit-SshModule: com.googlesource.gerrit.plugins.ratelimiter.SshModule",
+ "Gerrit-HttpModule: com.googlesource.gerrit.plugins.ratelimiter.HttpModule",
],
resources = glob(["src/main/resources/**/*"]),
)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/HttpModule.java
new file mode 100644
index 0000000..c466541
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/HttpModule.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.ratelimiter;
+
+import com.google.inject.servlet.ServletModule;
+
+class HttpModule extends ServletModule {
+ @Override
+ protected void configureServlets() {
+ serve("/list", "/replenish").with(RateLimiterServlet.class);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ListCommand.java b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ListCommand.java
index 30442e9..1b2547f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ListCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ListCommand.java
@@ -16,19 +16,12 @@
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
import static com.googlesource.gerrit.plugins.ratelimiter.Module.UPLOAD_PACK_PER_HOUR;
-import com.google.common.cache.LoadingCache;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.sshd.AdminHighPriorityCommand;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import com.google.inject.name.Named;
-import java.time.Duration;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
@AdminHighPriorityCommand
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@@ -37,18 +30,15 @@
description = "Display rate limits statistics",
runsAt = MASTER_OR_SLAVE)
final class ListCommand extends SshCommand {
- private static final String FORMAT = "%-26s %-17s %-19s %-15s %s";
private static final String DASHED_LINE =
"---------------------------------------------------------------------------------------------";
- private final LoadingCache<String, RateLimiter> uploadPackPerHour;
- private final UserResolver userResolver;
+ private final RateLimiterProcessing rateLimiterProcessing;
+
+ static final String FORMAT = "%-26s %-17s %-19s %-15s %s";
@Inject
- ListCommand(
- @Named(UPLOAD_PACK_PER_HOUR) LoadingCache<String, RateLimiter> uploadPackPerHour,
- UserResolver userResolver) {
- this.uploadPackPerHour = uploadPackPerHour;
- this.userResolver = userResolver;
+ ListCommand(RateLimiterProcessing rateLimiterProcessing) {
+ this.rateLimiterProcessing = rateLimiterProcessing;
}
@Override
@@ -66,32 +56,10 @@
"Used Permits",
"Replenish in"));
stdout.println(DASHED_LINE);
- uploadPackPerHour.asMap().entrySet().stream()
- .sorted(Map.Entry.comparingByValue())
- .forEach(this::printEntry);
+ stdout.println(rateLimiterProcessing.listPermits());
stdout.println(DASHED_LINE);
} catch (Exception e) {
throw die(e);
}
}
-
- private void printEntry(Entry<String, RateLimiter> entry) {
- stdout.println(
- String.format(
- FORMAT,
- getDisplayValue(entry.getKey()),
- permits(entry.getValue().permitsPerHour()),
- permits(entry.getValue().availablePermits()),
- permits(entry.getValue().usedPermits()),
- Duration.ofSeconds(entry.getValue().remainingTime(TimeUnit.SECONDS))));
- }
-
- private String permits(int value) {
- return value == Integer.MAX_VALUE ? "unlimited" : Integer.toString(value);
- }
-
- private String getDisplayValue(String key) {
- Optional<String> currentUser = userResolver.getUserName(key);
- return currentUser.map(name -> key + " (" + name + ")").orElse(key);
- }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/RateLimiterProcessing.java b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/RateLimiterProcessing.java
new file mode 100644
index 0000000..c8c2f60
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/RateLimiterProcessing.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.ratelimiter;
+
+import static com.googlesource.gerrit.plugins.ratelimiter.ListCommand.FORMAT;
+import static com.googlesource.gerrit.plugins.ratelimiter.Module.UPLOAD_PACK_PER_HOUR;
+
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gson.*;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class RateLimiterProcessing {
+
+ private final LoadingCache<String, RateLimiter> uploadPackPerHour;
+ private final UserResolver userResolver;
+ private final AccountResolver accountResolver;
+
+ @Inject
+ public RateLimiterProcessing(
+ @Named(UPLOAD_PACK_PER_HOUR) LoadingCache<String, RateLimiter> uploadPackPerHour,
+ UserResolver userResolver,
+ AccountResolver accountResolver) {
+ this.uploadPackPerHour = uploadPackPerHour;
+ this.userResolver = userResolver;
+ this.accountResolver = accountResolver;
+ }
+
+ public String listPermits() {
+ return uploadPackPerHour.asMap().entrySet().stream()
+ .sorted(Map.Entry.comparingByValue())
+ .map(
+ entry ->
+ String.format(
+ FORMAT,
+ getDisplayValue(entry.getKey(), userResolver),
+ permits(entry.getValue().permitsPerHour()),
+ permits(entry.getValue().availablePermits()),
+ permits(entry.getValue().usedPermits()),
+ Duration.ofSeconds(entry.getValue().remainingTime(TimeUnit.SECONDS))))
+ .reduce("", String::concat);
+ }
+
+ public String listPermitsAsJson() {
+ List<String> permitList = new ArrayList<>();
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ uploadPackPerHour.asMap().entrySet().stream()
+ .sorted(Map.Entry.comparingByValue())
+ .map(this::getJsonObjectString)
+ .forEach(permitList::add);
+ return gson.toJson(JsonParser.parseString(permitList.toString()));
+ }
+
+ private String getJsonObjectString(Map.Entry<String, RateLimiter> entry) {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("AccountId", getDisplayValue(entry.getKey(), userResolver));
+ jsonObject.addProperty("permits_per_hour", permits(entry.getValue().permitsPerHour()));
+ jsonObject.addProperty("available_permits", permits(entry.getValue().availablePermits()));
+ jsonObject.addProperty("used_permit", permits(entry.getValue().usedPermits()));
+ jsonObject.addProperty(
+ "replenish_in",
+ Duration.ofSeconds(entry.getValue().remainingTime(TimeUnit.SECONDS)).toString());
+ return jsonObject.toString();
+ }
+
+ private String permits(int value) {
+ return value == Integer.MAX_VALUE ? "unlimited" : Integer.toString(value);
+ }
+
+ private String getDisplayValue(String key, UserResolver userResolver) {
+ Optional<String> currentUser = userResolver.getUserName(key);
+ return currentUser.map(name -> key + " (" + name + ")").orElse(key);
+ }
+
+ public void replenish(boolean all, List<Account.Id> accountIds, List<String> remoteHosts) {
+ if (all && (!accountIds.isEmpty() || !remoteHosts.isEmpty())) {
+ throw new IllegalArgumentException("cannot use --all with --user or --remotehost");
+ }
+ if (all) {
+ for (RateLimiter rateLimiter : uploadPackPerHour.asMap().values()) {
+ rateLimiter.replenishPermits();
+ }
+ return;
+ }
+ for (Account.Id accountId : accountIds) {
+ replenishIfPresent(Integer.toString(accountId.get()));
+ }
+ for (String remoteHost : remoteHosts) {
+ replenishIfPresent(remoteHost);
+ }
+ return;
+ }
+
+ List<Account.Id> convertToAccountId(String[] usernames)
+ throws ConfigInvalidException, IOException, ResourceNotFoundException {
+ List<Account.Id> accountIds = new ArrayList<>();
+ for (String user : usernames) {
+ AccountResolver.Result accountId = accountResolver.resolve(user);
+ if (accountId.asIdSet().isEmpty())
+ throw new ResourceNotFoundException(String.format("User %s not found", user));
+ accountIds.addAll(accountId.asIdSet());
+ }
+ return accountIds;
+ }
+
+ private void replenishIfPresent(String key) {
+ RateLimiter limiter = uploadPackPerHour.getIfPresent(key);
+ if (limiter != null) {
+ limiter.replenishPermits();
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/RateLimiterServlet.java b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/RateLimiterServlet.java
new file mode 100644
index 0000000..2de7975
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/RateLimiterServlet.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.ratelimiter;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.annotations.CapabilityScope;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.*;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@RequiresCapability(value = GlobalCapability.ADMINISTRATE_SERVER, scope = CapabilityScope.CORE)
+@Singleton
+public class RateLimiterServlet extends HttpServlet {
+ private final RateLimiterProcessing rateLimiterProcessing;
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ RateLimiterServlet(
+ RateLimiterProcessing rateLimiterProcessing, PermissionBackend permissionBackend) {
+ this.rateLimiterProcessing = rateLimiterProcessing;
+ this.permissionBackend = permissionBackend;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ try {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+ } catch (AuthException | PermissionBackendException e) {
+ setResponse(res, HttpServletResponse.SC_FORBIDDEN, "Authentication failed");
+ return;
+ }
+ if ("/list".equals(req.getPathInfo())) {
+ setResponse(res, HttpServletResponse.SC_OK, rateLimiterProcessing.listPermitsAsJson());
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ try {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+ } catch (AuthException | PermissionBackendException e) {
+ setResponse(res, HttpServletResponse.SC_FORBIDDEN, "Authentication failed");
+ return;
+ }
+ if ("/replenish".equals(req.getPathInfo())) {
+ try {
+ String[] replenishForUsers = new String[0];
+ List<String> replenishForHosts = Collections.EMPTY_LIST;
+ boolean replenishForAll = false;
+ if (req.getParameter("user") != null) {
+ replenishForUsers = req.getParameterValues("user");
+ }
+ if (req.getParameter("remotehost") != null) {
+ replenishForHosts = Arrays.asList(req.getParameterValues("host"));
+ }
+ if (req.getParameter("all") != null) {
+ replenishForAll = "true".equals(req.getParameter("all"));
+ }
+
+ List<Account.Id> accountIds = rateLimiterProcessing.convertToAccountId(replenishForUsers);
+ rateLimiterProcessing.replenish(replenishForAll, accountIds, replenishForHosts);
+ setResponse(res, HttpServletResponse.SC_NO_CONTENT, accountIds.toString());
+ } catch (ResourceNotFoundException | ConfigInvalidException | IllegalArgumentException e) {
+ setResponse(res, HttpServletResponse.SC_FORBIDDEN, "Fatal: " + e.getMessage());
+ }
+ }
+ }
+
+ private void setResponse(HttpServletResponse httpResponse, int statusCode, String value)
+ throws IOException {
+ httpResponse.setContentType("application/json");
+ httpResponse.setStatus(statusCode);
+ PrintWriter writer = httpResponse.getWriter();
+ writer.print(value);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ReplenishCommand.java b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ReplenishCommand.java
index a4646d5..6e589f6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ReplenishCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/ratelimiter/ReplenishCommand.java
@@ -50,36 +50,22 @@
private List<String> remoteHosts = new ArrayList<>();
private final LoadingCache<String, RateLimiter> uploadPackPerHour;
+ private final RateLimiterProcessing rateLimiterProcessing;
@Inject
ReplenishCommand(
- @Named(UPLOAD_PACK_PER_HOUR) LoadingCache<String, RateLimiter> uploadPackPerHour) {
+ @Named(UPLOAD_PACK_PER_HOUR) LoadingCache<String, RateLimiter> uploadPackPerHour,
+ RateLimiterProcessing rateLimiterProcessing) {
this.uploadPackPerHour = uploadPackPerHour;
+ this.rateLimiterProcessing = rateLimiterProcessing;
}
@Override
protected void run() throws UnloggedFailure {
- if (all && (!accountIds.isEmpty() || !remoteHosts.isEmpty())) {
- throw die("cannot use --all with --user or --remotehost");
- }
- if (all) {
- for (RateLimiter rateLimiter : uploadPackPerHour.asMap().values()) {
- rateLimiter.replenishPermits();
- }
- return;
- }
- for (Account.Id accountId : accountIds) {
- replenishIfPresent(Integer.toString(accountId.get()));
- }
- for (String remoteHost : remoteHosts) {
- replenishIfPresent(remoteHost);
- }
- }
-
- private void replenishIfPresent(String key) {
- RateLimiter limiter = uploadPackPerHour.getIfPresent(key);
- if (limiter != null) {
- limiter.replenishPermits();
+ try {
+ rateLimiterProcessing.replenish(all, accountIds, remoteHosts);
+ } catch (IllegalArgumentException e) {
+ throw die(e.getMessage());
}
}
}
diff --git a/src/main/resources/Documentation/rest-api-rate-limiter.md b/src/main/resources/Documentation/rest-api-rate-limiter.md
new file mode 100644
index 0000000..b3fbb0e
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-rate-limiter.md
@@ -0,0 +1,76 @@
+@PLUGIN@ - /replication/ REST API
+===================================
+This page describes the REST endpoint that is added by the @PLUGIN@
+plugin.
+Please also take note of the general information on the
+[REST API](../../../Documentation/rest-api.html).
+This API implements a REST equivalent of the Ssh rename-project command.
+For more information, refer to:
+* [Ssh list command](cmd-list.md)
+* [Ssh replenish command](cmd-replenish.md)
+------------------------------------------
+REQUEST
+-------
+```
+GET /plugins/rate-limiter/list HTTP/1.0
+```
+To get list of rate limit statistics.
+
+RESPONSE
+--------
+```
+[
+ {
+ "AccountId": "1000000 (admin)",
+ "permits_per_hour": "unlimited",
+ "available_permits": "unlimited",
+ "used_permit": "0",
+ "replenish_in": "PT0S"
+ },
+ {
+ "AccountId": "1000001 (testUser)",
+ "permits_per_hour": "unlimited",
+ "available_permits": "unlimited",
+ "used_permit": "0",
+ "replenish_in": "PT0S"
+ }
+]
+```
+
+REQUEST
+-------
+```
+POST /plugins/rate-limiter/repenish HTTP/1.0
+```
+
+Replenish permits for a given remotehost/user, or all. Uses parameters to specify
+which permit to replenish.
+
+To replenish all permits for user ```admin``` use:
+
+```
+POST /plugins/rate-limiter/replenish?user=ttyt&user=admin HTTP/1.0
+```
+
+To replenish all permits for remotehost ```127.0.0.0``` use:
+
+```
+POST /plugins/rate-limiter/replenish?user=ttyt&host=127.0.0 HTTP/1.0
+```
+
+To replenish all permits use:
+```
+POST /plugins/rate-limiter/replenish?user=ttyt&all=true HTTP/1.0
+```
+
+RESPONSE
+--------
+```
+HTTP/1.1 204 NO_CONTENT
+```
+
+ACCESS
+------
+Same as ssh version of the command caller must be a member of a group that is granted
+the 'Administrate Server' capability.
+