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

com.yahoo.vespa.hosted.controller.restapi.routing.RoutingApiHandler Maven / Gradle / Ivy

There is a newer version: 8.253.3
Show newest version
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.routing;

import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.ResourceResponse;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.EndpointList;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
import com.yahoo.vespa.hosted.controller.routing.context.RoutingContext;
import com.yahoo.yolean.Exceptions;

import java.net.URI;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;

/**
 * This implements the /routing/v1 API, which provides operators and tenants routing control at both zone- (operator
 * only) and deployment-level.
 *
 * @author mpolden
 */
public class RoutingApiHandler extends AuditLoggingRequestHandler {

    private final Controller controller;

    public RoutingApiHandler(Context ctx, Controller controller) {
        super(ctx, controller.auditLogger());
        this.controller = Objects.requireNonNull(controller, "controller must be non-null");
    }

    @Override
    public HttpResponse auditAndHandle(HttpRequest request) {
        try {
            var path = new Path(request.getUri());
            switch (request.getMethod()) {
                case GET: return get(path, request);
                case POST: return post(path, request);
                case DELETE: return delete(path, request);
                default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
            }
        } catch (IllegalArgumentException e) {
            return ErrorResponse.badRequest(Exceptions.toMessageString(e));
        } catch (RuntimeException e) {
            log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
            return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
        }
    }

    private HttpResponse delete(Path path, HttpRequest request) {
        if (path.matches("/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return setDeploymentStatus(path, true, request);
        if (path.matches("/routing/v1/inactive/environment/{environment}/region/{region}")) return setZoneStatus(path, true);
        return ErrorResponse.notFoundError("Nothing at " + path);
    }

    private HttpResponse post(Path path, HttpRequest request) {
        if (path.matches("/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return setDeploymentStatus(path, false, request);
        if (path.matches("/routing/v1/inactive/environment/{environment}/region/{region}")) return setZoneStatus(path, false);
        return ErrorResponse.notFoundError("Nothing at " + path);
    }

    private HttpResponse get(Path path, HttpRequest request) {
        if (path.matches("/routing/v1/")) return status(request.getUri());
        if (path.matches("/routing/v1/status/tenant/{tenant}")) return tenant(path, request);
        if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}")) return application(path, request);
        if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}")) return instance(path, request);
        if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deployment(path);
        if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}/endpoint")) return endpoints(path);
        if (path.matches("/routing/v1/status/environment")) return environment(request);
        if (path.matches("/routing/v1/status/environment/{environment}/region/{region}")) return zone(path);
        return ErrorResponse.notFoundError("Nothing at " + path);
    }

    private HttpResponse endpoints(Path path) {
        ApplicationId instanceId = instanceFrom(path);
        List endpoints = controller.routing().readDeclaredEndpointsOf(instanceId)
                                             .sortedBy(Comparator.comparing(Endpoint::dnsName))
                                             .asList();

        List deployments = endpoints.stream()
                                                  .flatMap(e -> e.deployments().stream())
                                                  .distinct()
                                                  .collect(Collectors.toList());

        Map deploymentsStatus = deployments.stream()
                                                                        .collect(Collectors.toMap(
                                                                                deploymentId -> deploymentId,
                                                                                deploymentId -> controller.routing().of(deploymentId).routingStatus())
                                                                        );

        var slime = new Slime();
        var root = slime.setObject();
        var endpointsRoot = root.setArray("endpoints");
        endpoints.forEach(endpoint -> {
            var endpointRoot = endpointsRoot.addObject();
            endpointToSlime(endpointRoot, endpoint);
            var zonesRoot = endpointRoot.setArray("zones");
            endpoint.deployments().stream().sorted(Comparator.comparing(d -> d.zoneId().value()))
                    .forEach(deployment -> {
                        RoutingStatus status = deploymentsStatus.get(deployment);
                        deploymentStatusToSlime(zonesRoot.addObject(), deployment, status, endpoint.routingMethod());
                    });
        });

        return new SlimeJsonResponse(slime);
    }

    private HttpResponse environment(HttpRequest request) {
        var zones = controller.zoneRegistry().zones().all().ids();
        if (isRecursive(request)) {
            var slime = new Slime();
            var root = slime.setObject();
            var zonesArray = root.setArray("zones");
            for (var zone : zones) {
                toSlime(zone, zonesArray.addObject());
            }
            return new SlimeJsonResponse(slime);
        }
        var resources = controller.zoneRegistry().zones().all().ids().stream()
                                  .map(zone -> zone.environment().value() +
                                               "/region/" + zone.region().value())
                                  .sorted()
                                  .collect(Collectors.toList());
        return new ResourceResponse(request.getUri(), resources);
    }

    private HttpResponse status(URI requestUrl) {
        return new ResourceResponse(requestUrl, "status/tenant", "status/environment");
    }

    private HttpResponse tenant(Path path, HttpRequest request) {
        var tenantName = tenantFrom(path);
        if (isRecursive(request)) {
            var slime = new Slime();
            var root = slime.setObject();
            toSlime(controller.applications().asList(tenantName), null, null, root);
            return new SlimeJsonResponse(slime);
        }
        var resources = controller.applications().asList(tenantName).stream()
                                  .map(Application::id)
                                  .map(TenantAndApplicationId::application)
                                  .map(ApplicationName::value)
                                  .map(application -> "application/" + application)
                                  .sorted()
                                  .collect(Collectors.toList());
        return new ResourceResponse(request.getUri(), resources);
    }

    private HttpResponse application(Path path, HttpRequest request) {
        var tenantAndApplicationId = tenantAndApplicationIdFrom(path);
        if (isRecursive(request)) {
            var slime = new Slime();
            var root = slime.setObject();
            toSlime(List.of(controller.applications().requireApplication(tenantAndApplicationId)), null,
                    null, root);
            return new SlimeJsonResponse(slime);
        }
        var resources = controller.applications().requireApplication(tenantAndApplicationId).instances().keySet().stream()
                                  .map(InstanceName::value)
                                  .map(instance -> "instance/" + instance)
                                  .sorted()
                                  .collect(Collectors.toList());
        return new ResourceResponse(request.getUri(), resources);
    }

    private HttpResponse instance(Path path, HttpRequest request) {
        var instanceId = instanceFrom(path);
        if (isRecursive(request)) {
            var slime = new Slime();
            var root = slime.setObject();
            toSlime(List.of(controller.applications().requireApplication(TenantAndApplicationId.from(instanceId))),
                    instanceId, null, root);
            return new SlimeJsonResponse(slime);
        }
        var resources = controller.applications().requireInstance(instanceId).deployments().keySet().stream()
                                  .map(zone -> "environment/" + zone.environment().value() +
                                               "/region/" + zone.region().value())
                                  .sorted()
                                  .collect(Collectors.toList());
        return new ResourceResponse(request.getUri(), resources);
    }

    private HttpResponse setZoneStatus(Path path, boolean in) {
        ZoneId zone = zoneFrom(path);
        RoutingContext context = controller.routing().of(zone);
        RoutingStatus.Value newStatus = in ? RoutingStatus.Value.in : RoutingStatus.Value.out;
        context.setRoutingStatus(newStatus, RoutingStatus.Agent.operator);
        return new MessageResponse("Set global routing status for deployments in " + zone + " to " +
                                   (in ? "IN" : "OUT"));
    }

    private HttpResponse zone(Path path) {
        var zone = zoneFrom(path);
        var slime = new Slime();
        var root = slime.setObject();
        toSlime(zone, root);
        return new SlimeJsonResponse(slime);
    }

    private void toSlime(ZoneId zone, Cursor zoneObject) {
        RoutingContext context = controller.routing().of(zone);
        zoneStatusToSlime(zoneObject, zone, context.routingStatus(), context.routingMethod());
    }

    private HttpResponse setDeploymentStatus(Path path, boolean in, HttpRequest request) {
        var deployment = deploymentFrom(path);
        var instance = controller.applications().requireInstance(deployment.applicationId());
        var status = in ? RoutingStatus.Value.in : RoutingStatus.Value.out;
        var agent = isOperator(request) ? RoutingStatus.Agent.operator : RoutingStatus.Agent.tenant;
        requireDeployment(deployment, instance);
        controller.routing().of(deployment).setRoutingStatus(status, agent);
        return new MessageResponse("Set global routing status for " + deployment + " to " + (in ? "IN" : "OUT"));
    }

    private HttpResponse deployment(Path path) {
        var slime = new Slime();
        var root = slime.setObject();
        var deploymentId = deploymentFrom(path);
        var application = controller.applications().requireApplication(TenantAndApplicationId.from(deploymentId.applicationId()));
        toSlime(List.of(application), deploymentId.applicationId(), deploymentId.zoneId(), root);
        return new SlimeJsonResponse(slime);
    }

    private void toSlime(List applications, ApplicationId instanceId, ZoneId zoneId, Cursor root) {
        var deploymentsArray = root.setArray("deployments");
        for (var application : applications) {
            var instances = instanceId == null
                    ? application.instances().values()
                    : List.of(application.instances().get(instanceId.instance()));
            EndpointList declaredEndpoints = controller.routing().declaredEndpointsOf(application);
            for (var instance : instances) {
                var zones = zoneId == null
                        ? instance.deployments().keySet().stream().sorted(Comparator.comparing(ZoneId::value))
                                  .collect(Collectors.toList())
                        : List.of(zoneId);
                for (var zone : zones) {
                    DeploymentId deploymentId = requireDeployment(new DeploymentId(instance.id(), zone), instance);
                    DeploymentRoutingContext context = controller.routing().of(deploymentId);
                    if (declaredEndpoints.targets(deploymentId).isEmpty()) continue; // No declared endpoints point to this deployment
                    deploymentStatusToSlime(deploymentsArray.addObject(),
                                            deploymentId,
                                            context.routingStatus(),
                                            context.routingMethod());
                }
            }
        }

    }

    private static void zoneStatusToSlime(Cursor object, ZoneId zone, RoutingStatus routingStatus, RoutingMethod method) {
        object.setString("routingMethod", asString(method));
        object.setString("environment", zone.environment().value());
        object.setString("region", zone.region().value());
        object.setString("status", asString(routingStatus.value()));
        object.setString("agent", asString(routingStatus.agent()));
        object.setLong("changedAt", routingStatus.changedAt().toEpochMilli());
    }

    private static void deploymentStatusToSlime(Cursor object, DeploymentId deployment, RoutingStatus routingStatus, RoutingMethod method) {
        object.setString("routingMethod", asString(method));
        object.setString("instance", deployment.applicationId().serializedForm());
        object.setString("environment", deployment.zoneId().environment().value());
        object.setString("region", deployment.zoneId().region().value());
        object.setString("status", asString(routingStatus.value()));
        object.setString("agent", asString(routingStatus.agent()));
        object.setLong("changedAt", routingStatus.changedAt().toEpochMilli());
    }

    private static void endpointToSlime(Cursor object, Endpoint endpoint) {
        object.setString("name", endpoint.name());
        object.setString("dnsName", endpoint.dnsName());
        object.setString("routingMethod", endpoint.routingMethod().name());
        object.setString("cluster", endpoint.cluster().value());
        object.setString("scope", endpoint.scope().name());
    }

    private TenantName tenantFrom(Path path) {
        return TenantName.from(path.get("tenant"));
    }

    private ApplicationName applicationFrom(Path path) {
        return ApplicationName.from(path.get("application"));
    }

    private TenantAndApplicationId tenantAndApplicationIdFrom(Path path) {
       return TenantAndApplicationId.from(tenantFrom(path), applicationFrom(path));
    }

    private ApplicationId instanceFrom(Path path) {
        return ApplicationId.from(tenantFrom(path), applicationFrom(path), InstanceName.from(path.get("instance")));
    }

    private DeploymentId deploymentFrom(Path path) {
        return new DeploymentId(instanceFrom(path), zoneFrom(path));
    }

    private ZoneId zoneFrom(Path path) {
        var zone = ZoneId.from(path.get("environment"), path.get("region"));
        if (!controller.zoneRegistry().hasZone(zone)) {
            throw new IllegalArgumentException("No such zone: " + zone);
        }
        return zone;
    }

    private static DeploymentId requireDeployment(DeploymentId deployment, Instance instance) {
        if (!instance.deployments().containsKey(deployment.zoneId())) {
            throw new IllegalArgumentException("No such deployment: " + deployment);
        }
        return deployment;
    }

    private static boolean isOperator(HttpRequest request) {
        SecurityContext securityContext = Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME))
                                                  .filter(SecurityContext.class::isInstance)
                                                  .map(SecurityContext.class::cast)
                                                  .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request"));
        return securityContext.roles().stream()
                              .map(Role::definition)
                              .anyMatch(definition -> definition == RoleDefinition.hostedOperator);
    }

    private static boolean isRecursive(HttpRequest request) {
        return "true".equals(request.getProperty("recursive"));
    }

    private static String asString(RoutingStatus.Value value) {
        switch (value) {
            case in: return "in";
            case out: return "out";
            default: return "unknown";
        }
    }

    private static String asString(RoutingStatus.Agent agent) {
        switch (agent) {
            case operator: return "operator";
            case system: return "system";
            case tenant: return "tenant";
            default: return "unknown";
        }
    }

    private static String asString(RoutingMethod method) {
        switch (method) {
            case shared: return "shared";
            case exclusive: return "exclusive";
            case sharedLayer4: return "sharedLayer4";
            default: return "unknown";
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy