From f7653ac0beac7b658fe642ef683f8f3f7b4d5f20 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Fri, 26 Mar 2021 15:34:27 +0100 Subject: Decouple orchestrator resources into separate rest-api definitions --- .../main/resources/configserver-app/services.xml | 26 +- .../resources/ApplicationSuspensionResource.java | 122 ------- .../orchestrator/resources/HealthResource.java | 84 ----- .../vespa/orchestrator/resources/HostResource.java | 191 ---------- .../resources/HostSuspensionResource.java | 72 ---- .../orchestrator/resources/InstanceResource.java | 167 --------- .../ApplicationSuspensionResource.java | 122 +++++++ .../resources/health/HealthResource.java | 86 +++++ .../orchestrator/resources/host/HostResource.java | 192 ++++++++++ .../hostsuspension/HostSuspensionResource.java | 72 ++++ .../resources/instance/InstanceResource.java | 168 +++++++++ .../ApplicationSuspensionResourceTest.java | 166 --------- .../orchestrator/resources/HostResourceTest.java | 396 -------------------- .../resources/InstanceResourceTest.java | 91 ----- .../ApplicationSuspensionResourceTest.java | 168 +++++++++ .../resources/host/HostResourceTest.java | 397 +++++++++++++++++++++ .../resources/instance/InstanceResourceTest.java | 92 +++++ 17 files changed, 1321 insertions(+), 1291 deletions(-) delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionResource.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HealthResource.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostResource.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionResource.java delete mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceResource.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/health/HealthResource.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/hostsuspension/HostSuspensionResource.java create mode 100644 orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResource.java delete mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionResourceTest.java delete mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostResourceTest.java delete mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceResourceTest.java create mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResourceTest.java create mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/host/HostResourceTest.java create mode 100644 orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResourceTest.java diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml index 51fc26bdbaa..dca70987bdd 100644 --- a/configserver/src/main/resources/configserver-app/services.xml +++ b/configserver/src/main/resources/configserver-app/services.xml @@ -63,8 +63,30 @@ - - + + + com.yahoo.vespa.orchestrator.resources.appsuspension + + + + + com.yahoo.vespa.orchestrator.resources.health + + + + + com.yahoo.vespa.orchestrator.resources.host + + + + + com.yahoo.vespa.orchestrator.resources.hostsuspension + + + + + com.yahoo.vespa.orchestrator.resources.instance + diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionResource.java deleted file mode 100644 index 6a118f9d606..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/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; - -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.container.jaxrs.annotation.Component; -import java.util.logging.Level; -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.Logger; -import java.util.stream.Collectors; - -/** - * @author smorgrav - */ -@Path(ApplicationSuspensionApi.PATH_PREFIX) -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/HealthResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HealthResource.java deleted file mode 100644 index 47f2f063540..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HealthResource.java +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2019 Oath Inc. 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.jaxrs.annotation.Component; -import com.yahoo.vespa.applicationmodel.ServiceStatusInfo; -import com.yahoo.vespa.orchestrator.restapi.wire.ApplicationReferenceList; -import com.yahoo.vespa.orchestrator.restapi.wire.UrlReference; -import com.yahoo.vespa.service.manager.HealthMonitorApi; -import com.yahoo.vespa.service.monitor.ServiceId; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * @author hakonhall - */ -@Path("/v1/health") -public class HealthResource { - private final UriInfo uriInfo; - private final HealthMonitorApi healthMonitorApi; - - @Inject - public HealthResource(@Context UriInfo uriInfo, @Component HealthMonitorApi healthMonitorApi) { - this.uriInfo = uriInfo; - this.healthMonitorApi = healthMonitorApi; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - public ApplicationReferenceList getAllInstances() { - List applications = new ArrayList<>(healthMonitorApi.getMonitoredApplicationIds()); - applications.sort(Comparator.comparing(ApplicationId::serializedForm)); - - ApplicationReferenceList list = new ApplicationReferenceList(); - list.applicationList = applications.stream().map(applicationId -> { - UrlReference reference = new UrlReference(); - reference.url = uriInfo.getBaseUriBuilder() - .path(HealthResource.class) - .path(applicationId.serializedForm()) - .build() - .toString(); - return reference; - }).collect(Collectors.toList()); - - return list; - } - - @GET - @Path("/{applicationId}") - @Produces(MediaType.APPLICATION_JSON) - public ApplicationServices getInstance(@PathParam("applicationId") String applicationIdString) { - ApplicationId applicationId = ApplicationId.fromSerializedForm(applicationIdString); - - Map services = healthMonitorApi.getServices(applicationId); - - List serviceResources = services.entrySet().stream().map(entry -> { - ServiceResource serviceResource = new ServiceResource(); - serviceResource.clusterId = entry.getKey().getClusterId(); - serviceResource.serviceType = entry.getKey().getServiceType(); - serviceResource.configId = entry.getKey().getConfigId(); - serviceResource.serviceStatusInfo = entry.getValue(); - return serviceResource; - }) - .sorted(Comparator.comparing(resource -> resource.serviceType.s())) - .collect(Collectors.toList()); - - ApplicationServices applicationServices = new ApplicationServices(); - applicationServices.services = serviceResources; - return applicationServices; - } - -} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostResource.java deleted file mode 100644 index 9ed9e1b9063..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostResource.java +++ /dev/null @@ -1,191 +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; - -import com.google.common.util.concurrent.UncheckedTimeoutException; -import com.yahoo.container.jaxrs.annotation.Component; -import java.util.logging.Level; -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.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.Logger; -import java.util.stream.Collectors; - -/** - * @author oyving - */ -@Path(HostApi.PATH_PREFIX) -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/HostSuspensionResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionResource.java deleted file mode 100644 index 3d60d40dfcf..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/HostSuspensionResource.java +++ /dev/null @@ -1,72 +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; - -import com.google.common.util.concurrent.UncheckedTimeoutException; -import com.yahoo.container.jaxrs.annotation.Component; -import java.util.logging.Level; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.orchestrator.BatchHostNameNotFoundException; -import com.yahoo.vespa.orchestrator.BatchInternalErrorException; -import com.yahoo.vespa.orchestrator.Orchestrator; -import com.yahoo.vespa.orchestrator.policy.BatchHostStateChangeDeniedException; -import com.yahoo.vespa.orchestrator.restapi.HostSuspensionApi; -import com.yahoo.vespa.orchestrator.restapi.wire.BatchOperationResult; - -import javax.inject.Inject; -import javax.ws.rs.Path; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * @author hakonhall - */ -@Path(HostSuspensionApi.PATH_PREFIX) -public class HostSuspensionResource implements HostSuspensionApi { - - private static final Logger log = Logger.getLogger(HostSuspensionResource.class.getName()); - - private final Orchestrator orchestrator; - - @Inject - public HostSuspensionResource(@Component Orchestrator orchestrator) { - this.orchestrator = orchestrator; - } - - @Override - public BatchOperationResult suspendAll(String parentHostnameString, List hostnamesAsStrings) { - HostName parentHostname = new HostName(parentHostnameString); - List hostnames = hostnamesAsStrings.stream().map(HostName::new).collect(Collectors.toList()); - try { - orchestrator.suspendAll(parentHostname, hostnames); - } catch (BatchHostStateChangeDeniedException e) { - log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); - throw createWebApplicationException(e.getMessage(), Response.Status.CONFLICT); - } catch (UncheckedTimeoutException e) { - log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); - throw createWebApplicationException(e.getMessage(), Response.Status.CONFLICT); - } catch (BatchHostNameNotFoundException e) { - log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); - // Note that we're returning BAD_REQUEST instead of NOT_FOUND because the resource identified - // by the URL path was found. It's one of the hostnames in the request it failed to find. - throw createWebApplicationException(e.getMessage(), Response.Status.BAD_REQUEST); - } catch (BatchInternalErrorException e) { - log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); - throw createWebApplicationException(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); - } - log.log(Level.FINE, "Suspended " + hostnames + " with parent " + parentHostname); - return BatchOperationResult.successResult(); - } - - private WebApplicationException createWebApplicationException(String errorMessage, Response.Status status) { - return new WebApplicationException( - Response.status(status) - .entity(new BatchOperationResult(errorMessage)) - .type(MediaType.APPLICATION_JSON_TYPE) - .build()); - } - -} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceResource.java deleted file mode 100644 index 1cf2a2a4965..00000000000 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/InstanceResource.java +++ /dev/null @@ -1,167 +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; - -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.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("/v1/instances") -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/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java new file mode 100644 index 00000000000..361b1f5e361 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResource.java @@ -0,0 +1,122 @@ +// 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/health/HealthResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/health/HealthResource.java new file mode 100644 index 00000000000..3265ac75642 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/health/HealthResource.java @@ -0,0 +1,86 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.resources.health; + +import com.google.inject.Inject; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.vespa.applicationmodel.ServiceStatusInfo; +import com.yahoo.vespa.orchestrator.resources.ApplicationServices; +import com.yahoo.vespa.orchestrator.resources.ServiceResource; +import com.yahoo.vespa.orchestrator.restapi.wire.ApplicationReferenceList; +import com.yahoo.vespa.orchestrator.restapi.wire.UrlReference; +import com.yahoo.vespa.service.manager.HealthMonitorApi; +import com.yahoo.vespa.service.monitor.ServiceId; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author hakonhall + */ +@Path("") +public class HealthResource { + private final UriInfo uriInfo; + private final HealthMonitorApi healthMonitorApi; + + @Inject + public HealthResource(@Context UriInfo uriInfo, @Component HealthMonitorApi healthMonitorApi) { + this.uriInfo = uriInfo; + this.healthMonitorApi = healthMonitorApi; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public ApplicationReferenceList getAllInstances() { + List applications = new ArrayList<>(healthMonitorApi.getMonitoredApplicationIds()); + applications.sort(Comparator.comparing(ApplicationId::serializedForm)); + + ApplicationReferenceList list = new ApplicationReferenceList(); + list.applicationList = applications.stream().map(applicationId -> { + UrlReference reference = new UrlReference(); + reference.url = uriInfo.getBaseUriBuilder() + .path(HealthResource.class) + .path(applicationId.serializedForm()) + .build() + .toString(); + return reference; + }).collect(Collectors.toList()); + + return list; + } + + @GET + @Path("/{applicationId}") + @Produces(MediaType.APPLICATION_JSON) + public ApplicationServices getInstance(@PathParam("applicationId") String applicationIdString) { + ApplicationId applicationId = ApplicationId.fromSerializedForm(applicationIdString); + + Map services = healthMonitorApi.getServices(applicationId); + + List serviceResources = services.entrySet().stream().map(entry -> { + ServiceResource serviceResource = new ServiceResource(); + serviceResource.clusterId = entry.getKey().getClusterId(); + serviceResource.serviceType = entry.getKey().getServiceType(); + serviceResource.configId = entry.getKey().getConfigId(); + serviceResource.serviceStatusInfo = entry.getValue(); + return serviceResource; + }) + .sorted(Comparator.comparing(resource -> resource.serviceType.s())) + .collect(Collectors.toList()); + + ApplicationServices applicationServices = new ApplicationServices(); + applicationServices.services = serviceResources; + return applicationServices; + } + +} 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 new file mode 100644 index 00000000000..c55eeeef069 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/host/HostResource.java @@ -0,0 +1,192 @@ +// 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/hostsuspension/HostSuspensionResource.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/hostsuspension/HostSuspensionResource.java new file mode 100644 index 00000000000..4cb22792237 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/hostsuspension/HostSuspensionResource.java @@ -0,0 +1,72 @@ +// 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.hostsuspension; + +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.BatchHostNameNotFoundException; +import com.yahoo.vespa.orchestrator.BatchInternalErrorException; +import com.yahoo.vespa.orchestrator.Orchestrator; +import com.yahoo.vespa.orchestrator.policy.BatchHostStateChangeDeniedException; +import com.yahoo.vespa.orchestrator.restapi.HostSuspensionApi; +import com.yahoo.vespa.orchestrator.restapi.wire.BatchOperationResult; + +import javax.inject.Inject; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * @author hakonhall + */ +@Path("") +public class HostSuspensionResource implements HostSuspensionApi { + + private static final Logger log = Logger.getLogger(HostSuspensionResource.class.getName()); + + private final Orchestrator orchestrator; + + @Inject + public HostSuspensionResource(@Component Orchestrator orchestrator) { + this.orchestrator = orchestrator; + } + + @Override + public BatchOperationResult suspendAll(String parentHostnameString, List hostnamesAsStrings) { + HostName parentHostname = new HostName(parentHostnameString); + List hostnames = hostnamesAsStrings.stream().map(HostName::new).collect(Collectors.toList()); + try { + orchestrator.suspendAll(parentHostname, hostnames); + } catch (BatchHostStateChangeDeniedException e) { + log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); + throw createWebApplicationException(e.getMessage(), Response.Status.CONFLICT); + } catch (UncheckedTimeoutException e) { + log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); + throw createWebApplicationException(e.getMessage(), Response.Status.CONFLICT); + } catch (BatchHostNameNotFoundException e) { + log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); + // Note that we're returning BAD_REQUEST instead of NOT_FOUND because the resource identified + // by the URL path was found. It's one of the hostnames in the request it failed to find. + throw createWebApplicationException(e.getMessage(), Response.Status.BAD_REQUEST); + } catch (BatchInternalErrorException e) { + log.log(Level.FINE, "Failed to suspend nodes " + hostnames + " with parent host " + parentHostname, e); + throw createWebApplicationException(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); + } + log.log(Level.FINE, "Suspended " + hostnames + " with parent " + parentHostname); + return BatchOperationResult.successResult(); + } + + private WebApplicationException createWebApplicationException(String errorMessage, Response.Status status) { + return new WebApplicationException( + Response.status(status) + .entity(new BatchOperationResult(errorMessage)) + .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 new file mode 100644 index 00000000000..742f7d6bbd7 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResource.java @@ -0,0 +1,168 @@ +// 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/ApplicationSuspensionResourceTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionResourceTest.java deleted file mode 100644 index 4035e29d91a..00000000000 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionResourceTest.java +++ /dev/null @@ -1,166 +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; - -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" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n"; - } - -} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostResourceTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostResourceTest.java deleted file mode 100644 index 7f4ef1a336c..00000000000 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/HostResourceTest.java +++ /dev/null @@ -1,396 +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; - -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.BatchHostNameNotFoundException; -import com.yahoo.vespa.orchestrator.BatchInternalErrorException; -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.BatchHostStateChangeDeniedException; -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.BatchOperationResult; -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.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -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 final InMemoryFlagSource flagSource = new InMemoryFlagSource(); - - 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 = new OrchestratorImpl( - new AlwaysAllowPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - serviceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - flagSource); - - private final OrchestratorImpl hostNotFoundOrchestrator = new OrchestratorImpl( - new AlwaysAllowPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - alwaysEmptyServiceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - flagSource); - - private final UriInfo uriInfo = mock(UriInfo.class); - - @Before - public void setUp() { - when(clock.instant()).thenReturn(Instant.now()); - } - - @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 returns_200_on_success_batch() { - HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(alwaysAllowOrchestrator); - BatchOperationResult response = hostSuspensionResource.suspendAll("parentHostname", - Arrays.asList("hostname1", "hostname2")); - assertTrue(response.success()); - } - - @Test - public void returns_200_empty_batch() { - HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(alwaysAllowOrchestrator); - BatchOperationResult response = hostSuspensionResource.suspendAll("parentHostname", List.of()); - assertTrue(response.success()); - } - - @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()); - } - } - - // Note: Missing host is 404 for a single-host, but 400 for multi-host (batch). - // This is so because the hostname is part of the URL path for single-host, while the - // hostnames are part of the request body for multi-host. - @Test - public void throws_400_when_host_unknown_for_batch() { - try { - HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(hostNotFoundOrchestrator); - hostSuspensionResource.suspendAll("parentHostname", Arrays.asList("hostname1", "hostname2")); - fail(); - } catch (WebApplicationException w) { - assertEquals(400, 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, - flagSource); - - try { - HostResource hostResource = new HostResource(alwaysRejectResolver, uriInfo); - hostResource.suspend("hostname"); - fail(); - } catch (WebApplicationException w) { - assertEquals(409, w.getResponse().getStatus()); - } - } - - @Test - public void throws_409_when_request_rejected_by_policies_for_batch() { - final OrchestratorImpl alwaysRejectResolver = new OrchestratorImpl( - new AlwaysFailPolicy(), - new ClusterControllerClientFactoryMock(), - EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, - serviceMonitor, - SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, - clock, - applicationApiFactory, - flagSource); - - try { - HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(alwaysRejectResolver); - hostSuspensionResource.suspendAll("parentHostname", Arrays.asList("hostname1", "hostname2")); - 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()); - } - } - - @Test - public void throws_409_on_suspendAll_timeout() throws BatchHostStateChangeDeniedException, BatchHostNameNotFoundException, BatchInternalErrorException { - Orchestrator orchestrator = mock(Orchestrator.class); - doThrow(new UncheckedTimeoutException("Timeout Message")).when(orchestrator).suspendAll(any(), any()); - - try { - HostSuspensionResource resource = new HostSuspensionResource(orchestrator); - resource.suspendAll("parenthost", Arrays.asList("h1", "h2", "h3")); - fail(); - } catch (WebApplicationException w) { - assertEquals(409, w.getResponse().getStatus()); - } - } -} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceResourceTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceResourceTest.java deleted file mode 100644 index ef6e26d2e99..00000000000 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/InstanceResourceTest.java +++ /dev/null @@ -1,91 +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; - -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.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 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 new file mode 100644 index 00000000000..a7514de5acd --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/appsuspension/ApplicationSuspensionResourceTest.java @@ -0,0 +1,168 @@ +// 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 new file mode 100644 index 00000000000..b86dfb71e43 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/host/HostResourceTest.java @@ -0,0 +1,397 @@ +// 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.BatchHostNameNotFoundException; +import com.yahoo.vespa.orchestrator.BatchInternalErrorException; +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.BatchHostStateChangeDeniedException; +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.resources.hostsuspension.HostSuspensionResource; +import com.yahoo.vespa.orchestrator.restapi.wire.BatchOperationResult; +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.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +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 final InMemoryFlagSource flagSource = new InMemoryFlagSource(); + + 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 = new OrchestratorImpl( + new AlwaysAllowPolicy(), + new ClusterControllerClientFactoryMock(), + EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, + serviceMonitor, + SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, + clock, + applicationApiFactory, + flagSource); + + private final OrchestratorImpl hostNotFoundOrchestrator = new OrchestratorImpl( + new AlwaysAllowPolicy(), + new ClusterControllerClientFactoryMock(), + EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, + alwaysEmptyServiceMonitor, + SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, + clock, + applicationApiFactory, + flagSource); + + private final UriInfo uriInfo = mock(UriInfo.class); + + @Before + public void setUp() { + when(clock.instant()).thenReturn(Instant.now()); + } + + @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 returns_200_on_success_batch() { + HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(alwaysAllowOrchestrator); + BatchOperationResult response = hostSuspensionResource.suspendAll("parentHostname", + Arrays.asList("hostname1", "hostname2")); + assertTrue(response.success()); + } + + @Test + public void returns_200_empty_batch() { + HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(alwaysAllowOrchestrator); + BatchOperationResult response = hostSuspensionResource.suspendAll("parentHostname", List.of()); + assertTrue(response.success()); + } + + @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()); + } + } + + // Note: Missing host is 404 for a single-host, but 400 for multi-host (batch). + // This is so because the hostname is part of the URL path for single-host, while the + // hostnames are part of the request body for multi-host. + @Test + public void throws_400_when_host_unknown_for_batch() { + try { + HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(hostNotFoundOrchestrator); + hostSuspensionResource.suspendAll("parentHostname", Arrays.asList("hostname1", "hostname2")); + fail(); + } catch (WebApplicationException w) { + assertEquals(400, 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, + flagSource); + + try { + HostResource hostResource = new HostResource(alwaysRejectResolver, uriInfo); + hostResource.suspend("hostname"); + fail(); + } catch (WebApplicationException w) { + assertEquals(409, w.getResponse().getStatus()); + } + } + + @Test + public void throws_409_when_request_rejected_by_policies_for_batch() { + final OrchestratorImpl alwaysRejectResolver = new OrchestratorImpl( + new AlwaysFailPolicy(), + new ClusterControllerClientFactoryMock(), + EVERY_HOST_IS_UP_HOST_STATUS_SERVICE, + serviceMonitor, + SERVICE_MONITOR_CONVERGENCE_LATENCY_SECONDS, + clock, + applicationApiFactory, + flagSource); + + try { + HostSuspensionResource hostSuspensionResource = new HostSuspensionResource(alwaysRejectResolver); + hostSuspensionResource.suspendAll("parentHostname", Arrays.asList("hostname1", "hostname2")); + 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()); + } + } + + @Test + public void throws_409_on_suspendAll_timeout() throws BatchHostStateChangeDeniedException, BatchHostNameNotFoundException, BatchInternalErrorException { + Orchestrator orchestrator = mock(Orchestrator.class); + doThrow(new UncheckedTimeoutException("Timeout Message")).when(orchestrator).suspendAll(any(), any()); + + try { + HostSuspensionResource resource = new HostSuspensionResource(orchestrator); + resource.suspendAll("parenthost", Arrays.asList("h1", "h2", "h3")); + fail(); + } catch (WebApplicationException w) { + assertEquals(409, w.getResponse().getStatus()); + } + } +} 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 new file mode 100644 index 00000000000..8e2eeb7410d --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/resources/instance/InstanceResourceTest.java @@ -0,0 +1,92 @@ +// 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