diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-09-02 16:03:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-02 16:03:55 +0200 |
commit | 052bc52450372ae1a69c12c7fa1745a5c0b3c7cc (patch) | |
tree | d09f74acc7d1b974ded9d77d6dce0296097bb9ac /controller-server | |
parent | 25931432f58e53fb90068c143f61ee638dc54d69 (diff) | |
parent | 6eb01a503b0ecde0b6c740deaadf26b9c222ff19 (diff) |
Merge pull request #18955 from vespa-engine/bjorncs/controller-service-dump-api
Add controller API to request service dump
Diffstat (limited to 'controller-server')
2 files changed, 81 insertions, 1 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index e2f456b33f0..95d338d6da0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -51,6 +51,7 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbi import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.RestartAction; import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ServiceInfo; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; +import com.yahoo.vespa.hosted.controller.api.identifiers.Hostname; import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId; import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles; import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; @@ -60,6 +61,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServ import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log; import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter; +import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; @@ -143,12 +145,12 @@ import java.util.OptionalLong; import java.util.Scanner; import java.util.StringJoiner; import java.util.logging.Level; -import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; import static com.yahoo.jdisc.Response.Status.CONFLICT; +import static com.yahoo.yolean.Exceptions.uncheck; import static java.util.Map.Entry.comparingByKey; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -265,6 +267,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/content/{*}")) return content(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.getRest(), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return supportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap()); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/node/{node}/service-dump")) return getServiceDump(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("node"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/metrics")) return metrics(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("endpointId"))); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return getGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); @@ -315,6 +318,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/suspend")) return suspend(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return allowSupportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/node/{node}/service-dump")) return requestServiceDump(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("node"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/deploy")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); @@ -2048,6 +2052,71 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { controller.applications().reachableContentClustersByZone(deployments))); } + private HttpResponse requestServiceDump(String tenant, String application, String instance, String environment, + String region, String hostname, HttpRequest request) { + NodeRepository nodeRepository = controller.serviceRegistry().configServer().nodeRepository(); + ZoneId zone = requireZone(environment, region); + + // Check that no other service dump is in progress + Slime report = getReport(nodeRepository, zone, tenant, application, instance, hostname).orElse(null); + if (report != null) { + Cursor cursor = report.get(); + // Note: same behaviour for both value '0' and missing value. + if (cursor.field("failedAt").asLong() == 0 && cursor.field("completedAt").asLong() == 0) { + throw new IllegalArgumentException("Service dump already in progress for " + cursor.field("configId").asString()); + } + } + Slime requestPayload; + try { + requestPayload = SlimeUtils.jsonToSlimeOrThrow(request.getData().readAllBytes()); + } catch (Exception e) { + throw new IllegalArgumentException("Missing or invalid JSON in request content", e); + } + Cursor requestPayloadCursor = requestPayload.get(); + String configId = requestPayloadCursor.field("configId").asString(); + long expiresAt = requestPayloadCursor.field("expiresAt").asLong(); + if (configId.isEmpty()) { + throw new IllegalArgumentException("Missing configId"); + } + Slime dumpRequest = new Slime(); + Cursor dumpRequestCursor = dumpRequest.setObject(); + dumpRequestCursor.setLong("createdMillis", controller.clock().millis()); + dumpRequestCursor.setString("configId", configId); + if (expiresAt > 0) { + dumpRequestCursor.setLong("expiresAt", expiresAt); + } + var reportsUpdate = Map.of("serviceDump", new String(uncheck(() -> SlimeUtils.toJsonBytes(dumpRequest)))); + nodeRepository.updateReports(zone, hostname, reportsUpdate); + return new MessageResponse("Request created"); + } + + private HttpResponse getServiceDump(String tenant, String application, String instance, String environment, + String region, String hostname, HttpRequest request) { + NodeRepository nodeRepository = controller.serviceRegistry().configServer().nodeRepository(); + ZoneId zone = requireZone(environment, region); + Slime report = getReport(nodeRepository, zone, tenant, application, instance, hostname) + .orElseThrow(() -> new NotExistsException("No service dump for node " + hostname)); + return new SlimeJsonResponse(report); + } + + private Optional<Slime> getReport(NodeRepository nodeRepository, ZoneId zone, String tenant, + String application, String instance, String hostname) { + Node node; + try { + node = nodeRepository.getNode(zone, hostname); + } catch (IllegalArgumentException e) { + throw new NotExistsException(new Hostname(hostname)); + } + ApplicationId app = ApplicationId.from(tenant, application, instance); + ApplicationId owner = node.owner().orElseThrow(() -> new IllegalArgumentException("Node has no owner")); + if (!app.equals(owner)) { + throw new IllegalArgumentException("Node is not owned by " + app.toFullString()); + } + String json = node.reports().get("serviceDump"); + if (json == null) return Optional.empty(); + return Optional.of(SlimeUtils.jsonToSlimeOrThrow(json)); + } + private static SourceRevision toSourceRevision(Inspector object) { if (!object.field("repository").valid() || !object.field("branch").valid() || diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 8a182ce5eca..ed6fc776e8d 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -611,6 +611,17 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID), "{\"enabled\":true,\"clusters\":[{\"name\":\"cluster\",\"pending\":[{\"type\":\"type\",\"requiredGeneration\":100}],\"ready\":[{\"type\":\"type\",\"readyAtMillis\":345,\"startedAtMillis\":456,\"endedAtMillis\":567,\"state\":\"failed\",\"message\":\"(#`д´)ノ\",\"progress\":0.1}]}]}"); + // POST to request a service dump + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/node/host-tenant1:application1:instance1-prod.us-central-1/service-dump", POST) + .userIdentity(HOSTED_VESPA_OPERATOR) + .data("{\"configId\":\"default/container.1\"}"), + "{\"message\":\"Request created\"}"); + + // GET to get status of service dump + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/node/host-tenant1:application1:instance1-prod.us-central-1/service-dump", GET) + .userIdentity(HOSTED_VESPA_OPERATOR), + "{\"createdMillis\":" + tester.controller().clock().millis() + ",\"configId\":\"default/container.1\"}"); + // POST a 'restart application' command tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/restart", POST) .userIdentity(USER_ID), |