diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-04-12 20:49:31 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-04-12 20:57:24 +0200 |
commit | 97e6a063049a93d7a78301f29c27148f72dcef67 (patch) | |
tree | b515dcb09a5b6e1380e16ca4aaafe373f45a9d18 /orchestrator | |
parent | 8cf392f9573d4061bef71e426c4940426dd121a7 (diff) |
Convert remaining JAX-RS resources to request handlers
Diffstat (limited to 'orchestrator')
10 files changed, 693 insertions, 599 deletions
diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandler.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandler.java new file mode 100644 index 00000000000..4fecbefaffd --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandler.java @@ -0,0 +1,158 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.resources; + +import com.google.inject.Inject; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.container.jdisc.EmptyResponse; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.jdisc.http.HttpResponse.Status; +import com.yahoo.restapi.RestApi; +import com.yahoo.restapi.RestApiException; +import com.yahoo.restapi.RestApiRequestHandler; +import com.yahoo.vespa.orchestrator.ApplicationIdNotFoundException; +import com.yahoo.vespa.orchestrator.ApplicationStateChangeDeniedException; +import com.yahoo.vespa.orchestrator.Orchestrator; +import com.yahoo.vespa.orchestrator.status.ApplicationInstanceStatus; + +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * @author smorgrav + * @author bjorncs + */ +public class ApplicationSuspensionRequestHandler extends RestApiRequestHandler<ApplicationSuspensionRequestHandler> { + + private static final Logger log = Logger.getLogger(ApplicationSuspensionRequestHandler.class.getName()); + + private final Orchestrator orchestrator; + + @Inject + public ApplicationSuspensionRequestHandler(LoggingRequestHandler.Context context, Orchestrator orchestrator) { + super(context, ApplicationSuspensionRequestHandler::createRestApiDefinition); + this.orchestrator = orchestrator; + } + + private static RestApi createRestApiDefinition(ApplicationSuspensionRequestHandler self) { + return RestApi.builder() + .addRoute(RestApi.route("/orchestrator/v1/suspensions/applications") + .get(self::getApplications) + .post(String.class, self::suspend)) + .addRoute(RestApi.route("/orchestrator/v1/suspensions/applications/{application}") + .get(self::getApplication) + .delete(self::resume)) + .registerJacksonResponseEntity(Set.class) + .build(); + } + + /** + * Lists all applications that is currently suspended. + * + * HTTP Behavior: + * Always 200 + * + * @return A list of application ids of suspended applications + */ + private Set<String> getApplications(RestApi.RequestContext context) { + Set<ApplicationId> refs = orchestrator.getAllSuspendedApplications(); + return refs.stream().map(ApplicationId::serializedForm).collect(Collectors.toSet()); + } + + /** + * Shows the Orchestrator status for an application instance + * + * HTTP Behavior: + * 204 if the application is suspended + * 400 if the applicationId is invalid + * 404 if the application is not suspended + */ + private HttpResponse getApplication(RestApi.RequestContext context) { + String applicationIdString = context.pathParameters().getStringOrThrow("application"); + ApplicationId appId = toApplicationId(applicationIdString); + ApplicationInstanceStatus status; + + try { + status = orchestrator.getApplicationInstanceStatus(appId); + } catch (ApplicationIdNotFoundException e) { + throw new RestApiException.NotFoundException("Application " + applicationIdString + " could not be found", e); + } + + if (status.equals(ApplicationInstanceStatus.NO_REMARKS)) { + throw new RestApiException.NotFoundException("Application " + applicationIdString + " is not suspended"); + } + return new EmptyResponse(Status.NO_CONTENT); + } + + /** + * Ask for permission to temporarily suspend all services for an application instance. + * + * On success all content nodes for this application instance have been set in maintenance mode. + * + * Once the application is ready to resume normal operations, it must finish with resume() (see below). + * + * If the application has already been granted permission to suspend all services, requesting + * suspension again is idempotent and will succeed. + * + * HTTP Behavior: + * 204 is the suspend operation was successful + * 400 if the applicationId is invalid + * 409 if the suspend was denied + */ + private HttpResponse suspend(RestApi.RequestContext context, String applicationIdString) { + ApplicationId applicationId = toApplicationId(applicationIdString); + try { + orchestrator.suspend(applicationId); + } catch (ApplicationIdNotFoundException e) { + log.log(Level.INFO, "ApplicationId " + applicationIdString + " not found.", e); + throw new RestApiException.NotFoundException(e); + } catch (ApplicationStateChangeDeniedException e) { + log.log(Level.INFO, "Suspend for " + applicationIdString + " failed.", e); + throw new RestApiException.Conflict(); + } catch (RuntimeException e) { + log.log(Level.INFO, "Suspend for " + applicationIdString + " failed from unknown reasons", e); + throw new RestApiException.InternalServerError(e); + } + return new EmptyResponse(Status.NO_CONTENT); + } + + /** + * Resume normal operations for all services for an application + * instance that has previously been allowed suspension. + * + * If the host is already registered as running normal operations, then resume() is idempotent + * and will succeed. + * + * HTTP Behavior: + * Returns 204 is the resume operation was successful (or the application was not suspended) + * Returns 400 if the applicationId is invalid + */ + private HttpResponse resume(RestApi.RequestContext context) { + String applicationIdString = context.pathParameters().getStringOrThrow("application"); + ApplicationId applicationId = toApplicationId(applicationIdString); + try { + orchestrator.resume(applicationId); + } catch (ApplicationIdNotFoundException e) { + log.log(Level.INFO, "ApplicationId " + applicationIdString + " not found.", e); + throw new RestApiException.NotFoundException(e); + } catch (ApplicationStateChangeDeniedException e) { + log.log(Level.INFO, "Suspend for " + applicationIdString + " failed.", e); + throw new RestApiException.Conflict(); + } catch (RuntimeException e) { + log.log(Level.INFO, "Suspend for " + applicationIdString + " failed from unknown reasons", e); + throw new RestApiException.InternalServerError(e); + } + return new EmptyResponse(Status.NO_CONTENT); + } + + private ApplicationId toApplicationId(String applicationIdString) { + try { + return ApplicationId.fromSerializedForm(applicationIdString); + } catch (IllegalArgumentException e) { + throw new RestApiException.BadRequest(e); + } + } + +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandler.java index c55eeeef069..4dcd76cbb10 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandler.java @@ -1,8 +1,14 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator.resources.host; +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.resources; import com.google.common.util.concurrent.UncheckedTimeoutException; -import com.yahoo.container.jaxrs.annotation.Component; +import com.google.inject.Inject; +import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.jdisc.Response; +import com.yahoo.restapi.JacksonJsonResponse; +import com.yahoo.restapi.RestApi; +import com.yahoo.restapi.RestApiException; +import com.yahoo.restapi.RestApiRequestHandler; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.orchestrator.Host; import com.yahoo.vespa.orchestrator.HostNameNotFoundException; @@ -10,8 +16,6 @@ import com.yahoo.vespa.orchestrator.OrchestrationException; import com.yahoo.vespa.orchestrator.Orchestrator; import com.yahoo.vespa.orchestrator.policy.HostStateChangeDeniedException; import com.yahoo.vespa.orchestrator.policy.HostedVespaPolicy; -import com.yahoo.vespa.orchestrator.resources.instance.InstanceResource; -import com.yahoo.vespa.orchestrator.restapi.HostApi; import com.yahoo.vespa.orchestrator.restapi.wire.GetHostResponse; import com.yahoo.vespa.orchestrator.restapi.wire.HostService; import com.yahoo.vespa.orchestrator.restapi.wire.HostStateChangeDenialReason; @@ -20,16 +24,6 @@ import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostResponse; import com.yahoo.vespa.orchestrator.restapi.wire.UpdateHostResponse; import com.yahoo.vespa.orchestrator.status.HostStatus; -import javax.inject.Inject; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.Path; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.time.Instant; import java.util.List; @@ -39,30 +33,47 @@ import java.util.stream.Collectors; /** * @author oyving + * @author bjorncs */ -@Path("") -public class HostResource implements HostApi { - private static final Logger log = Logger.getLogger(HostResource.class.getName()); +public class HostRequestHandler extends RestApiRequestHandler<HostRequestHandler> { + + private static final Logger log = Logger.getLogger(HostRequestHandler.class.getName()); private final Orchestrator orchestrator; - private final UriInfo uriInfo; @Inject - public HostResource(@Component Orchestrator orchestrator, @Context UriInfo uriInfo) { + public HostRequestHandler(LoggingRequestHandler.Context context, Orchestrator orchestrator) { + super(context, HostRequestHandler::createRestApiDefinition); this.orchestrator = orchestrator; - this.uriInfo = uriInfo; } - @Override - public GetHostResponse getHost(String hostNameString) { + private static RestApi createRestApiDefinition(HostRequestHandler self) { + return RestApi.builder() + .addRoute(RestApi.route("/orchestrator/v1/hosts/{hostname}") + .get(self::getHost) + .patch(PatchHostRequest.class, self::patch)) + .addRoute(RestApi.route("/orchestrator/v1/hosts/{hostname}/suspended") + .put(self::suspend) + .delete(self::resume)) + .registerJacksonRequestEntity(PatchHostRequest.class) + .registerJacksonResponseEntity(GetHostResponse.class) + .registerJacksonResponseEntity(PatchHostResponse.class) + .registerJacksonResponseEntity(UpdateHostResponse.class) + .build(); + } + + /** + * Shows the Orchestrator state of a host. + */ + private GetHostResponse getHost(RestApi.RequestContext context) { + String hostNameString = context.pathParameters().getStringOrThrow("hostname"); HostName hostName = new HostName(hostNameString); try { Host host = orchestrator.getHost(hostName); - URI applicationUri = uriInfo.getBaseUriBuilder() - .path(InstanceResource.class) - .path(host.getApplicationInstanceReference().asString()) - .build(); + URI applicationUri = context.uriBuilder() + .withPath("/orchestrator/v1/instances/" + host.getApplicationInstanceReference().asString()) + .toURI(); List<HostService> hostServices = host.getServiceInstances().stream() .map(serviceInstance -> new HostService( @@ -80,15 +91,18 @@ public class HostResource implements HostApi { hostServices); } catch (UncheckedTimeoutException e) { log.log(Level.FINE, "Failed to get host " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("getHost", hostName, e); + throw restApiExceptionFromTimeout("getHost", hostName, e); } catch (HostNameNotFoundException e) { log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); + throw new RestApiException.NotFoundException(e); } } - @Override - public PatchHostResponse patch(String hostNameString, PatchHostRequest request) { + /** + * Tweak internal Orchestrator state for host. + */ + private PatchHostResponse patch(RestApi.RequestContext context, PatchHostRequest request) { + String hostNameString = context.pathParameters().getStringOrThrow("hostname"); HostName hostName = new HostName(hostNameString); if (request.state != null) { @@ -96,21 +110,21 @@ public class HostResource implements HostApi { try { state = HostStatus.valueOf(request.state); } catch (IllegalArgumentException dummy) { - throw new BadRequestException("Bad state in request: '" + request.state + "'"); + throw new RestApiException.BadRequest("Bad state in request: '" + request.state + "'"); } try { orchestrator.setNodeStatus(hostName, state); } catch (HostNameNotFoundException e) { log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); + throw new RestApiException.NotFoundException(e); } catch (UncheckedTimeoutException e) { log.log(Level.FINE, "Failed to patch " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("patch", hostName, e); + throw restApiExceptionFromTimeout("patch", hostName, e); } catch (OrchestrationException e) { String message = "Failed to set " + hostName + " to " + state + ": " + e.getMessage(); log.log(Level.FINE, message, e); - throw new InternalServerErrorException(message); + throw new RestApiException.InternalServerError(message); } } @@ -119,74 +133,82 @@ public class HostResource implements HostApi { return response; } - @Override - public UpdateHostResponse suspend(String hostNameString) { + /** + * Ask for permission to temporarily suspend all services on a host. + * + * On success, none, some, or all services on the host may already have been effectively suspended, + * e.g. as of Feb 2015, a content node would already be set in the maintenance state. + * + * Once the host is ready to resume normal operations, it must finish with resume() (see below). + * + * If the host has already been granted permission to suspend all services, requesting + * suspension again is idempotent and will succeed. + */ + private UpdateHostResponse suspend(RestApi.RequestContext context) { + String hostNameString = context.pathParameters().getStringOrThrow("hostname"); HostName hostName = new HostName(hostNameString); try { orchestrator.suspend(hostName); } catch (HostNameNotFoundException e) { log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); + throw new RestApiException.NotFoundException(e); } catch (UncheckedTimeoutException e) { log.log(Level.FINE, "Failed to suspend " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("suspend", hostName, e); + throw restApiExceptionFromTimeout("suspend", hostName, e); } catch (HostStateChangeDeniedException e) { log.log(Level.FINE, "Failed to suspend " + hostName + ": " + e.getMessage()); - throw webExceptionWithDenialReason("suspend", hostName, e); + throw restApiExceptionWithDenialReason("suspend", hostName, e); } return new UpdateHostResponse(hostName.s(), null); } - - @Override - public UpdateHostResponse resume(final String hostNameString) { + /** + * Resume normal operations for all services on a host that has previously been allowed suspension. + * + * If the host is already registered as running normal operations, then resume() is idempotent + * and will succeed. + */ + private UpdateHostResponse resume(RestApi.RequestContext context) { + String hostNameString = context.pathParameters().getStringOrThrow("hostname"); HostName hostName = new HostName(hostNameString); try { orchestrator.resume(hostName); } catch (HostNameNotFoundException e) { log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); + throw new RestApiException.NotFoundException(e); } catch (UncheckedTimeoutException e) { log.log(Level.FINE, "Failed to resume " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("resume", hostName, e); + throw restApiExceptionFromTimeout("resume", hostName, e); } catch (HostStateChangeDeniedException e) { log.log(Level.FINE, "Failed to resume " + hostName + ": " + e.getMessage()); - throw webExceptionWithDenialReason("resume", hostName, e); + throw restApiExceptionWithDenialReason("resume", hostName, e); } return new UpdateHostResponse(hostName.s(), null); } - private static WebApplicationException webExceptionFromTimeout(String operationDescription, - HostName hostName, - UncheckedTimeoutException e) { + private RestApiException restApiExceptionFromTimeout(String operationDescription, + HostName hostName, + UncheckedTimeoutException e) { // Return timeouts as 409 Conflict instead of 504 Gateway Timeout to reduce noise in 5xx graphs. - return createWebException(operationDescription, hostName, e, + return createRestApiException(operationDescription, hostName, e, HostedVespaPolicy.DEADLINE_CONSTRAINT, e.getMessage(), Response.Status.CONFLICT); } - private static WebApplicationException webExceptionWithDenialReason( + private RestApiException restApiExceptionWithDenialReason( String operationDescription, HostName hostName, HostStateChangeDeniedException e) { - return createWebException(operationDescription, hostName, e, e.getConstraintName(), e.getMessage(), + return createRestApiException(operationDescription, hostName, e, e.getConstraintName(), e.getMessage(), Response.Status.CONFLICT); } - private static WebApplicationException createWebException(String operationDescription, - HostName hostname, - Exception e, - String constraint, - String message, - Response.Status status) { + private RestApiException createRestApiException( + String operationDescription, HostName hostname, Exception e, String constraint, String message, int status) { HostStateChangeDenialReason hostStateChangeDenialReason = new HostStateChangeDenialReason( constraint, operationDescription + " failed: " + message); UpdateHostResponse response = new UpdateHostResponse(hostname.s(), hostStateChangeDenialReason); - return new WebApplicationException( + return new RestApiException( + new JacksonJsonResponse<>(status, response, restApi().jacksonJsonMapper(), true), hostStateChangeDenialReason.toString(), - e, - Response.status(status) - .entity(response) - .type(MediaType.APPLICATION_JSON_TYPE) - .build()); + e); } } - diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandler.java index 742f7d6bbd7..5f0c7caf931 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResource.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandler.java @@ -1,9 +1,17 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator.resources.instance; - +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.inject.Inject; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.restapi.RestApi; +import com.yahoo.restapi.RestApiException; +import com.yahoo.restapi.RestApiRequestHandler; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; import com.yahoo.vespa.applicationmodel.ClusterId; @@ -12,7 +20,6 @@ import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.applicationmodel.ServiceStatusInfo; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.orchestrator.OrchestratorUtil; -import com.yahoo.vespa.orchestrator.resources.InstanceStatusResponse; import com.yahoo.vespa.orchestrator.restapi.wire.SlobrokEntryResponse; import com.yahoo.vespa.orchestrator.restapi.wire.WireHostInfo; import com.yahoo.vespa.orchestrator.status.HostInfo; @@ -23,14 +30,7 @@ import com.yahoo.vespa.service.manager.UnionMonitorManager; import com.yahoo.vespa.service.monitor.ServiceMonitor; import com.yahoo.vespa.service.monitor.SlobrokApi; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.time.Instant; import java.util.List; @@ -45,10 +45,10 @@ import static com.yahoo.vespa.orchestrator.OrchestratorUtil.parseApplicationInst * This API can be unstable and is not meant to be used programmatically. * * @author andreer - * @author bakksjo + * @author Oyvind Bakksjo + * @author bjorncs */ -@Path("") -public class InstanceResource { +public class InstanceRequestHandler extends RestApiRequestHandler<InstanceRequestHandler> { public static final String DEFAULT_SLOBROK_PATTERN = "**"; @@ -58,26 +58,45 @@ public class InstanceResource { private final ServiceMonitor serviceMonitor; @Inject - public InstanceResource(@Component ServiceMonitor serviceMonitor, - @Component StatusService statusService, - @Component SlobrokApi slobrokApi, - @Component UnionMonitorManager rootManager) { - this.serviceMonitor = serviceMonitor; + public InstanceRequestHandler(LoggingRequestHandler.Context context, + ServiceMonitor serviceMonitor, + StatusService statusService, + SlobrokApi slobrokApi, + UnionMonitorManager rootManager) { + super(context, InstanceRequestHandler::createRestApiDefinition); this.statusService = statusService; this.slobrokApi = slobrokApi; this.rootManager = rootManager; + this.serviceMonitor = serviceMonitor; } - @GET - @Produces(MediaType.APPLICATION_JSON) - public List<ApplicationInstanceReference> getAllInstances() { + private static RestApi createRestApiDefinition(InstanceRequestHandler self) { + return RestApi.builder() + .addRoute(RestApi.route("/orchestrator/v1/instances") + .get(self::getAllInstances)) + .addRoute(RestApi.route("/orchestrator/v1/instances/{instanceId}") + .get(self::getInstance)) + .addRoute(RestApi.route("/orchestrator/v1/instances/{instanceId}/slobrok") + .get(self::getSlobrokEntries)) + .addRoute(RestApi.route("/orchestrator/v1/instances/{instanceId}/serviceStatusInfo") + .get(self::getServiceStatus)) + .registerJacksonResponseEntity(List.class) + .registerJacksonResponseEntity(InstanceStatusResponse.class) + .registerJacksonResponseEntity(ServiceStatusInfo.class) + // Overriding object mapper to change serialization of timestamps + .setObjectMapper(new ObjectMapper() + .registerModule(new JavaTimeModule()) + .registerModule(new Jdk8Module()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)) + .build(); + } + + private List<ApplicationInstanceReference> getAllInstances(RestApi.RequestContext context) { return serviceMonitor.getAllApplicationInstanceReferences().stream().sorted().collect(Collectors.toList()); } - @GET - @Path("/{instanceId}") - @Produces(MediaType.APPLICATION_JSON) - public InstanceStatusResponse getInstance(@PathParam("instanceId") String instanceIdString) { + private InstanceStatusResponse getInstance(RestApi.RequestContext context) { + String instanceIdString = context.pathParameters().getStringOrThrow("instanceId"); ApplicationInstanceReference instanceId = parseInstanceId(instanceIdString); ApplicationInstance applicationInstance @@ -102,12 +121,9 @@ public class InstanceResource { return new WireHostInfo(hostStatusString, suspendedSinceUtcOrNull); } - @GET - @Path("/{instanceId}/slobrok") - @Produces(MediaType.APPLICATION_JSON) - public List<SlobrokEntryResponse> getSlobrokEntries( - @PathParam("instanceId") String instanceId, - @QueryParam("pattern") String pattern) { + private List<SlobrokEntryResponse> getSlobrokEntries(RestApi.RequestContext context) { + String instanceId = context.pathParameters().getStringOrThrow("instanceId"); + String pattern = context.queryParameters().getString("pattern").orElse(null); ApplicationInstanceReference reference = parseInstanceId(instanceId); ApplicationId applicationId = OrchestratorUtil.toApplicationId(reference); @@ -121,29 +137,14 @@ public class InstanceResource { .collect(Collectors.toList()); } - @GET - @Path("/{instanceId}/serviceStatusInfo") - @Produces(MediaType.APPLICATION_JSON) - public ServiceStatusInfo getServiceStatus( - @PathParam("instanceId") String instanceId, - @QueryParam("clusterId") String clusterIdString, - @QueryParam("serviceType") String serviceTypeString, - @QueryParam("configId") String configIdString) { + private ServiceStatusInfo getServiceStatus(RestApi.RequestContext context) { + String instanceId = context.pathParameters().getStringOrThrow("instanceId"); + String clusterIdString = context.queryParameters().getStringOrThrow("clusterId"); + String serviceTypeString = context.queryParameters().getStringOrThrow("serviceType"); + String configIdString = context.queryParameters().getStringOrThrow("configId"); ApplicationInstanceReference reference = parseInstanceId(instanceId); ApplicationId applicationId = OrchestratorUtil.toApplicationId(reference); - if (clusterIdString == null) { - throwBadRequest("Missing clusterId query parameter"); - } - - if (serviceTypeString == null) { - throwBadRequest("Missing serviceType query parameter"); - } - - if (configIdString == null) { - throwBadRequest("Missing configId query parameter"); - } - ClusterId clusterId = new ClusterId(clusterIdString); ServiceType serviceType = new ServiceType(serviceTypeString); ConfigId configId = new ConfigId(configIdString); @@ -151,18 +152,12 @@ public class InstanceResource { return rootManager.getStatus(applicationId, clusterId, serviceType, configId); } - static ApplicationInstanceReference parseInstanceId(String instanceIdString) { + private static ApplicationInstanceReference parseInstanceId(String instanceIdString) { try { return parseApplicationInstanceReference(instanceIdString); } catch (IllegalArgumentException e) { - throwBadRequest(e.getMessage()); - return null; // Necessary for compiler + throw new RestApiException.BadRequest(e.getMessage(), e); } } - static void throwBadRequest(String message) { - throw new WebApplicationException( - Response.status(Response.Status.BAD_REQUEST).entity(message).build()); - } - } diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java deleted file mode 100644 index 361b1f5e361..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator.resources.appsuspension; - -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.container.jaxrs.annotation.Component; -import com.yahoo.vespa.orchestrator.ApplicationIdNotFoundException; -import com.yahoo.vespa.orchestrator.ApplicationStateChangeDeniedException; -import com.yahoo.vespa.orchestrator.OrchestratorImpl; -import com.yahoo.vespa.orchestrator.restapi.ApplicationSuspensionApi; -import com.yahoo.vespa.orchestrator.status.ApplicationInstanceStatus; - -import javax.inject.Inject; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.Path; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * @author smorgrav - */ -@Path("") -public class ApplicationSuspensionResource implements ApplicationSuspensionApi { - - private static final Logger log = Logger.getLogger(ApplicationSuspensionResource.class.getName()); - - private final OrchestratorImpl orchestrator; - - @Inject - public ApplicationSuspensionResource(@Component OrchestratorImpl orchestrator) { - this.orchestrator = orchestrator; - } - - @Override - public Set<String> getApplications() { - Set<ApplicationId> refs = orchestrator.getAllSuspendedApplications(); - return refs.stream().map(ApplicationId::serializedForm).collect(Collectors.toSet()); - } - - @Override - public void getApplication(String applicationIdString) { - ApplicationId appId = toApplicationId(applicationIdString); - ApplicationInstanceStatus status; - - try { - status = orchestrator.getApplicationInstanceStatus(appId); - } catch (ApplicationIdNotFoundException e) { - throw new NotFoundException("Application " + applicationIdString + " could not be found"); - } - - if (status.equals(ApplicationInstanceStatus.NO_REMARKS)) { - throw new NotFoundException("Application " + applicationIdString + " is not suspended"); - } - - // Return void as we have nothing to return except 204 No - // Content. Unfortunately, Jersey outputs a warning for this case: - // - // The following warnings have been detected: HINT: A HTTP GET - // method, public void com.yahoo.vespa.orchestrator.resources. - // ApplicationSuspensionResource.getApplication(java.lang.String), - // returns a void type. It can be intentional and perfectly fine, - // but it is a little uncommon that GET method returns always "204 - // No Content" - // - // We have whitelisted the warning for our systemtests. - // - // bakksjo has a pending jersey PR fix that avoids making the hint - // become a warning: - // https://github.com/jersey/jersey/pull/212 - // - // TODO: Remove whitelisting and this comment once jersey has been - // fixed. - } - - @Override - public void suspend(String applicationIdString) { - ApplicationId applicationId = toApplicationId(applicationIdString); - try { - orchestrator.suspend(applicationId); - } catch (ApplicationIdNotFoundException e) { - log.log(Level.INFO, "ApplicationId " + applicationIdString + " not found.", e); - throw new NotFoundException(e); - } catch (ApplicationStateChangeDeniedException e) { - log.log(Level.INFO, "Suspend for " + applicationIdString + " failed.", e); - throw new WebApplicationException(Response.Status.CONFLICT); - } catch (RuntimeException e) { - log.log(Level.INFO, "Suspend for " + applicationIdString + " failed from unknown reasons", e); - throw new InternalServerErrorException(e); - } - } - - @Override - public void resume(String applicationIdString) { - ApplicationId applicationId = toApplicationId(applicationIdString); - try { - orchestrator.resume(applicationId); - } catch (ApplicationIdNotFoundException e) { - log.log(Level.INFO, "ApplicationId " + applicationIdString + " not found.", e); - throw new NotFoundException(e); - } catch (ApplicationStateChangeDeniedException e) { - log.log(Level.INFO, "Suspend for " + applicationIdString + " failed.", e); - throw new WebApplicationException(Response.Status.CONFLICT); - } catch (RuntimeException e) { - log.log(Level.INFO, "Suspend for " + applicationIdString + " failed from unknown reasons", e); - throw new InternalServerErrorException(e); - } - } - - private ApplicationId toApplicationId(String applicationIdString) { - try { - return ApplicationId.fromSerializedForm(applicationIdString); - } catch (IllegalArgumentException e) { - throw new BadRequestException(e); - } - } - -} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandlerTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandlerTest.java new file mode 100644 index 00000000000..176a95d1c04 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandlerTest.java @@ -0,0 +1,157 @@ +package com.yahoo.vespa.orchestrator.resources;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +import com.fasterxml.jackson.core.type.TypeReference; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.jdisc.core.SystemTimer; +import com.yahoo.jdisc.test.MockMetric; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.flags.InMemoryFlagSource; +import com.yahoo.vespa.orchestrator.DummyServiceMonitor; +import com.yahoo.vespa.orchestrator.Orchestrator; +import com.yahoo.vespa.orchestrator.OrchestratorImpl; +import com.yahoo.vespa.orchestrator.config.OrchestratorConfig; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactoryMock; +import com.yahoo.vespa.orchestrator.status.ZkStatusService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.concurrent.Executors; + +import static com.yahoo.jdisc.http.HttpRequest.Method; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the implementation of the orchestrator Application API. + * + * @author smorgrav + * @author bjorncs + */ +class ApplicationSuspensionRequestHandlerTest { + private static final String RESOURCE_1 = "mediasearch:imagesearch:default"; + private static final String RESOURCE_2 = "test-tenant-id:application:instance"; + private static final String INVALID_RESOURCE_NAME = "something_without_colons"; + + ApplicationSuspensionRequestHandler handler; + + @BeforeEach + void createHandler() { + DummyServiceMonitor serviceMonitor = new DummyServiceMonitor(); + Orchestrator orchestrator = new OrchestratorImpl( + new ClusterControllerClientFactoryMock(), + new ZkStatusService(new MockCurator(), new MockMetric(), new SystemTimer(), serviceMonitor), + new OrchestratorConfig(new OrchestratorConfig.Builder()), + serviceMonitor, + new ConfigserverConfig(new ConfigserverConfig.Builder()), + new InMemoryFlagSource()); + var handlerContext = new LoggingRequestHandler.Context(Executors.newSingleThreadExecutor(), new MockMetric()); + this.handler = new ApplicationSuspensionRequestHandler(handlerContext, orchestrator); + } + + + @Test + void get_all_suspended_applications_return_empty_list_initially() throws IOException { + HttpResponse httpResponse = executeRequest(Method.GET, "", null); + assertEquals(200, httpResponse.getStatus()); + Set<String> set = parseResponseContent(httpResponse, new TypeReference<>() {}); + assertEquals(0, set.size()); + } + + @Test + void invalid_application_id_throws_http_400() throws IOException { + HttpResponse httpResponse = executeRequest(Method.POST, "", INVALID_RESOURCE_NAME); + assertEquals(400, httpResponse.getStatus()); + } + + @Test + void get_application_status_returns_404_for_not_suspended_and_204_for_suspended() throws IOException { + // Get on application that is not suspended + HttpResponse httpResponse = executeRequest(Method.GET, "/"+RESOURCE_1, null); + assertEquals(404, httpResponse.getStatus()); + + // Post application + httpResponse = executeRequest(Method.POST, "", RESOURCE_1); + assertEquals(204, httpResponse.getStatus()); + + // Get on the application that now should be in suspended + httpResponse = executeRequest(Method.GET, "/"+RESOURCE_1, null); + assertEquals(204, httpResponse.getStatus()); + } + + @Test + void delete_works_on_suspended_and_not_suspended_applications() throws IOException { + // Delete an application that is not suspended + HttpResponse httpResponse = executeRequest(Method.DELETE, "/"+RESOURCE_1, null); + assertEquals(204, httpResponse.getStatus()); + + // Put application in suspend + httpResponse = executeRequest(Method.POST, "", RESOURCE_1); + assertEquals(204, httpResponse.getStatus()); + + // Check that it is in suspend + httpResponse = executeRequest(Method.GET, "/"+RESOURCE_1, null); + assertEquals(204, httpResponse.getStatus()); + + // Delete it + httpResponse = executeRequest(Method.DELETE, "/"+RESOURCE_1, null); + assertEquals(204, httpResponse.getStatus()); + + // Check that it is not in suspend anymore + httpResponse = executeRequest(Method.GET, "/"+RESOURCE_1, null); + assertEquals(404, httpResponse.getStatus()); + } + + @Test + void list_applications_returns_the_correct_list_of_suspended_applications() throws IOException { + // Test that initially we have the empty set + HttpResponse httpResponse = executeRequest(Method.GET, "", null); + assertEquals(200, httpResponse.getStatus()); + Set<String> set = parseResponseContent(httpResponse, new TypeReference<>() {}); + assertEquals(0, set.size()); + + // Add a couple of applications to maintenance + executeRequest(Method.POST, "", RESOURCE_1); + executeRequest(Method.POST, "", RESOURCE_2); + + // Test that we get them back + httpResponse = executeRequest(Method.GET, "", null); + assertEquals(200, httpResponse.getStatus()); + set = parseResponseContent(httpResponse, new TypeReference<>() {}); + assertEquals(2, set.size()); + + // Remove suspend for the first resource + executeRequest(Method.DELETE, "/"+RESOURCE_1, null); + + // Test that we are back to the start with the empty set + httpResponse = executeRequest(Method.GET, "", null); + assertEquals(200, httpResponse.getStatus()); + set = parseResponseContent(httpResponse, new TypeReference<>() {}); + assertEquals(1, set.size()); + assertEquals(RESOURCE_2, set.iterator().next()); + } + + private HttpResponse executeRequest(Method method, String path, String applicationId) throws IOException { + String uri = "http://localhost/orchestrator/v1/suspensions/applications" + path; + com.yahoo.container.jdisc.HttpRequest request; + if (applicationId != null) { + ByteArrayInputStream requestData = new ByteArrayInputStream(applicationId.getBytes(StandardCharsets.UTF_8)); + request = com.yahoo.container.jdisc.HttpRequest.createTestRequest(uri, method, requestData); + } else { + request = com.yahoo.container.jdisc.HttpRequest.createTestRequest(uri, method); + } + return handler.handle(request); + } + + private <T> T parseResponseContent(HttpResponse response, TypeReference<T> responseEntityType) throws IOException { + assertEquals(200, response.getStatus()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.render(out); + return handler.restApi().jacksonJsonMapper().readValue(out.toByteArray(), responseEntityType); + } +}
\ No newline at end of file diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/host/HostResourceTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandlerTest.java index d056c3730fd..c34775c1910 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/host/HostResourceTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandlerTest.java @@ -1,9 +1,15 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator.resources.host; +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.resources; import com.google.common.util.concurrent.UncheckedTimeoutException; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.jdisc.test.MockMetric; import com.yahoo.jdisc.test.TestTimer; +import com.yahoo.test.json.JsonTestHelper; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; @@ -41,24 +47,21 @@ import com.yahoo.vespa.orchestrator.status.StatusService; import com.yahoo.vespa.orchestrator.status.ZkStatusService; import com.yahoo.vespa.service.monitor.ServiceModel; import com.yahoo.vespa.service.monitor.ServiceMonitor; -import org.junit.Before; -import org.junit.Test; - -import javax.ws.rs.BadRequestException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.net.URI; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.time.Clock; import java.time.Instant; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executors; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -68,8 +71,10 @@ import static org.mockito.Mockito.when; /** * @author hakonhall + * @author bjorncs */ -public class HostResourceTest { +class HostRequestHandlerTest { + private static final Clock clock = mock(Clock.class); private static final int SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS = 0; private static final TenantId TENANT_ID = new TenantId("tenantId"); @@ -121,14 +126,14 @@ public class HostResourceTest { private final OrchestratorImpl alwaysAllowOrchestrator = createAlwaysAllowOrchestrator(clock); private final OrchestratorImpl hostNotFoundOrchestrator = createHostNotFoundOrchestrator(clock); - private final UriInfo uriInfo = mock(UriInfo.class); + private final OrchestratorImpl alwaysRejectOrchestrator = createAlwaysRejectResolver(clock); - @Before - public void setUp() { + @BeforeEach + void setUp() { when(clock.instant()).thenReturn(Instant.now()); } - public static OrchestratorImpl createAlwaysAllowOrchestrator(Clock clock) { + static OrchestratorImpl createAlwaysAllowOrchestrator(Clock clock) { return new OrchestratorImpl( new AlwaysAllowPolicy(), new ClusterControllerClientFactoryMock(), @@ -140,7 +145,7 @@ public class HostResourceTest { new InMemoryFlagSource()); } - public static OrchestratorImpl createHostNotFoundOrchestrator(Clock clock) { + static OrchestratorImpl createHostNotFoundOrchestrator(Clock clock) { return new OrchestratorImpl( new AlwaysAllowPolicy(), new ClusterControllerClientFactoryMock(), @@ -152,9 +157,9 @@ public class HostResourceTest { new InMemoryFlagSource()); } - public static OrchestratorImpl createAlwaysRejectResolver(Clock clock) { + static OrchestratorImpl createAlwaysRejectResolver(Clock clock) { return new OrchestratorImpl( - new HostResourceTest.AlwaysFailPolicy(), + new AlwaysFailPolicy(), new ClusterControllerClientFactoryMock(), EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, serviceMonitor, @@ -165,27 +170,20 @@ public class HostResourceTest { } @Test - public void returns_200_on_success() { - HostResource hostResource = - new HostResource(alwaysAllowOrchestrator, uriInfo); - - final String hostName = "hostname"; + void returns_200_on_success() throws IOException { + HostRequestHandler handler = createHandler(alwaysAllowOrchestrator); - UpdateHostResponse response = hostResource.suspend(hostName); - - assertEquals(hostName, response.hostname()); + HttpResponse response = executeRequest(handler, Method.PUT, "/orchestrator/v1/hosts/hostname/suspended", null); + UpdateHostResponse updateHostResponse = parseResponseContent(handler, response, UpdateHostResponse.class); + assertEquals("hostname", updateHostResponse.hostname()); } @Test - public void throws_404_when_host_unknown() { - try { - HostResource hostResource = - new HostResource(hostNotFoundOrchestrator, uriInfo); - hostResource.suspend("hostname"); - fail(); - } catch (WebApplicationException w) { - assertEquals(404, w.getResponse().getStatus()); - } + void throws_404_when_host_unknown() throws IOException { + HostRequestHandler handler = createHandler(hostNotFoundOrchestrator); + + HttpResponse response = executeRequest(handler, Method.PUT, "/orchestrator/v1/hosts/hostname/suspended", null); + assertEquals(404, response.getStatus()); } private static class AlwaysFailPolicy implements Policy { @@ -221,79 +219,61 @@ public class HostResourceTest { } @Test - public void throws_409_when_request_rejected_by_policies() { - final OrchestratorImpl alwaysRejectResolver = new OrchestratorImpl( - new AlwaysFailPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - serviceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - new InMemoryFlagSource()); + void throws_409_when_request_rejected_by_policies() throws IOException { + HostRequestHandler handler = createHandler(alwaysRejectOrchestrator); - try { - HostResource hostResource = new HostResource(alwaysRejectResolver, uriInfo); - hostResource.suspend("hostname"); - fail(); - } catch (WebApplicationException w) { - assertEquals(409, w.getResponse().getStatus()); - } + HttpResponse response = executeRequest(handler, Method.PUT, "/orchestrator/v1/hosts/hostname/suspended", null); + assertEquals(409, response.getStatus()); } - @Test(expected = BadRequestException.class) - public void patch_state_may_throw_bad_request() { + @Test + void patch_state_may_throw_bad_request() throws IOException { Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); + HostRequestHandler handler = createHandler(orchestrator); - String hostNameString = "hostname"; PatchHostRequest request = new PatchHostRequest(); request.state = "bad state"; - hostResource.patch(hostNameString, request); + HttpResponse response = executeRequest(handler, Method.PATCH, "/orchestrator/v1/hosts/hostname", request); + assertEquals(400, response.getStatus()); } @Test - public void patch_works() throws OrchestrationException { + void patch_works() throws OrchestrationException, IOException { Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); + HostRequestHandler handler = createHandler(orchestrator); String hostNameString = "hostname"; PatchHostRequest request = new PatchHostRequest(); request.state = "NO_REMARKS"; - PatchHostResponse response = hostResource.patch(hostNameString, request); + HttpResponse httpResponse = executeRequest(handler, Method.PATCH, "/orchestrator/v1/hosts/hostname", request); + PatchHostResponse response = parseResponseContent(handler, httpResponse, PatchHostResponse.class); assertEquals(response.description, "ok"); verify(orchestrator, times(1)).setNodeStatus(new HostName(hostNameString), HostStatus.NO_REMARKS); } - @Test(expected = InternalServerErrorException.class) - public void patch_handles_exception_in_orchestrator() throws OrchestrationException { + @Test + void patch_handles_exception_in_orchestrator() throws OrchestrationException, IOException { Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); + HostRequestHandler handler = createHandler(orchestrator); String hostNameString = "hostname"; PatchHostRequest request = new PatchHostRequest(); request.state = "NO_REMARKS"; doThrow(new OrchestrationException("error")).when(orchestrator).setNodeStatus(new HostName(hostNameString), HostStatus.NO_REMARKS); - hostResource.patch(hostNameString, request); + HttpResponse httpResponse = executeRequest(handler, Method.PATCH, "/orchestrator/v1/hosts/hostname", request); + assertEquals(500, httpResponse.getStatus()); } @Test - public void getHost_works() throws Exception { + void getHost_works() throws Exception { Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); + HostRequestHandler handler = createHandler(orchestrator); HostName hostName = new HostName("hostname"); - UriBuilder baseUriBuilder = mock(UriBuilder.class); - when(uriInfo.getBaseUriBuilder()).thenReturn(baseUriBuilder); - when(baseUriBuilder.path(any(String.class))).thenReturn(baseUriBuilder); - when(baseUriBuilder.path(any(Class.class))).thenReturn(baseUriBuilder); - URI uri = new URI("https://foo.com/bar"); - when(baseUriBuilder.build()).thenReturn(uri); - ServiceInstance serviceInstance = new ServiceInstance( new ConfigId("configId"), hostName, @@ -312,8 +292,11 @@ public class HostResourceTest { new ApplicationInstanceId("applicationId")), Collections.singletonList(serviceInstance)); when(orchestrator.getHost(hostName)).thenReturn(host); - GetHostResponse response = hostResource.getHost(hostName.s()); - assertEquals("https://foo.com/bar", response.applicationUrl()); + + HttpResponse httpResponse = executeRequest(handler, Method.GET, "/orchestrator/v1/hosts/hostname", null); + GetHostResponse response = parseResponseContent(handler, httpResponse, GetHostResponse.class); + + assertEquals("http://localhost/orchestrator/v1/instances/tenantId:applicationId", response.applicationUrl()); assertEquals("hostname", response.hostname()); assertEquals("ALLOWED_TO_BE_DOWN", response.state()); assertEquals("1970-01-01T00:00:00Z", response.suspendedSince()); @@ -325,18 +308,48 @@ public class HostResourceTest { } @Test - public void throws_409_on_timeout() throws HostNameNotFoundException, HostStateChangeDeniedException { + void throws_409_on_timeout() throws HostNameNotFoundException, HostStateChangeDeniedException, IOException { Orchestrator orchestrator = mock(Orchestrator.class); doThrow(new UncheckedTimeoutException("Timeout Message")).when(orchestrator).resume(any(HostName.class)); - try { - HostResource hostResource = new HostResource(orchestrator, uriInfo); - hostResource.resume("hostname"); - fail(); - } catch (WebApplicationException w) { - assertEquals(409, w.getResponse().getStatus()); - assertEquals("resume failed: Timeout Message [deadline]", w.getMessage()); + HostRequestHandler handler = createHandler(orchestrator); + HttpResponse httpResponse = executeRequest(handler, Method.DELETE, "/orchestrator/v1/hosts/hostname/suspended", null); + assertEquals(409, httpResponse.getStatus()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + httpResponse.render(out); + JsonTestHelper.assertJsonEquals("{\n" + + " \"hostname\" : \"hostname\",\n" + + " \"reason\" : {\n" + + " \"constraint\" : \"deadline\",\n" + + " \"message\" : \"resume failed: Timeout Message\"\n" + + " }\n" + + "}", + out.toString()); + } + + private HostRequestHandler createHandler(Orchestrator orchestrator) { + var handlerContext = new LoggingRequestHandler.Context(Executors.newSingleThreadExecutor(), new MockMetric()); + return new HostRequestHandler(handlerContext, orchestrator); + } + + private HttpResponse executeRequest(HostRequestHandler handler, Method method, String path, Object requestEntity) throws IOException { + String uri = "http://localhost" + path; + HttpRequest request; + if (requestEntity != null) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + handler.restApi().jacksonJsonMapper().writeValue(out, requestEntity); + request = HttpRequest.createTestRequest(uri, method, new ByteArrayInputStream(out.toByteArray())); + } else { + request = HttpRequest.createTestRequest(uri, method); } + return handler.handle(request); + } + + private <T> T parseResponseContent(HostRequestHandler handler, HttpResponse response, Class<T> responseEntityType) throws IOException { + assertEquals(200, response.getStatus()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.render(out); + return handler.restApi().jacksonJsonMapper().readValue(out.toByteArray(), responseEntityType); } } diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionHandlerTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionHandlerTest.java index 9d413526037..be3cb047967 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionHandlerTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionHandlerTest.java @@ -12,7 +12,6 @@ import com.yahoo.vespa.orchestrator.BatchInternalErrorException; import com.yahoo.vespa.orchestrator.Orchestrator; import com.yahoo.vespa.orchestrator.OrchestratorImpl; import com.yahoo.vespa.orchestrator.policy.BatchHostStateChangeDeniedException; -import com.yahoo.vespa.orchestrator.resources.host.HostResourceTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,14 +45,14 @@ class HostSuspensionHandlerTest { @Test void returns_200_on_success_batch() throws IOException { - HostSuspensionHandler handler = createHandler(HostResourceTest.createAlwaysAllowOrchestrator(clock)); + HostSuspensionHandler handler = createHandler(HostRequestHandlerTest.createAlwaysAllowOrchestrator(clock)); HttpResponse response = executeSuspendAllRequest(handler, "parentHostname", List.of("hostname1", "hostname2")); assertSuccess(response); } @Test void returns_200_empty_batch() throws IOException { - HostSuspensionHandler handler = createHandler(HostResourceTest.createAlwaysAllowOrchestrator(clock)); + HostSuspensionHandler handler = createHandler(HostRequestHandlerTest.createAlwaysAllowOrchestrator(clock)); HttpResponse response = executeSuspendAllRequest(handler, "parentHostname", List.of()); assertSuccess(response); } @@ -63,14 +62,14 @@ class HostSuspensionHandlerTest { // hostnames are part of the request body for multi-host. @Test void returns_400_when_host_unknown_for_batch() { - HostSuspensionHandler handler = createHandler(HostResourceTest.createHostNotFoundOrchestrator(clock)); + HostSuspensionHandler handler = createHandler(HostRequestHandlerTest.createHostNotFoundOrchestrator(clock)); HttpResponse response = executeSuspendAllRequest(handler, "parentHostname", List.of("hostname1", "hostname2")); assertEquals(400, response.getStatus()); } @Test void returns_409_when_request_rejected_by_policies_for_batch() { - OrchestratorImpl alwaysRejectResolver = HostResourceTest.createAlwaysRejectResolver(clock); + OrchestratorImpl alwaysRejectResolver = HostRequestHandlerTest.createAlwaysRejectResolver(clock); HostSuspensionHandler handler = createHandler(alwaysRejectResolver); HttpResponse response = executeSuspendAllRequest(handler, "parentHostname", List.of("hostname1", "hostname2")); assertEquals(409, response.getStatus()); diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandlerTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandlerTest.java new file mode 100644 index 00000000000..bee19a6d6f5 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandlerTest.java @@ -0,0 +1,132 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.resources; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.jdisc.test.MockMetric; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceStatusInfo; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.orchestrator.restapi.wire.SlobrokEntryResponse; +import com.yahoo.vespa.service.manager.UnionMonitorManager; +import com.yahoo.vespa.service.monitor.SlobrokApi; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; + +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author bjorncs + */ +class InstanceRequestHandlerTest { + + private static final String APPLICATION_INSTANCE_REFERENCE = "tenant:app:prod:us-west-1:instance"; + private static final ApplicationId APPLICATION_ID = ApplicationId.from( + "tenant", "app", "instance"); + private static final List<Mirror.Entry> ENTRIES = Arrays.asList( + new Mirror.Entry("name1", "tcp/spec:1"), + new Mirror.Entry("name2", "tcp/spec:2")); + private static final ClusterId CLUSTER_ID = new ClusterId("cluster-id"); + private static final ObjectMapper jsonMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .registerModule(new Jdk8Module()); + + private final SlobrokApi slobrokApi = mock(SlobrokApi.class); + private final UnionMonitorManager rootManager = mock(UnionMonitorManager.class); + private final InstanceRequestHandler handler = new InstanceRequestHandler( + new LoggingRequestHandler.Context(Executors.newSingleThreadExecutor(), new MockMetric()), + null, + null, + slobrokApi, + rootManager); + + + @Test + void testGetSlobrokEntries() throws Exception { + testGetSlobrokEntriesWith("foo", "foo"); + } + + @Test + void testGetSlobrokEntriesWithoutPattern() throws Exception { + testGetSlobrokEntriesWith(null, InstanceRequestHandler.DEFAULT_SLOBROK_PATTERN); + } + + @Test + void testGetServiceStatusInfo() throws IOException { + ServiceType serviceType = new ServiceType("serviceType"); + ConfigId configId = new ConfigId("configId"); + ServiceStatus serviceStatus = ServiceStatus.UP; + when(rootManager.getStatus(APPLICATION_ID, CLUSTER_ID, serviceType, configId)) + .thenReturn(new ServiceStatusInfo(serviceStatus)); + + + String uriPath = String.format( + "/orchestrator/v1/instances/%s/serviceStatusInfo?clusterId=%s&serviceType=%s&configId=%s", + APPLICATION_INSTANCE_REFERENCE, + CLUSTER_ID.s(), + serviceType.s(), + configId.s()); + ServiceStatusInfo serviceStatusInfo = executeRequest(uriPath, new TypeReference<>(){}); + + ServiceStatus actualServiceStatus = serviceStatusInfo.serviceStatus(); + verify(rootManager).getStatus(APPLICATION_ID, CLUSTER_ID, serviceType, configId); + assertEquals(serviceStatus, actualServiceStatus); + } + + @Test + void testBadRequest() { + String uriPath = String.format( + "/orchestrator/v1/instances/%s/serviceStatusInfo?clusterId=%s", + APPLICATION_INSTANCE_REFERENCE, + CLUSTER_ID.s()); + HttpRequest request = HttpRequest.createTestRequest("http://localhost" + uriPath, GET); + HttpResponse response = handler.handle(request); + assertEquals(400, response.getStatus()); + } + + private void testGetSlobrokEntriesWith(String pattern, String expectedLookupPattern) + throws Exception{ + when(slobrokApi.lookup(APPLICATION_ID, expectedLookupPattern)) + .thenReturn(ENTRIES); + + String uriPath = String.format("/orchestrator/v1/instances/%s/slobrok", APPLICATION_INSTANCE_REFERENCE); + if (pattern != null) { + uriPath += "?pattern=" + pattern; + } + List<SlobrokEntryResponse> response = executeRequest(uriPath, new TypeReference<>() {}); + + verify(slobrokApi).lookup(APPLICATION_ID, expectedLookupPattern); + + String actualJson = jsonMapper.writeValueAsString(response); + assertEquals( + "[{\"name\":\"name1\",\"spec\":\"tcp/spec:1\"},{\"name\":\"name2\",\"spec\":\"tcp/spec:2\"}]", + actualJson); + } + + private <T> T executeRequest(String path, TypeReference<T> responseEntityType) throws IOException { + HttpRequest request = HttpRequest.createTestRequest("http://localhost" + path, GET); + HttpResponse response = handler.handle(request); + assertEquals(200, response.getStatus()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + response.render(out); + return jsonMapper.readValue(out.toByteArray(), responseEntityType); + } +} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResourceTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResourceTest.java deleted file mode 100644 index a7514de5acd..00000000000 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResourceTest.java +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator.resources.appsuspension; - -import com.yahoo.application.Application; -import com.yahoo.application.Networking; -import com.yahoo.container.Container; -import com.yahoo.jdisc.http.server.jetty.JettyHttpServer; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.GenericType; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import java.net.URI; -import java.util.Set; - -import static org.junit.Assert.assertEquals; - -/** - * Tests the implementation of the orchestrator Application API. - * - * @author smorgrav - */ -public class ApplicationSuspensionResourceTest { - - private static final String BASE_PATH = "/orchestrator/v1/suspensions/applications"; - private static final String RESOURCE_1 = "mediasearch:imagesearch:default"; - private static final String RESOURCE_2 = "test-tenant-id:application:instance"; - private static final String INVALID_RESOURCE_NAME = "something_without_colons"; - - private Application jdiscApplication; - private WebTarget webTarget; - - @Before - public void setup() throws Exception { - jdiscApplication = Application.fromServicesXml(servicesXml(), Networking.enable); - Client client = ClientBuilder.newClient(); - - JettyHttpServer serverProvider = (JettyHttpServer) Container.get().getServerProviderRegistry().allComponents().get(0); - String url = "http://localhost:" + serverProvider.getListenPort() + BASE_PATH; - webTarget = client.target(new URI(url)); - } - - @After - public void teardown() { - jdiscApplication.close(); - webTarget = null; - } - - @Ignore - @Test - public void run_application_locally_for_manual_browser_testing() throws Exception { - System.out.println(webTarget.getUri()); - Thread.sleep(3600 * 1000); - } - - @Test - public void get_all_suspended_applications_return_empty_list_initially() { - Response reply = webTarget.request().get(); - assertEquals(200, reply.getStatus()); - assertEquals("[]", reply.readEntity(String.class)); - } - - @Test - public void invalid_application_id_throws_http_400() { - Response reply = webTarget.request().post(Entity.entity(INVALID_RESOURCE_NAME, MediaType.APPLICATION_JSON_TYPE)); - assertEquals(400, reply.getStatus()); - } - - @Test - public void get_application_status_returns_404_for_not_suspended_and_204_for_suspended() { - // Get on application that is not suspended - Response reply = webTarget.path(RESOURCE_1).request().get(); - assertEquals(404, reply.getStatus()); - - // Post application - reply = webTarget.request().post(Entity.entity(RESOURCE_1, MediaType.APPLICATION_JSON_TYPE)); - assertEquals(204, reply.getStatus()); - - // Get on the application that now should be in suspended - reply = webTarget.path(RESOURCE_1).request().get(); - assertEquals(204, reply.getStatus()); - } - - @Test - public void delete_works_on_suspended_and_not_suspended_applications() { - // Delete an application that is not suspended - Response reply = webTarget.path(RESOURCE_1).request().delete(); - assertEquals(204, reply.getStatus()); - - // Put application in suspend - reply = webTarget.request().post(Entity.entity(RESOURCE_1, MediaType.APPLICATION_JSON_TYPE)); - assertEquals(204, reply.getStatus()); - - // Check that it is in suspend - reply = webTarget.path(RESOURCE_1).request(MediaType.APPLICATION_JSON).get(); - assertEquals(204, reply.getStatus()); - - // Delete it - reply = webTarget.path(RESOURCE_1).request().delete(); - assertEquals(204, reply.getStatus()); - - // Check that it is not in suspend anymore - reply = webTarget.path(RESOURCE_1).request(MediaType.APPLICATION_JSON).get(); - assertEquals(404, reply.getStatus()); - } - - @Test - public void list_applications_returns_the_correct_list_of_suspended_applications() { - // Test that initially we have the empty set - Response reply = webTarget.request(MediaType.APPLICATION_JSON).get(); - assertEquals(200, reply.getStatus()); - assertEquals("[]", reply.readEntity(String.class)); - - // Add a couple of applications to maintenance - webTarget.request().post(Entity.entity(RESOURCE_1, MediaType.APPLICATION_JSON_TYPE)); - webTarget.request().post(Entity.entity(RESOURCE_2, MediaType.APPLICATION_JSON_TYPE)); - assertEquals(200, reply.getStatus()); - - // Test that we get them back - Set<String> responses = webTarget.request(MediaType.APPLICATION_JSON_TYPE) - .get(new GenericType<Set<String>>() {}); - assertEquals(2, responses.size()); - - // Remove suspend for the first resource - webTarget.path(RESOURCE_1).request().delete(); - - // Test that we are back to the start with the empty set - responses = webTarget.request(MediaType.APPLICATION_JSON_TYPE) - .get(new GenericType<Set<String>>() {}); - assertEquals(1, responses.size()); - assertEquals(RESOURCE_2, responses.iterator().next()); - } - - private String servicesXml() { - return "<services>\n" + - " <container version=\"1.0\" jetty=\"true\">\n" + - " <accesslog type=\"disabled\"/>\n" + - " <config name=\"container.handler.threadpool\">\n" + - " <maxthreads>10</maxthreads>\n" + - " </config>\n" + - " <component id=\"com.yahoo.vespa.flags.InMemoryFlagSource\" bundle=\"flags\" />\n" + - " <component id=\"com.yahoo.vespa.curator.mock.MockCurator\" bundle=\"zkfacade\" />\n" + - " <component id=\"com.yahoo.vespa.orchestrator.status.ZkStatusService\" bundle=\"orchestrator\" />\n" + - " <component id=\"com.yahoo.vespa.orchestrator.DummyServiceMonitor\" bundle=\"orchestrator\" />\n" + - " <component id=\"com.yahoo.vespa.orchestrator.OrchestratorImpl\" bundle=\"orchestrator\" />\n" + - " <component id=\"com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactoryMock\" bundle=\"orchestrator\" />\n" + - "\n" + - " <rest-api path=\"orchestrator/v1/suspensions/applications\" jersey2=\"true\">\n" + - " <components bundle=\"orchestrator\">\n" + - " <package>com.yahoo.vespa.orchestrator.resources.appsuspension</package>\n" + - " </components>\n" + - " </rest-api>\n" + - "\n" + - " <http>\n" + - " <server id=\"foo\" port=\"0\"/>\n" + - " </http>\n" + - " </container>\n" + - "</services>\n"; - } - -} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResourceTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResourceTest.java deleted file mode 100644 index 8e2eeb7410d..00000000000 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResourceTest.java +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator.resources.instance; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.jrt.slobrok.api.Mirror; -import com.yahoo.vespa.applicationmodel.ClusterId; -import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.ServiceStatus; -import com.yahoo.vespa.applicationmodel.ServiceStatusInfo; -import com.yahoo.vespa.applicationmodel.ServiceType; -import com.yahoo.vespa.orchestrator.resources.instance.InstanceResource; -import com.yahoo.vespa.orchestrator.restapi.wire.SlobrokEntryResponse; -import com.yahoo.vespa.service.manager.UnionMonitorManager; -import com.yahoo.vespa.service.monitor.SlobrokApi; -import org.junit.Test; - -import javax.ws.rs.WebApplicationException; -import java.util.Arrays; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class InstanceResourceTest { - private static final String APPLICATION_INSTANCE_REFERENCE = "tenant:app:prod:us-west-1:instance"; - private static final ApplicationId APPLICATION_ID = ApplicationId.from( - "tenant", "app", "instance"); - private static final List<Mirror.Entry> ENTRIES = Arrays.asList( - new Mirror.Entry("name1", "tcp/spec:1"), - new Mirror.Entry("name2", "tcp/spec:2")); - private static final ClusterId CLUSTER_ID = new ClusterId("cluster-id"); - - private final SlobrokApi slobrokApi = mock(SlobrokApi.class); - private final UnionMonitorManager rootManager = mock(UnionMonitorManager.class); - private final InstanceResource resource = new InstanceResource( - null, - null, - slobrokApi, - rootManager); - - @Test - public void testGetSlobrokEntries() throws Exception { - testGetSlobrokEntriesWith("foo", "foo"); - } - - @Test - public void testGetSlobrokEntriesWithoutPattern() throws Exception { - testGetSlobrokEntriesWith(null, InstanceResource.DEFAULT_SLOBROK_PATTERN); - } - - @Test - public void testGetServiceStatusInfo() { - ServiceType serviceType = new ServiceType("serviceType"); - ConfigId configId = new ConfigId("configId"); - ServiceStatus serviceStatus = ServiceStatus.UP; - when(rootManager.getStatus(APPLICATION_ID, CLUSTER_ID, serviceType, configId)) - .thenReturn(new ServiceStatusInfo(serviceStatus)); - ServiceStatus actualServiceStatus = resource.getServiceStatus( - APPLICATION_INSTANCE_REFERENCE, - CLUSTER_ID.s(), - serviceType.s(), - configId.s()).serviceStatus(); - verify(rootManager).getStatus(APPLICATION_ID, CLUSTER_ID, serviceType, configId); - assertEquals(serviceStatus, actualServiceStatus); - } - - @Test(expected = WebApplicationException.class) - public void testBadRequest() { - resource.getServiceStatus(APPLICATION_INSTANCE_REFERENCE, CLUSTER_ID.s(), null, null); - } - - private void testGetSlobrokEntriesWith(String pattern, String expectedLookupPattern) - throws Exception{ - when(slobrokApi.lookup(APPLICATION_ID, expectedLookupPattern)) - .thenReturn(ENTRIES); - - List<SlobrokEntryResponse> response = resource.getSlobrokEntries( - APPLICATION_INSTANCE_REFERENCE, - pattern); - - verify(slobrokApi).lookup(APPLICATION_ID, expectedLookupPattern); - - ObjectMapper mapper = new ObjectMapper(); - String actualJson = mapper.writeValueAsString(response); - assertEquals( - "[{\"name\":\"name1\",\"spec\":\"tcp/spec:1\"},{\"name\":\"name2\",\"spec\":\"tcp/spec:2\"}]", - actualJson); - } -}
\ No newline at end of file |