aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
diff options
context:
space:
mode:
authorgjoranv <gjoranv@gmail.com>2023-11-03 18:08:32 +0100
committergjoranv <gjoranv@gmail.com>2023-11-06 00:31:08 +0100
commit596421557e3165ef25dd478edf64b2812d5b4777 (patch)
tree36ed938c7fe0519caf83cbb798d64bd98aa8aa0e /controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
parentc5d8e300da1bee0cff8e83a3c0a4b9a9a4fa8375 (diff)
More controller code to internal repo.
Diffstat (limited to 'controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java1111
1 files changed, 0 insertions, 1111 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
deleted file mode 100644
index d7a3d4fb9e5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ /dev/null
@@ -1,1111 +0,0 @@
-// 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;
-
-import com.yahoo.component.Version;
-import com.yahoo.component.VersionCompatibility;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.DockerImage;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.Tags;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.ListFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.flags.StringFlag;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentEndpoints;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult.LogEntry;
-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.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics.Warning;
-import com.yahoo.vespa.hosted.controller.application.DeploymentQuotaCalculator;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageValidator;
-import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml;
-import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificates;
-import com.yahoo.vespa.hosted.controller.concurrent.Once;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.routing.PreparedEndpoints;
-import com.yahoo.vespa.hosted.controller.security.AccessControl;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessGrant;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.ByteArrayInputStream;
-import java.security.Principal;
-import java.security.cert.X509Certificate;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.function.UnaryOperator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.flags.FetchVector.Dimension.INSTANCE_ID;
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.active;
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.reserved;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.broken;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.high;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.low;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.normal;
-import static java.util.Comparator.naturalOrder;
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.counting;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
-
-/**
- * A singleton owned by {@link Controller} which contains the methods and state for controlling applications.
- *
- * @author bratseth
- */
-public class ApplicationController {
-
- private static final Logger log = Logger.getLogger(ApplicationController.class.getName());
-
- /** The controller owning this */
- private final Controller controller;
-
- /** For persistence */
- private final CuratorDb curator;
-
- private final ArtifactRepository artifactRepository;
- private final ApplicationStore applicationStore;
- private final AccessControl accessControl;
- private final ConfigServer configServer;
- private final Clock clock;
- private final DeploymentTrigger deploymentTrigger;
- private final ApplicationPackageValidator applicationPackageValidator;
- private final EndpointCertificates endpointCertificates;
- private final StringFlag dockerImageRepoFlag;
- private final ListFlag<String> incompatibleVersions;
- private final BillingController billingController;
- private final ListFlag<String> cloudAccountsFlag;
-
- private final Map<DeploymentId, com.yahoo.vespa.hosted.controller.api.integration.configserver.Application> deploymentInfo = new ConcurrentHashMap<>();
-
- ApplicationController(Controller controller, CuratorDb curator, AccessControl accessControl, Clock clock,
- FlagSource flagSource, BillingController billingController) {
- this.controller = Objects.requireNonNull(controller);
- this.curator = Objects.requireNonNull(curator);
- this.accessControl = Objects.requireNonNull(accessControl);
- this.configServer = controller.serviceRegistry().configServer();
- this.clock = Objects.requireNonNull(clock);
- this.billingController = Objects.requireNonNull(billingController);
-
- artifactRepository = controller.serviceRegistry().artifactRepository();
- applicationStore = controller.serviceRegistry().applicationStore();
- dockerImageRepoFlag = PermanentFlags.DOCKER_IMAGE_REPO.bindTo(flagSource);
- incompatibleVersions = PermanentFlags.INCOMPATIBLE_VERSIONS.bindTo(flagSource);
- cloudAccountsFlag = PermanentFlags.CLOUD_ACCOUNTS.bindTo(flagSource);
- deploymentTrigger = new DeploymentTrigger(controller, clock);
- applicationPackageValidator = new ApplicationPackageValidator(controller);
- endpointCertificates = new EndpointCertificates(controller,
- controller.serviceRegistry().endpointCertificateProvider(),
- controller.serviceRegistry().endpointCertificateValidator());
-
- // Update serialization format of all applications
- Once.after(Duration.ofMinutes(1), () -> {
- Instant start = clock.instant();
- int count = 0;
- for (TenantAndApplicationId id : curator.readApplicationIds()) {
- lockApplicationIfPresent(id, application -> {
- for (var declaredInstance : application.get().deploymentSpec().instances())
- if ( ! application.get().instances().containsKey(declaredInstance.name()))
- application = withNewInstance(application, id.instance(declaredInstance.name()));
- store(application);
- });
- count++;
- }
- log.log(Level.INFO, Text.format("Wrote %d applications in %s", count,
- Duration.between(start, clock.instant())));
- });
- }
-
- /** Validate the given application package */
- public void validatePackage(ApplicationPackage applicationPackage, Application application) {
- applicationPackageValidator.validate(application, applicationPackage, clock.instant());
- }
-
- public Set<CloudAccount> accountsOf(TenantName tenant) {
- return cloudAccountsFlag.with(FetchVector.Dimension.TENANT_ID, tenant.value())
- .value().stream()
- .map(CloudAccount::from)
- .collect(Collectors.toSet());
- }
-
- /** Returns the application with the given id, or null if it is not present */
- public Optional<Application> getApplication(TenantAndApplicationId id) {
- return curator.readApplication(id);
- }
-
- /** Returns the instance with the given id, or null if it is not present */
- public Optional<Instance> getInstance(ApplicationId id) {
- return getApplication(TenantAndApplicationId.from(id)).flatMap(application -> application.get(id.instance()));
- }
-
- /**
- * Returns in-memory info for the given deployment pulled from the node repo.
- * Info on any existing deployment can be missing if it has not yet been fetched since this instance was started.
- * This is kept up to date by DeploymentInfoMaintainer.
- * Accessing this is thread safe.
- */
- // TODO: Replace the wire level Application by a DeploymentInfo class in the model
- public Map<DeploymentId, com.yahoo.vespa.hosted.controller.api.integration.configserver.Application> deploymentInfo() { return deploymentInfo; }
-
- /**
- * Triggers reindexing for the given document types in the given clusters, for the given application.
- * <p>
- * If no clusters are given, reindexing is triggered for the entire application; otherwise
- * if no documents types are given, reindexing is triggered for all given clusters; otherwise
- * reindexing is triggered for the cartesian product of the given clusters and document types.
- */
- public void reindex(ApplicationId id, ZoneId zoneId, List<String> clusterNames, List<String> documentTypes, boolean indexedOnly, Double speed, String cause) {
- configServer.reindex(new DeploymentId(id, zoneId), clusterNames, documentTypes, indexedOnly, speed, cause);
- }
-
- /** Returns the reindexing status for the given application in the given zone. */
- public ApplicationReindexing applicationReindexing(ApplicationId id, ZoneId zoneId) {
- return configServer.getReindexing(new DeploymentId(id, zoneId));
- }
-
- /** Enables reindexing for the given application in the given zone. */
- public void enableReindexing(ApplicationId id, ZoneId zoneId) {
- configServer.enableReindexing(new DeploymentId(id, zoneId));
- }
-
- /** Disables reindexing for the given application in the given zone. */
- public void disableReindexing(ApplicationId id, ZoneId zoneId) {
- configServer.disableReindexing(new DeploymentId(id, zoneId));
- }
-
- /**
- * Returns the application with the given id
- *
- * @throws IllegalArgumentException if it does not exist
- */
- public Application requireApplication(TenantAndApplicationId id) {
- return getApplication(id).orElseThrow(() -> new IllegalArgumentException(id + " not found"));
- }
-
- /**
- * Returns the instance with the given id
- *
- * @throws IllegalArgumentException if it does not exist
- */
- // TODO jonvm: remove or inline
- public Instance requireInstance(ApplicationId id) {
- return getInstance(id).orElseThrow(() -> new IllegalArgumentException(id + " not found"));
- }
-
- /** Returns a snapshot of all applications */
- public List<Application> asList() {
- return curator.readApplications(false);
- }
-
- /**
- * Returns a snapshot of all readable applications. Unlike {@link ApplicationController#asList()} this ignores
- * applications that cannot currently be read (e.g. due to serialization issues) and may return an incomplete
- * snapshot.
- *
- * This should only be used in cases where acting on a subset of applications is better than none.
- */
- public List<Application> readable() {
- return curator.readApplications(true);
- }
-
- /** Returns the ID of all known applications. */
- public List<TenantAndApplicationId> idList() {
- return curator.readApplicationIds();
- }
-
- /** Returns a snapshot of all applications of a tenant */
- public List<Application> asList(TenantName tenant) {
- return curator.readApplications(tenant);
- }
-
- public ArtifactRepository artifacts() { return artifactRepository; }
-
- public ApplicationStore applicationStore() { return applicationStore; }
-
- /** Returns all currently reachable content clusters among the given deployments. */
- public Map<ZoneId, List<String>> reachableContentClustersByZone(Collection<DeploymentId> ids) {
- Map<ZoneId, List<String>> clusters = new TreeMap<>(Comparator.comparing(ZoneId::value));
- for (DeploymentId id : ids)
- if (isHealthy(id))
- clusters.put(id.zoneId(), List.copyOf(configServer.getContentClusters(id)));
-
- return Collections.unmodifiableMap(clusters);
- }
-
- /** Reads the oldest installed platform for the given application and zone from job history, or a node repo. */
- private Optional<Version> oldestInstalledPlatform(JobStatus job) {
- Version oldest = null;
- for (Run run : job.runs().descendingMap().values()) {
- Version version = run.versions().targetPlatform();
- if (oldest == null || version.isBefore(oldest))
- oldest = version;
-
- if (run.hasSucceeded())
- return Optional.of(oldest);
- }
- // If no successful run was found, ask the node repository in the relevant zone.
- return oldestInstalledPlatform(job.id());
- }
-
- /** Reads the oldest installed platform for the given application and zone from the node repo of that zone. */
- private Optional<Version> oldestInstalledPlatform(JobId job) {
- return configServer.nodeRepository().list(job.type().zone(),
- NodeFilter.all()
- .applications(job.application())
- .states(active, reserved))
- .stream()
- .map(Node::currentVersion)
- .filter(version -> ! version.isEmpty())
- .min(naturalOrder());
- }
-
- /** Returns the oldest Vespa version installed on any active or reserved production node for the given application. */
- public Optional<Version> oldestInstalledPlatform(Application application) {
- return controller.jobController().deploymentStatus(application).jobs()
- .production()
- .not().test()
- .asList().stream()
- .map(this::oldestInstalledPlatform)
- .flatMap(Optional::stream)
- .min(naturalOrder());
- }
-
- /**
- * Returns the preferred Vespa version to compile against, for
- * <p>
- * The returned version is not newer than the oldest deployed platform for the application, unless
- * the target major differs from the oldest deployed platform, in which case it is not newer than
- * the oldest available platform version on that major instead.
- * <p>
- * The returned version is compatible with a platform version available in the system.
- * <p>
- * A candidate is sought first among versions with non-broken confidence, then among those with forgotten confidence.
- * <p>
- * The returned version is the latest in the relevant candidate set.
- * <p>
- * If no such version exists, an {@link IllegalArgumentException} is thrown.
- */
- public Version compileVersion(TenantAndApplicationId id, OptionalInt wantedMajor) {
-
- // Read version status, and pick out target platforms we could run the compiled package on.
- Optional<Application> application = getApplication(id);
- Optional<Version> oldestInstalledPlatform = application.flatMap(this::oldestInstalledPlatform);
- VersionStatus versionStatus = controller.readVersionStatus();
- UpgradePolicy policy = application.flatMap(app -> app.deploymentSpec().instances().stream()
- .map(DeploymentInstanceSpec::upgradePolicy)
- .max(naturalOrder()))
- .orElse(UpgradePolicy.defaultPolicy);
- Confidence targetConfidence = switch (policy) {
- case canary -> broken;
- case defaultPolicy -> normal;
- case conservative -> high;
- };
-
- // Target platforms are all versions not older than the oldest installed platform, unless forcing a major version change.
- // Only platforms not older than the system version, and with appropriate confidence, are considered targets.
- Predicate<Version> isTargetPlatform = wantedMajor.isEmpty() && oldestInstalledPlatform.isEmpty()
- ? __ -> true // No preferences for version: any platform version is ok.
- : wantedMajor.isEmpty() || (oldestInstalledPlatform.isPresent() && wantedMajor.getAsInt() == oldestInstalledPlatform.get().getMajor())
- ? version -> ! version.isBefore(oldestInstalledPlatform.get()) // Major empty, or on same as oldest: ensure not a platform downgrade.
- : version -> wantedMajor.getAsInt() == version.getMajor(); // Major specified, and not on same as oldest (possibly empty): any on that major.
- Set<Version> platformVersions = versionStatus.deployableVersions().stream()
- .filter(version -> version.confidence().equalOrHigherThan(targetConfidence))
- .map(VespaVersion::versionNumber)
- .filter(isTargetPlatform)
- .collect(toSet());
- oldestInstalledPlatform.ifPresent(fallback -> {
- if (wantedMajor.isEmpty() || wantedMajor.getAsInt() == fallback.getMajor())
- platformVersions.add(fallback);
- });
-
- if (platformVersions.isEmpty())
- throw new IllegalArgumentException("this system has no available versions" +
- (wantedMajor.isPresent() ? " on specified major: " + wantedMajor.getAsInt() : ""));
-
- // The returned compile version must be compatible with at least one target platform.
- // If it is incompatible with any of the current platforms, the system will trigger a platform change.
- // The returned compile version should also be at least as old as both the oldest target platform version,
- // and the oldest current platform, unless the two are incompatible, in which case only the target matters,
- // or there are no installed platforms, in which case we prefer the newest target platform.
- VersionCompatibility compatibility = versionCompatibility(id.defaultInstance()); // Wrong id level >_<
- Version oldestTargetPlatform = platformVersions.stream().min(naturalOrder()).get();
- Version newestVersion = oldestInstalledPlatform.isEmpty()
- ? platformVersions.stream().max(naturalOrder()).get()
- : compatibility.accept(oldestInstalledPlatform.get(), oldestTargetPlatform)
- && oldestInstalledPlatform.get().isBefore(oldestTargetPlatform)
- ? oldestInstalledPlatform.get()
- : oldestTargetPlatform;
- Predicate<Version> systemCompatible = version -> ! version.isAfter(newestVersion)
- && platformVersions.stream().anyMatch(platform -> compatibility.accept(platform, version));
-
- // Find the newest, system-compatible version with non-broken confidence.
- Optional<Version> nonBroken = versionStatus.versions().stream()
- .filter(VespaVersion::isReleased)
- .filter(version -> version.confidence().equalOrHigherThan(low))
- .map(VespaVersion::versionNumber)
- .filter(systemCompatible)
- .max(naturalOrder());
-
- // Fall back to the newest, system-compatible version with unknown confidence. For public systems, this implies high confidence.
- Set<Version> knownVersions = versionStatus.versions().stream().map(VespaVersion::versionNumber).collect(toSet());
- Optional<Version> unknown = controller.mavenRepository().metadata().versions(clock.instant()).stream()
- .filter(version -> ! knownVersions.contains(version))
- .filter(systemCompatible)
- .max(naturalOrder());
-
- if (nonBroken.isPresent()) {
- if (controller.system().isPublic() && unknown.isPresent() && unknown.get().isAfter(nonBroken.get()))
- return unknown.get();
-
- return nonBroken.get();
- }
-
- if (unknown.isPresent())
- return unknown.get();
-
- throw new IllegalArgumentException("no suitable, released compile version exists" +
- (wantedMajor.isPresent() ? " for specified major: " + wantedMajor.getAsInt() : ""));
- }
-
- /**
- * Creates a new application for an existing tenant.
- *
- * @throws IllegalArgumentException if the application already exists
- */
- public Application createApplication(TenantAndApplicationId id, Credentials credentials) {
- try (Mutex lock = lock(id)) {
- if (getApplication(id).isPresent())
- throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
- if (getApplication(dashToUnderscore(id)).isPresent()) // VESPA-1945
- throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists");
-
- com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value());
-
- if (controller.tenants().get(id.tenant()).isEmpty())
- throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist");
- accessControl.createApplication(id, credentials);
-
- LockedApplication locked = new LockedApplication(new Application(id, clock.instant()), lock);
- store(locked);
- log.info("Created " + locked);
- return locked.get();
- }
- }
-
- /**
- * Creates a new instance for an existing application.
- *
- * @throws IllegalArgumentException if the instance already exists, or has an invalid instance name.
- */
- public void createInstance(ApplicationId id) {
- lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- store(withNewInstance(application, id));
- });
- }
-
- /** Returns given application with a new instance */
- public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance) {
- if (instance.instance().isTester())
- throw new IllegalArgumentException("'" + instance + "' is a tester application!");
- InstanceId.validate(instance.instance().value());
-
- if (getInstance(instance).isPresent())
- throw new IllegalArgumentException("Could not create '" + instance + "': Instance already exists");
- if (getInstance(dashToUnderscore(instance)).isPresent()) // VESPA-1945
- throw new IllegalArgumentException("Could not create '" + instance + "': Instance " + dashToUnderscore(instance) + " already exists");
-
- log.info("Created " + instance);
- return application.withNewInstance(instance.instance());
- }
-
- /** Deploys an application package for an existing application instance. */
- public DeploymentResult deploy(JobId job, boolean deploySourceVersions, Consumer<String> deployLogger, UnaryOperator<Optional<CloudAccount>> cloudAccountOverride) {
- if (job.application().instance().isTester())
- throw new IllegalArgumentException("'" + job.application() + "' is a tester application!");
-
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(job.application());
- ZoneId zone = job.type().zone();
- DeploymentId deployment = new DeploymentId(job.application(), zone);
-
- try (Mutex deploymentLock = lockForDeployment(job.application(), zone)) {
- Run run = controller.jobController().last(job)
- .orElseThrow(() -> new IllegalStateException("No known run of '" + job + "'"));
-
- if (run.hasEnded())
- throw new IllegalStateException("No deployment expected for " + job + " now, as no job is running");
-
- Version platform = run.versions().sourcePlatform().filter(__ -> deploySourceVersions).orElse(run.versions().targetPlatform());
- RevisionId revision = run.versions().sourceRevision().filter(__ -> deploySourceVersions).orElse(run.versions().targetRevision());
- ApplicationPackageStream applicationPackage = new ApplicationPackageStream(() -> applicationStore.stream(deployment, revision));
- AtomicReference<RevisionId> lastRevision = new AtomicReference<>();
- // Prepare endpoints lazily
- Supplier<PreparedEndpoints> preparedEndpoints = () -> {
- try (Mutex lock = lock(applicationId)) {
- LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
- application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set);
- return prepareEndpoints(deployment, job, application, applicationPackage, deployLogger, lock);
- }
- };
-
- // Carry out deployment without holding the application lock.
- DeploymentDataAndResult dataAndResult = deploy(job.application(), applicationPackage, zone, platform, preparedEndpoints,
- run.isDryRun(), run.testerCertificate(), cloudAccountOverride);
-
- // Record the quota usage for this application
- var quotaUsage = deploymentQuotaUsage(zone, job.application());
-
- // For direct deployments use the full deployment ID, but otherwise use just the tenant and application as
- // the source since it's the same application, so it should have the same warnings.
- // These notifications are only updated when the last submitted revision is deployed here.
- NotificationSource source = zone.environment().isManuallyDeployed()
- ? NotificationSource.from(deployment)
- : revision.equals(lastRevision.get()) ? NotificationSource.from(applicationId) : null;
- if (source != null) {
- List<String> warnings = Optional.ofNullable(dataAndResult.result().log())
- .map(logs -> logs.stream()
- .filter(LogEntry::concernsPackage)
- .filter(log -> log.level().intValue() >= Level.WARNING.intValue())
- .map(LogEntry::message)
- .sorted()
- .distinct()
- .toList())
- .orElseGet(List::of);
- if (warnings.isEmpty())
- controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage);
- else
- controller.notificationsDb().setApplicationPackageNotification(source, warnings);
- }
-
- lockApplicationOrThrow(applicationId, application ->
- store(application.with(job.application().instance(),
- i -> i.withNewDeployment(zone, revision, platform,
- clock.instant(), warningsFrom(dataAndResult.result().log()),
- quotaUsage, dataAndResult.data().cloudAccount().orElse(CloudAccount.empty),
- dataAndResult.data.dataPlaneTokens()))));
- return dataAndResult.result();
- }
- }
-
- private PreparedEndpoints prepareEndpoints(DeploymentId deployment, JobId job, LockedApplication application,
- ApplicationPackageStream applicationPackage,
- Consumer<String> deployLogger,
- Mutex applicationLock) {
- Instance instance = application.get().require(job.application().instance());
- Tags tags = applicationPackage.truncatedPackage().deploymentSpec().instance(instance.name())
- .map(DeploymentInstanceSpec::tags)
- .orElseGet(Tags::empty);
- EndpointCertificate certificate = endpointCertificates.get(deployment,
- applicationPackage.truncatedPackage().deploymentSpec(),
- applicationLock);
- deployLogger.accept("Using CA signed certificate version %s".formatted(certificate.version()));
- BasicServicesXml services = applicationPackage.truncatedPackage().services(deployment, tags);
- return controller.routing().of(deployment).prepare(services, certificate, application);
- }
-
- /** Stores the deployment spec and validation overrides from the application package, and runs cleanup. Returns new instances. */
- public List<InstanceName> storeWithUpdatedConfig(LockedApplication application, ApplicationPackage applicationPackage) {
- validatePackage(applicationPackage, application.get());
-
- application = application.with(applicationPackage.deploymentSpec());
- application = application.with(applicationPackage.validationOverrides());
-
- var existingInstances = application.get().instances();
- var declaredInstances = applicationPackage.deploymentSpec().instances();
- for (var declaredInstance : declaredInstances) {
- if ( ! existingInstances.containsKey(declaredInstance.name()))
- application = withNewInstance(application, application.get().id().instance(declaredInstance.name()));
- }
-
- // Delete zones not listed in DeploymentSpec, if allowed
- // We do this at deployment time for externally built applications, and at submission time
- // for internally built ones, to be able to return a validation failure message when necessary
- for (InstanceName name : existingInstances.keySet()) {
- application = withoutDeletedDeployments(application, name);
- }
-
- // Validate new deployment spec thoroughly before storing it.
- DeploymentStatus status = controller.jobController().deploymentStatus(application.get());
- Change dummyChange = Change.of(RevisionId.forProduction(Long.MAX_VALUE)); // Should always run everywhere.
- for (var jobs : status.jobsToRun(applicationPackage.deploymentSpec().instanceNames().stream()
- .collect(toMap(name -> name, __ -> dummyChange)))
- .entrySet()) {
- for (var job : jobs.getValue()) {
- decideCloudAccountOf(new DeploymentId(jobs.getKey().application(), job.type().zone()),
- applicationPackage.deploymentSpec());
- }
- }
-
- for (Notification notification : controller.notificationsDb().listNotifications(NotificationSource.from(application.get().id()), true)) {
- if ( notification.source().instance().isPresent()
- && ( ! declaredInstances.contains(notification.source().instance().get())
- || ! notification.source().zoneId().map(application.get().require(notification.source().instance().get()).deployments()::containsKey).orElse(false)))
- controller.notificationsDb().removeNotifications(notification.source());
- }
-
- store(application);
- return declaredInstances.stream()
- .map(DeploymentInstanceSpec::name)
- .filter(instance -> ! existingInstances.containsKey(instance))
- .toList();
- }
-
- /** Deploy a system application to given zone */
- public void deploy(SystemApplication application, ZoneId zone, Version version, boolean allowDowngrade) {
- if (application.hasApplicationPackage()) {
- deploySystemApplicationPackage(application, zone, version);
- } else {
- // Deploy by calling node repository directly
- configServer.nodeRepository().upgrade(zone, application.nodeType(), version, allowDowngrade);
- }
- }
-
- /** Deploy a system application to given zone */
- public DeploymentResult deploySystemApplicationPackage(SystemApplication application, ZoneId zone, Version version) {
- if (application.hasApplicationPackage()) {
- ApplicationPackageStream applicationPackage = new ApplicationPackageStream(
- () -> new ByteArrayInputStream(artifactRepository.getSystemApplicationPackage(application.id(), zone, version))
- );
- return deploy(application.id(), applicationPackage, zone, version, null, false, Optional.empty(), UnaryOperator.identity()).result();
- } else {
- throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString());
- }
- }
-
- /** Deploys the given tester application to the given zone. */
- public DeploymentResult deployTester(TesterId tester, ApplicationPackageStream applicationPackage, ZoneId zone, Version platform, UnaryOperator<Optional<CloudAccount>> cloudAccountOverride) {
- return deploy(tester.id(), applicationPackage, zone, platform, null, false, Optional.empty(), cloudAccountOverride).result();
- }
-
- private record DeploymentDataAndResult(DeploymentData data, DeploymentResult result) {}
-
- private DeploymentDataAndResult deploy(ApplicationId application, ApplicationPackageStream applicationPackage,
- ZoneId zone, Version platform, Supplier<PreparedEndpoints> preparedEndpoints,
- boolean dryRun, Optional<X509Certificate> testerCertificate,
- UnaryOperator<Optional<CloudAccount>> cloudAccountOverride) {
- DeploymentId deployment = new DeploymentId(application, zone);
- // Routing and metadata may have changed, so we need to refresh state after deployment, even if deployment fails.
- interface CleanCloseable extends AutoCloseable { void close(); }
- AtomicReference<EndpointList> generatedEndpoints = new AtomicReference<>(EndpointList.EMPTY);
- try (CleanCloseable postDeployment = () -> updateRoutingAndMeta(deployment, applicationPackage, generatedEndpoints)) {
- Optional<DockerImage> dockerImageRepo = Optional.ofNullable(
- dockerImageRepoFlag
- .with(FetchVector.Dimension.ZONE_ID, zone.value())
- .with(INSTANCE_ID, application.serializedForm())
- .value())
- .filter(s -> !s.isBlank())
- .map(DockerImage::fromString);
-
- Optional<AthenzDomain> domain = controller.tenants().get(application.tenant())
- .filter(tenant-> tenant instanceof AthenzTenant)
- .map(tenant -> ((AthenzTenant)tenant).domain());
-
- Supplier<Quota> deploymentQuota = () -> DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()),
- asList(application.tenant()), application, zone, applicationPackage.truncatedPackage().deploymentSpec());
-
- List<TenantSecretStore> tenantSecretStores = controller.tenants()
- .get(application.tenant())
- .filter(tenant-> tenant instanceof CloudTenant)
- .map(tenant -> ((CloudTenant) tenant).tenantSecretStores())
- .orElse(List.of());
- List<X509Certificate> operatorCertificates = controller.supportAccess().activeGrantsFor(deployment).stream()
- .map(SupportAccessGrant::certificate)
- .toList();
- if (testerCertificate.isPresent()) {
- operatorCertificates = Stream.concat(operatorCertificates.stream(), testerCertificate.stream()).toList();
- }
- Supplier<Optional<CloudAccount>> cloudAccount = () -> cloudAccountOverride.apply(decideCloudAccountOf(deployment, applicationPackage.truncatedPackage().deploymentSpec()));
- Supplier<DeploymentEndpoints> endpoints = () -> {
- if (preparedEndpoints == null) return DeploymentEndpoints.none;
- PreparedEndpoints prepared = preparedEndpoints.get();
- generatedEndpoints.set(prepared.endpoints().generated());
- return new DeploymentEndpoints(prepared.containerEndpoints(), Optional.of(prepared.certificate()));
- };
- Supplier<List<DataplaneTokenVersions>> dataplaneTokenVersions = () -> {
- Tags tags = applicationPackage.truncatedPackage().deploymentSpec()
- .instance(application.instance())
- .map(DeploymentInstanceSpec::tags)
- .orElse(Tags.empty());
- BasicServicesXml services = applicationPackage.truncatedPackage().services(deployment, tags);
- Set<TokenId> referencedTokens = services.containers().stream()
- .flatMap(container -> container.dataPlaneTokens().stream())
- .collect(toSet());
- List<DataplaneTokenVersions> currentTokens = controller.dataplaneTokenService().listTokens(application.tenant()).stream()
- .filter(token -> referencedTokens.contains(token.tokenId()))
- .toList();
- return Stream.concat(currentTokens.stream(),
- referencedTokens.stream()
- .filter(token -> currentTokens.stream().noneMatch(t -> t.tokenId().equals(token)))
- .map(token -> new DataplaneTokenVersions(token, List.of(), Instant.EPOCH)))
- .toList();
- };
- DeploymentData deploymentData = new DeploymentData(application, zone, applicationPackage::zipStream, platform,
- endpoints, dockerImageRepo, domain, deploymentQuota, tenantSecretStores, operatorCertificates, cloudAccount, dataplaneTokenVersions, dryRun);
- ConfigServer.PreparedApplication preparedApplication = configServer.deploy(deploymentData);
-
- return new DeploymentDataAndResult(deploymentData, preparedApplication.deploymentResult());
- }
- }
-
- private void updateRoutingAndMeta(DeploymentId id, ApplicationPackageStream data, AtomicReference<EndpointList> generatedEndpoints) {
- if (id.applicationId().instance().isTester()) return;
- controller.routing().of(id).activate(data.truncatedPackage().deploymentSpec(), generatedEndpoints.get());
- if ( ! id.zoneId().environment().isManuallyDeployed()) return;
- controller.applications().applicationStore().putMeta(id, clock.instant(), data.truncatedPackage().metaDataZip());
- }
-
- public Optional<CloudAccount> decideCloudAccountOf(DeploymentId deployment, DeploymentSpec spec) {
- ZoneId zoneId = deployment.zoneId();
- CloudName cloud = controller.zoneRegistry().get(zoneId).getCloudName();
- CloudAccount requestedAccount = spec.cloudAccount(cloud, deployment.applicationId().instance(), deployment.zoneId());
- if (requestedAccount.isUnspecified())
- return Optional.empty();
-
- TenantName tenant = deployment.applicationId().tenant();
- Set<CloudAccount> tenantAccounts = accountsOf(tenant);
- if ( ! tenantAccounts.contains(requestedAccount)) {
- throw new IllegalArgumentException("Requested cloud account '" + requestedAccount.value() +
- "' is not valid for tenant '" + tenant + "'");
- }
- if ( ! controller.zoneRegistry().hasZone(zoneId, requestedAccount)) {
- throw new IllegalArgumentException("Zone " + zoneId + " is not configured in requested cloud account '" +
- requestedAccount.value() + "'");
- }
- return Optional.of(requestedAccount);
- }
-
- private LockedApplication withoutDeletedDeployments(LockedApplication application, InstanceName instance) {
- DeploymentSpec deploymentSpec = application.get().deploymentSpec();
- List<ZoneId> deploymentsToRemove = application.get().require(instance).productionDeployments().values().stream()
- .map(Deployment::zone)
- .filter(zone -> deploymentSpec.instance(instance).isEmpty()
- || ! deploymentSpec.requireInstance(instance).deploysTo(zone.environment(),
- zone.region()))
- .toList();
-
- if (deploymentsToRemove.isEmpty())
- return application;
-
- if ( ! application.get().validationOverrides().allows(ValidationId.deploymentRemoval, clock.instant()))
- throw new IllegalArgumentException(ValidationId.deploymentRemoval.value() + ": " + application.get().require(instance) +
- " is deployed in " +
- deploymentsToRemove.stream()
- .map(zone -> zone.region().value())
- .collect(joining(", ")) +
- ", but " + (deploymentsToRemove.size() > 1 ? "these " : "this ") +
- "instance and region combination" +
- (deploymentsToRemove.size() > 1 ? "s are" : " is") +
- " removed from deployment.xml. " +
- ValidationOverrides.toAllowMessage(ValidationId.deploymentRemoval));
- // Remove the instance as well, if it is no longer referenced, and contains only production deployments that are removed now.
- boolean removeInstance = ! deploymentSpec.instanceNames().contains(instance)
- && application.get().require(instance).deployments().size() == deploymentsToRemove.size();
- for (ZoneId zone : deploymentsToRemove) {
- application = deactivate(application.get().id().instance(instance), zone, Optional.of(application)).get();
- }
- if (removeInstance) {
- application = application.without(instance);
- }
- return application;
- }
-
- /**
- * Deletes the given application. All known instances of the applications will be deleted.
- *
- * @throws IllegalArgumentException if the application has deployments or the caller is not authorized
- */
- public void deleteApplication(TenantAndApplicationId id, Credentials credentials) {
- deleteApplication(id, Optional.of(credentials));
- }
-
- public void deleteApplication(TenantAndApplicationId id, Optional<Credentials> credentials) {
- lockApplicationOrThrow(id, application -> {
- var deployments = application.get().instances().values().stream()
- .filter(instance -> ! instance.deployments().isEmpty())
- .collect(toMap(instance -> instance.name(),
- instance -> instance.deployments().keySet().stream()
- .map(ZoneId::toString)
- .collect(joining(", "))));
- if ( ! deployments.isEmpty())
- throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments: " + deployments);
-
- for (Instance instance : application.get().instances().values()) {
- controller.routing().removeRotationEndpointsFromDns(application.get(), instance.name());
- application = application.without(instance.name());
- }
-
- applicationStore.removeAll(id.tenant(), id.application());
- applicationStore.putMetaTombstone(id.tenant(), id.application(), clock.instant());
-
- credentials.ifPresent(creds -> accessControl.deleteApplication(id, creds));
- curator.removeApplication(id);
-
- controller.jobController().collectGarbage();
- controller.notificationsDb().removeNotifications(NotificationSource.from(id));
- log.info("Deleted " + id);
- });
- }
-
- /**
- * Deletes the the given application instance.
- *
- * @throws IllegalArgumentException if the application has deployments or the caller is not authorized
- * @throws NotExistsException if the instance does not exist
- */
- public void deleteInstance(ApplicationId instanceId) {
- if (getInstance(instanceId).isEmpty())
- throw new NotExistsException("Could not delete instance '" + instanceId + "': Instance not found");
-
- lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> {
- if ( ! application.get().require(instanceId.instance()).deployments().isEmpty())
- throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments in: " +
- application.get().require(instanceId.instance()).deployments().keySet().stream().map(ZoneId::toString)
- .sorted().collect(joining(", ")));
-
- if ( ! application.get().deploymentSpec().equals(DeploymentSpec.empty)
- && application.get().deploymentSpec().instanceNames().contains(instanceId.instance()))
- throw new IllegalArgumentException("Can not delete '" + instanceId + "', which is specified in 'deployment.xml'; remove it there first");
-
- controller.routing().removeRotationEndpointsFromDns(application.get(), instanceId.instance());
- curator.writeApplication(application.without(instanceId.instance()).get());
- controller.jobController().collectGarbage();
- controller.notificationsDb().removeNotifications(NotificationSource.from(instanceId));
- log.info("Deleted " + instanceId);
- });
- }
-
- /**
- * Replace any previous version of this application by this instance
- *
- * @param application a locked application to store
- */
- public void store(LockedApplication application) {
- curator.writeApplication(application.get());
- }
-
- /**
- * Acquire a locked application to modify and store, if there is an application with the given id.
- *
- * @param applicationId ID of the application to lock and get.
- * @param action Function which acts on the locked application.
- */
- public void lockApplicationIfPresent(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) {
- try (Mutex lock = lock(applicationId)) {
- getApplication(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(action);
- }
- }
-
- /**
- * Acquire a locked application to modify and store, or throw an exception if no application has the given id.
- *
- * @param applicationId ID of the application to lock and require.
- * @param action Function which acts on the locked application.
- * @throws IllegalArgumentException when application does not exist.
- */
- public void lockApplicationOrThrow(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) {
- try (Mutex lock = lock(applicationId)) {
- action.accept(new LockedApplication(requireApplication(applicationId), lock));
- }
- }
-
- /**
- * Tells config server to schedule a restart of all nodes in this deployment
- *
- * @param restartFilter Variables to filter which nodes to restart.
- */
- public void restart(DeploymentId deploymentId, RestartFilter restartFilter) {
- configServer.restart(deploymentId, restartFilter);
- }
-
- /**
- * Asks the config server whether this deployment is currently healthy, i.e., serving traffic as usual.
- * If this cannot be ascertained, we must assume it is not.
- */
- public boolean isHealthy(DeploymentId deploymentId) {
- try {
- return ! isSuspended(deploymentId); // consider adding checks again global routing status, etc.?
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed getting suspension status of " + deploymentId + ": " + Exceptions.toMessageString(e));
- return false;
- }
- }
-
- /**
- * Asks the config server whether this deployment is currently <i>suspended</i>:
- * Not in a state where it should receive traffic.
- */
- public boolean isSuspended(DeploymentId deploymentId) {
- return configServer.isSuspended(deploymentId);
- }
-
- /** Sets suspension status of the given deployment in its zone. */
- public void setSuspension(DeploymentId deploymentId, boolean suspend) {
- configServer.setSuspension(deploymentId, suspend);
- }
-
- /** Deactivate application in the given zone. Even if the application itself does not exist, deactivation of the deployment will still be attempted */
- public void deactivate(ApplicationId instanceId, ZoneId zone) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(instanceId);
- try (Mutex deploymentLock = lockForDeployment(instanceId, zone)) {
- try (Mutex lock = lock(applicationId)) {
- Optional<LockedApplication> application = getApplication(applicationId).map(app -> new LockedApplication(app, lock));
- deactivate(instanceId, zone, application).ifPresent(this::store);
- }
- }
- }
-
- /**
- * Deactivates a locked application without storing it
- *
- * @return the application with the deployment in the given zone removed
- */
- private Optional<LockedApplication> deactivate(ApplicationId instanceId, ZoneId zone, Optional<LockedApplication> application) {
- DeploymentId id = new DeploymentId(instanceId, zone);
- interface CleanCloseable extends AutoCloseable { void close(); }
- try (CleanCloseable postDeactivation = () -> {
- application.ifPresent(app -> controller.routing().of(id).deactivate(app.get().deploymentSpec()));
- if (id.zoneId().environment().isManuallyDeployed())
- applicationStore.putMetaTombstone(id, clock.instant());
- if ( ! id.zoneId().environment().isTest())
- controller.notificationsDb().removeNotifications(NotificationSource.from(id));
- }) {
- configServer.deactivate(id);
- return application.map(app -> app.with(instanceId.instance(), instance -> instance.withoutDeploymentIn(id.zoneId())));
- }
- }
-
- public DeploymentTrigger deploymentTrigger() { return deploymentTrigger; }
-
- /**
- * Returns a lock which provides exclusive rights to changing this application.
- * Any operation which stores an application need to first acquire this lock, then read, modify
- * and store the application, and finally release (close) the lock.
- */
- Mutex lock(TenantAndApplicationId application) {
- return curator.lock(application);
- }
-
- /**
- * Returns a lock which provides exclusive rights to deploying this application to the given zone.
- */
- private Mutex lockForDeployment(ApplicationId application, ZoneId zone) {
- return curator.lockForDeployment(application, zone);
- }
-
- public VersionCompatibility versionCompatibility(ApplicationId id) {
- return VersionCompatibility.fromVersionList(incompatibleVersions.with(INSTANCE_ID, id.serializedForm()).value());
- }
-
- /**
- * Verifies that the application can be deployed to the tenant, following these rules:
- *
- * 1. Verify that the Athenz service can be launched by the config server
- * 2. If the principal is given, verify that the principal is tenant admin or admin of the tenant domain
- * 3. If the principal is not given, verify that the Athenz domain of the tenant equals Athenz domain given in deployment.xml
- *
- * @param tenantName tenant where application should be deployed
- * @param applicationPackage application package
- * @param deployer principal initiating the deployment, possibly empty
- */
- public void verifyApplicationIdentityConfiguration(TenantName tenantName, Optional<DeploymentId> deployment, ApplicationPackage applicationPackage, Optional<Principal> deployer) {
- Optional<AthenzDomain> identityDomain = applicationPackage.deploymentSpec().athenzDomain()
- .map(domain -> new AthenzDomain(domain.value()));
- if (identityDomain.isEmpty()) {
- // If there is no domain configured in deployment.xml there is nothing to do.
- return;
- }
-
- // Verify that the system supports launching services.
- // Consider adding a capability to the system.
- if ( ! (accessControl instanceof AthenzFacade)) {
- throw new IllegalArgumentException("Athenz domain and service specified in deployment.xml, but not supported by system.");
- }
-
- // Verify that the config server is allowed to launch the service specified
- verifyAllowedLaunchAthenzService(applicationPackage.deploymentSpec());
-
- // If a user principal is initiating the request, verify that the user is allowed to launch the service.
- // Either the user is member of the domain admin role, or is given the "launch" privilege on the service.
- Optional<AthenzUser> athenzUser = getUser(deployer);
- if (athenzUser.isPresent()) {
- // This is a direct deployment, and we need only validate what the configserver will actually launch.
- DeploymentId id = deployment.orElseThrow(() -> new IllegalArgumentException("Unable to evaluate access, no zone provided in deployment"));
- var serviceToLaunch = applicationPackage.deploymentSpec().athenzService(id.applicationId().instance(),
- id.zoneId().environment(),
- id.zoneId().region())
- .map(service -> new AthenzService(identityDomain.get(), service.value()));
- if (serviceToLaunch.isPresent()) {
- if (
- ! ((AthenzFacade) accessControl).canLaunch(athenzUser.get(), serviceToLaunch.get()) && // launch privilege
- ! ((AthenzFacade) accessControl).hasTenantAdminAccess(athenzUser.get(), identityDomain.get()) // tenant admin
- ) {
- throw new IllegalArgumentException("User " + athenzUser.get().getFullName() + " is not allowed to launch " +
- "service " + serviceToLaunch.get().getFullName() + ". " +
- "Please reach out to the domain admin.");
- }
- } else {
- // This is a rare edge case where deployment.xml specifies athenz-service on each step, but not on the root.
- // It is undefined which service should be launched, so handle this as an error.
- throw new IllegalArgumentException("Athenz domain configured, but no service defined for deployment to " + id.zoneId().value());
- }
- } else {
- // If this is a deployment pipeline, verify that the domain in deployment.xml is the same as the tenant domain. Access control is already validated before this step.
- Tenant tenant = controller.tenants().require(tenantName);
- AthenzDomain tenantDomain = ((AthenzTenant) tenant).domain();
- if ( ! Objects.equals(tenantDomain, identityDomain.get()))
- throw new IllegalArgumentException("Athenz domain in deployment.xml: [" + identityDomain.get().getName() + "] " +
- "must match tenant domain: [" + tenantDomain.getName() + "]");
- }
- }
-
- private TenantAndApplicationId dashToUnderscore(TenantAndApplicationId id) {
- return TenantAndApplicationId.from(id.tenant().value(), id.application().value().replaceAll("-", "_"));
- }
-
- private ApplicationId dashToUnderscore(ApplicationId id) {
- return dashToUnderscore(TenantAndApplicationId.from(id)).instance(id.instance());
- }
-
- private QuotaUsage deploymentQuotaUsage(ZoneId zoneId, ApplicationId applicationId) {
- var application = configServer.nodeRepository().getApplication(zoneId, applicationId);
- return DeploymentQuotaCalculator.calculateQuotaUsage(application);
- }
-
- /*
- * Get the AthenzUser from this principal or Optional.empty if this does not represent a user.
- */
- private Optional<AthenzUser> getUser(Optional<Principal> deployer) {
- return deployer
- .filter(AthenzPrincipal.class::isInstance)
- .map(AthenzPrincipal.class::cast)
- .map(AthenzPrincipal::getIdentity)
- .filter(AthenzUser.class::isInstance)
- .map(AthenzUser.class::cast);
- }
-
- /*
- * Verifies that the configured athenz service (if any) can be launched.
- */
- private void verifyAllowedLaunchAthenzService(DeploymentSpec deploymentSpec) {
- deploymentSpec.athenzDomain().ifPresent(domain -> {
- controller.zoneRegistry().zones().reachable().ids().forEach(zone -> {
- AthenzIdentity configServerAthenzIdentity = controller.zoneRegistry().getConfigServerHttpsIdentity(zone);
- deploymentSpec.athenzService().ifPresent(service -> {
- verifyAthenzServiceCanBeLaunchedBy(configServerAthenzIdentity, new AthenzService(domain.value(), service.value()));
- });
- deploymentSpec.instances().forEach(spec -> {
- spec.athenzService(zone.environment(), zone.region()).ifPresent(service -> {
- verifyAthenzServiceCanBeLaunchedBy(configServerAthenzIdentity, new AthenzService(domain.value(), service.value()));
- });
- });
- });
- });
- }
-
- private void verifyAthenzServiceCanBeLaunchedBy(AthenzIdentity configServerAthenzIdentity, AthenzService athenzService) {
- if ( ! ((AthenzFacade) accessControl).canLaunch(configServerAthenzIdentity, athenzService))
- throw new IllegalArgumentException("Not allowed to launch Athenz service " + athenzService.getFullName());
- }
-
- /** Extract deployment warnings metric from deployment result */
- private static Map<DeploymentMetrics.Warning, Integer> warningsFrom(List<DeploymentResult.LogEntry> log) {
- return log.stream()
- .filter(entry -> entry.level().intValue() >= Level.WARNING.intValue())
- // TODO: Categorize warnings. Response from config server should be updated to include the appropriate
- // category and typed log level
- .collect(groupingBy(__ -> Warning.all,
- collectingAndThen(counting(), Long::intValue)));
- }
-
- public void verifyPlan(TenantName tenantName) {
- var planId = controller.serviceRegistry().billingController().getPlan(tenantName);
- Optional<Plan> plan = controller.serviceRegistry().planRegistry().plan(planId);
- if (plan.isEmpty())
- throw new IllegalArgumentException("Tenant '" + tenantName.value() + "' has no plan, not allowed to deploy. See https://cloud.vespa.ai/support");
- if (plan.get().quota().calculate().equals(Quota.zero()))
- throw new IllegalArgumentException("Tenant '" + tenantName.value() + "' has a plan '" +
- plan.get().displayName() + "' with zero quota, not allowed to deploy. See https://cloud.vespa.ai/support");
- }
-
-}