diff options
Diffstat (limited to 'controller-server')
40 files changed, 641 insertions, 461 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 index bfe7fc1ee2e..8592460a24f 100644 --- 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 @@ -214,7 +214,7 @@ public class ApplicationController { public ApplicationStore applicationStore() { return applicationStore; } /** Returns all content clusters in all current deployments of the given application. */ - public Map<ZoneId, List<String>> listClusters(ApplicationId id, Iterable<ZoneId> zones) { + public Map<ZoneId, List<String>> contentClustersByZone(ApplicationId id, Iterable<ZoneId> zones) { ImmutableMap.Builder<ZoneId, List<String>> clusters = ImmutableMap.builder(); for (ZoneId zone : zones) clusters.put(zone, ImmutableList.copyOf(configServer.getContentClusters(new DeploymentId(id, zone)))); @@ -385,10 +385,9 @@ public class ApplicationController { } if (zone.environment().isProduction()) // Assign and register endpoints - application = withRotation(application, instance); - - endpoints = registerEndpointsInDns(application.get().deploymentSpec(), application.get().require(instanceId.instance()), zone); + application = withRotation(applicationPackage.deploymentSpec(), application, instance); + endpoints = registerEndpointsInDns(applicationPackage.deploymentSpec(), application.get().require(instanceId.instance()), zone); if (controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) { // Provisions a new certificate if missing @@ -518,9 +517,9 @@ public class ApplicationController { } /** Makes sure the application has a global rotation, if eligible. */ - private LockedApplication withRotation(LockedApplication application, InstanceName instanceName) { + private LockedApplication withRotation(DeploymentSpec deploymentSpec, LockedApplication application, InstanceName instanceName) { try (RotationLock rotationLock = rotationRepository.lock()) { - var rotations = rotationRepository.getOrAssignRotations(application.get().deploymentSpec(), + var rotations = rotationRepository.getOrAssignRotations(deploymentSpec, application.get().require(instanceName), rotationLock); application = application.with(instanceName, instance -> instance.with(rotations)); @@ -536,7 +535,7 @@ public class ApplicationController { */ private Set<ContainerEndpoint> registerEndpointsInDns(DeploymentSpec deploymentSpec, Instance instance, ZoneId zone) { var containerEndpoints = new HashSet<ContainerEndpoint>(); - var registerLegacyNames = deploymentSpec.globalServiceId().isPresent(); + boolean registerLegacyNames = deploymentSpec.instance(instance.name()).flatMap(i -> i.globalServiceId()).isPresent(); for (var assignedRotation : instance.rotations()) { var names = new ArrayList<String>(); var endpoints = instance.endpointsIn(controller.system(), assignedRotation.endpointId()) @@ -628,8 +627,8 @@ public class ApplicationController { private LockedApplication withoutDeletedDeployments(LockedApplication application, InstanceName instance) { DeploymentSpec deploymentSpec = application.get().deploymentSpec(); List<Deployment> deploymentsToRemove = application.get().require(instance).productionDeployments().values().stream() - .filter(deployment -> ! deploymentSpec.includes(deployment.zone().environment(), - Optional.of(deployment.zone().region()))) + .filter(deployment -> ! deploymentSpec.requireInstance(instance).includes(deployment.zone().environment(), + Optional.of(deployment.zone().region()))) .collect(Collectors.toList()); if (deploymentsToRemove.isEmpty()) return application; @@ -653,7 +652,7 @@ public class ApplicationController { private Instance withoutUnreferencedDeploymentJobs(DeploymentSpec deploymentSpec, Instance instance) { for (JobType job : JobList.from(instance).production().mapToList(JobStatus::type)) { ZoneId zone = job.zone(controller.system()); - if (deploymentSpec.includes(zone.environment(), Optional.of(zone.region()))) + if (deploymentSpec.requireInstance(instance.name()).includes(zone.environment(), Optional.of(zone.region()))) continue; instance = instance.withoutDeploymentJob(job); } @@ -911,9 +910,9 @@ public class ApplicationController { * 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 + * @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, ApplicationPackage applicationPackage, Optional<Principal> deployer) { verifyAllowedLaunchAthenzService(applicationPackage.deploymentSpec()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java index f885b7a146e..627cde28fd0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java @@ -13,7 +13,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationV import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; -import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; @@ -87,19 +86,12 @@ public class Instance { Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, applicationVersion, version, instant)); Deployment newDeployment = new Deployment(zone, applicationVersion, version, instant, - previousDeployment.clusterUtils(), previousDeployment.clusterInfo(), previousDeployment.metrics().with(warnings), previousDeployment.activity()); return with(newDeployment); } - public Instance withClusterUtilization(ZoneId zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) { - Deployment deployment = deployments.get(zone); - if (deployment == null) return this; // No longer deployed in this zone. - return with(deployment.withClusterUtils(clusterUtilization)); - } - public Instance withClusterInfo(ZoneId zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) { Deployment deployment = deployments.get(zone); if (deployment == null) return this; // No longer deployed in this zone. diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java index 03d084cd9e3..361dcf9dbf9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java @@ -1,7 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.application; -import com.google.common.collect.ImmutableMap; import com.yahoo.component.Version; import com.yahoo.config.provision.ClusterSpec.Id; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; @@ -9,7 +8,6 @@ import com.yahoo.config.provision.zone.ZoneId; import java.time.Instant; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -25,26 +23,24 @@ public class Deployment { private final ApplicationVersion applicationVersion; private final Version version; private final Instant deployTime; - private final Map<Id, ClusterUtilization> clusterUtilization; private final Map<Id, ClusterInfo> clusterInfo; private final DeploymentMetrics metrics; private final DeploymentActivity activity; public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime) { - this(zone, applicationVersion, version, deployTime, Collections.emptyMap(), Collections.emptyMap(), + this(zone, applicationVersion, version, deployTime, Collections.emptyMap(), DeploymentMetrics.none, DeploymentActivity.none); } public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime, - Map<Id, ClusterUtilization> clusterUtilization, Map<Id, ClusterInfo> clusterInfo, + Map<Id, ClusterInfo> clusterInfo, DeploymentMetrics metrics, DeploymentActivity activity) { this.zone = Objects.requireNonNull(zone, "zone cannot be null"); this.applicationVersion = Objects.requireNonNull(applicationVersion, "applicationVersion cannot be null"); this.version = Objects.requireNonNull(version, "version cannot be null"); this.deployTime = Objects.requireNonNull(deployTime, "deployTime cannot be null"); - this.clusterUtilization = ImmutableMap.copyOf(Objects.requireNonNull(clusterUtilization, "clusterUtilization cannot be null")); - this.clusterInfo = ImmutableMap.copyOf(Objects.requireNonNull(clusterInfo, "clusterInfo cannot be null")); + this.clusterInfo = Map.copyOf(Objects.requireNonNull(clusterInfo, "clusterInfo cannot be null")); this.metrics = Objects.requireNonNull(metrics, "deploymentMetrics cannot be null"); this.activity = Objects.requireNonNull(activity, "activity cannot be null"); } @@ -74,52 +70,26 @@ public class Deployment { return clusterInfo; } - /** Returns utilization of the clusters allocated to this */ - // TODO(mpolden): No longer updated. Remove this and associated serialization - public Map<Id, ClusterUtilization> clusterUtils() { - return clusterUtilization; - } - public Deployment recordActivityAt(Instant instant) { - return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics, + return new Deployment(zone, applicationVersion, version, deployTime, clusterInfo, metrics, activity.recordAt(instant, metrics)); } - public Deployment withClusterUtils(Map<Id, ClusterUtilization> clusterUtilization) { - return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics, + public Deployment withClusterUtils() { + return new Deployment(zone, applicationVersion, version, deployTime, clusterInfo, metrics, activity); } public Deployment withClusterInfo(Map<Id, ClusterInfo> newClusterInfo) { - return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, newClusterInfo, metrics, + return new Deployment(zone, applicationVersion, version, deployTime, newClusterInfo, metrics, activity); } public Deployment withMetrics(DeploymentMetrics metrics) { - return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics, + return new Deployment(zone, applicationVersion, version, deployTime, clusterInfo, metrics, activity); } - /** - * Calculate cost for this deployment. - * - * This is based on cluster utilization and cluster info. - */ - public DeploymentCost calculateCost() { - - Map<String, ClusterCost> costClusters = new HashMap<>(); - for (Id clusterId : clusterUtilization.keySet()) { - - // Only include cluster cost if we have both cluster utilization and cluster info - if (clusterInfo.containsKey(clusterId)) { - costClusters.put(clusterId.value(), new ClusterCost(clusterInfo.get(clusterId), - clusterUtilization.get(clusterId))); - } - } - - return new DeploymentCost(costClusters); - } - @Override public String toString() { return "deployment to " + zone + " of " + applicationVersion + " on version " + version + " at " + deployTime; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentCost.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentCost.java index 371e1c41e32..393c14b35d3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentCost.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentCost.java @@ -17,7 +17,7 @@ public class DeploymentCost { private final Map<String, ClusterCost> clusters; - DeploymentCost(Map<String, ClusterCost> clusterCosts) { + public DeploymentCost(Map<String, ClusterCost> clusterCosts) { clusters = new HashMap<>(clusterCosts); double tco = 0; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentSpecValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentSpecValidator.java index ce7904dc829..5c4d5874e53 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentSpecValidator.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentSpecValidator.java @@ -39,7 +39,7 @@ public class DeploymentSpecValidator { /** Verify that each of the production zones listed in the deployment spec exist in this system */ private void validateSteps(DeploymentSpec deploymentSpec) { new DeploymentSteps(deploymentSpec, controller::system).jobs(); - deploymentSpec.zones().stream() + deploymentSpec.instances().stream().flatMap(instance -> instance.zones().stream()) .filter(zone -> zone.environment() == Environment.prod) .forEach(zone -> { if ( ! controller.zoneRegistry().hasZone(ZoneId.from(zone.environment(), @@ -51,16 +51,19 @@ public class DeploymentSpecValidator { /** Verify that no single endpoint contains regions in different clouds */ private void validateEndpoints(DeploymentSpec deploymentSpec) { - for (var endpoint : deploymentSpec.endpoints()) { - var clouds = new HashSet<CloudName>(); - for (var region : endpoint.regions()) { - for (ZoneApi zone : controller.zoneRegistry().zones().all().in(region).zones()) { - clouds.add(zone.getCloudName()); + for (var instance : deploymentSpec.instances()) { + for (var endpoint : instance.endpoints()) { + var clouds = new HashSet<CloudName>(); + for (var region : endpoint.regions()) { + for (ZoneApi zone : controller.zoneRegistry().zones().all().in(region).zones()) { + clouds.add(zone.getCloudName()); + } + } + if (clouds.size() != 1) { + throw new IllegalArgumentException("Endpoint '" + endpoint.endpointId() + "' in " + instance + + " cannot contain regions in different clouds: " + + endpoint.regions().stream().sorted().collect(Collectors.toList())); } - } - if (clouds.size() != 1) { - throw new IllegalArgumentException("Endpoint '" + endpoint.endpointId() + "' cannot contain regions in different clouds: " + - endpoint.regions().stream().sorted().collect(Collectors.toList())); } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java index 376048143d9..3df889d7a88 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java @@ -372,9 +372,8 @@ public class DeploymentTrigger { } else { // All jobs are complete; find the time of completion of this step. if (stepJobs.isEmpty()) { // No jobs means this is a delay step. - Duration delay = ((DeploymentSpec.Delay) step).duration(); - completedAt = completedAt.map(at -> at.plus(delay)).filter(at -> !at.isAfter(clock.instant())); - reason += " after a delay of " + delay; + completedAt = completedAt.map(at -> at.plus(step.delay())).filter(at -> !at.isAfter(clock.instant())); + reason += " after a delay of " + step.delay(); } else { completedAt = stepJobs.stream().map(job -> instance.deploymentJobs().statusOf(job).get().lastCompleted().get().at()).max(naturalOrder()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java index 42e270edd5e..50af8bd8611 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java @@ -1,8 +1,6 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.deployment; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; @@ -468,7 +466,7 @@ public class InternalStepRunner implements StepRunner { testConfigSerializer.configJson(id.application(), id.type(), endpoints, - controller.applications().listClusters(id.application(), zones))); + controller.applications().contentClustersByZone(id.application(), zones))); return Optional.of(running); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java index 9253e249765..361cc43da50 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java @@ -12,6 +12,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.JobList; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.rotation.RotationLock; +import com.yahoo.vespa.hosted.controller.versions.NodeVersions; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import java.time.Clock; @@ -20,6 +21,7 @@ import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -36,6 +38,7 @@ public class MetricsReporter extends Maintainer { public static final String DEPLOYMENT_BUILD_AGE_SECONDS = "deployment.buildAgeSeconds"; public static final String DEPLOYMENT_WARNINGS = "deployment.warnings"; public static final String NODES_FAILING_SYSTEM_UPGRADE = "deployment.nodesFailingSystemUpgrade"; + public static final String NODES_FAILING_OS_UPGRADE = "deployment.nodesFailingOsUpgrade"; public static final String REMAINING_ROTATIONS = "remaining_rotations"; public static final String NAME_SERVICE_REQUESTS_QUEUED = "dns.queuedRequests"; @@ -56,6 +59,7 @@ public class MetricsReporter extends Maintainer { reportRemainingRotations(); reportQueuedNameServiceRequests(); reportNodesFailingSystemUpgrade(); + reportNodesFailingOsUpgrade(); } private void reportRemainingRotations() { @@ -103,13 +107,31 @@ public class MetricsReporter extends Maintainer { metric.set(NODES_FAILING_SYSTEM_UPGRADE, nodesFailingSystemUpgrade(), metric.createContext(Map.of())); } + private void reportNodesFailingOsUpgrade() { + metric.set(NODES_FAILING_OS_UPGRADE, nodesFailingOsUpgrade(), metric.createContext(Map.of())); + } + private int nodesFailingSystemUpgrade() { if (!controller().versionStatus().isUpgrading()) return 0; + return nodesFailingUpgrade(controller().versionStatus().versions(), (vespaVersion) -> { + if (vespaVersion.confidence() == VespaVersion.Confidence.broken) return NodeVersions.EMPTY; + return vespaVersion.nodeVersions(); + }); + } + + private int nodesFailingOsUpgrade() { + return nodesFailingUpgrade(controller().osVersionStatus().versions().entrySet(), (kv) -> { + var osVersion = kv.getKey(); + if (osVersion.version().isEmpty()) return NodeVersions.EMPTY; + return kv.getValue(); + }); + } + + private <V> int nodesFailingUpgrade(Collection<V> collection, Function<V, NodeVersions> nodeVersionsFunction) { var nodesFailingUpgrade = 0; var acceptableInstant = clock.instant().minus(NODE_UPGRADE_TIMEOUT); - for (var vespaVersion : controller().versionStatus().versions()) { - if (vespaVersion.confidence() == VespaVersion.Confidence.broken) continue; - for (var nodeVersion : vespaVersion.nodeVersions().asMap().values()) { + for (var object : collection) { + for (var nodeVersion : nodeVersionsFunction.apply(object).asMap().values()) { if (!nodeVersion.changing()) continue; if (nodeVersion.changedAt().isBefore(acceptableInstant)) nodesFailingUpgrade++; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java index 60bc3d15ec6..93d1dac7382 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java @@ -61,7 +61,7 @@ public class OsUpgrader extends InfrastructureUpgrader { // Return target if we have nodes in this cloud on a lower version return controller().osVersion(cloud) .filter(target -> controller().osVersionStatus().nodesIn(cloud).stream() - .anyMatch(node -> node.version().isBefore(target.version()))) + .anyMatch(node -> node.currentVersion().isBefore(target.version()))) .map(OsVersion::version); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java index c700ddac51c..0e14b61c5c5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java @@ -3,6 +3,8 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.zone.ZoneApi; +import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.jdisc.Metric; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; @@ -15,6 +17,7 @@ import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -47,33 +50,44 @@ public class ResourceMeterMaintainer extends Maintainer { @Override protected void maintain() { - Collection<ResourceSnapshot> resourceSnapshots = getResourceSnapshots(allocatedNodes()); + + Collection<ResourceSnapshot> resourceSnapshots = getAllResourceSnapshots(); meteringClient.consume(resourceSnapshots); metric.set(METERING_LAST_REPORTED, clock.millis() / 1000, metric.createContext(Collections.emptyMap())); // total metered resource usage, for alerting on drastic changes metric.set(METERING_TOTAL_REPORTED, - resourceSnapshots.stream().mapToDouble(r -> r.getCpuCores() + r.getMemoryGb() + r.getDiskGb()).sum(), + resourceSnapshots.stream() + .mapToDouble(r -> r.getCpuCores() + r.getMemoryGb() + r.getDiskGb()).sum(), metric.createContext(Collections.emptyMap())); } - private List<Node> allocatedNodes() { + private Collection<ResourceSnapshot> getAllResourceSnapshots() { return controller().zoneRegistry().zones() .ofCloud(CloudName.from("aws")) .reachable().zones().stream() - .flatMap(zone -> nodeRepository.list(zone.getId()).stream()) - .filter(node -> node.owner().isPresent()) - .filter(node -> ! node.owner().get().tenant().value().equals("hosted-vespa")) + .map(ZoneApi::getId) + .map(zoneId -> createResourceSnapshotsFromNodes(zoneId, nodeRepository.list(zoneId))) + .flatMap(Collection::stream) .collect(Collectors.toList()); } - private Collection<ResourceSnapshot> getResourceSnapshots(List<Node> nodes) { + private Collection<ResourceSnapshot> createResourceSnapshotsFromNodes(ZoneId zoneId, List<Node> nodes) { return nodes.stream() - .collect(Collectors.groupingBy(node -> node.owner().get(), - Collectors.collectingAndThen(Collectors.toList(), - nodeList -> ResourceSnapshot.from(nodeList, - clock.instant())) - )).values(); + .filter(unlessNodeOwnerIsHostedVespa()) + .collect(Collectors.groupingBy(node -> + node.owner().get(), + Collectors.collectingAndThen(Collectors.toList(), + nodeList -> ResourceSnapshot.from( + nodeList, + clock.instant(), + zoneId)) + )).values(); } + private Predicate<Node> unlessNodeOwnerIsHostedVespa() { + return node -> node.owner().map(owner -> + !owner.tenant().value().equals("hosted-vespa") + ).orElse(false); + } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java index 61fd0b67ec9..e67d5aea45d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java @@ -23,7 +23,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; -import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentActivity; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; @@ -88,7 +87,6 @@ public class ApplicationSerializer { private static final String pemDeployKeysField = "pemDeployKeys"; private static final String assignedRotationClusterField = "clusterId"; private static final String assignedRotationRotationField = "rotationId"; - private static final String applicationCertificateField = "applicationCertificate"; // Instance fields private static final String instanceNameField = "instanceName"; @@ -147,13 +145,6 @@ public class ApplicationSerializer { private static final String clusterInfoTypeField = "clusterType"; private static final String clusterInfoHostnamesField = "hostnames"; - // ClusterUtils fields - private static final String clusterUtilsField = "clusterUtils"; - private static final String clusterUtilsCpuField = "cpu"; - private static final String clusterUtilsMemField = "mem"; - private static final String clusterUtilsDiskField = "disk"; - private static final String clusterUtilsDiskBusyField = "diskbusy"; - // Deployment metrics fields private static final String deploymentMetricsField = "metrics"; private static final String deploymentMetricsQPSField = "queriesPerSecond"; @@ -220,7 +211,6 @@ public class ApplicationSerializer { object.setLong(deployTimeField, deployment.at().toEpochMilli()); toSlime(deployment.applicationVersion(), object.setObject(applicationPackageRevisionField)); clusterInfoToSlime(deployment.clusterInfo(), object); - clusterUtilsToSlime(deployment.clusterUtils(), object); deploymentMetricsToSlime(deployment.metrics(), object); deployment.activity().lastQueried().ifPresent(instant -> object.setLong(lastQueriedField, instant.toEpochMilli())); deployment.activity().lastWritten().ifPresent(instant -> object.setLong(lastWrittenField, instant.toEpochMilli())); @@ -262,20 +252,6 @@ public class ApplicationSerializer { } } - private void clusterUtilsToSlime(Map<ClusterSpec.Id, ClusterUtilization> clusters, Cursor object) { - Cursor root = object.setObject(clusterUtilsField); - for (Map.Entry<ClusterSpec.Id, ClusterUtilization> entry : clusters.entrySet()) { - toSlime(entry.getValue(), root.setObject(entry.getKey().value())); - } - } - - private void toSlime(ClusterUtilization utils, Cursor object) { - object.setDouble(clusterUtilsCpuField, utils.getCpu()); - object.setDouble(clusterUtilsMemField, utils.getMemory()); - object.setDouble(clusterUtilsDiskField, utils.getDisk()); - object.setDouble(clusterUtilsDiskBusyField, utils.getDiskBusy()); - } - private void zoneIdToSlime(ZoneId zone, Cursor object) { object.setString(environmentField, zone.environment().value()); object.setString(regionField, zone.region().value()); @@ -425,7 +401,6 @@ public class ApplicationSerializer { applicationVersionFromSlime(deploymentObject.field(applicationPackageRevisionField)), Version.fromString(deploymentObject.field(versionField).asString()), Instant.ofEpochMilli(deploymentObject.field(deployTimeField).asLong()), - Map.of(), clusterInfoMapFromSlime(deploymentObject.field(clusterInfoField)), deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)), DeploymentActivity.create(Serializers.optionalInstant(deploymentObject.field(lastQueriedField)), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java index 357dbb37b27..dbd52fc6d02 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java @@ -6,7 +6,6 @@ import com.google.inject.Inject; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.path.Path; @@ -82,14 +81,15 @@ public class CuratorDb { private static final Path applicationCertificateRoot = root.append("applicationCertificates"); private final StringSetSerializer stringSetSerializer = new StringSetSerializer(); - private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer(); + private final NodeVersionSerializer nodeVersionSerializer = new NodeVersionSerializer(); + private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer(nodeVersionSerializer); private final ControllerVersionSerializer controllerVersionSerializer = new ControllerVersionSerializer(); private final ConfidenceOverrideSerializer confidenceOverrideSerializer = new ConfidenceOverrideSerializer(); private final TenantSerializer tenantSerializer = new TenantSerializer(); private final ApplicationSerializer applicationSerializer = new ApplicationSerializer(); private final RunSerializer runSerializer = new RunSerializer(); private final OsVersionSerializer osVersionSerializer = new OsVersionSerializer(); - private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer); + private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer, nodeVersionSerializer); private final RoutingPolicySerializer routingPolicySerializer = new RoutingPolicySerializer(); private final AuditLogSerializer auditLogSerializer = new AuditLogSerializer(); private final NameServiceQueueSerializer nameServiceQueueSerializer = new NameServiceQueueSerializer(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java new file mode 100644 index 00000000000..4b6e997241d --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java @@ -0,0 +1,78 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.persistence; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.component.Version; +import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.slime.ArrayTraverser; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Inspector; +import com.yahoo.vespa.hosted.controller.versions.NodeVersion; +import com.yahoo.vespa.hosted.controller.versions.NodeVersions; + +import java.time.Instant; + +/** + * Serializer for {@link com.yahoo.vespa.hosted.controller.versions.NodeVersion}. + * + * @author mpolden + */ +public class NodeVersionSerializer { + + // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one + // (and rewrite all nodes on startup), changes to the serialized format must be made + // such that what is serialized on version N+1 can be read by version N: + // - ADDING FIELDS: Always ok + // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version. + // - CHANGING THE FORMAT OF A FIELD: Don't do it bro. + + private static final String hostnameField = "hostname"; + private static final String zoneField = "zone"; + private static final String wantedVersionField = "wantedVersion"; + private static final String changedAtField = "changedAt"; + + // Legacy fields + private static final String environmentField = "environment"; + private static final String regionField = "region"; + + public void nodeVersionsToSlime(NodeVersions nodeVersions, Cursor array) { + for (var nodeVersion : nodeVersions.asMap().values()) { + var nodeVersionObject = array.addObject(); + nodeVersionObject.setString(hostnameField, nodeVersion.hostname().value()); + nodeVersionObject.setString(zoneField, nodeVersion.zone().value()); + nodeVersionObject.setString(wantedVersionField, nodeVersion.wantedVersion().toFullString()); + nodeVersionObject.setLong(changedAtField, nodeVersion.changedAt().toEpochMilli()); + } + } + + public NodeVersions nodeVersionsFromSlime(Inspector array, Version version) { + var nodeVersions = ImmutableMap.<HostName, NodeVersion>builder(); + array.traverse((ArrayTraverser) (i, entry) -> { + var hostname = HostName.from(entry.field(hostnameField).asString()); + var zone = zoneFromSlime(entry); + // TODO(mpolden): Make the following fields non-optional after September 2019 + var wantedVersion = Serializers.optionalString(entry.field(wantedVersionField)) + .map(Version::fromString) + .orElse(Version.emptyVersion); + var changedAt = Serializers.optionalInstant(entry.field(changedAtField)).orElse(Instant.EPOCH); + nodeVersions.put(hostname, new NodeVersion(hostname, zone, version, wantedVersion, changedAt)); + }); + return new NodeVersions(nodeVersions.build()); + } + + // TODO(mpolden): Simplify and in-line after September 2019 + private ZoneId zoneFromSlime(Inspector object) { + var zoneInspector = object.field(zoneField); + if (zoneInspector.valid()) { + return ZoneId.from(zoneInspector.asString()); + } + var regionInspector = object.field(regionField); + var environmentInspector = object.field(environmentField); + if (regionInspector.valid() && environmentInspector.valid()) { + return ZoneId.from(environmentInspector.asString(), regionInspector.asString()); + } + return ZoneId.defaultId(); + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java index 88805f54d65..fa29969f166 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java @@ -1,23 +1,19 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.persistence; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; import com.yahoo.component.Version; -import com.yahoo.config.provision.Environment; -import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.RegionName; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; +import com.yahoo.vespa.hosted.controller.versions.NodeVersion; +import com.yahoo.vespa.hosted.controller.versions.NodeVersions; import com.yahoo.vespa.hosted.controller.versions.OsVersion; import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.TreeMap; /** * Serializer for {@link OsVersionStatus}. @@ -39,11 +35,14 @@ public class OsVersionStatusSerializer { private static final String hostnameField = "hostname"; private static final String regionField = "region"; private static final String environmentField = "environment"; + private static final String nodeVersionsField = "nodeVersions"; private final OsVersionSerializer osVersionSerializer; + private final NodeVersionSerializer nodeVersionSerializer; - public OsVersionStatusSerializer(OsVersionSerializer osVersionSerializer) { + public OsVersionStatusSerializer(OsVersionSerializer osVersionSerializer, NodeVersionSerializer nodeVersionSerializer) { this.osVersionSerializer = Objects.requireNonNull(osVersionSerializer, "osVersionSerializer must be non-null"); + this.nodeVersionSerializer = Objects.requireNonNull(nodeVersionSerializer, "nodeVersionSerializer must be non-null"); } public Slime toSlime(OsVersionStatus status) { @@ -53,6 +52,8 @@ public class OsVersionStatusSerializer { status.versions().forEach((version, nodes) -> { Cursor object = versions.addObject(); osVersionSerializer.toSlime(version, object); + nodeVersionSerializer.nodeVersionsToSlime(nodes, object.setArray(nodeVersionsField)); + // TODO(mpolden): Stop writing this after September 2019 nodesToSlime(nodes, object.setArray(nodesField)); }); return slime; @@ -62,40 +63,33 @@ public class OsVersionStatusSerializer { return new OsVersionStatus(osVersionsFromSlime(slime.get().field(versionsField))); } - private void nodesToSlime(List<OsVersionStatus.Node> nodes, Cursor array) { - nodes.forEach(node -> nodeToSlime(node, array.addObject())); + private void nodesToSlime(NodeVersions nodeVersions, Cursor array) { + nodeVersions.asMap().values().forEach(node -> nodeToSlime(node, array.addObject())); } - private void nodeToSlime(OsVersionStatus.Node node, Cursor object) { + private void nodeToSlime(NodeVersion node, Cursor object) { object.setString(hostnameField, node.hostname().value()); - object.setString(versionField, node.version().toFullString()); - object.setString(regionField, node.region().value()); - object.setString(environmentField, node.environment().value()); + object.setString(versionField, node.currentVersion().toFullString()); + object.setString(regionField, node.zone().region().value()); + object.setString(environmentField, node.zone().environment().value()); } - private Map<OsVersion, List<OsVersionStatus.Node>> osVersionsFromSlime(Inspector array) { - Map<OsVersion, List<OsVersionStatus.Node>> versions = new TreeMap<>(); + private ImmutableMap<OsVersion, NodeVersions> osVersionsFromSlime(Inspector array) { + var versions = ImmutableSortedMap.<OsVersion, NodeVersions>naturalOrder(); array.traverse((ArrayTraverser) (i, object) -> { OsVersion osVersion = osVersionSerializer.fromSlime(object); - List<OsVersionStatus.Node> nodes = nodesFromSlime(object.field(nodesField)); - versions.put(osVersion, nodes); + versions.put(osVersion, nodesFromSlime(object, osVersion.version())); }); - return Collections.unmodifiableMap(versions); + return versions.build(); } - private List<OsVersionStatus.Node> nodesFromSlime(Inspector array) { - List<OsVersionStatus.Node> nodes = new ArrayList<>(); - array.traverse((ArrayTraverser) (i, object) -> nodes.add(nodeFromSlime(object))); - return Collections.unmodifiableList(nodes); - } - - private OsVersionStatus.Node nodeFromSlime(Inspector object) { - return new OsVersionStatus.Node( - HostName.from(object.field(hostnameField).asString()), - Version.fromString(object.field(versionField).asString()), - Environment.from(object.field(environmentField).asString()), - RegionName.from(object.field(regionField).asString()) - ); + // TODO(mpolden): Simplify and in-line after September 2019 + private NodeVersions nodesFromSlime(Inspector object, Version version) { + var newField = object.field(nodeVersionsField); + if (newField.valid()) { + return nodeVersionSerializer.nodeVersionsFromSlime(newField, version); + } + return nodeVersionSerializer.nodeVersionsFromSlime(object.field(nodesField), version); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java index 5061f32da68..366e2c9af4b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java @@ -1,16 +1,13 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.persistence; -import com.google.common.collect.ImmutableMap; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.HostName; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics; -import com.yahoo.vespa.hosted.controller.versions.NodeVersion; import com.yahoo.vespa.hosted.controller.versions.NodeVersions; import com.yahoo.vespa.hosted.controller.versions.VersionStatus; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; @@ -19,9 +16,8 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; +import java.util.Objects; /** * Serializer for {@link VersionStatus}. @@ -53,17 +49,18 @@ public class VersionStatusSerializer { // NodeVersions fields private static final String nodeVersionsField = "nodeVersions"; - // NodeVersion fields - private static final String hostnameField = "hostname"; - private static final String wantedVersionField = "wantedVersion"; - private static final String changedAtField = "changedAt"; - // DeploymentStatistics fields private static final String versionField = "version"; private static final String failingField = "failing"; private static final String productionField = "production"; private static final String deployingField = "deploying"; + private final NodeVersionSerializer nodeVersionSerializer; + + public VersionStatusSerializer(NodeVersionSerializer nodeVersionSerializer) { + this.nodeVersionSerializer = Objects.requireNonNull(nodeVersionSerializer, "nodeVersionSerializer must be non-null"); + } + public Slime toSlime(VersionStatus status) { Slime slime = new Slime(); Cursor root = slime.setObject(); @@ -88,22 +85,11 @@ public class VersionStatusSerializer { object.setBool(isReleasedField, version.isReleased()); deploymentStatisticsToSlime(version.statistics(), object.setObject(deploymentStatisticsField)); object.setString(confidenceField, version.confidence().name()); - configServersToSlime(version.nodeVersions().hostnames(), object.setArray(configServersField)); nodeVersionsToSlime(version.nodeVersions(), object.setArray(nodeVersionsField)); } private void nodeVersionsToSlime(NodeVersions nodeVersions, Cursor array) { - for (NodeVersion nodeVersion : nodeVersions.asMap().values()) { - var nodeVersionObject = array.addObject(); - nodeVersionObject.setString(hostnameField, nodeVersion.hostname().value()); - nodeVersionObject.setString(wantedVersionField, nodeVersion.wantedVersion().toFullString()); - nodeVersionObject.setLong(changedAtField, nodeVersion.changedAt().toEpochMilli()); - } - } - - // TODO(mpolden): Remove after October 2019 - private void configServersToSlime(Set<HostName> configServerHostnames, Cursor array) { - configServerHostnames.stream().map(HostName::value).forEach(array::addString); + nodeVersionSerializer.nodeVersionsToSlime(nodeVersions, array); } private void deploymentStatisticsToSlime(DeploymentStatistics statistics, Cursor object) { @@ -131,37 +117,11 @@ public class VersionStatusSerializer { object.field(isControllerVersionField).asBool(), object.field(isSystemVersionField).asBool(), object.field(isReleasedField).asBool(), - nodeVersionsFromSlime(object, deploymentStatistics.version()), + nodeVersionSerializer.nodeVersionsFromSlime(object.field(nodeVersionsField), deploymentStatistics.version()), VespaVersion.Confidence.valueOf(object.field(confidenceField).asString()) ); } - private NodeVersions nodeVersionsFromSlime(Inspector root, Version version) { - var nodeVersions = ImmutableMap.<HostName, NodeVersion>builder(); - var nodeVersionsRoot = root.field(nodeVersionsField); - if (nodeVersionsRoot.valid()) { - nodeVersionsRoot.traverse((ArrayTraverser) (i, entry) -> { - var hostname = HostName.from(entry.field(hostnameField).asString()); - var wantedVersion = Version.fromString(entry.field(wantedVersionField).asString()); - var changedAt = Instant.ofEpochMilli(entry.field(changedAtField).asLong()); - nodeVersions.put(hostname, new NodeVersion(hostname, version, wantedVersion, changedAt)); - }); - } else { - // TODO(mpolden): Remove after October 2019 - var configServerHostnames = configServersFromSlime(root.field(configServersField)); - for (var hostname : configServerHostnames) { - nodeVersions.put(hostname, NodeVersion.empty(hostname)); - } - } - return new NodeVersions(nodeVersions.build()); - } - - private Set<HostName> configServersFromSlime(Inspector array) { - Set<HostName> configServerHostnames = new LinkedHashSet<>(); - array.traverse((ArrayTraverser) (i, entry) -> configServerHostnames.add(HostName.from(entry.asString()))); - return Collections.unmodifiableSet(configServerHostnames); - } - private DeploymentStatistics deploymentStatisticsFromSlime(Inspector object) { return new DeploymentStatistics(Version.fromString(object.field(versionField).asString()), applicationsFromSlime(object.field(failingField)), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index b76d0ae1094..c37309b87ad 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -102,7 +102,6 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Base64; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -381,9 +380,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Principal user = request.getJDiscRequest().getUserPrincipal(); String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString(); PublicKey developerKey = KeyUtils.fromPemEncodedPublicKey(pemDeveloperKey); - controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> - controller.tenants().store(tenant.withDeveloperKey(developerKey, user))); - return new MessageResponse("Set developer key " + pemDeveloperKey + " for " + user); + Slime root = new Slime(); + controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> { + tenant = tenant.withDeveloperKey(developerKey, user); + toSlime(root.setObject().setArray("keys"), tenant.get().developerKeys()); + controller.tenants().store(tenant); + }); + return new SlimeJsonResponse(root); } private HttpResponse removeDeveloperKey(String tenantName, HttpRequest request) { @@ -393,27 +396,49 @@ public class ApplicationApiHandler extends LoggingRequestHandler { String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString(); PublicKey developerKey = KeyUtils.fromPemEncodedPublicKey(pemDeveloperKey); Principal user = ((CloudTenant) controller.tenants().require(TenantName.from(tenantName))).developerKeys().get(developerKey); - controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> - controller.tenants().store(tenant.withoutDeveloperKey(developerKey))); - return new MessageResponse("Removed developer key " + pemDeveloperKey + " for " + user); + Slime root = new Slime(); + controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> { + tenant = tenant.withoutDeveloperKey(developerKey); + toSlime(root.setObject().setArray("keys"), tenant.get().developerKeys()); + controller.tenants().store(tenant); + }); + return new SlimeJsonResponse(root); + } + + private void toSlime(Cursor keysArray, Map<PublicKey, Principal> keys) { + keys.forEach((key, principal) -> { + Cursor keyObject = keysArray.addObject(); + keyObject.setString("key", KeyUtils.toPem(key)); + keyObject.setString("user", principal.getName()); + }); } private HttpResponse addDeployKey(String tenantName, String applicationName, HttpRequest request) { String pemDeployKey = toSlime(request.getData()).get().field("key").asString(); PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey); - controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> - controller.applications().store(application.withDeployKey(deployKey))); - - return new MessageResponse("Added deploy key " + pemDeployKey); + Slime root = new Slime(); + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> { + application = application.withDeployKey(deployKey); + application.get().deployKeys().stream() + .map(KeyUtils::toPem) + .forEach(root.setObject().setArray("keys")::addString); + controller.applications().store(application); + }); + return new SlimeJsonResponse(root); } private HttpResponse removeDeployKey(String tenantName, String applicationName, HttpRequest request) { String pemDeployKey = toSlime(request.getData()).get().field("key").asString(); PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey); - controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> - controller.applications().store(application.withoutDeployKey(deployKey))); - - return new MessageResponse("Removed deploy key " + pemDeployKey); + Slime root = new Slime(); + controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> { + application = application.withoutDeployKey(deployKey); + application.get().deployKeys().stream() + .map(KeyUtils::toPem) + .forEach(root.setObject().setArray("keys")::addString); + controller.applications().store(application); + }); + return new SlimeJsonResponse(root); } private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) { @@ -752,7 +777,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { deployment.activity().lastWritesPerSecond().ifPresent(value -> activity.setDouble("lastWritesPerSecond", value)); // Cost - DeploymentCost appCost = deployment.calculateCost(); + DeploymentCost appCost = new DeploymentCost(Map.of()); Cursor costObject = response.setObject("cost"); toSlime(appCost, costObject); @@ -1321,7 +1346,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(testConfigSerializer.configSlime(id, type, controller.applications().clusterEndpoints(id, zones), - controller.applications().listClusters(id, zones))); + controller.applications().contentClustersByZone(id, zones))); } private static DeploymentJobs.JobReport toJobReport(String tenantName, String applicationName, Inspector report) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java index 450f4481c5f..c168a057bfb 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java @@ -160,17 +160,17 @@ public class OsApiHandler extends AuditLoggingRequestHandler { Set<OsVersion> osVersions = controller.osVersions(); Cursor versions = root.setArray("versions"); - controller.osVersionStatus().versions().forEach((osVersion, nodes) -> { + controller.osVersionStatus().versions().forEach((osVersion, nodeVersions) -> { Cursor currentVersionObject = versions.addObject(); currentVersionObject.setString("version", osVersion.version().toFullString()); currentVersionObject.setBool("targetVersion", osVersions.contains(osVersion)); currentVersionObject.setString("cloud", osVersion.cloud().value()); Cursor nodesArray = currentVersionObject.setArray("nodes"); - nodes.forEach(node -> { + nodeVersions.asMap().values().forEach(nodeVersion -> { Cursor nodeObject = nodesArray.addObject(); - nodeObject.setString("hostname", node.hostname().value()); - nodeObject.setString("environment", node.environment().value()); - nodeObject.setString("region", node.region().value()); + nodeObject.setString("hostname", nodeVersion.hostname().value()); + nodeObject.setString("environment", nodeVersion.zone().environment().value()); + nodeObject.setString("region", nodeVersion.zone().region().value()); }); }); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java index a16ca5cb201..9f6bbcd2a5a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java @@ -77,22 +77,25 @@ public class RotationRepository { * If a rotation is already assigned to the application, that rotation will be returned. * If no rotation is assigned, return an available rotation. The caller is responsible for assigning the rotation. * - * @param deploymentSpec The deployment spec for the application - * @param instance The instance requesting a rotation - * @param lock Lock which must be acquired by the caller + * @param deploymentSpec the deployment spec for the application + * @param instance the instance requesting a rotation + * @param lock lock which must be acquired by the caller */ public Rotation getOrAssignRotation(DeploymentSpec deploymentSpec, Instance instance, RotationLock lock) { if ( ! instance.rotations().isEmpty()) { return allRotations.get(instance.rotations().get(0).rotationId()); } - if (deploymentSpec.globalServiceId().isEmpty()) { - throw new IllegalArgumentException("global-service-id is not set in deployment spec"); + + if (deploymentSpec.requireInstance(instance.name()).globalServiceId().isEmpty()) { + throw new IllegalArgumentException("global-service-id is not set in deployment spec for instance '" + + instance.name() + "'"); } - long productionZones = deploymentSpec.zones().stream() - .filter(zone -> zone.deploysTo(Environment.prod)) - .count(); + long productionZones = deploymentSpec.requireInstance(instance.name()).zones().stream() + .filter(zone -> zone.deploysTo(Environment.prod)) + .count(); if (productionZones < 2) { - throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined"); + throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined " + + "in instance '" + instance.name() + "'"); } return findAvailableRotation(instance.id(), lock); } @@ -110,22 +113,23 @@ public class RotationRepository { * @return List of rotation assignments - either new or existing */ public List<AssignedRotation> getOrAssignRotations(DeploymentSpec deploymentSpec, Instance instance, RotationLock lock) { - if (deploymentSpec.globalServiceId().isPresent() && ! deploymentSpec.endpoints().isEmpty()) { + if (deploymentSpec.requireInstance(instance.name()).globalServiceId().isPresent() + && ! deploymentSpec.requireInstance(instance.name()).endpoints().isEmpty()) { throw new IllegalArgumentException("Cannot provision rotations with both global-service-id and 'endpoints'"); } // Support the older case of setting global-service-id - if (deploymentSpec.globalServiceId().isPresent()) { - final var regions = deploymentSpec.zones().stream() - .filter(zone -> zone.environment().isProduction()) - .flatMap(zone -> zone.region().stream()) - .collect(Collectors.toSet()); + if (deploymentSpec.requireInstance(instance.name()).globalServiceId().isPresent()) { + var regions = deploymentSpec.requireInstance(instance.name()).zones().stream() + .filter(zone -> zone.environment().isProduction()) + .flatMap(zone -> zone.region().stream()) + .collect(Collectors.toSet()); - final var rotation = getOrAssignRotation(deploymentSpec, instance, lock); + var rotation = getOrAssignRotation(deploymentSpec, instance, lock); return List.of( new AssignedRotation( - new ClusterSpec.Id(deploymentSpec.globalServiceId().get()), + new ClusterSpec.Id(deploymentSpec.requireInstance(instance.name()).globalServiceId().get()), EndpointId.default_(), rotation.id(), regions @@ -133,8 +137,8 @@ public class RotationRepository { ); } - final Map<EndpointId, AssignedRotation> existingAssignments = existingEndpointAssignments(deploymentSpec, instance); - final Map<EndpointId, AssignedRotation> updatedAssignments = assignRotationsToEndpoints(deploymentSpec, existingAssignments, lock); + Map<EndpointId, AssignedRotation> existingAssignments = existingEndpointAssignments(deploymentSpec, instance); + Map<EndpointId, AssignedRotation> updatedAssignments = assignRotationsToEndpoints(deploymentSpec, existingAssignments, lock); existingAssignments.putAll(updatedAssignments); @@ -142,11 +146,11 @@ public class RotationRepository { } private Map<EndpointId, AssignedRotation> assignRotationsToEndpoints(DeploymentSpec deploymentSpec, Map<EndpointId, AssignedRotation> existingAssignments, RotationLock lock) { - final var availableRotations = new ArrayList<>(availableRotations(lock).values()); + var availableRotations = new ArrayList<>(availableRotations(lock).values()); - final var neededRotations = deploymentSpec.endpoints().stream() - .filter(Predicate.not(endpoint -> existingAssignments.containsKey(EndpointId.of(endpoint.endpointId())))) - .collect(Collectors.toSet()); + var neededRotations = deploymentSpec.endpoints().stream() + .filter(Predicate.not(endpoint -> existingAssignments.containsKey(EndpointId.of(endpoint.endpointId())))) + .collect(Collectors.toSet()); if (neededRotations.size() > availableRotations.size()) { throw new IllegalStateException("Hosted Vespa ran out of rotations, unable to assign rotation: need " + neededRotations.size() + ", have " + availableRotations.size()); @@ -172,34 +176,26 @@ public class RotationRepository { } private Map<EndpointId, AssignedRotation> existingEndpointAssignments(DeploymentSpec deploymentSpec, Instance instance) { - // // Get the regions that has been configured for an endpoint. Empty set if the endpoint // is no longer mentioned in the configuration file. - // - final Function<EndpointId, Set<RegionName>> configuredRegionsForEndpoint = endpointId -> { - return deploymentSpec.endpoints().stream() + Function<EndpointId, Set<RegionName>> configuredRegionsForEndpoint = endpointId -> + deploymentSpec.requireInstance(instance.name()).endpoints().stream() .filter(endpoint -> endpointId.id().equals(endpoint.endpointId())) .map(Endpoint::regions) .findFirst() .orElse(Set.of()); - }; - // // Build a new AssignedRotation instance where we update set of regions from the configuration instead - // of using the one already mentioned in the assignment. This allows us to overwrite the set of regions - // when - final Function<AssignedRotation, AssignedRotation> assignedRotationWithConfiguredRegions = assignedRotation -> { - return new AssignedRotation( + // of using the one already mentioned in the assignment. This allows us to overwrite the set of regions. + Function<AssignedRotation, AssignedRotation> assignedRotationWithConfiguredRegions = assignedRotation -> + new AssignedRotation( assignedRotation.clusterId(), assignedRotation.endpointId(), assignedRotation.rotationId(), - configuredRegionsForEndpoint.apply(assignedRotation.endpointId()) - ); - }; + configuredRegionsForEndpoint.apply(assignedRotation.endpointId())); return instance.rotations().stream() - .collect( - Collectors.toMap( + .collect(Collectors.toMap( AssignedRotation::endpointId, assignedRotationWithConfiguredRegions, (a, b) -> { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java index 0a690b90410..8d0232afa58 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.versions; import com.yahoo.component.Version; import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.zone.ZoneId; import java.time.Instant; import java.util.Objects; @@ -17,12 +18,14 @@ import java.util.Objects; public class NodeVersion { private final HostName hostname; + private final ZoneId zone; private final Version currentVersion; private final Version wantedVersion; private final Instant changedAt; - public NodeVersion(HostName hostname, Version currentVersion, Version wantedVersion, Instant changedAt) { + public NodeVersion(HostName hostname, ZoneId zone, Version currentVersion, Version wantedVersion, Instant changedAt) { this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); + this.zone = Objects.requireNonNull(zone, "zone must be non-null"); this.currentVersion = Objects.requireNonNull(currentVersion, "version must be non-null"); this.wantedVersion = Objects.requireNonNull(wantedVersion, "wantedVersion must be non-null"); this.changedAt = Objects.requireNonNull(changedAt, "changedAt must be non-null"); @@ -33,6 +36,11 @@ public class NodeVersion { return hostname; } + /** Zone of this */ + public ZoneId zone() { + return zone; + } + /** Current version of this */ public Version currentVersion() { return currentVersion; @@ -56,18 +64,18 @@ public class NodeVersion { /** Returns a copy of this with current version set to given version */ public NodeVersion withCurrentVersion(Version version, Instant changedAt) { if (currentVersion.equals(version)) return this; - return new NodeVersion(hostname, version, wantedVersion, changedAt); + return new NodeVersion(hostname, zone, version, wantedVersion, changedAt); } /** Returns a copy of this with wanted version set to given version */ public NodeVersion withWantedVersion(Version version) { if (wantedVersion.equals(version)) return this; - return new NodeVersion(hostname, currentVersion, version, changedAt); + return new NodeVersion(hostname, zone, currentVersion, version, changedAt); } @Override public String toString() { - return hostname + ": " + currentVersion + " -> " + wantedVersion + " [changedAt=" + changedAt + "]"; + return hostname + ": " + currentVersion + " -> " + wantedVersion + " [zone=" + zone + ", changedAt=" + changedAt + "]"; } @Override @@ -76,6 +84,7 @@ public class NodeVersion { if (o == null || getClass() != o.getClass()) return false; NodeVersion that = (NodeVersion) o; return hostname.equals(that.hostname) && + zone.equals(that.zone) && currentVersion.equals(that.currentVersion) && wantedVersion.equals(that.wantedVersion) && changedAt.equals(that.changedAt); @@ -83,11 +92,11 @@ public class NodeVersion { @Override public int hashCode() { - return Objects.hash(hostname, currentVersion, wantedVersion, changedAt); + return Objects.hash(hostname, zone, currentVersion, wantedVersion, changedAt); } public static NodeVersion empty(HostName hostname) { - return new NodeVersion(hostname, Version.emptyVersion, Version.emptyVersion, Instant.EPOCH); + return new NodeVersion(hostname, ZoneId.defaultId(), Version.emptyVersion, Version.emptyVersion, Instant.EPOCH); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java index a73a20198f0..d5e83d99cdd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java @@ -4,9 +4,7 @@ package com.yahoo.vespa.hosted.controller.versions; import com.google.common.collect.ImmutableMap; import com.yahoo.component.Version; import com.yahoo.config.provision.CloudName; -import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.application.SystemApplication; @@ -14,11 +12,11 @@ import com.yahoo.vespa.hosted.controller.maintenance.OsUpgrader; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -29,25 +27,25 @@ import java.util.stream.Collectors; */ public class OsVersionStatus { - public static final OsVersionStatus empty = new OsVersionStatus(Collections.emptyMap()); + public static final OsVersionStatus empty = new OsVersionStatus(ImmutableMap.of()); - private final Map<OsVersion, List<Node>> versions; + private final Map<OsVersion, NodeVersions> versions; /** Public for serialization purpose only. Use {@link OsVersionStatus#compute(Controller)} for an up-to-date status */ - public OsVersionStatus(Map<OsVersion, List<Node>> versions) { + public OsVersionStatus(ImmutableMap<OsVersion, NodeVersions> versions) { this.versions = ImmutableMap.copyOf(Objects.requireNonNull(versions, "versions must be non-null")); } /** All known OS versions and their nodes */ - public Map<OsVersion, List<Node>> versions() { + public Map<OsVersion, NodeVersions> versions() { return versions; } /** Returns nodes eligible for OS upgrades that exist in given cloud */ - public List<Node> nodesIn(CloudName cloud) { + public List<NodeVersion> nodesIn(CloudName cloud) { return versions.entrySet().stream() .filter(entry -> entry.getKey().cloud().equals(cloud)) - .flatMap(entry -> entry.getValue().stream()) + .flatMap(entry -> entry.getValue().asMap().values().stream()) .collect(Collectors.toUnmodifiableList()); } @@ -61,28 +59,52 @@ public class OsVersionStatus { /** Compute the current OS versions in this system. This is expensive and should be called infrequently */ public static OsVersionStatus compute(Controller controller) { - Map<OsVersion, List<Node>> versions = new HashMap<>(); - - // Always include all target versions - controller.osVersions().forEach(osVersion -> versions.put(osVersion, new ArrayList<>())); - - for (SystemApplication application : SystemApplication.all()) { - if (!application.isEligibleForOsUpgrades()) { - continue; // Avoid querying applications that are not eligible for OS upgrades - } - for (ZoneApi zone : zonesToUpgrade(controller)) { - controller.serviceRegistry().configServer().nodeRepository().list(zone.getId(), application.id()).stream() + var osVersionStatus = controller.osVersionStatus(); + var osVersions = new HashMap<OsVersion, List<NodeVersion>>(); + var now = controller.clock().instant(); + controller.osVersions().forEach(osVersion -> osVersions.put(osVersion, new ArrayList<>())); + + for (var application : SystemApplication.all()) { + if (!application.isEligibleForOsUpgrades()) continue; + for (var zone : zonesToUpgrade(controller)) { + var targetOsVersion = controller.serviceRegistry().configServer().nodeRepository() + .targetVersionsOf(zone.getId()) + .osVersion(application.nodeType()) + .orElse(Version.emptyVersion); + controller.serviceRegistry().configServer().nodeRepository() + .list(zone.getId(), application.id()).stream() .filter(node -> OsUpgrader.eligibleForUpgrade(node, application)) - .map(node -> new Node(node.hostname(), node.currentOsVersion(), zone.getEnvironment(), zone.getRegionName())) - .forEach(node -> { - var version = new OsVersion(node.version(), zone.getCloudName()); - versions.putIfAbsent(version, new ArrayList<>()); - versions.get(version).add(node); + .map(node -> new NodeVersion(node.hostname(), zone.getId(), node.currentOsVersion(), targetOsVersion, now)) + .forEach(nodeVersion -> { + var newNodeVersion = osVersionStatus.of(nodeVersion.hostname()) + .map(nv -> nv.withCurrentVersion(nodeVersion.currentVersion(), now) + .withWantedVersion(nodeVersion.wantedVersion())) + .orElse(nodeVersion); + var version = new OsVersion(newNodeVersion.currentVersion(), zone.getCloudName()); + osVersions.putIfAbsent(version, new ArrayList<>()); + osVersions.get(version).add(newNodeVersion); }); } } - return new OsVersionStatus(versions); + var newOsVersions = ImmutableMap.<OsVersion, NodeVersions>builder(); + for (var osVersion : osVersions.entrySet()) { + var nodeVersions = ImmutableMap.<HostName, NodeVersion>builder(); + for (var nodeVersion : osVersion.getValue()) { + nodeVersions.put(nodeVersion.hostname(), nodeVersion); + } + newOsVersions.put(osVersion.getKey(), new NodeVersions(nodeVersions.build())); + } + return new OsVersionStatus(newOsVersions.build()); + } + + /** Returns version of node identified by given host name */ + private Optional<NodeVersion> of(HostName hostname) { + return versions.values().stream() + .map(nodeVersions -> nodeVersions.asMap().get(hostname)) + .map(Optional::ofNullable) + .flatMap(Optional::stream) + .findFirst(); } private static List<ZoneApi> zonesToUpgrade(Controller controller) { @@ -92,52 +114,4 @@ public class OsVersionStatus { .collect(Collectors.toUnmodifiableList()); } - /** A node in this system and its current OS version */ - public static class Node { - - private final HostName hostname; - private final Version version; - private final Environment environment; - private final RegionName region; - - public Node(HostName hostname, Version version, Environment environment, RegionName region) { - this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); - this.version = Objects.requireNonNull(version, "version must be non-null"); - this.environment = Objects.requireNonNull(environment, "environment must be non-null"); - this.region = Objects.requireNonNull(region, "region must be non-null"); - } - - public HostName hostname() { - return hostname; - } - - public Version version() { - return version; - } - - public Environment environment() { - return environment; - } - - public RegionName region() { - return region; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Node node = (Node) o; - return Objects.equals(hostname, node.hostname) && - Objects.equals(version, node.version) && - environment == node.environment && - Objects.equals(region, node.region); - } - - @Override - public int hashCode() { - return Objects.hash(hostname, version, environment, region); - } - } - } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java index bb43ec20234..ab445de5a7f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java @@ -172,7 +172,7 @@ public class VersionStatus { for (var node : nodes) { // Only use current node version if config has converged Version version = configConverged ? node.currentVersion() : controller.systemVersion(); - newNodeVersions.add(new NodeVersion(node.hostname(), version, node.wantedVersion(), now)); + newNodeVersions.add(new NodeVersion(node.hostname(), zone.getId(), version, node.wantedVersion(), now)); } } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index ebf80eb9daa..e3682a78b7d 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -72,7 +72,6 @@ public class ControllerTest { @Test public void testDeployment() { // Setup system - ApplicationController applications = tester.controller().applications(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") @@ -753,7 +752,7 @@ public class ControllerTest { tester.deployCompletely(application, applicationPackage); fail("Expected exception"); } catch (IllegalArgumentException e) { - assertEquals("Endpoint 'default' cannot contain regions in different clouds: [aws-us-east-1, us-west-1]", e.getMessage()); + assertEquals("Endpoint 'default' in instance 'default' cannot contain regions in different clouds: [aws-us-east-1, us-west-1]", e.getMessage()); } var applicationPackage2 = new ApplicationPackageBuilder() @@ -766,7 +765,7 @@ public class ControllerTest { tester.deployCompletely(application, applicationPackage2); fail("Expected exception"); } catch (IllegalArgumentException e) { - assertEquals("Endpoint 'foo' cannot contain regions in different clouds: [aws-us-east-1, us-west-1]", e.getMessage()); + assertEquals("Endpoint 'foo' in instance 'default' cannot contain regions in different clouds: [aws-us-east-1, us-west-1]", e.getMessage()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java index 25e562ed046..9449f2b0854 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java @@ -47,6 +47,7 @@ public class ApplicationPackageBuilder { private final List<X509Certificate> trustedCertificates = new ArrayList<>(); private OptionalInt majorVersion = OptionalInt.empty(); + private String instances = "default"; private String upgradePolicy = null; private Environment environment = Environment.prod; private String globalServiceId = null; @@ -58,6 +59,11 @@ public class ApplicationPackageBuilder { return this; } + public ApplicationPackageBuilder instances(String instances) { + this.instances = instances; + return this; + } + public ApplicationPackageBuilder upgradePolicy(String upgradePolicy) { this.upgradePolicy = upgradePolicy; return this; @@ -90,7 +96,7 @@ public class ApplicationPackageBuilder { } public ApplicationPackageBuilder region(String regionName) { - environmentBody.append(" <region active='true'>"); + environmentBody.append(" <region active='true'>"); environmentBody.append(regionName); environmentBody.append("</region>\n"); return this; @@ -112,7 +118,7 @@ public class ApplicationPackageBuilder { public ApplicationPackageBuilder blockChange(boolean revision, boolean version, String daySpec, String hourSpec, String zoneSpec) { - blockChange.append(" <block-change"); + blockChange.append(" <block-change"); blockChange.append(" revision='").append(revision).append("'"); blockChange.append(" version='").append(version).append("'"); blockChange.append(" days='").append(daySpec).append("'"); @@ -166,14 +172,15 @@ public class ApplicationPackageBuilder { xml.append(athenzIdentityAttributes); } xml.append(">\n"); + xml.append(" <instance id='").append(instances).append("'>\n"); if (upgradePolicy != null) { - xml.append("<upgrade policy='"); + xml.append(" <upgrade policy='"); xml.append(upgradePolicy); xml.append("'/>\n"); } xml.append(notifications); xml.append(blockChange); - xml.append(" <"); + xml.append(" <"); xml.append(environment.value()); if (globalServiceId != null) { xml.append(" global-service-id='"); @@ -182,13 +189,14 @@ public class ApplicationPackageBuilder { } xml.append(">\n"); xml.append(environmentBody); - xml.append(" </"); + xml.append(" </"); xml.append(environment.value()); xml.append(">\n"); - xml.append(" <endpoints>\n"); + xml.append(" <endpoints>\n"); xml.append(endpointsBody); - xml.append(" </endpoints>\n"); - xml.append("</deployment>"); + xml.append(" </endpoints>\n"); + xml.append(" </instance>\n"); + xml.append("</deployment>\n"); return xml.toString().getBytes(UTF_8); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java index 6da77a967f1..6e7a50b5f81 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java @@ -151,15 +151,44 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer /** Set version for an application in a given zone */ public void setVersion(ApplicationId application, ZoneId zone, Version version) { - setVersion(application, zone, version, -1); + setVersion(application, zone, version, -1, false); } /** Set version for nodeCount number of nodes in application in a given zone */ public void setVersion(ApplicationId application, ZoneId zone, Version version, int nodeCount) { + setVersion(application, zone, version, nodeCount, false); + } + + /** Set OS version for an application in a given zone */ + public void setOsVersion(ApplicationId application, ZoneId zone, Version version) { + setOsVersion(application, zone, version, -1); + } + + /** Set OS version for an application in a given zone */ + public void setOsVersion(ApplicationId application, ZoneId zone, Version version, int nodeCount) { + setVersion(application, zone, version, nodeCount, true); + } + + private void setVersion(ApplicationId application, ZoneId zone, Version version, int nodeCount, boolean osVersion) { int n = 0; for (Node node : nodeRepository().list(zone, application)) { - nodeRepository().putByHostname(zone, new Node(node.hostname(), node.state(), node.type(), node.owner(), - version, version)); + Node newNode; + if (osVersion) { + newNode = new Node(node.hostname(), node.state(), node.type(), node.owner(), node.currentVersion(), + node.wantedVersion(), version, version, node.serviceState(), + node.restartGeneration(), node.wantedRestartGeneration(), node.rebootGeneration(), + node.wantedRebootGeneration(), node.vcpu(), node.memoryGb(), node.diskGb(), + node.bandwidthGbps(), node.fastDisk(), node.cost(), node.canonicalFlavor(), + node.clusterId(), node.clusterType()); + } else { + newNode = new Node(node.hostname(), node.state(), node.type(), node.owner(), version, + version, node.currentOsVersion(), node.wantedOsVersion(), node.serviceState(), + node.restartGeneration(), node.wantedRestartGeneration(), node.rebootGeneration(), + node.wantedRebootGeneration(), node.vcpu(), node.memoryGb(), node.diskGb(), + node.bandwidthGbps(), node.fastDisk(), node.cost(), node.canonicalFlavor(), + node.clusterId(), node.clusterType()); + } + nodeRepository().putByHostname(zone, newNode); if (++n == nodeCount) break; } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java index 9cb40d60677..44785407874 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.zone.UpgradePolicy; import com.yahoo.config.provision.zone.ZoneId; @@ -262,6 +263,57 @@ public class MetricsReporterTest { } } + @Test + public void test_nodes_failing_os_upgrade() { + var tester = new DeploymentTester(); + var reporter = createReporter(tester.controller()); + var zone = ZoneApiMock.fromId("prod.eu-west-1"); + var cloud = CloudName.defaultName(); + tester.controllerTester().zoneRegistry().setOsUpgradePolicy(cloud, UpgradePolicy.create().upgrade(zone)); + var osUpgrader = new OsUpgrader(tester.controller(), Duration.ofDays(1), + new JobControl(tester.controllerTester().curator()), CloudName.defaultName());; + var statusUpdater = new OsVersionStatusUpdater(tester.controller(), Duration.ofDays(1), + new JobControl(tester.controller().curator())); + tester.configServer().bootstrap(List.of(zone.getId()), SystemApplication.tenantHost); + + // All nodes upgrade to initial OS version + var version0 = Version.fromString("8.0"); + tester.controller().upgradeOsIn(cloud, version0, false); + osUpgrader.maintain(); + tester.configServer().setOsVersion(SystemApplication.tenantHost.id(), zone.getId(), version0); + statusUpdater.maintain(); + reporter.maintain(); + assertEquals(0, getNodesFailingOsUpgrade()); + + for (var version : List.of(Version.fromString("8.1"), Version.fromString("8.2"))) { + // System starts upgrading to next OS version + tester.controller().upgradeOsIn(cloud, version, false); + osUpgrader.maintain(); + statusUpdater.maintain(); + reporter.maintain(); + assertEquals(0, getNodesFailingOsUpgrade()); + + // 30 minutes pass and nothing happens + tester.clock().advance(Duration.ofMinutes(30)); + statusUpdater.maintain(); + reporter.maintain(); + assertEquals(0, getNodesFailingOsUpgrade()); + + // 1/3 nodes upgrade within timeout + tester.configServer().setOsVersion(SystemApplication.tenantHost.id(), zone.getId(), version, 1); + tester.clock().advance(Duration.ofMinutes(30).plus(Duration.ofSeconds(1))); + statusUpdater.maintain(); + reporter.maintain(); + assertEquals(2, getNodesFailingOsUpgrade()); + + // 3/3 nodes upgrade + tester.configServer().setOsVersion(SystemApplication.tenantHost.id(), zone.getId(), version); + statusUpdater.maintain(); + reporter.maintain(); + assertEquals(0, getNodesFailingOsUpgrade()); + } + } + private Duration getAverageDeploymentDuration(ApplicationId id) { return Duration.ofSeconds(getMetric(MetricsReporter.DEPLOYMENT_AVERAGE_DURATION, id).longValue()); } @@ -278,6 +330,10 @@ public class MetricsReporterTest { return metrics.getMetric(MetricsReporter.NODES_FAILING_SYSTEM_UPGRADE).intValue(); } + private int getNodesFailingOsUpgrade() { + return metrics.getMetric(MetricsReporter.NODES_FAILING_OS_UPGRADE).intValue(); + } + private Number getMetric(String name, ApplicationId id) { return metrics.getMetric((dimensions) -> id.tenant().value().equals(dimensions.get("tenant")) && appDimension(id).equals(dimensions.get("app")), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java index 1af5fafbb79..5e92112d465 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java @@ -12,7 +12,7 @@ import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; -import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus; +import com.yahoo.vespa.hosted.controller.versions.NodeVersion; import org.junit.Before; import org.junit.Test; @@ -111,13 +111,13 @@ public class OsUpgraderTest { assertWanted(version1, SystemApplication.tenantHost, zone1.getId(), zone2.getId(), zone3.getId(), zone4.getId()); statusUpdater.maintain(); assertTrue("All nodes on target version", tester.controller().osVersionStatus().nodesIn(cloud).stream() - .allMatch(node -> node.version().equals(version1))); + .allMatch(node -> node.currentVersion().equals(version1))); } - private List<OsVersionStatus.Node> nodesOn(Version version) { + private List<NodeVersion> nodesOn(Version version) { return tester.controller().osVersionStatus().versions().entrySet().stream() .filter(entry -> entry.getKey().version().equals(version)) - .flatMap(entry -> entry.getValue().stream()) + .flatMap(entry -> entry.getValue().asMap().values().stream()) .collect(Collectors.toList()); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java index fe7f39fd66d..e51fcff33d1 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java @@ -3,18 +3,15 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.provision.CloudName; +import com.yahoo.config.provision.zone.UpgradePolicy; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.vespa.hosted.controller.ControllerTester; -import com.yahoo.config.provision.zone.UpgradePolicy; -import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; import com.yahoo.vespa.hosted.controller.versions.OsVersion; import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus; import org.junit.Test; import java.time.Duration; -import java.util.List; -import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -47,10 +44,10 @@ public class OsVersionStatusUpdaterTest { tester.controller().upgradeOsIn(cloud, version1, false); statusUpdater.maintain(); - Map<OsVersion, List<OsVersionStatus.Node>> osVersions = tester.controller().osVersionStatus().versions(); + var osVersions = tester.controller().osVersionStatus().versions(); assertEquals(2, osVersions.size()); - assertFalse("All nodes on unknown version", osVersions.get(new OsVersion(Version.emptyVersion, cloud)).isEmpty()); - assertTrue("No nodes on current target", osVersions.get(new OsVersion(version1, cloud)).isEmpty()); + assertFalse("All nodes on unknown version", osVersions.get(new OsVersion(Version.emptyVersion, cloud)).asMap().isEmpty()); + assertTrue("No nodes on current target", osVersions.get(new OsVersion(version1, cloud)).asMap().isEmpty()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java index f28ce83e643..0245e7475f7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java @@ -11,7 +11,6 @@ import org.junit.Test; import java.time.Duration; import java.util.Collection; -import java.util.List; import static org.junit.Assert.assertEquals; @@ -27,20 +26,14 @@ public class ResourceMeterMaintainerTest { @Test public void testMaintainer() { - var awsZone = ZoneApiMock.newBuilder().withId("prod.aws-us-east-1").withCloud("aws").build(); - tester.zoneRegistry().setZones( - ZoneApiMock.newBuilder().withId("prod.us-east-3").build(), - ZoneApiMock.newBuilder().withId("prod.us-west-1").build(), - ZoneApiMock.newBuilder().withId("prod.us-central-1").build(), - awsZone); - tester.configServer().nodeRepository().addFixedNodes(awsZone.getId()); + setUpZones(); ResourceMeterMaintainer resourceMeterMaintainer = new ResourceMeterMaintainer(tester.controller(), Duration.ofMinutes(5), new JobControl(tester.curator()), metrics, snapshotConsumer); resourceMeterMaintainer.maintain(); Collection<ResourceSnapshot> consumedResources = snapshotConsumer.consumedResources(); // The mocked repository contains two applications, so we should also consume two ResourceSnapshots - assertEquals(2, consumedResources.size()); + assertEquals(4, consumedResources.size()); ResourceSnapshot app1 = consumedResources.stream().filter(snapshot -> snapshot.getApplicationId().equals(ApplicationId.from("tenant1", "app1", "default"))).findFirst().orElseThrow(); ResourceSnapshot app2 = consumedResources.stream().filter(snapshot -> snapshot.getApplicationId().equals(ApplicationId.from("tenant2", "app2", "default"))).findFirst().orElseThrow(); @@ -53,7 +46,19 @@ public class ResourceMeterMaintainerTest { assertEquals(500, app2.getDiskGb(), DELTA); assertEquals(tester.clock().millis()/1000, metrics.getMetric("metering_last_reported")); - assertEquals(1112.0d, (Double) metrics.getMetric("metering_total_reported"), DELTA); + assertEquals(2224.0d, (Double) metrics.getMetric("metering_total_reported"), DELTA); } + private void setUpZones() { + ZoneApiMock nonAwsZone = ZoneApiMock.newBuilder().withId("test.region-1").build(); + ZoneApiMock awsZone1 = ZoneApiMock.newBuilder().withId("prod.region-2").withCloud("aws").build(); + ZoneApiMock awsZone2 = ZoneApiMock.newBuilder().withId("test.region-3").withCloud("aws").build(); + tester.zoneRegistry().setZones( + nonAwsZone, + awsZone1, + awsZone2); + tester.configServer().nodeRepository().addFixedNodes(nonAwsZone.getId()); + tester.configServer().nodeRepository().addFixedNodes(awsZone1.getId()); + tester.configServer().nodeRepository().addFixedNodes(awsZone2.getId()); + } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index 08963b9fec7..447bce0a544 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -19,7 +19,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.User; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; -import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentActivity; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; @@ -92,7 +91,7 @@ public class ApplicationSerializerTest { Instant activityAt = Instant.parse("2018-06-01T10:15:30.00Z"); deployments.add(new Deployment(zone1, applicationVersion1, Version.fromString("1.2.3"), Instant.ofEpochMilli(3))); // One deployment without cluster info and utils deployments.add(new Deployment(zone2, applicationVersion2, Version.fromString("1.2.3"), Instant.ofEpochMilli(5), - createClusterUtils(3, 0.2), createClusterInfo(3, 4), + createClusterInfo(3, 4), new DeploymentMetrics(2, 3, 4, 5, 6, Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS)), Map.of(DeploymentMetrics.Warning.all, 3)), @@ -191,10 +190,6 @@ public class ApplicationSerializerTest { assertEquals(original.require(id1.instance()).rotations(), serialized.require(id1.instance()).rotations()); assertEquals(original.require(id1.instance()).rotationStatus(), serialized.require(id1.instance()).rotationStatus()); - // Test cluster utilization - assertEquals(0, serialized.require(id1.instance()).deployments().get(zone1).clusterUtils().size()); - assertEquals(0, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().size()); - // Test cluster info assertEquals(3, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().size()); assertEquals(10, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorCost()); @@ -232,21 +227,6 @@ public class ApplicationSerializerTest { return result; } - private Map<ClusterSpec.Id, ClusterUtilization> createClusterUtils(int clusters, double inc) { - Map<ClusterSpec.Id, ClusterUtilization> result = new HashMap<>(); - - ClusterUtilization util = new ClusterUtilization(0,0,0,0); - for (int cluster = 0; cluster < clusters; cluster++) { - double agg = cluster*inc; - result.put(ClusterSpec.Id.from("id" + cluster), new ClusterUtilization( - util.getMemory()+ agg, - util.getCpu()+ agg, - util.getDisk() + agg, - util.getDiskBusy() + agg)); - } - return result; - } - @Test public void testCompleteApplicationDeserialization() throws Exception { byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-application.json")); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java index 5073f651fd3..ba771d70d26 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java @@ -1,18 +1,23 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.persistence; +import com.google.common.collect.ImmutableMap; import com.yahoo.component.Version; import com.yahoo.config.provision.CloudName; -import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.config.SlimeUtils; +import com.yahoo.vespa.hosted.controller.versions.NodeVersion; +import com.yahoo.vespa.hosted.controller.versions.NodeVersions; import com.yahoo.vespa.hosted.controller.versions.OsVersion; import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus; import org.junit.Test; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; import java.util.List; -import java.util.Map; -import java.util.TreeMap; import static org.junit.Assert.assertEquals; @@ -25,22 +30,41 @@ public class OsVersionStatusSerializerTest { public void test_serialization() { Version version1 = Version.fromString("7.1"); Version version2 = Version.fromString("7.2"); - Map<OsVersion, List<OsVersionStatus.Node>> versions = new TreeMap<>(); + var versions = ImmutableMap.<OsVersion, NodeVersions>builder(); - versions.put(new OsVersion(version1, CloudName.defaultName()), List.of( - new OsVersionStatus.Node(HostName.from("node1"), version1, Environment.prod, RegionName.from("us-west")), - new OsVersionStatus.Node(HostName.from("node2"), version1, Environment.prod, RegionName.from("us-east")) - )); - versions.put(new OsVersion(version2, CloudName.defaultName()), List.of( - new OsVersionStatus.Node(HostName.from("node3"), version2, Environment.prod, RegionName.from("us-west")), - new OsVersionStatus.Node(HostName.from("node4"), version2, Environment.prod, RegionName.from("us-east")) + versions.put(new OsVersion(version1, CloudName.defaultName()), NodeVersions.EMPTY.with(List.of( + new NodeVersion(HostName.from("node1"), ZoneId.from("prod", "us-west"), version1, version2, Instant.ofEpochMilli(1)), + new NodeVersion(HostName.from("node2"), ZoneId.from("prod", "us-east"), version1, version2, Instant.ofEpochMilli(2)) + ))); + versions.put(new OsVersion(version2, CloudName.defaultName()), NodeVersions.EMPTY.with(List.of( + new NodeVersion(HostName.from("node3"), ZoneId.from("prod", "us-west"), version2, version2, Instant.ofEpochMilli(3)), + new NodeVersion(HostName.from("node4"), ZoneId.from("prod", "us-east"), version2, version2, Instant.ofEpochMilli(4)) + ))); - )); - - OsVersionStatusSerializer serializer = new OsVersionStatusSerializer(new OsVersionSerializer()); - OsVersionStatus status = new OsVersionStatus(versions); + OsVersionStatusSerializer serializer = new OsVersionStatusSerializer(new OsVersionSerializer(), new NodeVersionSerializer()); + OsVersionStatus status = new OsVersionStatus(versions.build()); OsVersionStatus serialized = serializer.fromSlime(serializer.toSlime(status)); assertEquals(status.versions(), serialized.versions()); } + @Test + public void testLegacySerialization() throws Exception { + var data = Files.readAllBytes(Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/os-version-status-legacy-format.json")); + var serializer = new OsVersionStatusSerializer(new OsVersionSerializer(), new NodeVersionSerializer()); + var versions = ImmutableMap.of( + new OsVersion(Version.fromString("7.42"), CloudName.from("yahoo")), + NodeVersions.EMPTY.with(List.of(new NodeVersion(HostName.from("node1"), ZoneId.from("prod", "us-north-1"), + Version.fromString("7.42"), Version.emptyVersion, Instant.EPOCH), + new NodeVersion(HostName.from("node2"), ZoneId.from("test", "us-north-2"), + Version.fromString("7.42"), Version.emptyVersion, Instant.EPOCH)))); + + var deserialized = serializer.fromSlime(SlimeUtils.jsonToSlime(data)); + assertEquals(versions, deserialized.versions()); + + + var serialized = new String(SlimeUtils.toJsonBytes(serializer.toSlime(new OsVersionStatus(versions))), StandardCharsets.UTF_8); + assertEquals("{\"versions\":[{\"version\":\"7.42.0\",\"cloud\":\"yahoo\",\"nodeVersions\":[{\"hostname\":\"node1\",\"zone\":\"prod.us-north-1\",\"wantedVersion\":\"0.0.0\",\"changedAt\":0},{\"hostname\":\"node2\",\"zone\":\"test.us-north-2\",\"wantedVersion\":\"0.0.0\",\"changedAt\":0}],\"nodes\":[{\"hostname\":\"node1\",\"version\":\"7.42.0\",\"region\":\"us-north-1\",\"environment\":\"prod\"},{\"hostname\":\"node2\",\"version\":\"7.42.0\",\"region\":\"us-north-2\",\"environment\":\"test\"}]}]}", + serialized); + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java index 5d65cf0381e..a80dcc118dc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.persistence; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics; import com.yahoo.vespa.hosted.controller.versions.NodeVersion; @@ -45,7 +46,7 @@ public class VersionStatusSerializerTest { false, nodeVersions(Version.fromString("5.0"), Version.fromString("5.1"), Instant.ofEpochMilli(456), "cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal)); VersionStatus status = new VersionStatus(vespaVersions); - VersionStatusSerializer serializer = new VersionStatusSerializer(); + VersionStatusSerializer serializer = new VersionStatusSerializer(new NodeVersionSerializer()); VersionStatus deserialized = serializer.fromSlime(serializer.toSlime(status)); assertEquals(status.versions().size(), deserialized.versions().size()); @@ -67,7 +68,7 @@ public class VersionStatusSerializerTest { @Test public void testLegacySerialization() throws Exception { var data = Files.readAllBytes(Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json")); - var serializer = new VersionStatusSerializer(); + var serializer = new VersionStatusSerializer(new NodeVersionSerializer()); var deserializedStatus = serializer.fromSlime(SlimeUtils.jsonToSlime(data)); var statistics = new DeploymentStatistics( @@ -76,11 +77,16 @@ public class VersionStatusSerializerTest { List.of(), List.of() ); + var nodeVersions = List.of(new NodeVersion(HostName.from("cfg1"), ZoneId.defaultId(), Version.fromString("7.0"), + Version.fromString("7.1"), Instant.ofEpochMilli(1111)), + new NodeVersion(HostName.from("cfg2"), ZoneId.defaultId(), Version.fromString("7.0"), + Version.fromString("7.1"), Instant.ofEpochMilli(2222)), + new NodeVersion(HostName.from("cfg3"), ZoneId.defaultId(), Version.fromString("7.0"), + Version.fromString("7.1"), Instant.ofEpochMilli(3333))); var vespaVersion = new VespaVersion(statistics, "badc0ffee", Instant.ofEpochMilli(123), true, true, true, - nodeVersions(Version.emptyVersion, Version.emptyVersion, - Instant.EPOCH, "cfg1", "cfg2", "cfg3"), + NodeVersions.EMPTY.with(nodeVersions), VespaVersion.Confidence.normal); VespaVersion deserialized = deserializedStatus.versions().get(0); @@ -97,7 +103,7 @@ public class VersionStatusSerializerTest { private static NodeVersions nodeVersions(Version version, Version wantedVersion, Instant changedAt, String... hostnames) { var nodeVersions = new ArrayList<NodeVersion>(); for (var hostname : hostnames) { - nodeVersions.add(new NodeVersion(HostName.from(hostname), version, wantedVersion, changedAt)); + nodeVersions.add(new NodeVersion(HostName.from(hostname), ZoneId.from("prod", "us-north-1"), version, wantedVersion, changedAt)); } return NodeVersions.EMPTY.with(nodeVersions); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/os-version-status-legacy-format.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/os-version-status-legacy-format.json new file mode 100644 index 00000000000..5a6a864cbf8 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/os-version-status-legacy-format.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "7.42", + "cloud": "yahoo", + "nodes": [ + { + "hostname": "node1", + "version": "7.42", + "region": "us-north-1", + "environment": "prod" + }, + { + "hostname": "node2", + "version": "7.42", + "region": "us-north-2", + "environment": "test" + } + ] + } + ] +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json index 96ca22e1c1a..08463ed7cb4 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json @@ -13,10 +13,22 @@ "deploying": [] }, "confidence": "normal", - "configServerHostnames": [ - "cfg1", - "cfg2", - "cfg3" + "nodeVersions": [ + { + "hostname": "cfg1", + "wantedVersion": "7.1", + "changedAt": 1111 + }, + { + "hostname": "cfg2", + "wantedVersion": "7.1", + "changedAt": 2222 + }, + { + "hostname": "cfg3", + "wantedVersion": "7.1", + "changedAt": 3333 + } ] } ] diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 7cacd91a5c4..9c957785606 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -45,7 +45,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMeteringClien import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; -import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; @@ -65,8 +64,6 @@ import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; -import com.yahoo.vespa.hosted.controller.rotation.RotationState; -import com.yahoo.vespa.hosted.controller.rotation.RotationStatus; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.yolean.Exceptions; @@ -84,7 +81,6 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -117,7 +113,18 @@ public class ApplicationApiTest extends ControllerContainerTest { "-----END PUBLIC KEY-----\n"; private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n"); - private static final ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + private static final ApplicationPackage applicationPackageDefault = new ApplicationPackageBuilder() + .instances("default") + .environment(Environment.prod) + .globalServiceId("foo") + .region("us-central-1") + .region("us-east-3") + .region("us-west-1") + .blockChange(false, true, "mon-fri", "0-8", "UTC") + .build(); + + private static final ApplicationPackage applicationPackageInstance1 = new ApplicationPackageBuilder() + .instances("instance1") .environment(Environment.prod) .globalServiceId("foo") .region("us-central-1") @@ -225,7 +232,7 @@ public class ApplicationApiTest extends ControllerContainerTest { addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); // POST (deploy) an application to a zone - manual user deployment (includes a content hash for verification) - MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/instance1/deploy", POST) .data(entity) .header("X-Content-Hash", Base64.getEncoder().encodeToString(Signatures.sha256Digest(entity::data))) @@ -245,7 +252,7 @@ public class ApplicationApiTest extends ControllerContainerTest { controllerTester.jobCompletion(JobType.component) .application(id) .projectId(screwdriverProjectId) - .uploadArtifact(applicationPackage) + .uploadArtifact(applicationPackageInstance1) .submit(); // ... systemtest @@ -309,6 +316,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST (create) another application ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("instance1") .environment(Environment.prod) .region("us-west-1") .build(); @@ -354,7 +362,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", POST) .userIdentity(USER_ID) .data("{\"key\":\"" + pemPublicKey + "\"}"), - "{\"message\":\"Added deploy key " + quotedPemPublicKey + "\"}"); + "{\"keys\":[\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\\n-----END PUBLIC KEY-----\\n\"]}"); // PATCH in a pem deploy key at deprecated path tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", PATCH) @@ -377,7 +385,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", DELETE) .userIdentity(USER_ID) .data("{\"key\":\"" + pemPublicKey + "\"}"), - "{\"message\":\"Removed deploy key " + quotedPemPublicKey + "\"}"); + "{\"keys\":[]}"); tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET) .userIdentity(USER_ID), @@ -585,6 +593,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Second attempt has a service under a different domain than the tenant of the application, and fails. ApplicationPackage packageWithServiceForWrongDomain = new ApplicationPackageBuilder() + .instances("instance1") .environment(Environment.prod) .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN_2.getName()), AthenzService.from("service")) .region("us-west-1") @@ -597,6 +606,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Third attempt finally has a service under the domain of the tenant, and succeeds. ApplicationPackage packageWithService = new ApplicationPackageBuilder() + .instances("instance1") .environment(Environment.prod) .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN.getName()), AthenzService.from("service")) .region("us-west-1") @@ -710,6 +720,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("instance1") .globalServiceId("foo") .region("us-west-1") .region("us-east-3") @@ -718,7 +729,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Create tenant and deploy ApplicationId id = createTenantAndApplication(); long projectId = 1; - MultiPartStreamer deployData = createApplicationDeployData(Optional.empty(), false); + MultiPartStreamer deployData = createApplicationDeployData(Optional.of(applicationPackage), false); startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); // us-west-1 @@ -781,6 +792,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("instance1") .region("us-west-1") .region("us-east-3") .region("eu-west-1") @@ -857,7 +869,7 @@ public class ApplicationApiTest extends ControllerContainerTest { new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId("application1")); // POST (deploy) an application to a prod zone - allowed when project ID is not specified - MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/deploy", POST) .data(entity) .screwdriverIdentity(SCREWDRIVER_ID), @@ -889,6 +901,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Deploy ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("instance1") .region("us-east-3") .build(); ApplicationId id = createTenantAndApplication(); @@ -908,6 +921,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // New zone is added before us-east-3 applicationPackage = new ApplicationPackageBuilder() + .instances("instance1") .globalServiceId("foo") // These decides the ordering of deploymentJobs and instances in the response .region("us-west-1") @@ -953,9 +967,9 @@ public class ApplicationApiTest extends ControllerContainerTest { ResourceAllocation lastMonth = new ResourceAllocation(24, 48, 2000); ApplicationId applicationId = ApplicationId.from("doesnotexist", "doesnotexist", "default"); Map<ApplicationId, List<ResourceSnapshot>> snapshotHistory = Map.of(applicationId, List.of( - new ResourceSnapshot(applicationId, 1, 2,3, Instant.ofEpochMilli(123)), - new ResourceSnapshot(applicationId, 1, 2,3, Instant.ofEpochMilli(246)), - new ResourceSnapshot(applicationId, 1, 2,3, Instant.ofEpochMilli(492)))); + new ResourceSnapshot(applicationId, 1, 2,3, Instant.ofEpochMilli(123), ZoneId.defaultId()), + new ResourceSnapshot(applicationId, 1, 2,3, Instant.ofEpochMilli(246), ZoneId.defaultId()), + new ResourceSnapshot(applicationId, 1, 2,3, Instant.ofEpochMilli(492), ZoneId.defaultId()))); mockMeteringClient.setMeteringInfo(new MeteringInfo(thisMonth, lastMonth, currentSnapshot, snapshotHistory)); @@ -1060,7 +1074,7 @@ public class ApplicationApiTest extends ControllerContainerTest { configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to prepare application", ConfigServerException.ErrorCode.INVALID_APPLICATION_PACKAGE, null)); // POST (deploy) an application with an invalid application package - MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/instance1/deploy", POST) .data(entity) .userIdentity(USER_ID), @@ -1180,7 +1194,7 @@ public class ApplicationApiTest extends ControllerContainerTest { 200); // Deploy to an authorized zone by a user tenant is disallowed - MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); + MultiPartStreamer entity = createApplicationDeployData(applicationPackageDefault, true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST) .data(entity) .userIdentity(USER_ID), @@ -1593,7 +1607,7 @@ public class ApplicationApiTest extends ControllerContainerTest { } private MultiPartStreamer createApplicationDeployData(Optional<ApplicationPackage> applicationPackage, - Optional<ApplicationVersion> applicationVersion, boolean deployDirectly) { + Optional<ApplicationVersion> applicationVersion, boolean deployDirectly) { MultiPartStreamer streamer = new MultiPartStreamer(); streamer.addJson("deployOptions", deployOptions(deployDirectly, applicationVersion)); applicationPackage.ifPresent(ap -> streamer.addBytes("applicationZip", ap.zippedContent())); @@ -1745,14 +1759,11 @@ public class ApplicationApiTest extends ControllerContainerTest { clusterInfo.put(ClusterSpec.Id.from("cluster1"), new ClusterInfo("flavor1", 37, 2, 4, 50, ClusterSpec.Type.content, hostnames)); - Map<ClusterSpec.Id, ClusterUtilization> clusterUtils = new HashMap<>(); - clusterUtils.put(ClusterSpec.Id.from("cluster1"), new ClusterUtilization(0.3, 0.6, 0.4, 0.3)); DeploymentMetrics metrics = new DeploymentMetrics(1, 2, 3, 4, 5, Optional.of(Instant.ofEpochMilli(123123)), Map.of()); lockedApplication = lockedApplication.with(instance.name(), lockedInstance -> lockedInstance.withClusterInfo(deployment.zone(), clusterInfo) - .withClusterUtilization(deployment.zone(), clusterUtils) .with(deployment.zone(), metrics) .recordActivityAt(Instant.parse("2018-06-01T10:15:30.00Z"), deployment.zone())); } @@ -1771,17 +1782,6 @@ public class ApplicationApiTest extends ControllerContainerTest { new RotationStatusUpdater(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator())).run(); } - private RotationStatus rotationStatus(Instance instance) { - return controllerTester.controller().applications().rotationRepository().getRotation(instance) - .map(rotation -> { - var rotationStatus = controllerTester.controller().serviceRegistry().globalRoutingService().getHealthStatus(rotation.name()); - var statusMap = new LinkedHashMap<ZoneId, RotationState>(); - rotationStatus.forEach((zone, status) -> statusMap.put(zone, RotationState.in)); - return RotationStatus.from(Map.of(rotation.id(), statusMap)); - }) - .orElse(RotationStatus.EMPTY); - } - private void updateContactInformation() { Contact contact = new Contact(URI.create("www.contacts.tld/1234"), URI.create("www.properties.tld/1234"), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java index 0a4d046e318..bb1e6b6256a 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java @@ -76,8 +76,8 @@ public class DeploymentApiTest extends ControllerContainerTest { version.isControllerVersion(), version.isSystemVersion(), version.isReleased(), - NodeVersions.EMPTY.with(List.of(new NodeVersion(HostName.from("config1.test"), version.versionNumber(), version.versionNumber(), Instant.EPOCH), - new NodeVersion(HostName.from("config2.test"), version.versionNumber(), version.versionNumber(), Instant.EPOCH))), + NodeVersions.EMPTY.with(List.of(new NodeVersion(HostName.from("config1.test"), ZoneId.defaultId(), version.versionNumber(), version.versionNumber(), Instant.EPOCH), + new NodeVersion(HostName.from("config2.test"), ZoneId.defaultId(), version.versionNumber(), version.versionNumber(), Instant.EPOCH))), VespaVersion.confidenceFrom(version.statistics(), controller) ); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java index f2410c47908..b1f5f33b960 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java @@ -143,14 +143,14 @@ public class UserApiTest extends ControllerContainerCloudTest { tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST) .roles(Set.of(Role.tenantOperator(id.tenant()))) .data("{\"key\":\"" + pemPublicKey + "\"}"), - "{\"message\":\"Added deploy key " + quotedPemPublicKey + "\"}"); + new File("first-deploy-key.json")); // POST a pem developer key tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST) .user("joe@dev") .roles(Set.of(Role.tenantOperator(id.tenant()))) .data("{\"key\":\"" + pemPublicKey + "\"}"), - "{\"message\":\"Set developer key " + quotedPemPublicKey + " for joe@dev\"}"); + new File("first-developer-key.json")); // POST the same pem developer key for a different user is forbidden tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST) @@ -165,7 +165,7 @@ public class UserApiTest extends ControllerContainerCloudTest { .user("operator@tenant") .roles(Set.of(Role.tenantOperator(id.tenant()))) .data("{\"key\":\"" + otherPemPublicKey + "\"}"), - "{\"message\":\"Set developer key " + otherQuotedPemPublicKey + " for operator@tenant\"}"); + new File("both-developer-keys.json")); // GET tenant information with keys tester.assertResponse(request("/application/v4/tenant/my-tenant/") @@ -176,7 +176,7 @@ public class UserApiTest extends ControllerContainerCloudTest { tester.assertResponse(request("/application/v4/tenant/my-tenant/key", DELETE) .roles(Set.of(Role.tenantOperator(id.tenant()))) .data("{\"key\":\"" + pemPublicKey + "\"}"), - "{\"message\":\"Removed developer key " + quotedPemPublicKey + " for joe@dev\"}"); + new File("second-developer-key.json")); // DELETE an application role is allowed for an application admin. tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", DELETE) diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json new file mode 100644 index 00000000000..2ff1c29fe29 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n", + "user": "joe@dev" + }, + { + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n", + "user": "operator@tenant" + } + ] +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json new file mode 100644 index 00000000000..1c86877b77d --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json @@ -0,0 +1,5 @@ +{ + "keys": [ + "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n" + ] +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json new file mode 100644 index 00000000000..b7d48f283f3 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json @@ -0,0 +1,9 @@ +{ + "keys": [ + { + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n", + "user": "joe@dev" + } + ] +} + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json new file mode 100644 index 00000000000..f7d90f31116 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json @@ -0,0 +1,8 @@ +{ + "keys": [ + { + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n", + "user": "operator@tenant" + } + ] +} |