
com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageValidator 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.application.pkg;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.DeploymentSpec.DeclaredZone;
import com.yahoo.config.application.api.Endpoint;
import com.yahoo.config.application.api.Endpoint.Level;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.joining;
/**
* This contains validators for a {@link ApplicationPackage} that depend on a {@link Controller} to perform validation.
*
* @author mpolden
*/
public class ApplicationPackageValidator {
private final Controller controller;
public ApplicationPackageValidator(Controller controller) {
this.controller = Objects.requireNonNull(controller, "controller must be non-null");
}
/**
* Validate the given application package
*
* @throws IllegalArgumentException if any validations fail
*/
public void validate(Application application, ApplicationPackage applicationPackage, Instant instant) {
validateSteps(applicationPackage.deploymentSpec());
validateEndpointRegions(applicationPackage.deploymentSpec());
validateEndpointChange(application, applicationPackage, instant);
validateCompactedEndpoint(applicationPackage);
validateDeprecatedElements(applicationPackage);
validateCloudAccounts(application, applicationPackage);
}
private void validateCloudAccounts(Application application, ApplicationPackage applicationPackage) {
Set tenantAccounts = new TreeSet<>(controller.applications().accountsOf(application.id().tenant()));
Set declaredAccounts = new TreeSet<>(applicationPackage.deploymentSpec().cloudAccounts().values());
for (DeploymentInstanceSpec instance : applicationPackage.deploymentSpec().instances())
for (ZoneId zone : controller.zoneRegistry().zones().controllerUpgraded().ids())
declaredAccounts.addAll(instance.cloudAccounts(zone.environment(), zone.region()).values());
declaredAccounts.removeIf(tenantAccounts::contains);
declaredAccounts.removeIf(CloudAccount::isUnspecified);
if ( ! declaredAccounts.isEmpty())
throw new IllegalArgumentException("cloud accounts " +
declaredAccounts.stream().map(CloudAccount::value).collect(joining(", ", "[", "]")) +
" are not valid for tenant " +
application.id().tenant());
}
/** Verify that deployment spec does not use elements deprecated on a major version older than wanted major version */
private void validateDeprecatedElements(ApplicationPackage applicationPackage) {
int wantedMajor = applicationPackage.compileVersion().map(Version::getMajor)
.or(() -> applicationPackage.deploymentSpec().majorVersion())
.orElseGet(() -> controller.readSystemVersion().getMajor());
for (var deprecatedElement : applicationPackage.deploymentSpec().deprecatedElements()) {
if (deprecatedElement.majorVersion() >= wantedMajor) continue;
throw new IllegalArgumentException(deprecatedElement.humanReadableString());
}
}
/** Verify that each of the production zones listed in the deployment spec exist in this system */
private void validateSteps(DeploymentSpec deploymentSpec) {
for (var spec : deploymentSpec.instances()) {
for (var zone : spec.zones()) {
Environment environment = zone.environment();
if (zone.region().isEmpty()) continue;
ZoneId zoneId = ZoneId.from(environment, zone.region().get());
if (!controller.zoneRegistry().hasZone(zoneId)) {
throw new IllegalArgumentException("Zone " + zone + " in deployment spec was not found in this system!");
}
}
}
}
/** Verify that:
*
* - no single endpoint contains regions in different clouds
* - application endpoints with different regions must be contained in CGP and AWS
*
*/
private void validateEndpointRegions(DeploymentSpec deploymentSpec) {
for (var instance : deploymentSpec.instances()) {
validateEndpointRegions(instance.endpoints(), instance);
}
validateEndpointRegions(deploymentSpec.endpoints(), null);
}
private void validateEndpointRegions(List endpoints, DeploymentInstanceSpec instance) {
for (var endpoint : endpoints) {
RegionName[] regions = new HashSet<>(endpoint.regions()).toArray(RegionName[]::new);
Set clouds = controller.zoneRegistry().zones().all().in(Environment.prod)
.in(regions)
.zones().stream()
.map(ZoneApi::getCloudName)
.collect(Collectors.toSet());
String endpointString = instance == null ? "Application endpoint '" + endpoint.endpointId() + "'"
: "Endpoint '" + endpoint.endpointId() + "' in " + instance;
if (Set.of(CloudName.GCP, CloudName.AWS).containsAll(clouds)) { } // Everything is fine!
else if (Set.of(CloudName.YAHOO).containsAll(clouds) || Set.of(CloudName.DEFAULT).containsAll(clouds)) {
if (endpoint.level() == Level.application && regions.length != 1) {
throw new IllegalArgumentException(endpointString + " cannot contain different regions: " +
endpoint.regions().stream().sorted().toList());
}
}
else if (clouds.size() == 1) {
throw new IllegalArgumentException("unknown cloud '" + clouds.iterator().next() + "'");
}
else {
throw new IllegalArgumentException(endpointString + " cannot contain regions in different clouds: " +
endpoint.regions().stream().sorted().toList());
}
}
}
/** Verify endpoint configuration of given application package */
private void validateEndpointChange(Application application, ApplicationPackage applicationPackage, Instant instant) {
for (DeploymentInstanceSpec instance : applicationPackage.deploymentSpec().instances()) {
validateGlobalEndpointChanges(application, instance.name(), applicationPackage, instant);
validateZoneEndpointChanges(application, instance.name(), applicationPackage, instant);
}
}
/** Verify that compactable endpoint parts (instance name and endpoint ID) do not clash */
private void validateCompactedEndpoint(ApplicationPackage applicationPackage) {
Map, InstanceEndpoint> instanceEndpoints = new HashMap<>();
for (var instanceSpec : applicationPackage.deploymentSpec().instances()) {
for (var endpoint : instanceSpec.endpoints()) {
List nonCompactableIds = nonCompactableIds(instanceSpec.name(), endpoint);
InstanceEndpoint instanceEndpoint = new InstanceEndpoint(instanceSpec.name(), endpoint.endpointId());
InstanceEndpoint existingEndpoint = instanceEndpoints.get(nonCompactableIds);
if (existingEndpoint != null) {
throw new IllegalArgumentException("Endpoint with ID '" + endpoint.endpointId() + "' in instance '"
+ instanceSpec.name().value() +
"' clashes with endpoint '" + existingEndpoint.endpointId +
"' in instance '" + existingEndpoint.instance + "'");
}
instanceEndpoints.put(nonCompactableIds, instanceEndpoint);
}
}
}
/** Verify changes to endpoint configuration by comparing given application package to the existing one, if any */
private void validateGlobalEndpointChanges(Application application, InstanceName instanceName, ApplicationPackage applicationPackage, Instant instant) {
var validationId = ValidationId.globalEndpointChange;
if (applicationPackage.validationOverrides().allows(validationId, instant)) return;
var endpoints = application.deploymentSpec().instance(instanceName)
.map(deploymentInstanceSpec1 -> deploymentInstanceSpec1.endpoints())
.orElseGet(List::of);
DeploymentInstanceSpec deploymentInstanceSpec = applicationPackage.deploymentSpec().requireInstance(instanceName);
var newEndpoints = new ArrayList<>(deploymentInstanceSpec.endpoints());
if (newEndpoints.containsAll(endpoints)) return; // Adding new endpoints is fine
if (containsAllDestinationsOf(endpoints, newEndpoints)) return; // Adding destinations is fine
var removedEndpoints = new ArrayList<>(endpoints);
removedEndpoints.removeAll(newEndpoints);
newEndpoints.removeAll(endpoints);
throw new IllegalArgumentException(validationId.value() + ": application '" + application.id() +
(instanceName.isDefault() ? "" : "." + instanceName.value()) +
"' has endpoints " + endpoints +
", but does not include all of these in deployment.xml. Deploying given " +
"deployment.xml will remove " + removedEndpoints +
(newEndpoints.isEmpty() ? "" : " and add " + newEndpoints) +
". " + ValidationOverrides.toAllowMessage(validationId));
}
/** Verify changes to endpoint configuration by comparing given application package to the existing one, if any */
private void validateZoneEndpointChanges(Application application, InstanceName instance, ApplicationPackage applicationPackage, Instant now) {
ValidationId validationId = ValidationId.zoneEndpointChange;
if (applicationPackage.validationOverrides().allows(validationId, now)) return;;
String prefix = validationId + ": application '" + application.id() +
(instance.isDefault() ? "" : "." + instance.value()) + "' ";
DeploymentInstanceSpec spec = applicationPackage.deploymentSpec().requireInstance(instance);
for (DeclaredZone zone : spec.zones()) {
if (zone.environment() == Environment.prod) {
Map newEndpoints = spec.zoneEndpoints(ZoneId.from(zone.environment(), zone.region().get()));
application.deploymentSpec().instance(instance) // If old spec has this instance ...
.filter(oldSpec -> oldSpec.concerns(zone.environment(), zone.region())) // ... and deploys to this zone ...
.map(oldSpec -> oldSpec.zoneEndpoints(ZoneId.from(zone.environment(), zone.region().get())))
.ifPresent(oldEndpoints -> { // ... then we compare the endpoints present in both.
oldEndpoints.forEach((cluster, oldEndpoint) -> {
ZoneEndpoint newEndpoint = newEndpoints.getOrDefault(cluster, ZoneEndpoint.defaultEndpoint);
if ( ! newEndpoint.allowedUrns().containsAll(oldEndpoint.allowedUrns()))
throw new IllegalArgumentException(prefix + "allows access to cluster '" + cluster.value() +
"' in '" + zone.region().get().value() + "' to " +
oldEndpoint.allowedUrns().stream().map(AllowedUrn::toString).collect(joining(", ", "[", "]")) +
", but does not include all these in the new deployment spec. " +
"Deploying with the new settings will allow access to " +
(newEndpoint.allowedUrns().isEmpty() ? "no one" : newEndpoint.allowedUrns().stream().map(AllowedUrn::toString).collect(joining(", ", "[", "]")) +
". " + ValidationOverrides.toAllowMessage(validationId)));
});
newEndpoints.forEach((cluster, newEndpoint) -> {
ZoneEndpoint oldEndpoint = oldEndpoints.getOrDefault(cluster, ZoneEndpoint.defaultEndpoint);
if (oldEndpoint.isPublicEndpoint() && ! newEndpoint.isPublicEndpoint())
throw new IllegalArgumentException(prefix + "has a public endpoint for cluster '" + cluster.value() +
"' in '" + zone.region().get().value() + "', but the new deployment spec " +
"disables this. " + ValidationOverrides.toAllowMessage(validationId));
});
});
}
}
}
/** Returns whether newEndpoints contains all destinations in endpoints */
private static boolean containsAllDestinationsOf(List endpoints, List newEndpoints) {
var containsAllRegions = true;
var hasSameCluster = true;
for (var endpoint : endpoints) {
var endpointContainsAllRegions = false;
var endpointHasSameCluster = false;
for (var newEndpoint : newEndpoints) {
if (endpoint.endpointId().equals(newEndpoint.endpointId())) {
endpointContainsAllRegions = newEndpoint.regions().containsAll(endpoint.regions());
endpointHasSameCluster = newEndpoint.containerId().equals(endpoint.containerId());
}
}
containsAllRegions &= endpointContainsAllRegions;
hasSameCluster &= endpointHasSameCluster;
}
return containsAllRegions && hasSameCluster;
}
/** Returns a list of the non-compactable IDs of given instance and endpoint */
private static List nonCompactableIds(InstanceName instance, Endpoint endpoint) {
List ids = new ArrayList<>(2);
if (!instance.isDefault()) {
ids.add(instance.value());
}
if (!"default".equals(endpoint.endpointId())) {
ids.add(endpoint.endpointId());
}
return ids;
}
private record InstanceEndpoint(InstanceName instance, String endpointId) {}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy