diff options
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\"}"); + } + } |