summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2023-02-07 15:54:38 +0100
committerGitHub <noreply@github.com>2023-02-07 15:54:38 +0100
commitc71bc7f0582f0c29476310dec2c1c12762bcbb8d (patch)
treec46df6a87e63a52d6f21a8a69e277cc71bc8a8a8
parent76bf11cdf1edc7eeb9291bc3e3c388fd568ad587 (diff)
parent3a3919963dcbbbe3d2637ca258f5de937a3b9aad (diff)
Merge pull request #25926 from vespa-engine/mpolden/lb-set-state
Support changing load balancer state through REST API
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/Path.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiHandler.java73
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiTest.java13
3 files changed, 74 insertions, 14 deletions
diff --git a/container-core/src/main/java/com/yahoo/restapi/Path.java b/container-core/src/main/java/com/yahoo/restapi/Path.java
index 01bcb627639..2fae8da0c2d 100644
--- a/container-core/src/main/java/com/yahoo/restapi/Path.java
+++ b/container-core/src/main/java/com/yahoo/restapi/Path.java
@@ -85,7 +85,7 @@ public class Path {
*
* Returns whether this path matches the given template string.
* If the given template has placeholders, their values (accessible by get) are reset by calling this,
- * whether or not the path matches the given template.
+ * whether the path matches the given template.
*
* This will NOT match empty path elements.
*
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiHandler.java
index 1ba686772c7..a23b1273fe3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiHandler.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiHandler.java
@@ -1,37 +1,86 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.restapi;
+import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.RestApi;
-import com.yahoo.restapi.RestApiRequestHandler;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.MessageResponse;
+import com.yahoo.restapi.Path;
import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.lb.LoadBalancer;
+import com.yahoo.vespa.hosted.provision.lb.LoadBalancerId;
+import com.yahoo.yolean.Exceptions;
import javax.inject.Inject;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.logging.Level;
/**
* @author mpolden
* @author jonmv
*/
-public class LoadBalancersV1ApiHandler extends RestApiRequestHandler<LoadBalancersV1ApiHandler> {
+public class LoadBalancersV1ApiHandler extends ThreadedHttpRequestHandler {
private final NodeRepository nodeRepository;
@Inject
public LoadBalancersV1ApiHandler(ThreadedHttpRequestHandler.Context parentCtx, NodeRepository nodeRepository) {
- super(parentCtx, LoadBalancersV1ApiHandler::createRestApiDefinition);
- this.nodeRepository = nodeRepository;
+ super(parentCtx);
+ this.nodeRepository = Objects.requireNonNull(nodeRepository);
}
- private static RestApi createRestApiDefinition(LoadBalancersV1ApiHandler self) {
- return RestApi.builder()
- .addRoute(RestApi.route("/loadbalancers/v1")
- .get(self::getLoadBalancers))
- .build();
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ try {
+ return switch (request.getMethod()) {
+ case GET -> get(request);
+ case PUT -> put(request);
+ default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
+ };
+ } catch (NotFoundException e) {
+ return ErrorResponse.notFoundError(Exceptions.toMessageString(e));
+ } catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
}
- private HttpResponse getLoadBalancers(RestApi.RequestContext context) {
- return new LoadBalancersResponse(context.request(), nodeRepository);
+ private HttpResponse get(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/loadbalancers/v1")) return new LoadBalancersResponse(request, nodeRepository);
+ throw new NotFoundException("Nothing at " + path);
+ }
+
+ private HttpResponse put(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/loadbalancers/v1/state/{state}/{id}")) return setState(path.get("state"), path.get("id"));
+ throw new NotFoundException("Nothing at " + path);
+ }
+
+ private HttpResponse setState(String state, String id) {
+ LoadBalancer.State toState = stateFrom(state);
+ LoadBalancerId loadBalancerId = LoadBalancerId.fromSerializedForm(id);
+ try (var lock = nodeRepository.database().lock(loadBalancerId.application(), Duration.ofSeconds(1))) {
+ LoadBalancer loadBalancer = nodeRepository.database().readLoadBalancer(loadBalancerId)
+ .orElseThrow(() -> new NotFoundException(loadBalancerId + " does not exist"));
+ nodeRepository.database().writeLoadBalancer(loadBalancer.with(toState, nodeRepository.clock().instant()),
+ loadBalancer.state());
+ }
+ return new MessageResponse("Moved " + loadBalancerId + " to " + toState);
+ }
+
+ private LoadBalancer.State stateFrom(String state) {
+ return switch (state) {
+ case "reserved" -> LoadBalancer.State.reserved;
+ case "inactive" -> LoadBalancer.State.inactive;
+ case "active" -> LoadBalancer.State.active;
+ case "removable" -> LoadBalancer.State.removable;
+ default -> throw new IllegalArgumentException("Invalid state '" + state + "'");
+ };
}
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiTest.java
index 3c20f6ddb09..240d0daf96f 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersV1ApiTest.java
@@ -7,6 +7,9 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+/**
+ * @author mpolden
+ */
public class LoadBalancersV1ApiTest {
private RestApiTester tester;
@@ -22,11 +25,19 @@ public class LoadBalancersV1ApiTest {
}
@Test
- public void test_load_balancers() throws Exception {
+ public void load_balancers() throws Exception {
tester.assertFile(new Request("http://localhost:8080/loadbalancers/v1"), "load-balancers.json");
tester.assertFile(new Request("http://localhost:8080/loadbalancers/v1/"), "load-balancers.json");
tester.assertFile(new Request("http://localhost:8080/loadbalancers/v1/?application=tenant4.application4.instance4"), "load-balancers-single.json");
tester.assertResponse(new Request("http://localhost:8080/loadbalancers/v1/?application=tenant.nonexistent.default"), "{\"loadBalancers\":[]}");
}
+ @Test
+ public void set_state() throws Exception {
+ tester.assertResponse(new Request("http://localhost:8080/loadbalancers/v1/state/removable/tenant42:application42:instance42:id42", "", Request.Method.PUT),
+ 404, "{\"error-code\":\"NOT_FOUND\",\"message\":\"load balancer tenant42:application42:instance42:id42 does not exist\"}");
+ tester.assertResponse(new Request("http://localhost:8080/loadbalancers/v1/state/removable/tenant4:application4:instance4:id4", "", Request.Method.PUT),
+ "{\"message\":\"Moved load balancer tenant4:application4:instance4:id4 to removable\"}");
+ }
+
}