All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler Maven / Gradle / Ivy

// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.deployment;

import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.container.jdisc.EmptyResponse;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.restapi.UriBuilder;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
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;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.DelayCause;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.Readiness;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
import com.yahoo.vespa.hosted.controller.deployment.Versions;
import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.yolean.Exceptions;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableMap;

/**
 * This implements the deployment/v1 API which provides information about the status of Vespa platform and
 * application deployments.
 *
 * @author bratseth
 */
@SuppressWarnings("unused") // Injected
public class DeploymentApiHandler extends ThreadedHttpRequestHandler {

    private final Controller controller;

    public DeploymentApiHandler(ThreadedHttpRequestHandler.Context parentCtx, Controller controller) {
        super(parentCtx);
        this.controller = controller;
    }

    @Override
    public HttpResponse handle(HttpRequest request) {
        try {
            return switch (request.getMethod()) {
                case GET -> handleGET(request);
                case OPTIONS -> handleOPTIONS();
                default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
            };
        }
        catch (IllegalArgumentException e) {
            return ErrorResponse.badRequest(Exceptions.toMessageString(e));
        }
        catch (RuntimeException e) {
            return ErrorResponses.logThrowing(request, log, e);
        }
    }

    private HttpResponse handleGET(HttpRequest request) {
        Path path = new Path(request.getUri());
        if (path.matches("/deployment/v1/")) return root(request);
        return ErrorResponse.notFoundError("Nothing at " + path);
    }

    private HttpResponse handleOPTIONS() {
        // We implement this to avoid redirect loops on OPTIONS requests from browsers, but do not really bother
        // spelling out the methods supported at each path, which we should
        EmptyResponse response = new EmptyResponse();
        response.headers().put("Allow", "GET,OPTIONS");
        return response;
    }

    private HttpResponse root(HttpRequest request) {
        Slime slime = new Slime();
        Cursor root = slime.setObject();
        Cursor platformArray = root.setArray("versions");
        var versionStatus = controller.readVersionStatus();
        ApplicationList applications = ApplicationList.from(controller.applications().asList()).withJobs();
        var deploymentStatuses = controller.jobController().deploymentStatuses(applications, versionStatus);
        Map deploymentStatistics = DeploymentStatistics.compute(versionStatus.versions().stream().map(VespaVersion::versionNumber).toList(),
                                                                                               deploymentStatuses)
                                                                                      .stream().collect(toMap(DeploymentStatistics::version, identity()));
        for (VespaVersion version : versionStatus.versions()) {
            Cursor versionObject = platformArray.addObject();
            versionObject.setString("version", version.versionNumber().toString());
            versionObject.setString("confidence", version.confidence().name());
            versionObject.setString("commit", version.releaseCommit());
            versionObject.setLong("date", version.committedAt().toEpochMilli());
            versionObject.setBool("controllerVersion", version.isControllerVersion());
            versionObject.setBool("systemVersion", version.isSystemVersion());

            Cursor configServerArray = versionObject.setArray("configServers");
            for (var nodeVersion : version.nodeVersions()) {
                Cursor configServerObject = configServerArray.addObject();
                configServerObject.setString("hostname", nodeVersion.hostname().value());
            }

            DeploymentStatistics statistics = deploymentStatistics.get(version.versionNumber());
            Cursor failingArray = versionObject.setArray("failingApplications");
            for (Run run : statistics.failingUpgrades()) {
                Cursor applicationObject = failingArray.addObject();
                toSlime(applicationObject, run.id().application(), request);
                applicationObject.setString("failing", run.id().type().jobName());
                applicationObject.setString("status", nameOf(run.status()));
            }

            var statusByInstance = deploymentStatuses.asList().stream()
                                                     .flatMap(status -> status.instanceJobs().keySet().stream()
                                                                              .map(instance -> Map.entry(instance, status)))
                                                     .collect(toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
            var jobsByInstance = statusByInstance.entrySet().stream()
                                                 .collect(toUnmodifiableMap(Map.Entry::getKey,
                                                                            entry -> entry.getValue().instanceJobs().get(entry.getKey())));
            Cursor productionArray = versionObject.setArray("productionApplications");
            statistics.productionSuccesses().stream()
                      .collect(groupingBy(run -> run.id().application(), TreeMap::new, toList()))
                      .forEach((id, runs) -> {
                          Cursor applicationObject = productionArray.addObject();
                          toSlime(applicationObject, id, request);
                          applicationObject.setLong("productionJobs", jobsByInstance.get(id).production().size());
                          applicationObject.setLong("productionSuccesses", runs.size());
                      });

            Cursor runningArray = versionObject.setArray("deployingApplications");
            for (Run run : statistics.runningUpgrade()) {
                Cursor applicationObject = runningArray.addObject();
                toSlime(applicationObject, run.id().application(), request);
                applicationObject.setString("running", run.id().type().jobName());
            }

            Cursor instancesArray = versionObject.setArray("applications");
            Stream.of(statistics.failingUpgrades().stream().map(run -> new RunInfo(run, true)),
                      statistics.otherFailing().stream().map(run -> new RunInfo(run, false)),
                      statistics.runningUpgrade().stream().map(run -> new RunInfo(run, true)),
                      statistics.otherRunning().stream().map(run -> new RunInfo(run, false)),
                      statistics.productionSuccesses().stream().map(run -> new RunInfo(run, true)))
                  .flatMap(identity())
                  .collect(Collectors.groupingBy(run -> run.run.id().application(),
                                                 LinkedHashMap::new, // Put apps with failing and running jobs first.
                                                 groupingBy(run -> run.run.id().type(),
                                                            LinkedHashMap::new,
                                                            toList())))
                  .forEach((instance, runs) -> {
                      var status = statusByInstance.get(instance);
                      var jobsToRun = status.jobsToRun();
                      Cursor instanceObject = instancesArray.addObject();
                      instanceObject.setString("tenant", instance.tenant().value());
                      instanceObject.setString("application", instance.application().value());
                      instanceObject.setString("instance", instance.instance().value());
                      instanceObject.setBool("upgrading", status.application().require(instance.instance()).change().platform().equals(Optional.of(statistics.version())));
                      instanceObject.setBool("pinned", status.application().require(instance.instance()).change().isPlatformPinned());
                      instanceObject.setBool("platformPinned", status.application().require(instance.instance()).change().isPlatformPinned());
                      instanceObject.setBool("revisionPinned", status.application().require(instance.instance()).change().isRevisionPinned());
                      DeploymentStatus.StepStatus stepStatus = status.instanceSteps().get(instance.instance());
                      if (stepStatus != null) { // Instance may not have any steps, i.e. an empty deployment spec has been submitted
                          Readiness platformReadiness = stepStatus.blockedUntil(Change.of(statistics.version()));
                          if (platformReadiness.cause() == DelayCause.changeBlocked)
                              instanceObject.setLong("blockedUntil", platformReadiness.at().toEpochMilli());
                      }
                      instanceObject.setString("upgradePolicy", toString(status.application().deploymentSpec().instance(instance.instance())
                                                                               .map(DeploymentInstanceSpec::upgradePolicy)
                                                                               .orElse(DeploymentSpec.UpgradePolicy.defaultPolicy)));
                      status.application().revisions().last().flatMap(ApplicationVersion::compileVersion)
                            .ifPresent(compiled -> instanceObject.setString("compileVersion", compiled.toFullString()));
                      Cursor jobsArray = instanceObject.setArray("jobs");
                      status.jobSteps().forEach((job, jobStatus) -> {
                          if ( ! job.application().equals(instance)) return;
                          Cursor jobObject = jobsArray.addObject();
                          jobObject.setString("name", job.type().jobName());
                          if (jobsToRun.containsKey(job)) {
                              Readiness readiness = jobsToRun.get(job).get(0).readiness();
                              switch (readiness.cause()) {
                                  case paused -> jobObject.setLong("pausedUntil", readiness.at().toEpochMilli());
                                  case coolingDown -> jobObject.setLong("coolingDownUntil", readiness.at().toEpochMilli());
                              }
                              List versionsOnThisPlatform = jobsToRun.get(job).stream()
                                                                               .map(DeploymentStatus.Job::versions)
                                                                               .filter(versions -> versions.targetPlatform().equals(statistics.version()))
                                                                               .toList();
                              if ( ! versionsOnThisPlatform.isEmpty())
                                  jobObject.setString("pending", versionsOnThisPlatform.stream()
                                                                                       .allMatch(versions -> versions.sourcePlatform()
                                                                                                                     .map(statistics.version()::equals)
                                                                                                                     .orElse(true))
                                                                 ? "application" : "platform");
                          }
                      });
                      Cursor allRunsObject = instanceObject.setObject("allRuns");
                      Cursor upgradeRunsObject = instanceObject.setObject("upgradeRuns");
                      runs.forEach((type, rs) -> {
                          Cursor runObject = allRunsObject.setObject(type.jobName());
                          Cursor upgradeObject = upgradeRunsObject.setObject(type.jobName());
                          CloudAccount cloudAccount = controller.applications().decideCloudAccountOf(new DeploymentId(instance, type.zone()),
                                                                                                     status.application().deploymentSpec())
                                                                .orElse(null);
                          for (RunInfo run : rs) {
                              toSlime(runObject, run.run, cloudAccount);
                              if (run.upgrade)
                                  toSlime(upgradeObject, run.run, cloudAccount);
                          }
                      });
                  });
        }
        JobType.allIn(controller.zoneRegistry()).stream()
               .filter(job -> ! job.environment().isManuallyDeployed())
               .map(JobType::jobName).forEach(root.setArray("jobs")::addString);
        return new SlimeJsonResponse(slime);
    }

    private void toSlime(Cursor jobObject, Run run, CloudAccount cloudAccount) {
        String key = run.hasFailed() ? "failing" : run.hasEnded() ? "success" : "running";
        Cursor runObject = jobObject.setObject(key);
        runObject.setLong("number", run.id().number());
        runObject.setLong("start", run.start().toEpochMilli());
        run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli()));
        runObject.setString("status", nameOf(run.status()));
        if (cloudAccount != null) runObject.setObject("enclave").setString("cloudAccount", cloudAccount.value());
    }

    private void toSlime(Cursor object, ApplicationId id, HttpRequest request) {
        object.setString("tenant", id.tenant().value());
        object.setString("application", id.application().value());
        object.setString("instance", id.instance().value());
        object.setString("url", new UriBuilder(request.getUri()).withPath("/application/v4/tenant/" +
                                                                   id.tenant().value() +
                                                                   "/application/" +
                                                                   id.application().value()).toString());
        object.setString("upgradePolicy", toString(controller.applications().requireApplication(TenantAndApplicationId.from(id))
                                                             .deploymentSpec().instance(id.instance()).map(DeploymentInstanceSpec::upgradePolicy)
                                                             .orElse(DeploymentSpec.UpgradePolicy.defaultPolicy)));
    }

    private static String toString(DeploymentSpec.UpgradePolicy upgradePolicy) {
        if (upgradePolicy == DeploymentSpec.UpgradePolicy.defaultPolicy) {
            return "default";
        }
        return upgradePolicy.name();
    }

    public static String nameOf(RunStatus status) {
        return switch (status) {
            case reset, running                       -> "running";
            case cancelled, aborted                   -> "aborted";
            case error                                -> "error";
            case testFailure                          -> "testFailure";
            case noTests                              -> "noTests";
            case endpointCertificateTimeout           -> "endpointCertificateTimeout";
            case nodeAllocationFailure                -> "nodeAllocationFailure";
            case installationFailed                   -> "installationFailed";
            case invalidApplication, deploymentFailed -> "deploymentFailed";
            case success                              -> "success";
            case quotaExceeded                        -> "quotaExceeded";
        };
    }

    private static class RunInfo {
        final Run run;
        final boolean upgrade;

        RunInfo(Run run, boolean upgrade) {
            this.run = run;
            this.upgrade = upgrade;
        }

        @Override
        public String toString() {
            return run.id().toString();
        }

    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy