From 97e6a063049a93d7a78301f29c27148f72dcef67 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Mon, 12 Apr 2021 20:49:31 +0200 Subject: Convert remaining JAX-RS resources to request handlers --- .../ApplicationSuspensionRequestHandler.java | 158 +++++++++ .../orchestrator/resources/HostRequestHandler.java | 214 +++++++++++++ .../resources/InstanceRequestHandler.java | 163 ++++++++++ .../ApplicationSuspensionResource.java | 122 ------- .../orchestrator/resources/host/HostResource.java | 192 ----------- .../resources/instance/InstanceResource.java | 168 ---------- .../ApplicationSuspensionRequestHandlerTest.java | 157 +++++++++ .../resources/HostRequestHandlerTest.java | 355 +++++++++++++++++++++ .../resources/HostSuspensionHandlerTest.java | 9 +- .../resources/InstanceRequestHandlerTest.java | 132 ++++++++ .../ApplicationSuspensionResourceTest.java | 168 ---------- .../resources/host/HostResourceTest.java | 342 -------------------- .../resources/instance/InstanceResourceTest.java | 92 ------ 13 files changed, 1183 insertions(+), 1089 deletions(-) create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandler.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandler.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandler.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResource.java create mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandlerTest.java create mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandlerTest.java create mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandlerTest.java delete mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResourceTest.java delete mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/host/HostResourceTest.java delete mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResourceTest.java (limited to 'orchestrator') 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 { + + 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 getApplications(RestApi.RequestContext context) { + Set 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/HostRequestHandler.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandler.java new file mode 100644 index 00000000000..4dcd76cbb10 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandler.java @@ -0,0 +1,214 @@ +// 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.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; +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.restapi.wire.GetHostResponse; +import com.yahoo.vespa.orchestrator.restapi.wire.HostService; +import com.yahoo.vespa.orchestrator.restapi.wire.HostStateChangeDenialReason; +import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostRequest; +import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostResponse; +import com.yahoo.vespa.orchestrator.restapi.wire.UpdateHostResponse; +import com.yahoo.vespa.orchestrator.status.HostStatus; + +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * @author oyving + * @author bjorncs + */ +public class HostRequestHandler extends RestApiRequestHandler { + + private static final Logger log = Logger.getLogger(HostRequestHandler.class.getName()); + + private final Orchestrator orchestrator; + + @Inject + public HostRequestHandler(LoggingRequestHandler.Context context, Orchestrator orchestrator) { + super(context, HostRequestHandler::createRestApiDefinition); + this.orchestrator = orchestrator; + } + + 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 = context.uriBuilder() + .withPath("/orchestrator/v1/instances/" + host.getApplicationInstanceReference().asString()) + .toURI(); + + List hostServices = host.getServiceInstances().stream() + .map(serviceInstance -> new HostService( + serviceInstance.getServiceCluster().clusterId().s(), + serviceInstance.getServiceCluster().serviceType().s(), + serviceInstance.configId().s(), + serviceInstance.serviceStatus().name())) + .collect(Collectors.toList()); + + return new GetHostResponse( + host.getHostName().s(), + host.getHostInfo().status().name(), + host.getHostInfo().suspendedSince().map(Instant::toString).orElse(null), + applicationUri.toString(), + hostServices); + } catch (UncheckedTimeoutException e) { + log.log(Level.FINE, "Failed to get host " + hostName + ": " + e.getMessage()); + throw restApiExceptionFromTimeout("getHost", hostName, e); + } catch (HostNameNotFoundException e) { + log.log(Level.FINE, "Host not found: " + hostName); + throw new RestApiException.NotFoundException(e); + } + } + + /** + * 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) { + HostStatus state; + try { + state = HostStatus.valueOf(request.state); + } catch (IllegalArgumentException dummy) { + 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 RestApiException.NotFoundException(e); + } catch (UncheckedTimeoutException e) { + log.log(Level.FINE, "Failed to patch " + hostName + ": " + e.getMessage()); + 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 RestApiException.InternalServerError(message); + } + } + + PatchHostResponse response = new PatchHostResponse(); + response.description = "ok"; + return response; + } + + /** + * 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 RestApiException.NotFoundException(e); + } catch (UncheckedTimeoutException e) { + log.log(Level.FINE, "Failed to suspend " + hostName + ": " + e.getMessage()); + throw restApiExceptionFromTimeout("suspend", hostName, e); + } catch (HostStateChangeDeniedException e) { + log.log(Level.FINE, "Failed to suspend " + hostName + ": " + e.getMessage()); + throw restApiExceptionWithDenialReason("suspend", hostName, e); + } + return new UpdateHostResponse(hostName.s(), null); + } + /** + * 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 RestApiException.NotFoundException(e); + } catch (UncheckedTimeoutException e) { + log.log(Level.FINE, "Failed to resume " + hostName + ": " + e.getMessage()); + throw restApiExceptionFromTimeout("resume", hostName, e); + } catch (HostStateChangeDeniedException e) { + log.log(Level.FINE, "Failed to resume " + hostName + ": " + e.getMessage()); + throw restApiExceptionWithDenialReason("resume", hostName, e); + } + return new UpdateHostResponse(hostName.s(), null); + } + + 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 createRestApiException(operationDescription, hostName, e, + HostedVespaPolicy.DEADLINE_CONSTRAINT, e.getMessage(), Response.Status.CONFLICT); + } + + private RestApiException restApiExceptionWithDenialReason( + String operationDescription, + HostName hostName, + HostStateChangeDeniedException e) { + return createRestApiException(operationDescription, hostName, e, e.getConstraintName(), e.getMessage(), + Response.Status.CONFLICT); + } + + 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 RestApiException( + new JacksonJsonResponse<>(status, response, restApi().jacksonJsonMapper(), true), + hostStateChangeDenialReason.toString(), + e); + } +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandler.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandler.java new file mode 100644 index 00000000000..5f0c7caf931 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceRequestHandler.java @@ -0,0 +1,163 @@ +// 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.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; +import com.yahoo.vespa.applicationmodel.ConfigId; +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.restapi.wire.SlobrokEntryResponse; +import com.yahoo.vespa.orchestrator.restapi.wire.WireHostInfo; +import com.yahoo.vespa.orchestrator.status.HostInfo; +import com.yahoo.vespa.orchestrator.status.HostInfos; +import com.yahoo.vespa.orchestrator.status.StatusService; +import com.yahoo.vespa.service.manager.MonitorManager; +import com.yahoo.vespa.service.manager.UnionMonitorManager; +import com.yahoo.vespa.service.monitor.ServiceMonitor; +import com.yahoo.vespa.service.monitor.SlobrokApi; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.time.Instant; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import static com.yahoo.vespa.orchestrator.OrchestratorUtil.getHostsUsedByApplicationInstance; +import static com.yahoo.vespa.orchestrator.OrchestratorUtil.parseApplicationInstanceReference; + +/** + * Provides a read-only API for looking into the current state as seen by the Orchestrator. + * This API can be unstable and is not meant to be used programmatically. + * + * @author andreer + * @author Oyvind Bakksjo + * @author bjorncs + */ +public class InstanceRequestHandler extends RestApiRequestHandler { + + public static final String DEFAULT_SLOBROK_PATTERN = "**"; + + private final StatusService statusService; + private final SlobrokApi slobrokApi; + private final MonitorManager rootManager; + private final ServiceMonitor serviceMonitor; + + @Inject + 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; + } + + 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 getAllInstances(RestApi.RequestContext context) { + return serviceMonitor.getAllApplicationInstanceReferences().stream().sorted().collect(Collectors.toList()); + } + + private InstanceStatusResponse getInstance(RestApi.RequestContext context) { + String instanceIdString = context.pathParameters().getStringOrThrow("instanceId"); + ApplicationInstanceReference instanceId = parseInstanceId(instanceIdString); + + ApplicationInstance applicationInstance + = serviceMonitor.getApplication(instanceId) + .orElseThrow(() -> new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build())); + + HostInfos hostInfos = statusService.getHostInfosByApplicationResolver().apply(applicationInstance.reference()); + TreeMap hostStatusMap = + getHostsUsedByApplicationInstance(applicationInstance) + .stream() + .collect(Collectors.toMap( + hostName -> hostName, + hostName -> hostInfoToWire(hostInfos.getOrNoRemarks(hostName)), + (u, v) -> { throw new IllegalStateException(); }, + TreeMap::new)); + return InstanceStatusResponse.create(applicationInstance, hostStatusMap); + } + + private WireHostInfo hostInfoToWire(HostInfo hostInfo) { + String hostStatusString = hostInfo.status().asString(); + String suspendedSinceUtcOrNull = hostInfo.suspendedSince().map(Instant::toString).orElse(null); + return new WireHostInfo(hostStatusString, suspendedSinceUtcOrNull); + } + + private List 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); + + if (pattern == null) { + pattern = DEFAULT_SLOBROK_PATTERN; + } + + List entries = slobrokApi.lookup(applicationId, pattern); + return entries.stream() + .map(entry -> new SlobrokEntryResponse(entry.getName(), entry.getSpecString())) + .collect(Collectors.toList()); + } + + 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); + + ClusterId clusterId = new ClusterId(clusterIdString); + ServiceType serviceType = new ServiceType(serviceTypeString); + ConfigId configId = new ConfigId(configIdString); + + return rootManager.getStatus(applicationId, clusterId, serviceType, configId); + } + + private static ApplicationInstanceReference parseInstanceId(String instanceIdString) { + try { + return parseApplicationInstanceReference(instanceIdString); + } catch (IllegalArgumentException e) { + throw new RestApiException.BadRequest(e.getMessage(), e); + } + } + +} 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 getApplications() { - Set 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/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java deleted file mode 100644 index c55eeeef069..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java +++ /dev/null @@ -1,192 +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.host; - -import com.google.common.util.concurrent.UncheckedTimeoutException; -import com.yahoo.container.jaxrs.annotation.Component; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.orchestrator.Host; -import com.yahoo.vespa.orchestrator.HostNameNotFoundException; -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; -import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostRequest; -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; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * @author oyving - */ -@Path("") -public class HostResource implements HostApi { - private static final Logger log = Logger.getLogger(HostResource.class.getName()); - - private final Orchestrator orchestrator; - private final UriInfo uriInfo; - - @Inject - public HostResource(@Component Orchestrator orchestrator, @Context UriInfo uriInfo) { - this.orchestrator = orchestrator; - this.uriInfo = uriInfo; - } - - @Override - public GetHostResponse getHost(String hostNameString) { - HostName hostName = new HostName(hostNameString); - try { - Host host = orchestrator.getHost(hostName); - - URI applicationUri = uriInfo.getBaseUriBuilder() - .path(InstanceResource.class) - .path(host.getApplicationInstanceReference().asString()) - .build(); - - List hostServices = host.getServiceInstances().stream() - .map(serviceInstance -> new HostService( - serviceInstance.getServiceCluster().clusterId().s(), - serviceInstance.getServiceCluster().serviceType().s(), - serviceInstance.configId().s(), - serviceInstance.serviceStatus().name())) - .collect(Collectors.toList()); - - return new GetHostResponse( - host.getHostName().s(), - host.getHostInfo().status().name(), - host.getHostInfo().suspendedSince().map(Instant::toString).orElse(null), - applicationUri.toString(), - hostServices); - } catch (UncheckedTimeoutException e) { - log.log(Level.FINE, "Failed to get host " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("getHost", hostName, e); - } catch (HostNameNotFoundException e) { - log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); - } - } - - @Override - public PatchHostResponse patch(String hostNameString, PatchHostRequest request) { - HostName hostName = new HostName(hostNameString); - - if (request.state != null) { - HostStatus state; - try { - state = HostStatus.valueOf(request.state); - } catch (IllegalArgumentException dummy) { - throw new BadRequestException("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); - } catch (UncheckedTimeoutException e) { - log.log(Level.FINE, "Failed to patch " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("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); - } - } - - PatchHostResponse response = new PatchHostResponse(); - response.description = "ok"; - return response; - } - - @Override - public UpdateHostResponse suspend(String hostNameString) { - HostName hostName = new HostName(hostNameString); - try { - orchestrator.suspend(hostName); - } catch (HostNameNotFoundException e) { - log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); - } catch (UncheckedTimeoutException e) { - log.log(Level.FINE, "Failed to suspend " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("suspend", hostName, e); - } catch (HostStateChangeDeniedException e) { - log.log(Level.FINE, "Failed to suspend " + hostName + ": " + e.getMessage()); - throw webExceptionWithDenialReason("suspend", hostName, e); - } - return new UpdateHostResponse(hostName.s(), null); - } - - @Override - public UpdateHostResponse resume(final String hostNameString) { - HostName hostName = new HostName(hostNameString); - try { - orchestrator.resume(hostName); - } catch (HostNameNotFoundException e) { - log.log(Level.FINE, "Host not found: " + hostName); - throw new NotFoundException(e); - } catch (UncheckedTimeoutException e) { - log.log(Level.FINE, "Failed to resume " + hostName + ": " + e.getMessage()); - throw webExceptionFromTimeout("resume", hostName, e); - } catch (HostStateChangeDeniedException e) { - log.log(Level.FINE, "Failed to resume " + hostName + ": " + e.getMessage()); - throw webExceptionWithDenialReason("resume", hostName, e); - } - return new UpdateHostResponse(hostName.s(), null); - } - - private static WebApplicationException webExceptionFromTimeout(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, - HostedVespaPolicy.DEADLINE_CONSTRAINT, e.getMessage(), Response.Status.CONFLICT); - } - - private static WebApplicationException webExceptionWithDenialReason( - String operationDescription, - HostName hostName, - HostStateChangeDeniedException e) { - return createWebException(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) { - HostStateChangeDenialReason hostStateChangeDenialReason = new HostStateChangeDenialReason( - constraint, operationDescription + " failed: " + message); - UpdateHostResponse response = new UpdateHostResponse(hostname.s(), hostStateChangeDenialReason); - return new WebApplicationException( - hostStateChangeDenialReason.toString(), - e, - Response.status(status) - .entity(response) - .type(MediaType.APPLICATION_JSON_TYPE) - .build()); - } -} - 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/instance/InstanceResource.java deleted file mode 100644 index 742f7d6bbd7..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResource.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.instance; - -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.container.jaxrs.annotation.Component; -import com.yahoo.jrt.slobrok.api.Mirror; -import com.yahoo.vespa.applicationmodel.ApplicationInstance; -import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; -import com.yahoo.vespa.applicationmodel.ClusterId; -import com.yahoo.vespa.applicationmodel.ConfigId; -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; -import com.yahoo.vespa.orchestrator.status.HostInfos; -import com.yahoo.vespa.orchestrator.status.StatusService; -import com.yahoo.vespa.service.manager.MonitorManager; -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; -import java.util.TreeMap; -import java.util.stream.Collectors; - -import static com.yahoo.vespa.orchestrator.OrchestratorUtil.getHostsUsedByApplicationInstance; -import static com.yahoo.vespa.orchestrator.OrchestratorUtil.parseApplicationInstanceReference; - -/** - * Provides a read-only API for looking into the current state as seen by the Orchestrator. - * This API can be unstable and is not meant to be used programmatically. - * - * @author andreer - * @author bakksjo - */ -@Path("") -public class InstanceResource { - - public static final String DEFAULT_SLOBROK_PATTERN = "**"; - - private final StatusService statusService; - private final SlobrokApi slobrokApi; - private final MonitorManager rootManager; - private final ServiceMonitor serviceMonitor; - - @Inject - public InstanceResource(@Component ServiceMonitor serviceMonitor, - @Component StatusService statusService, - @Component SlobrokApi slobrokApi, - @Component UnionMonitorManager rootManager) { - this.serviceMonitor = serviceMonitor; - this.statusService = statusService; - this.slobrokApi = slobrokApi; - this.rootManager = rootManager; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - public List getAllInstances() { - return serviceMonitor.getAllApplicationInstanceReferences().stream().sorted().collect(Collectors.toList()); - } - - @GET - @Path("/{instanceId}") - @Produces(MediaType.APPLICATION_JSON) - public InstanceStatusResponse getInstance(@PathParam("instanceId") String instanceIdString) { - ApplicationInstanceReference instanceId = parseInstanceId(instanceIdString); - - ApplicationInstance applicationInstance - = serviceMonitor.getApplication(instanceId) - .orElseThrow(() -> new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build())); - - HostInfos hostInfos = statusService.getHostInfosByApplicationResolver().apply(applicationInstance.reference()); - TreeMap hostStatusMap = - getHostsUsedByApplicationInstance(applicationInstance) - .stream() - .collect(Collectors.toMap( - hostName -> hostName, - hostName -> hostInfoToWire(hostInfos.getOrNoRemarks(hostName)), - (u, v) -> { throw new IllegalStateException(); }, - TreeMap::new)); - return InstanceStatusResponse.create(applicationInstance, hostStatusMap); - } - - private WireHostInfo hostInfoToWire(HostInfo hostInfo) { - String hostStatusString = hostInfo.status().asString(); - String suspendedSinceUtcOrNull = hostInfo.suspendedSince().map(Instant::toString).orElse(null); - return new WireHostInfo(hostStatusString, suspendedSinceUtcOrNull); - } - - @GET - @Path("/{instanceId}/slobrok") - @Produces(MediaType.APPLICATION_JSON) - public List getSlobrokEntries( - @PathParam("instanceId") String instanceId, - @QueryParam("pattern") String pattern) { - ApplicationInstanceReference reference = parseInstanceId(instanceId); - ApplicationId applicationId = OrchestratorUtil.toApplicationId(reference); - - if (pattern == null) { - pattern = DEFAULT_SLOBROK_PATTERN; - } - - List entries = slobrokApi.lookup(applicationId, pattern); - return entries.stream() - .map(entry -> new SlobrokEntryResponse(entry.getName(), entry.getSpecString())) - .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) { - 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); - - return rootManager.getStatus(applicationId, clusterId, serviceType, configId); - } - - static ApplicationInstanceReference parseInstanceId(String instanceIdString) { - try { - return parseApplicationInstanceReference(instanceIdString); - } catch (IllegalArgumentException e) { - throwBadRequest(e.getMessage()); - return null; // Necessary for compiler - } - } - - static void throwBadRequest(String message) { - throw new WebApplicationException( - Response.status(Response.Status.BAD_REQUEST).entity(message).build()); - } - -} 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 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 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 parseResponseContent(HttpResponse response, TypeReference 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/HostRequestHandlerTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandlerTest.java new file mode 100644 index 00000000000..c34775c1910 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostRequestHandlerTest.java @@ -0,0 +1,355 @@ +// 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; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.flags.InMemoryFlagSource; +import com.yahoo.vespa.orchestrator.DummyAntiServiceMonitor; +import com.yahoo.vespa.orchestrator.Host; +import com.yahoo.vespa.orchestrator.HostNameNotFoundException; +import com.yahoo.vespa.orchestrator.OrchestrationException; +import com.yahoo.vespa.orchestrator.Orchestrator; +import com.yahoo.vespa.orchestrator.OrchestratorContext; +import com.yahoo.vespa.orchestrator.OrchestratorImpl; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactoryMock; +import com.yahoo.vespa.orchestrator.model.ApplicationApi; +import com.yahoo.vespa.orchestrator.model.ApplicationApiFactory; +import com.yahoo.vespa.orchestrator.policy.HostStateChangeDeniedException; +import com.yahoo.vespa.orchestrator.policy.Policy; +import com.yahoo.vespa.orchestrator.policy.SuspensionReasons; +import com.yahoo.vespa.orchestrator.restapi.wire.GetHostResponse; +import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostRequest; +import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostResponse; +import com.yahoo.vespa.orchestrator.restapi.wire.UpdateHostResponse; +import com.yahoo.vespa.orchestrator.status.ApplicationLock; +import com.yahoo.vespa.orchestrator.status.HostInfo; +import com.yahoo.vespa.orchestrator.status.HostStatus; +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.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.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author hakonhall + * @author bjorncs + */ +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"); + private static final ApplicationInstanceId APPLICATION_INSTANCE_ID = new ApplicationInstanceId("applicationId"); + private static final ServiceMonitor serviceMonitor = mock(ServiceMonitor.class); + private static final StatusService EVERY_HOST_IS_UP_HOST_STATUS_SERVICE = new ZkStatusService( + new MockCurator(), mock(Metric.class), new TestTimer(), new DummyAntiServiceMonitor()); + private static final ApplicationApiFactory applicationApiFactory = new ApplicationApiFactory(3, clock); + + static { + when(serviceMonitor.getApplication(any(HostName.class))) + .thenReturn(Optional.of( + new ApplicationInstance( + TENANT_ID, + APPLICATION_INSTANCE_ID, + Set.of()))); + } + + private static final ServiceMonitor alwaysEmptyServiceMonitor = new ServiceMonitor() { + private final ServiceModel emptyServiceModel = new ServiceModel(Map.of()); + + @Override + public ServiceModel getServiceModelSnapshot() { + return emptyServiceModel; + } + }; + + private static class AlwaysAllowPolicy implements Policy { + @Override + public SuspensionReasons grantSuspensionRequest(OrchestratorContext context, ApplicationApi applicationApi) { + return SuspensionReasons.nothingNoteworthy(); + } + + @Override + public void releaseSuspensionGrant(OrchestratorContext context, ApplicationApi application) { + } + + @Override + public void acquirePermissionToRemove(OrchestratorContext context, ApplicationApi applicationApi) { + } + + @Override + public void releaseSuspensionGrant( + OrchestratorContext context, ApplicationInstance applicationInstance, + HostName hostName, + ApplicationLock hostStatusRegistry) { + } + } + + private final OrchestratorImpl alwaysAllowOrchestrator = createAlwaysAllowOrchestrator(clock); + private final OrchestratorImpl hostNotFoundOrchestrator = createHostNotFoundOrchestrator(clock); + private final OrchestratorImpl alwaysRejectOrchestrator = createAlwaysRejectResolver(clock); + + @BeforeEach + void setUp() { + when(clock.instant()).thenReturn(Instant.now()); + } + + static OrchestratorImpl createAlwaysAllowOrchestrator(Clock clock) { + return new OrchestratorImpl( + new AlwaysAllowPolicy(), + new ClusterControllerClientFactoryMock(), + EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, + serviceMonitor, + SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, + clock, + applicationApiFactory, + new InMemoryFlagSource()); + } + + static OrchestratorImpl createHostNotFoundOrchestrator(Clock clock) { + return new OrchestratorImpl( + new AlwaysAllowPolicy(), + new ClusterControllerClientFactoryMock(), + EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, + alwaysEmptyServiceMonitor, + SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, + clock, + applicationApiFactory, + new InMemoryFlagSource()); + } + + static OrchestratorImpl createAlwaysRejectResolver(Clock clock) { + return new OrchestratorImpl( + new AlwaysFailPolicy(), + new ClusterControllerClientFactoryMock(), + EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, + serviceMonitor, + SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, + clock, + applicationApiFactory, + new InMemoryFlagSource()); + } + + @Test + void returns_200_on_success() throws IOException { + HostRequestHandler handler = createHandler(alwaysAllowOrchestrator); + + HttpResponse response = executeRequest(handler, Method.PUT, "/orchestrator/v1/hosts/hostname/suspended", null); + UpdateHostResponse updateHostResponse = parseResponseContent(handler, response, UpdateHostResponse.class); + assertEquals("hostname", updateHostResponse.hostname()); + } + + @Test + 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 { + @Override + public SuspensionReasons grantSuspensionRequest(OrchestratorContext context, ApplicationApi applicationApi) throws HostStateChangeDeniedException { + throw newHostStateChangeDeniedException(); + } + + @Override + public void releaseSuspensionGrant(OrchestratorContext context, ApplicationApi application) throws HostStateChangeDeniedException { + throw newHostStateChangeDeniedException(); + } + + @Override + public void acquirePermissionToRemove(OrchestratorContext context, ApplicationApi applicationApi) throws HostStateChangeDeniedException { + throw newHostStateChangeDeniedException(); + } + + @Override + public void releaseSuspensionGrant( + OrchestratorContext context, ApplicationInstance applicationInstance, + HostName hostName, + ApplicationLock hostStatusRegistry) throws HostStateChangeDeniedException { + throw newHostStateChangeDeniedException(); + } + + private static HostStateChangeDeniedException newHostStateChangeDeniedException() { + return new HostStateChangeDeniedException( + new HostName("some-host"), + "impossible-policy", + "This policy rejects all requests"); + } + } + + @Test + void throws_409_when_request_rejected_by_policies() throws IOException { + HostRequestHandler handler = createHandler(alwaysRejectOrchestrator); + + HttpResponse response = executeRequest(handler, Method.PUT, "/orchestrator/v1/hosts/hostname/suspended", null); + assertEquals(409, response.getStatus()); + } + + @Test + void patch_state_may_throw_bad_request() throws IOException { + Orchestrator orchestrator = mock(Orchestrator.class); + HostRequestHandler handler = createHandler(orchestrator); + + PatchHostRequest request = new PatchHostRequest(); + request.state = "bad state"; + + HttpResponse response = executeRequest(handler, Method.PATCH, "/orchestrator/v1/hosts/hostname", request); + assertEquals(400, response.getStatus()); + } + + @Test + void patch_works() throws OrchestrationException, IOException { + Orchestrator orchestrator = mock(Orchestrator.class); + HostRequestHandler handler = createHandler(orchestrator); + + String hostNameString = "hostname"; + PatchHostRequest request = new PatchHostRequest(); + request.state = "NO_REMARKS"; + + 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 + void patch_handles_exception_in_orchestrator() throws OrchestrationException, IOException { + Orchestrator orchestrator = mock(Orchestrator.class); + 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); + HttpResponse httpResponse = executeRequest(handler, Method.PATCH, "/orchestrator/v1/hosts/hostname", request); + assertEquals(500, httpResponse.getStatus()); + } + + @Test + void getHost_works() throws Exception { + Orchestrator orchestrator = mock(Orchestrator.class); + HostRequestHandler handler = createHandler(orchestrator); + + HostName hostName = new HostName("hostname"); + + ServiceInstance serviceInstance = new ServiceInstance( + new ConfigId("configId"), + hostName, + ServiceStatus.UP); + ServiceCluster serviceCluster = new ServiceCluster( + new ClusterId("clusterId"), + new ServiceType("serviceType"), + Collections.singleton(serviceInstance)); + serviceInstance.setServiceCluster(serviceCluster); + + Host host = new Host( + hostName, + HostInfo.createSuspended(HostStatus.ALLOWED_TO_BE_DOWN, Instant.EPOCH), + new ApplicationInstanceReference( + new TenantId("tenantId"), + new ApplicationInstanceId("applicationId")), + Collections.singletonList(serviceInstance)); + when(orchestrator.getHost(hostName)).thenReturn(host); + + 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()); + assertEquals(1, response.services().size()); + assertEquals("clusterId", response.services().get(0).clusterId); + assertEquals("configId", response.services().get(0).configId); + assertEquals("UP", response.services().get(0).serviceStatus); + assertEquals("serviceType", response.services().get(0).serviceType); + } + + @Test + 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)); + + 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 parseResponseContent(HostRequestHandler handler, HttpResponse response, Class 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 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 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 executeRequest(String path, TypeReference 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 responses = webTarget.request(MediaType.APPLICATION_JSON_TYPE) - .get(new GenericType>() {}); - 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>() {}); - assertEquals(1, responses.size()); - assertEquals(RESOURCE_2, responses.iterator().next()); - } - - private String servicesXml() { - return "\n" + - " \n" + - " \n" + - " \n" + - " 10\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - " com.yahoo.vespa.orchestrator.resources.appsuspension\n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n"; - } - -} 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/host/HostResourceTest.java deleted file mode 100644 index d056c3730fd..00000000000 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/host/HostResourceTest.java +++ /dev/null @@ -1,342 +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.host; - -import com.google.common.util.concurrent.UncheckedTimeoutException; -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.test.TestTimer; -import com.yahoo.vespa.applicationmodel.ApplicationInstance; -import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; -import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; -import com.yahoo.vespa.applicationmodel.ClusterId; -import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceCluster; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.applicationmodel.ServiceStatus; -import com.yahoo.vespa.applicationmodel.ServiceType; -import com.yahoo.vespa.applicationmodel.TenantId; -import com.yahoo.vespa.curator.mock.MockCurator; -import com.yahoo.vespa.flags.InMemoryFlagSource; -import com.yahoo.vespa.orchestrator.DummyAntiServiceMonitor; -import com.yahoo.vespa.orchestrator.Host; -import com.yahoo.vespa.orchestrator.HostNameNotFoundException; -import com.yahoo.vespa.orchestrator.OrchestrationException; -import com.yahoo.vespa.orchestrator.Orchestrator; -import com.yahoo.vespa.orchestrator.OrchestratorContext; -import com.yahoo.vespa.orchestrator.OrchestratorImpl; -import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactoryMock; -import com.yahoo.vespa.orchestrator.model.ApplicationApi; -import com.yahoo.vespa.orchestrator.model.ApplicationApiFactory; -import com.yahoo.vespa.orchestrator.policy.HostStateChangeDeniedException; -import com.yahoo.vespa.orchestrator.policy.Policy; -import com.yahoo.vespa.orchestrator.policy.SuspensionReasons; -import com.yahoo.vespa.orchestrator.restapi.wire.GetHostResponse; -import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostRequest; -import com.yahoo.vespa.orchestrator.restapi.wire.PatchHostResponse; -import com.yahoo.vespa.orchestrator.restapi.wire.UpdateHostResponse; -import com.yahoo.vespa.orchestrator.status.ApplicationLock; -import com.yahoo.vespa.orchestrator.status.HostInfo; -import com.yahoo.vespa.orchestrator.status.HostStatus; -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 java.time.Clock; -import java.time.Instant; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * @author hakonhall - */ -public class HostResourceTest { - 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"); - private static final ApplicationInstanceId APPLICATION_INSTANCE_ID = new ApplicationInstanceId("applicationId"); - private static final ServiceMonitor serviceMonitor = mock(ServiceMonitor.class); - private static final StatusService EVERY_HOST_IS_UP_HOST_STATUS_SERVICE = new ZkStatusService( - new MockCurator(), mock(Metric.class), new TestTimer(), new DummyAntiServiceMonitor()); - private static final ApplicationApiFactory applicationApiFactory = new ApplicationApiFactory(3, clock); - - static { - when(serviceMonitor.getApplication(any(HostName.class))) - .thenReturn(Optional.of( - new ApplicationInstance( - TENANT_ID, - APPLICATION_INSTANCE_ID, - Set.of()))); - } - - private static final ServiceMonitor alwaysEmptyServiceMonitor = new ServiceMonitor() { - private final ServiceModel emptyServiceModel = new ServiceModel(Map.of()); - - @Override - public ServiceModel getServiceModelSnapshot() { - return emptyServiceModel; - } - }; - - private static class AlwaysAllowPolicy implements Policy { - @Override - public SuspensionReasons grantSuspensionRequest(OrchestratorContext context, ApplicationApi applicationApi) { - return SuspensionReasons.nothingNoteworthy(); - } - - @Override - public void releaseSuspensionGrant(OrchestratorContext context, ApplicationApi application) { - } - - @Override - public void acquirePermissionToRemove(OrchestratorContext context, ApplicationApi applicationApi) { - } - - @Override - public void releaseSuspensionGrant( - OrchestratorContext context, ApplicationInstance applicationInstance, - HostName hostName, - ApplicationLock hostStatusRegistry) { - } - } - - private final OrchestratorImpl alwaysAllowOrchestrator = createAlwaysAllowOrchestrator(clock); - private final OrchestratorImpl hostNotFoundOrchestrator = createHostNotFoundOrchestrator(clock); - private final UriInfo uriInfo = mock(UriInfo.class); - - @Before - public void setUp() { - when(clock.instant()).thenReturn(Instant.now()); - } - - public static OrchestratorImpl createAlwaysAllowOrchestrator(Clock clock) { - return new OrchestratorImpl( - new AlwaysAllowPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - serviceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - new InMemoryFlagSource()); - } - - public static OrchestratorImpl createHostNotFoundOrchestrator(Clock clock) { - return new OrchestratorImpl( - new AlwaysAllowPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - alwaysEmptyServiceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - new InMemoryFlagSource()); - } - - public static OrchestratorImpl createAlwaysRejectResolver(Clock clock) { - return new OrchestratorImpl( - new HostResourceTest.AlwaysFailPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - serviceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - new InMemoryFlagSource()); - } - - @Test - public void returns_200_on_success() { - HostResource hostResource = - new HostResource(alwaysAllowOrchestrator, uriInfo); - - final String hostName = "hostname"; - - UpdateHostResponse response = hostResource.suspend(hostName); - - assertEquals(hostName, response.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()); - } - } - - private static class AlwaysFailPolicy implements Policy { - @Override - public SuspensionReasons grantSuspensionRequest(OrchestratorContext context, ApplicationApi applicationApi) throws HostStateChangeDeniedException { - throw newHostStateChangeDeniedException(); - } - - @Override - public void releaseSuspensionGrant(OrchestratorContext context, ApplicationApi application) throws HostStateChangeDeniedException { - throw newHostStateChangeDeniedException(); - } - - @Override - public void acquirePermissionToRemove(OrchestratorContext context, ApplicationApi applicationApi) throws HostStateChangeDeniedException { - throw newHostStateChangeDeniedException(); - } - - @Override - public void releaseSuspensionGrant( - OrchestratorContext context, ApplicationInstance applicationInstance, - HostName hostName, - ApplicationLock hostStatusRegistry) throws HostStateChangeDeniedException { - throw newHostStateChangeDeniedException(); - } - - private static HostStateChangeDeniedException newHostStateChangeDeniedException() { - return new HostStateChangeDeniedException( - new HostName("some-host"), - "impossible-policy", - "This policy rejects all requests"); - } - } - - @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()); - - try { - HostResource hostResource = new HostResource(alwaysRejectResolver, uriInfo); - hostResource.suspend("hostname"); - fail(); - } catch (WebApplicationException w) { - assertEquals(409, w.getResponse().getStatus()); - } - } - - @Test(expected = BadRequestException.class) - public void patch_state_may_throw_bad_request() { - Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); - - String hostNameString = "hostname"; - PatchHostRequest request = new PatchHostRequest(); - request.state = "bad state"; - - hostResource.patch(hostNameString, request); - } - - @Test - public void patch_works() throws OrchestrationException { - Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); - - String hostNameString = "hostname"; - PatchHostRequest request = new PatchHostRequest(); - request.state = "NO_REMARKS"; - - PatchHostResponse response = hostResource.patch(hostNameString, request); - 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 { - Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); - - 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); - } - - @Test - public void getHost_works() throws Exception { - Orchestrator orchestrator = mock(Orchestrator.class); - HostResource hostResource = new HostResource(orchestrator, uriInfo); - - 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, - ServiceStatus.UP); - ServiceCluster serviceCluster = new ServiceCluster( - new ClusterId("clusterId"), - new ServiceType("serviceType"), - Collections.singleton(serviceInstance)); - serviceInstance.setServiceCluster(serviceCluster); - - Host host = new Host( - hostName, - HostInfo.createSuspended(HostStatus.ALLOWED_TO_BE_DOWN, Instant.EPOCH), - new ApplicationInstanceReference( - new TenantId("tenantId"), - 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()); - assertEquals("hostname", response.hostname()); - assertEquals("ALLOWED_TO_BE_DOWN", response.state()); - assertEquals("1970-01-01T00:00:00Z", response.suspendedSince()); - assertEquals(1, response.services().size()); - assertEquals("clusterId", response.services().get(0).clusterId); - assertEquals("configId", response.services().get(0).configId); - assertEquals("UP", response.services().get(0).serviceStatus); - assertEquals("serviceType", response.services().get(0).serviceType); - } - - @Test - public void throws_409_on_timeout() throws HostNameNotFoundException, HostStateChangeDeniedException { - 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()); - } - } - -} 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 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 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 -- cgit v1.2.3