summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java270
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java39
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateException.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java77
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java68
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java267
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java55
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporter.java89
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContainerImageExpirer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java22
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java95
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java106
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java79
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java31
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializer.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/Serializers.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java46
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java373
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java198
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java52
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java21
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageTest.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java21
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java15
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java46
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java80
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java202
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java60
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporterTest.java66
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java58
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java66
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java84
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java54
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java92
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java96
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializerTest.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java15
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java121
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java110
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json14
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json20
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-activation-conflict.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-failure.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-internal-server-error.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-no-deployment.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-out-of-capacity.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list-with-user.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list.json11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java103
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance_api.json10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java33
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json38
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java14
136 files changed, 2838 insertions, 1304 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
index 84ca2fb1a8e..159e0bb1f0f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
@@ -182,17 +182,17 @@ public class Application {
.min(Comparator.naturalOrder());
}
- /** Returns the total quota usage for this application */
+ /** Returns the total quota usage for this application, excluding temporary deployments */
public QuotaUsage quotaUsage() {
return instances().values().stream()
- .map(Instance::quotaUsage).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
+ .map(Instance::quotaUsage).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
}
- /** Returns the total quota usage for this application, excluding one specific deployment */
+ /** Returns the total quota usage for this application, excluding one specific deployment (and temporary deployments) */
public QuotaUsage quotaUsage(ApplicationId application, ZoneId zone) {
return instances().values().stream()
- .map(instance -> instance.quotaUsageExcluding(application, zone))
- .reduce(QuotaUsage::add).orElse(QuotaUsage.none);
+ .map(instance -> instance.quotaUsageExcluding(application, zone))
+ .reduce(QuotaUsage::add).orElse(QuotaUsage.none);
}
/** Returns the set of deploy keys for this application. */
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 8447353a869..4351ac17001 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
@@ -11,27 +11,22 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzPrincipal;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.flags.BooleanFlag;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.flags.StringFlag;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ConfigChangeActions;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
import com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.ApplicationRoles;
+import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
@@ -42,14 +37,13 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerE
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NotFoundException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.PrepareResponse;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackageValidator;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -65,11 +59,11 @@ import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Versions;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.security.AccessControl;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.yolean.Exceptions;
@@ -78,13 +72,11 @@ import java.security.Principal;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -92,20 +84,15 @@ import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.active;
import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.reserved;
import static java.util.Comparator.naturalOrder;
-import static java.util.function.Function.identity;
-import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toUnmodifiableMap;
/**
* A singleton owned by the Controller which contains the methods and state for controlling applications.
@@ -131,11 +118,10 @@ public class ApplicationController {
private final ApplicationPackageValidator applicationPackageValidator;
private final EndpointCertificateManager endpointCertificateManager;
private final StringFlag dockerImageRepoFlag;
- private final BooleanFlag provisionApplicationRoles;
private final BillingController billingController;
ApplicationController(Controller controller, CuratorDb curator, AccessControl accessControl, Clock clock,
- SecretStore secretStore, FlagSource flagSource, BillingController billingController) {
+ FlagSource flagSource, BillingController billingController) {
this.controller = controller;
this.curator = curator;
@@ -145,13 +131,16 @@ public class ApplicationController {
this.artifactRepository = controller.serviceRegistry().artifactRepository();
this.applicationStore = controller.serviceRegistry().applicationStore();
this.dockerImageRepoFlag = PermanentFlags.DOCKER_IMAGE_REPO.bindTo(flagSource);
- this.provisionApplicationRoles = Flags.PROVISION_APPLICATION_ROLES.bindTo(flagSource);
this.billingController = billingController;
deploymentTrigger = new DeploymentTrigger(controller, clock);
applicationPackageValidator = new ApplicationPackageValidator(controller);
- endpointCertificateManager = new EndpointCertificateManager(controller.zoneRegistry(), curator, secretStore,
- controller.serviceRegistry().endpointCertificateProvider(), clock, flagSource);
+ endpointCertificateManager = new EndpointCertificateManager(
+ controller.zoneRegistry(),
+ curator,
+ controller.serviceRegistry().endpointCertificateProvider(),
+ controller.serviceRegistry().endpointCertificateValidator(),
+ clock);
// Update serialization format of all applications
Once.after(Duration.ofMinutes(1), () -> {
@@ -340,28 +329,28 @@ public class ApplicationController {
});
}
- public LockedApplication withNewInstance(LockedApplication application, ApplicationId id) {
- if (id.instance().isTester())
- throw new IllegalArgumentException("'" + id + "' is a tester application!");
- InstanceId.validate(id.instance().value());
+ /** Fetches the requested application package from the artifact store(s). */
+ public ApplicationPackage getApplicationPackage(ApplicationId id, ApplicationVersion version) {
+ return new ApplicationPackage(applicationStore.get(id.tenant(), id.application(), version));
+ }
- if (getInstance(id).isPresent())
- throw new IllegalArgumentException("Could not create '" + id + "': Instance already exists");
- if (getInstance(dashToUnderscore(id)).isPresent()) // VESPA-1945
- throw new IllegalArgumentException("Could not create '" + id + "': Instance " + dashToUnderscore(id) + " already exists");
+ /** Returns given application with a new instance */
+ public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance) {
+ if (instance.instance().isTester())
+ throw new IllegalArgumentException("'" + instance + "' is a tester application!");
+ InstanceId.validate(instance.instance().value());
- log.info("Created " + id);
- return application.withNewInstance(id.instance());
- }
+ if (getInstance(instance).isPresent())
+ throw new IllegalArgumentException("Could not create '" + instance + "': Instance already exists");
+ if (getInstance(dashToUnderscore(instance)).isPresent()) // VESPA-1945
+ throw new IllegalArgumentException("Could not create '" + instance + "': Instance " + dashToUnderscore(instance) + " already exists");
- public ActivateResult deploy(ApplicationId applicationId, ZoneId zone,
- Optional<ApplicationPackage> applicationPackageFromDeployer,
- DeployOptions options) {
- return deploy(applicationId, zone, applicationPackageFromDeployer, Optional.empty(), options);
+ log.info("Created " + instance);
+ return application.withNewInstance(instance.instance());
}
/** Deploys an application package for an existing application instance. */
- public ActivateResult deploy2(JobId job, boolean deploySourceVersions) { // TODO jonmv: make it number one!
+ public ActivateResult deploy(JobId job, boolean deploySourceVersions) {
if (job.application().instance().isTester())
throw new IllegalArgumentException("'" + job.application() + "' is a tester application!");
@@ -371,7 +360,7 @@ public class ApplicationController {
try (Lock deploymentLock = lockForDeployment(job.application(), zone)) {
Set<ContainerEndpoint> endpoints;
Optional<EndpointCertificateMetadata> endpointCertificateMetadata;
- Optional<ApplicationRoles> applicationRoles = Optional.empty();
+ Optional<TenantRoles> tenantRoles = Optional.empty();
Run run = controller.jobController().last(job)
.orElseThrow(() -> new IllegalStateException("No known run of '" + job + "'"));
@@ -386,14 +375,7 @@ public class ApplicationController {
try (Lock lock = lock(applicationId)) {
LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
Instance instance = application.get().require(job.application().instance());
-
- Deployment deployment = instance.deployments().get(zone);
- if ( zone.environment().isProduction() && deployment != null
- && ( platform.compareTo(deployment.version()) < 0 && ! instance.change().isPinned()
- || revision.compareTo(deployment.applicationVersion()) < 0 && ! (revision.isUnknown() && controller.system().isCd())))
- throw new IllegalArgumentException(String.format("Rejecting deployment of application %s to %s, as the requested versions (platform: %s, application: %s)" +
- " are older than the currently deployed (platform: %s, application: %s).",
- job.application(), zone, platform, revision, deployment.version(), deployment.applicationVersion()));
+ rejectOldChange(instance, platform, revision, job, zone);
if ( ! applicationPackage.trustedCertificates().isEmpty()
&& run.testerCertificate().isPresent())
@@ -403,19 +385,10 @@ public class ApplicationController {
endpoints = controller.routing().registerEndpointsInDns(application.get(), job.application().instance(), zone);
- // Provision application roles if enabled for the zone
- if (provisionApplicationRoles.with(FetchVector.Dimension.ZONE_ID, zone.value()).value()) {
- try {
- applicationRoles = controller.serviceRegistry().applicationRoleService().createApplicationRoles(instance.id());
- } catch (Exception e) {
- log.log(Level.SEVERE, "Exception creating application roles for application: " + instance.id(), e);
- throw new RuntimeException("Unable to provision iam roles for application");
- }
- }
} // Release application lock while doing the deployment, which is a lengthy task.
// Carry out deployment without holding the application lock.
- ActivateResult result = deploy(job.application(), applicationPackage, zone, platform, endpoints, endpointCertificateMetadata, applicationRoles);
+ ActivateResult result = deploy(job.application(), applicationPackage, zone, platform, endpoints, endpointCertificateMetadata, tenantRoles);
// Record the quota usage for this application
var quotaUsage = deploymentQuotaUsage(zone, job.application());
@@ -429,108 +402,6 @@ public class ApplicationController {
}
}
- private QuotaUsage deploymentQuotaUsage(ZoneId zoneId, ApplicationId applicationId) {
- var application = configServer.nodeRepository().getApplication(zoneId, applicationId);
- return DeploymentQuotaCalculator.calculateQuotaUsage(application);
- }
-
- private ApplicationPackage getApplicationPackage(ApplicationId application, ZoneId zone, ApplicationVersion revision) {
- return new ApplicationPackage(revision.isUnknown() ? applicationStore.getDev(application, zone)
- : applicationStore.get(application.tenant(), application.application(), revision));
- }
-
- public ActivateResult deploy(ApplicationId instanceId, ZoneId zone,
- Optional<ApplicationPackage> applicationPackageFromDeployer,
- Optional<ApplicationVersion> applicationVersionFromDeployer,
- DeployOptions options) {
- if (instanceId.instance().isTester())
- throw new IllegalArgumentException("'" + instanceId + "' is a tester application!");
-
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(instanceId);
- if (getInstance(instanceId).isEmpty())
- createInstance(instanceId);
-
- try (Lock deploymentLock = lockForDeployment(instanceId, zone)) {
- Version platformVersion;
- ApplicationVersion applicationVersion;
- ApplicationPackage applicationPackage;
- Set<ContainerEndpoint> endpoints;
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata;
-
- try (Lock lock = lock(applicationId)) {
- LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
- InstanceName instance = instanceId.instance();
-
- boolean manuallyDeployed = options.deployDirectly || zone.environment().isManuallyDeployed();
- boolean preferOldestVersion = options.deployCurrentVersion;
-
- // Determine versions to use.
- if (manuallyDeployed) {
- applicationVersion = applicationVersionFromDeployer.orElse(ApplicationVersion.unknown);
- applicationPackage = applicationPackageFromDeployer.orElseThrow(
- () -> new IllegalArgumentException("Application package must be given when deploying to " + zone));
- platformVersion = options.vespaVersion.map(Version::new)
- .orElse(applicationPackage.deploymentSpec().majorVersion()
- .flatMap(this::lastCompatibleVersion)
- .orElseGet(controller::readSystemVersion));
- }
- else {
- JobType jobType = JobType.from(controller.system(), zone)
- .orElseThrow(() -> new IllegalArgumentException("No job is known for " + zone + "."));
- var run = controller.jobController().last(instanceId, jobType);
- if (run.map(Run::hasEnded).orElse(true))
- return unexpectedDeployment(instanceId, zone);
- Versions versions = run.get().versions();
- platformVersion = preferOldestVersion ? versions.sourcePlatform().orElse(versions.targetPlatform())
- : versions.targetPlatform();
- applicationVersion = preferOldestVersion ? versions.sourceApplication().orElse(versions.targetApplication())
- : versions.targetApplication();
-
- applicationPackage = getApplicationPackage(instanceId, applicationVersion);
- applicationPackage = withTesterCertificate(applicationPackage, instanceId, jobType);
- validateRun(application.get().require(instance), zone, platformVersion, applicationVersion);
- }
-
- endpointCertificateMetadata = endpointCertificateManager.getEndpointCertificateMetadata(
- application.get().require(instance), zone, applicationPackage.deploymentSpec().instance(instance));
-
- endpoints = controller.routing().registerEndpointsInDns(application.get(), instance, zone);
- } // Release application lock while doing the deployment, which is a lengthy task.
-
- // Carry out deployment without holding the application lock.
- ActivateResult result = deploy(instanceId, applicationPackage, zone, platformVersion,
- endpoints, endpointCertificateMetadata, Optional.empty());
-
- // Record the quota usage for this application
- var quotaUsage = deploymentQuotaUsage(zone, instanceId);
-
- lockApplicationOrThrow(applicationId, application ->
- store(application.with(instanceId.instance(),
- instance -> instance.withNewDeployment(zone, applicationVersion, platformVersion,
- clock.instant(), warningsFrom(result),
- quotaUsage))));
- return result;
- }
- }
-
- private ApplicationPackage withTesterCertificate(ApplicationPackage applicationPackage, ApplicationId id, JobType type) {
- if (applicationPackage.trustedCertificates().isEmpty())
- return applicationPackage;
-
- // TODO jonmv: move this to the caller, when external build service is removed.
- Run run = controller.jobController().last(id, type)
- .orElseThrow(() -> new IllegalStateException("Last run of " + type + " for " + id + " not found"));
- if (run.testerCertificate().isEmpty())
- return applicationPackage;
-
- return applicationPackage.withTrustedCertificate(run.testerCertificate().get());
- }
-
- /** Fetches the requested application package from the artifact store(s). */
- public ApplicationPackage getApplicationPackage(ApplicationId id, ApplicationVersion version) {
- return new ApplicationPackage(applicationStore.get(id.tenant(), id.application(), version));
- }
-
/** Stores the deployment spec and validation overrides from the application package, and runs cleanup. */
public LockedApplication storeWithUpdatedConfig(LockedApplication application, ApplicationPackage applicationPackage) {
applicationPackageValidator.validate(application.get(), applicationPackage, clock.instant());
@@ -589,7 +460,7 @@ public class ApplicationController {
private ActivateResult deploy(ApplicationId application, ApplicationPackage applicationPackage,
ZoneId zone, Version platform, Set<ContainerEndpoint> endpoints,
Optional<EndpointCertificateMetadata> endpointCertificateMetadata,
- Optional<ApplicationRoles> applicationRoles) {
+ Optional<TenantRoles> tenantRoles) {
try {
Optional<DockerImage> dockerImageRepo = Optional.ofNullable(
dockerImageRepoFlag
@@ -611,10 +482,16 @@ public class ApplicationController {
Quota deploymentQuota = DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()),
asList(application.tenant()), application, zone, applicationPackage.deploymentSpec());
+ List<TenantSecretStore> tenantSecretStores = controller.tenants()
+ .get(application.tenant())
+ .filter(tenant-> tenant instanceof CloudTenant)
+ .map(tenant -> ((CloudTenant) tenant).tenantSecretStores())
+ .orElse(List.of());
+
ConfigServer.PreparedApplication preparedApplication =
configServer.deploy(new DeploymentData(application, zone, applicationPackage.zippedContent(), platform,
endpoints, endpointCertificateMetadata, dockerImageRepo, domain,
- applicationRoles, deploymentQuota));
+ tenantRoles, deploymentQuota, tenantSecretStores));
return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(),
applicationPackage.zippedContent().length);
@@ -625,18 +502,6 @@ public class ApplicationController {
}
}
- private ActivateResult unexpectedDeployment(ApplicationId application, ZoneId zone) {
- Log logEntry = new Log();
- logEntry.level = "WARNING";
- logEntry.time = clock.instant().toEpochMilli();
- logEntry.message = "Ignoring deployment of application '" + application + "' to " + zone +
- " as a deployment is not currently expected";
- PrepareResponse prepareResponse = new PrepareResponse();
- prepareResponse.log = List.of(logEntry);
- prepareResponse.configChangeActions = new ConfigChangeActions(List.of(), List.of(), List.of());
- return new ActivateResult(new RevisionId("0"), prepareResponse, 0);
- }
-
private LockedApplication withoutDeletedDeployments(LockedApplication application, InstanceName instance) {
DeploymentSpec deploymentSpec = application.get().deploymentSpec();
List<ZoneId> deploymentsToRemove = application.get().require(instance).productionDeployments().values().stream()
@@ -802,6 +667,11 @@ public class ApplicationController {
}
}
+ /** Sets suspension status of the given deployment in its zone. */
+ public void setSuspension(DeploymentId deploymentId, boolean suspend) {
+ configServer.setSuspension(deploymentId, suspend);
+ }
+
/** Deactivate application in the given zone */
public void deactivate(ApplicationId id, ZoneId zone) {
lockApplicationOrThrow(TenantAndApplicationId.from(id),
@@ -829,14 +699,6 @@ public class ApplicationController {
public DeploymentTrigger deploymentTrigger() { return deploymentTrigger; }
- private TenantAndApplicationId dashToUnderscore(TenantAndApplicationId id) {
- return TenantAndApplicationId.from(id.tenant().value(), id.application().value().replaceAll("-", "_"));
- }
-
- private ApplicationId dashToUnderscore(ApplicationId id) {
- return dashToUnderscore(TenantAndApplicationId.from(id)).instance(id.instance());
- }
-
/**
* Returns a lock which provides exclusive rights to changing this application.
* Any operation which stores an application need to first acquire this lock, then read, modify
@@ -853,17 +715,6 @@ public class ApplicationController {
return curator.lockForDeployment(application, zone);
}
- /** Verify that we don't downgrade an existing production deployment. */
- private void validateRun(Instance instance, ZoneId zone, Version platformVersion, ApplicationVersion applicationVersion) {
- Deployment deployment = instance.deployments().get(zone);
- if ( zone.environment().isProduction() && deployment != null
- && ( platformVersion.compareTo(deployment.version()) < 0 && ! instance.change().isPinned()
- || applicationVersion.compareTo(deployment.applicationVersion()) < 0))
- throw new IllegalArgumentException(String.format("Rejecting deployment of application %s to %s, as the requested versions (platform: %s, application: %s)" +
- " are older than the currently deployed (platform: %s, application: %s).",
- instance.id(), zone, platformVersion, applicationVersion, deployment.version(), deployment.applicationVersion()));
- }
-
/**
* Verifies that the application can be deployed to the tenant, following these rules:
*
@@ -928,6 +779,38 @@ public class ApplicationController {
}
}
+ private void rejectOldChange(Instance instance, Version platform, ApplicationVersion revision, JobId job, ZoneId zone) {
+ Deployment deployment = instance.deployments().get(zone);
+ if (deployment == null) return;
+ if (!zone.environment().isProduction()) return;
+
+ boolean platformIsOlder = platform.compareTo(deployment.version()) < 0 && !instance.change().isPinned();
+ boolean revisionIsOlder = revision.compareTo(deployment.applicationVersion()) < 0 &&
+ !(revision.isUnknown() && controller.system().isCd());
+ if (platformIsOlder || revisionIsOlder)
+ throw new IllegalArgumentException(String.format("Rejecting deployment of application %s to %s, as the requested versions (platform: %s, application: %s)" +
+ " are older than the currently deployed (platform: %s, application: %s).",
+ job.application(), zone, platform, revision, deployment.version(), deployment.applicationVersion()));
+ }
+
+ private TenantAndApplicationId dashToUnderscore(TenantAndApplicationId id) {
+ return TenantAndApplicationId.from(id.tenant().value(), id.application().value().replaceAll("-", "_"));
+ }
+
+ private ApplicationId dashToUnderscore(ApplicationId id) {
+ return dashToUnderscore(TenantAndApplicationId.from(id)).instance(id.instance());
+ }
+
+ private QuotaUsage deploymentQuotaUsage(ZoneId zoneId, ApplicationId applicationId) {
+ var application = configServer.nodeRepository().getApplication(zoneId, applicationId);
+ return DeploymentQuotaCalculator.calculateQuotaUsage(application);
+ }
+
+ private ApplicationPackage getApplicationPackage(ApplicationId application, ZoneId zone, ApplicationVersion revision) {
+ return new ApplicationPackage(revision.isUnknown() ? applicationStore.getDev(application, zone)
+ : applicationStore.get(application.tenant(), application.application(), revision));
+ }
+
/*
* Get the AthenzUser from this principal or Optional.empty if this does not represent a user.
*/
@@ -985,9 +868,4 @@ public class ApplicationController {
return Map.copyOf(warnings);
}
- /** Sets suspension status of the given deployment in its zone. */
- public void setSuspension(DeploymentId deploymentId, boolean suspend) {
- configServer.setSuspension(deploymentId, suspend);
- }
-
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
index aa5f0ae0fdc..abc0784396c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
@@ -110,8 +110,8 @@ public class Controller extends AbstractComponent {
metrics = new ConfigServerMetrics(serviceRegistry.configServer());
nameServiceForwarder = new NameServiceForwarder(curator);
jobController = new JobController(this);
- applicationController = new ApplicationController(this, curator, accessControl, clock, secretStore, flagSource, serviceRegistry.billingController());
- tenantController = new TenantController(this, curator, accessControl);
+ applicationController = new ApplicationController(this, curator, accessControl, clock, flagSource, serviceRegistry.billingController());
+ tenantController = new TenantController(this, curator, accessControl, flagSource);
routingController = new RoutingController(this, Objects.requireNonNull(rotationsConfig, "RotationsConfig cannot be null"));
auditLogger = new AuditLogger(curator, clock);
jobControl = new JobControl(new JobControlFlags(curator, flagSource));
@@ -235,6 +235,8 @@ public class Controller extends AbstractComponent {
targets.removeIf(target -> target.osVersion().cloud().equals(cloudName)); // Only allow a single target per cloud
targets.add(new OsVersionTarget(new OsVersion(version, cloudName), upgradeBudget));
curator.writeOsVersionTargets(targets);
+ log.info("Triggered OS upgrade to " + version.toFullString() + " in cloud " +
+ cloudName.value() + upgradeBudget.map(b -> ", with upgrade budget " + b).orElse(""));
}
}
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 dcfc1cbc606..025b785a693 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
@@ -12,6 +12,7 @@ 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.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
+import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
@@ -66,7 +67,10 @@ public class Instance {
Instant instant, Map<DeploymentMetrics.Warning, Integer> warnings, QuotaUsage quotaUsage) {
// Use info from previous deployment if available, otherwise create a new one.
Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, applicationVersion,
- version, instant));
+ version, instant,
+ DeploymentMetrics.none,
+ DeploymentActivity.none,
+ QuotaUsage.none));
Deployment newDeployment = new Deployment(zone, applicationVersion, version, instant,
previousDeployment.metrics().with(warnings),
previousDeployment.activity(),
@@ -166,15 +170,17 @@ public class Instance {
return change;
}
- /** Returns the total quota usage for this instance **/
+ /** Returns the total quota usage for this instance, excluding temporary deployments **/
public QuotaUsage quotaUsage() {
return deployments.values().stream()
+ .filter(d -> !d.zone().environment().isTest()) // Exclude temporary deployments
.map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
}
- /** Returns the total quota usage for this instance, excluding one deployment */
+ /** Returns the total quota usage for this instance, excluding one specific deployment (and temporary deployments) */
public QuotaUsage quotaUsageExcluding(ApplicationId application, ZoneId zone) {
return deployments.values().stream()
+ .filter(d -> !d.zone().environment().isTest()) // Exclude temporary deployments
.filter(d -> !(application.equals(id) && d.zone().equals(zone)))
.map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
index e45bda0708e..f25f1c64372 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
@@ -11,6 +11,7 @@ import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
@@ -20,6 +21,8 @@ import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
import java.security.Principal;
import java.security.PublicKey;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import static java.util.Objects.requireNonNull;
@@ -117,21 +120,27 @@ public abstract class LockedTenant {
private final Optional<Principal> creator;
private final BiMap<PublicKey, Principal> developerKeys;
private final TenantInfo info;
+ private final List<TenantSecretStore> tenantSecretStores;
+ private final Optional<String> archiveAccessRole;
- private Cloud(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator, BiMap<PublicKey, Principal> developerKeys, TenantInfo info) {
+ private Cloud(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator,
+ BiMap<PublicKey, Principal> developerKeys, TenantInfo info,
+ List<TenantSecretStore> tenantSecretStores, Optional<String> archiveAccessRole) {
super(name, createdAt, lastLoginInfo);
this.developerKeys = ImmutableBiMap.copyOf(developerKeys);
this.creator = creator;
this.info = info;
+ this.tenantSecretStores = tenantSecretStores;
+ this.archiveAccessRole = archiveAccessRole;
}
private Cloud(CloudTenant tenant) {
- this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), Optional.empty(), tenant.developerKeys(), tenant.info());
+ this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), Optional.empty(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccessRole());
}
@Override
public CloudTenant get() {
- return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info);
+ return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole);
}
public Cloud withDeveloperKey(PublicKey key, Principal principal) {
@@ -139,22 +148,38 @@ public abstract class LockedTenant {
if (keys.containsKey(key))
throw new IllegalArgumentException("Key " + KeyUtils.toPem(key) + " is already owned by " + keys.get(key));
keys.put(key, principal);
- return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccessRole);
}
public Cloud withoutDeveloperKey(PublicKey key) {
BiMap<PublicKey, Principal> keys = HashBiMap.create(developerKeys);
keys.remove(key);
- return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccessRole);
}
public Cloud withInfo(TenantInfo newInfo) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccessRole);
}
@Override
public LockedTenant with(LastLoginInfo lastLoginInfo) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole);
+ }
+
+ public Cloud withSecretStore(TenantSecretStore tenantSecretStore) {
+ ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores);
+ secretStores.add(tenantSecretStore);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccessRole);
+ }
+
+ public Cloud withoutSecretStore(TenantSecretStore tenantSecretStore) {
+ ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores);
+ secretStores.remove(tenantSecretStore);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccessRole);
+ }
+
+ public Cloud withArchiveAccessRole(Optional<String> role) {
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, role);
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
index 4c9cf4f105f..d3992290f20 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
@@ -3,6 +3,10 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.curator.Lock;
+import com.yahoo.vespa.flags.BooleanFlag;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
@@ -37,11 +41,15 @@ public class TenantController {
private final Controller controller;
private final CuratorDb curator;
private final AccessControl accessControl;
+ private final BooleanFlag provisionTenantRoles;
- public TenantController(Controller controller, CuratorDb curator, AccessControl accessControl) {
+
+ public TenantController(Controller controller, CuratorDb curator, AccessControl accessControl, FlagSource flagSource) {
this.controller = Objects.requireNonNull(controller, "controller must be non-null");
this.curator = Objects.requireNonNull(curator, "curator must be non-null");
this.accessControl = accessControl;
+ this.provisionTenantRoles = Flags.PROVISION_TENANT_ROLES.bindTo(flagSource);
+
// Update serialization format of all tenants
Once.after(Duration.ofMinutes(1), () -> {
@@ -101,6 +109,16 @@ public class TenantController {
requireNonExistent(tenantSpec.tenant());
TenantId.validate(tenantSpec.tenant().value());
curator.writeTenant(accessControl.createTenant(tenantSpec, controller.clock().instant(), credentials, asList()));
+
+ // Provision tenant role if enabled
+ if (provisionTenantRoles.with(FetchVector.Dimension.TENANT_ID, tenantSpec.tenant().value()).value()) {
+ try {
+ controller.serviceRegistry().roleService().createTenantRole(tenantSpec.tenant());
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to create tenant role for tenant: " + tenantSpec.tenant());
+ }
+ }
+
}
}
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 800680d7327..3d17a7f8681 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
@@ -24,10 +24,6 @@ public class Deployment {
private final DeploymentActivity activity;
private final QuotaUsage quota;
- public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime) {
- this(zone, applicationVersion, version, deployTime, DeploymentMetrics.none, DeploymentActivity.none, QuotaUsage.none);
- }
-
public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime,
DeploymentMetrics metrics, DeploymentActivity activity, QuotaUsage quota) {
this.zone = Objects.requireNonNull(zone, "zone cannot be null");
@@ -76,6 +72,25 @@ public class Deployment {
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Deployment that = (Deployment) o;
+ return zone.equals(that.zone) &&
+ applicationVersion.equals(that.applicationVersion) &&
+ version.equals(that.version) &&
+ deployTime.equals(that.deployTime) &&
+ metrics.equals(that.metrics) &&
+ activity.equals(that.activity) &&
+ quota.equals(that.quota);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(zone, applicationVersion, version, deployTime, metrics, activity, quota);
+ }
+
+ @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/DeploymentActivity.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java
index 03c08509a5e..71f0d64c43a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java
@@ -61,6 +61,19 @@ public class DeploymentActivity {
activeRate(metrics.writesPerSecond(), lastWritesPerSecond));
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DeploymentActivity that = (DeploymentActivity) o;
+ return lastQueried.equals(that.lastQueried) && lastWritten.equals(that.lastWritten) && lastQueriesPerSecond.equals(that.lastQueriesPerSecond) && lastWritesPerSecond.equals(that.lastWritesPerSecond);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lastQueried, lastWritten, lastQueriesPerSecond, lastWritesPerSecond);
+ }
+
public static DeploymentActivity create(Optional<Instant> queriedAt, Optional<Instant> writtenAt,
OptionalDouble lastQueriesPerSecond, OptionalDouble lastWritesPerSecond) {
if (queriedAt.isEmpty() && writtenAt.isEmpty()) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
index 7a50184e7a4..094cb9a19b0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
@@ -111,6 +111,25 @@ public class DeploymentMetrics {
writeLatencyMills, instant, warnings);
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DeploymentMetrics that = (DeploymentMetrics) o;
+ return Double.compare(that.queriesPerSecond, queriesPerSecond) == 0 &&
+ Double.compare(that.writesPerSecond, writesPerSecond) == 0 &&
+ Double.compare(that.documentCount, documentCount) == 0 &&
+ Double.compare(that.queryLatencyMillis, queryLatencyMillis) == 0 &&
+ Double.compare(that.writeLatencyMills, writeLatencyMills) == 0 &&
+ instant.equals(that.instant) &&
+ warnings.equals(that.warnings);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis, writeLatencyMills, instant, warnings);
+ }
+
/** Types of deployment warnings. We currently have only one */
public enum Warning {
all
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java
index de57f1a5676..c17530fd9e2 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.application;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterResources;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
@@ -36,6 +37,7 @@ public class DeploymentQuotaCalculator {
// correctly we retrieve the maximum of .current() and .max() - otherwise we would keep adding 0s for those
// that are not using autoscaling.
var quotaUsageRate = application.clusters().values().stream()
+ .filter(cluster -> ! cluster.type().equals(ClusterSpec.Type.admin))
.map(cluster -> largestQuotaUsage(cluster.current(), cluster.max()))
.mapToDouble(resources -> resources.nodes() * resources.nodeResources().cost())
.sum();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
index c6046751696..4954628a46a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. 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;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java
index 13384b63c84..1e070d5a66b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java
@@ -1,12 +1,14 @@
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.application;
+import java.util.Objects;
import java.util.OptionalDouble;
/**
* @author ogronnesby
*/
public class QuotaUsage {
+
public static final QuotaUsage none = new QuotaUsage(0.0);
private final double rate;
@@ -39,6 +41,19 @@ public class QuotaUsage {
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ QuotaUsage that = (QuotaUsage) o;
+ return Double.compare(that.rate, rate) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rate);
+ }
+
+ @Override
public String toString() {
return "QuotaUsage{" +
"rate=" + rate +
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
index 91be3a4df21..1a1b6988a96 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
@@ -65,11 +65,8 @@ public enum SystemApplication {
.orElse(false);
}
- /** Returns whether this should receive OS upgrades in given zone */
- public boolean shouldUpgradeOsIn(ZoneId zone, Controller controller) {
- if (controller.zoneRegistry().zones().reprovisionToUpgradeOs().ids().contains(zone)) {
- return nodeType == NodeType.host; // TODO(mpolden): Remove once all node types are supported
- }
+ /** Returns whether this should receive OS upgrades */
+ public boolean shouldUpgradeOs() {
return nodeType.isHost();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateException.java
deleted file mode 100644
index 321eb783dab..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateException.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.certificate;
-
-public class EndpointCertificateException extends RuntimeException {
-
- private final Type type;
-
- public EndpointCertificateException(Type type, String message) {
- super(message);
- this.type = type;
- }
-
- public EndpointCertificateException(Type type, String message, Throwable cause) {
- super(message, cause);
- this.type = type;
- }
-
- public Type type() {
- return type;
- }
-
- public enum Type {
- CERT_NOT_AVAILABLE,
- VERIFICATION_FAILURE
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java
index 87531240752..6f964999fba 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManager.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.certificate;
import com.google.common.hash.Hashing;
@@ -9,16 +10,10 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.secretstore.SecretNotFoundException;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.security.SubjectAlternativeName;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
@@ -26,7 +21,6 @@ import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.Charset;
-import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
@@ -43,7 +37,7 @@ import java.util.stream.Collectors;
/**
* Looks up stored endpoint certificate metadata, provisions new certificates if none is found,
* re-provisions if zone is not covered, and uses refreshed certificates if a newer version is available.
- *
+ * <p>
* See also EndpointCertificateMaintainer, which handles refreshes, deletions and triggers deployments
*
* @author andreer
@@ -54,22 +48,20 @@ public class EndpointCertificateManager {
private final ZoneRegistry zoneRegistry;
private final CuratorDb curator;
- private final SecretStore secretStore;
private final EndpointCertificateProvider endpointCertificateProvider;
private final Clock clock;
- private final BooleanFlag validateEndpointCertificates;
+ private final EndpointCertificateValidator endpointCertificateValidator;
public EndpointCertificateManager(ZoneRegistry zoneRegistry,
CuratorDb curator,
- SecretStore secretStore,
EndpointCertificateProvider endpointCertificateProvider,
- Clock clock, FlagSource flagSource) {
+ EndpointCertificateValidator endpointCertificateValidator,
+ Clock clock) {
this.zoneRegistry = zoneRegistry;
this.curator = curator;
- this.secretStore = secretStore;
this.endpointCertificateProvider = endpointCertificateProvider;
this.clock = clock;
- this.validateEndpointCertificates = Flags.VALIDATE_ENDPOINT_CERTIFICATES.bindTo(flagSource);
+ this.endpointCertificateValidator = endpointCertificateValidator;
}
public Optional<EndpointCertificateMetadata> getEndpointCertificateMetadata(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) {
@@ -98,14 +90,16 @@ public class EndpointCertificateManager {
// Re-provision certificate if it is missing SANs for the zone we are deploying to
var requiredSansForZone = dnsNamesOf(instance.id(), zone);
if (!currentCertificateMetadata.get().requestedDnsSans().containsAll(requiredSansForZone)) {
- var reprovisionedCertificateMetadata = provisionEndpointCertificate(instance, currentCertificateMetadata, zone, instanceSpec);
+ var reprovisionedCertificateMetadata =
+ provisionEndpointCertificate(instance, currentCertificateMetadata, zone, instanceSpec)
+ .withRequestId(currentCertificateMetadata.get().request_id()); // We're required to keep the original request_id
curator.writeEndpointCertificateMetadata(instance.id(), reprovisionedCertificateMetadata);
// Verification is unlikely to succeed in this case, as certificate must be available first - controller will retry
- validateEndpointCertificate(reprovisionedCertificateMetadata, instance, zone);
+ endpointCertificateValidator.validate(reprovisionedCertificateMetadata, instance.id().serializedForm(), zone, requiredSansForZone);
return Optional.of(reprovisionedCertificateMetadata);
}
- validateEndpointCertificate(currentCertificateMetadata.get(), instance, zone);
+ endpointCertificateValidator.validate(currentCertificateMetadata.get(), instance.id().serializedForm(), zone, requiredSansForZone);
return currentCertificateMetadata;
}
@@ -145,53 +139,6 @@ public class EndpointCertificateManager {
return endpointCertificateProvider.requestCaSignedCertificate(instance.id(), List.copyOf(requiredNames), currentMetadata);
}
- private void validateEndpointCertificate(EndpointCertificateMetadata endpointCertificateMetadata, Instance instance, ZoneId zone) {
- if (validateEndpointCertificates.value())
- try {
- var pemEncodedEndpointCertificate = secretStore.getSecret(endpointCertificateMetadata.certName(), endpointCertificateMetadata.version());
-
- if (pemEncodedEndpointCertificate == null)
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Secret store returned null for certificate");
-
- List<X509Certificate> x509CertificateList = X509CertificateUtils.certificateListFromPem(pemEncodedEndpointCertificate);
-
- if (x509CertificateList.isEmpty())
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Empty certificate list");
- if (x509CertificateList.size() < 2)
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Only a single certificate found in chain - intermediate certificates likely missing");
-
- Instant now = clock.instant();
- Instant firstExpiry = Instant.MAX;
- for (X509Certificate x509Certificate : x509CertificateList) {
- Instant notBefore = x509Certificate.getNotBefore().toInstant();
- Instant notAfter = x509Certificate.getNotAfter().toInstant();
- if (now.isBefore(notBefore))
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate is not yet valid");
- if (now.isAfter(notAfter))
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate has expired");
- if (notAfter.isBefore(firstExpiry)) firstExpiry = notAfter;
- }
-
- X509Certificate endEntityCertificate = x509CertificateList.get(0);
- Set<String> subjectAlternativeNames = X509CertificateUtils.getSubjectAlternativeNames(endEntityCertificate).stream()
- .filter(san -> san.getType().equals(SubjectAlternativeName.Type.DNS_NAME))
- .map(SubjectAlternativeName::getValue).collect(Collectors.toSet());
-
- var dnsNamesOfZone = dnsNamesOf(instance.id(), zone);
- if (!subjectAlternativeNames.containsAll(dnsNamesOfZone))
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate is missing required SANs for zone " + zone.value());
-
- } catch (SecretNotFoundException s) {
- // Normally because the cert is in the process of being provisioned - this will cause a retry in InternalStepRunner
- throw new EndpointCertificateException(EndpointCertificateException.Type.CERT_NOT_AVAILABLE, "Certificate not found in secret store");
- } catch (EndpointCertificateException e) {
- log.log(Level.WARNING, "Certificate validation failure for " + instance.id().serializedForm(), e);
- throw e;
- } catch (Exception e) {
- log.log(Level.WARNING, "Certificate validation failure for " + instance.id().serializedForm(), e);
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate validation failure for app " + instance.id().serializedForm(), e);
- }
- }
private List<String> dnsNamesOf(ApplicationId applicationId, ZoneId zone) {
List<String> endpointDnsNames = new ArrayList<>();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
index e4b0ff0f6fa..3f1e8831e83 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
@@ -26,6 +26,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -39,7 +40,6 @@ import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
import static java.util.function.BinaryOperator.maxBy;
import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
@@ -117,14 +117,12 @@ public class DeploymentStatus {
return allJobs.asList().stream()
.filter(job -> job.id().application().equals(application.id().instance(instance)))
.collect(Collectors.toUnmodifiableMap(job -> job.id().type(),
- job -> job));
+ Function.identity()));
}
/** Filterable job status lists for each instance of this application. */
public Map<ApplicationId, JobList> instanceJobs() {
- return allJobs.asList().stream()
- .collect(groupingBy(job -> job.id().application(),
- collectingAndThen(toUnmodifiableList(), JobList::from)));
+ return allJobs.groupingBy(job -> job.id().application());
}
/**
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 e789974ea13..f2df1cce15b 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
@@ -43,7 +43,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificateException;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateException;
import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
@@ -184,7 +184,7 @@ public class InternalStepRunner implements StepRunner {
}
private Optional<RunStatus> deployReal(RunId id, boolean setTheStage, DualLogger logger) {
- return deploy(() -> controller.applications().deploy2(id.job(), setTheStage),
+ return deploy(() -> controller.applications().deploy(id.job(), setTheStage),
controller.jobController().run(id).get()
.stepInfo(setTheStage ? deployInitialReal : deployReal).get()
.startTime().get(),
@@ -299,7 +299,7 @@ public class InternalStepRunner implements StepRunner {
}
List<Node> nodes = controller.serviceRegistry().configServer().nodeRepository().list(id.type().zone(controller.system()),
id.application(),
- ImmutableSet.of(active, reserved));
+ Set.of(active));
List<Node> parents = controller.serviceRegistry().configServer().nodeRepository().list(id.type().zone(controller.system()),
nodes.stream().map(node -> node.parentHostname().get()).collect(toList()));
NodeList nodeList = NodeList.of(nodes, parents, services.get());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
index 4e715908ec4..65d3f666309 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
@@ -41,10 +41,8 @@ import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
-import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
-import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@@ -95,7 +93,7 @@ public class JobController {
this.logs = new BufferedLogStore(curator, controller.serviceRegistry().runDataStore());
this.cloud = controller.serviceRegistry().testerCloud();
this.badges = new Badges(controller.zoneRegistry().badgeUrl());
- this.metric = new JobMetrics(controller.metric(), controller.system());
+ this.metric = new JobMetrics(controller.metric(), controller::system);
}
public TesterCloud cloud() { return cloud; }
@@ -224,9 +222,9 @@ public class JobController {
/** Returns all job types which have been run for the given application. */
public List<JobType> jobs(ApplicationId id) {
- return copyOf(Stream.of(JobType.values())
- .filter(type -> last(id, type).isPresent())
- .iterator());
+ return JobType.allIn(controller.system()).stream()
+ .filter(type -> last(id, type).isPresent())
+ .collect(toUnmodifiableList());
}
/** Returns an immutable map of all known runs for the given application and job type. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
index 80924c3c0aa..49da987ea18 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
@@ -6,6 +6,7 @@ import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import java.util.Map;
+import java.util.function.Supplier;
/**
* Records metrics related to deployment jobs.
@@ -25,9 +26,9 @@ public class JobMetrics {
public static final String success = "deployment.success";
private final Metric metric;
- private final SystemName system;
+ private final Supplier<SystemName> system;
- public JobMetrics(Metric metric, SystemName system) {
+ public JobMetrics(Metric metric, Supplier<SystemName> system) {
this.metric = metric;
this.system = system;
}
@@ -45,7 +46,7 @@ public class JobMetrics {
"tenantName", id.application().tenant().value(),
"app", id.application().application().value() + "." + id.application().instance().value(),
"test", Boolean.toString(id.type().isTest()),
- "zone", id.type().zone(system).value());
+ "zone", id.type().zone(system.get()).value());
}
static String valueOf(RunStatus status) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java
index 2d336143138..080d600005f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java
@@ -10,8 +10,6 @@ import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
-import static java.util.stream.Collectors.groupingBy;
-
/**
* @author jonmv
*/
@@ -26,7 +24,7 @@ public class NodeList extends AbstractFilteringList<NodeWithServices, NodeList>
public static NodeList of(List<Node> nodes, List<Node> parents, ServiceConvergence services) {
var servicesByHostName = services.services().stream()
- .collect(groupingBy(service -> service.host()));
+ .collect(Collectors.groupingBy(service -> service.host()));
var parentsByHostName = parents.stream()
.collect(Collectors.toMap(node -> node.hostname(), node -> node));
return new NodeList(nodes.stream()
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java
index 921bf045873..9326035d4c7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. 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.yahoo.component.Version;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java
index 42b442bf7b0..7d94a4c728f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.vespa.hosted.controller.Controller;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java
new file mode 100644
index 00000000000..5dce4d7b344
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java
@@ -0,0 +1,68 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Updates archive URIs for tenants in all zones.
+ *
+ * @author freva
+ */
+public class ArchiveUriUpdater extends ControllerMaintainer {
+
+ private static final Set<TenantName> INFRASTRUCTURE_TENANTS = Set.of(TenantName.from("hosted-vespa"));
+
+ private final ApplicationController applications;
+ private final NodeRepository nodeRepository;
+ private final ArchiveService archiveService;
+
+ public ArchiveUriUpdater(Controller controller, Duration duration) {
+ super(controller, duration, ArchiveUriUpdater.class.getSimpleName(), SystemName.all());
+ this.applications = controller.applications();
+ this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
+ this.archiveService = controller.serviceRegistry().archiveService();
+ }
+
+ @Override
+ protected boolean maintain() {
+ Map<ZoneId, Set<TenantName>> tenantsByZone = new HashMap<>();
+ for (var application : applications.asList()) {
+ for (var instance : application.instances().values()) {
+ for (var deployment : instance.deployments().values()) {
+ tenantsByZone
+ .computeIfAbsent(deployment.zone(), zone -> new HashSet<>(INFRASTRUCTURE_TENANTS))
+ .add(instance.id().tenant());
+ }
+ }
+ }
+
+ tenantsByZone.forEach((zone, tenants) -> {
+ Map<TenantName, URI> zoneArchiveUris = nodeRepository.getArchiveUris(zone);
+ for (TenantName tenant : tenants) {
+ archiveService.archiveUriFor(zone, tenant)
+ .filter(uri -> !uri.equals(zoneArchiveUris.get(tenant)))
+ .ifPresent(uri -> nodeRepository.setArchiveUri(zone, tenant, uri));
+ }
+
+ zoneArchiveUris.keySet().stream()
+ .filter(tenant -> ! tenants.contains(tenant))
+ .forEach(tenant -> nodeRepository.removeArchiveUri(zone, tenant));
+ });
+
+ return true;
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java
new file mode 100644
index 00000000000..11f36201f32
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java
@@ -0,0 +1,267 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.config.provision.HostName;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeState;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeType;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class ChangeManagementAssessor {
+
+ private final NodeRepository nodeRepository;
+
+ public ChangeManagementAssessor(NodeRepository nodeRepository) {
+ this.nodeRepository = nodeRepository;
+ }
+
+ public Assessment assessment(List<String> impactedHostnames, ZoneId zone) {
+ return assessmentInner(impactedHostnames, nodeRepository.listNodes(zone).nodes(), zone);
+ }
+
+ Assessment assessmentInner(List<String> impactedHostnames, List<NodeRepositoryNode> allNodes, ZoneId zone) {
+
+ List<String> impactedParentHosts = toParentHosts(impactedHostnames, allNodes);
+ // Group impacted application nodes by parent host
+ Map<NodeRepositoryNode, List<NodeRepositoryNode>> prParentHost = allNodes.stream()
+ .filter(nodeRepositoryNode -> nodeRepositoryNode.getState() == NodeState.active) //TODO look at more states?
+ .filter(node -> impactedParentHosts.contains(node.getParentHostname() == null ? "" : node.getParentHostname()))
+ .collect(Collectors.groupingBy(node ->
+ allNodes.stream()
+ .filter(parent -> parent.getHostname().equals(node.getParentHostname()))
+ .findFirst().orElseThrow()
+ ));
+
+ // Group nodes pr cluster
+ Map<Cluster, List<NodeRepositoryNode>> prCluster = prParentHost.values()
+ .stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.groupingBy(ChangeManagementAssessor::clusterKey));
+
+ boolean allHostsReplacable = nodeRepository.isReplaceable(
+ zone,
+ prParentHost.keySet().stream()
+ .filter(node -> node.getType() == NodeType.host)
+ .map(node -> HostName.from(node.getHostname()))
+ .collect(Collectors.toList())
+ );
+
+ // Report assessment pr cluster
+ var clusterAssessments = prCluster.entrySet().stream().map((entry) -> {
+ Cluster cluster = entry.getKey();
+ List<NodeRepositoryNode> nodes = entry.getValue();
+
+ long[] totalStats = clusterStats(cluster, allNodes);
+ long[] impactedStats = clusterStats(cluster, nodes);
+
+ ClusterAssessment assessment = new ClusterAssessment();
+ assessment.app = cluster.getApp();
+ assessment.zone = zone.value();
+ assessment.cluster = cluster.getClusterType() + ":" + cluster.getClusterId();
+ assessment.clusterSize = totalStats[0];
+ assessment.clusterImpact = impactedStats[0];
+ assessment.groupsTotal = totalStats[1];
+ assessment.groupsImpact = impactedStats[1];
+
+
+ // TODO check upgrade policy
+ assessment.upgradePolicy = "na";
+ // TODO do some heuristic on suggestion action
+ assessment.suggestedAction = allHostsReplacable ? "Retire all hosts" : "nothing";
+ // TODO do some heuristic on impact
+ assessment.impact = getImpact(cluster, impactedStats, totalStats);
+
+ return assessment;
+ }).collect(Collectors.toList());
+
+ var hostAssessments = prParentHost.entrySet().stream().map((entry) -> {
+ HostAssessment hostAssessment = new HostAssessment();
+ hostAssessment.hostName = entry.getKey().getHostname();
+ hostAssessment.switchName = entry.getKey().getSwitchHostname();
+ hostAssessment.numberOfChildren = entry.getValue().size();
+
+ //TODO: Some better heuristic for what's considered problematic
+ hostAssessment.numberOfProblematicChildren = (int) entry.getValue().stream()
+ .mapToInt(node -> prCluster.get(clusterKey(node)).size())
+ .filter(i -> i > 1)
+ .count();
+
+ return hostAssessment;
+ }).collect(Collectors.toList());
+
+ return new Assessment(clusterAssessments, hostAssessments);
+ }
+
+ private List<String> toParentHosts(List<String> impactedHostnames, List<NodeRepositoryNode> allNodes) {
+ return impactedHostnames.stream()
+ .map(hostname ->
+ allNodes.stream()
+ .filter(node -> List.of(NodeType.config, NodeType.proxy, NodeType.host).contains(node.getType()))
+ .filter(node -> hostname.equals(node.getHostname()) || hostname.equals(node.getParentHostname()))
+ .map(node -> {
+ if (node.getType() == NodeType.host)
+ return node.getHostname();
+ return node.getParentHostname();
+ }).findFirst().orElseThrow()
+ )
+ .collect(Collectors.toList());
+ }
+
+ private static Cluster clusterKey(NodeRepositoryNode node) {
+ String appId = String.format("%s:%s:%s", node.getOwner().tenant, node.getOwner().application, node.getOwner().instance);
+ return new Cluster(Node.ClusterType.valueOf(node.getMembership().clustertype), node.getMembership().clusterid, appId, node.getType());
+ }
+
+ private static long[] clusterStats(Cluster cluster, List<NodeRepositoryNode> containerNodes) {
+ List<NodeRepositoryNode> clusterNodes = containerNodes.stream().filter(nodeRepositoryNode -> cluster.equals(clusterKey(nodeRepositoryNode))).collect(Collectors.toList());
+ long groups = clusterNodes.stream().map(nodeRepositoryNode -> nodeRepositoryNode.getMembership() != null ? nodeRepositoryNode.getMembership().group : "").distinct().count();
+ return new long[] { clusterNodes.size(), groups};
+ }
+
+ private String getImpact(Cluster cluster, long[] impactedStats, long[] totalStats) {
+ switch (cluster.getNodeType()) {
+ case tenant:
+ return getTenantImpact(cluster, impactedStats, totalStats);
+ case proxy:
+ return getProxyImpact(impactedStats[0], totalStats[0]);
+ case config:
+ return getConfigServerImpact(impactedStats[0]);
+ default:
+ return "Unkown impact";
+ }
+ }
+
+ private String getTenantImpact(Cluster cluster, long[] impactedStats, long[] totalStats) {
+ switch (cluster.getClusterType()) {
+ case container:
+ return getContainerImpact(impactedStats[0], totalStats[0]);
+ case content:
+ case combined:
+ return getContentImpact(totalStats[1] > 1, impactedStats[0], impactedStats[1]);
+ default:
+ return "Unknown impact";
+ }
+ }
+
+ private String getProxyImpact(long impactedNodes, long totalNodes) {
+ int impact = (int) (100.0 * impactedNodes / totalNodes);
+ return impact + "% of routing nodes impacted. Consider reprovisioning if too many";
+ }
+
+ private String getConfigServerImpact(long impactedNodes) {
+ if (impactedNodes == 1) {
+ return "Acceptable impact";
+ }
+ return "Large impact. Consider reprovisioning one or more config servers";
+ }
+
+ private String getContainerImpact(long impactedNodes, long totalNodes) {
+ if ((double) impactedNodes / totalNodes <= 0.1) {
+ return "Impact not larger than upgrade policy";
+ }
+ return "Impact larger than upgrade policy";
+ }
+
+ private String getContentImpact(boolean isGrouped, long impactedNodes, long impactedGroups) {
+ if ((isGrouped && impactedGroups == 1) || impactedNodes == 1)
+ return "Impact not larger than upgrade policy";
+ return "Impact larger than upgrade policy";
+ }
+
+
+ public static class Assessment {
+ List<ClusterAssessment> clusterAssessments;
+ List<HostAssessment> hostAssessments;
+
+ Assessment(List<ClusterAssessment> clusterAssessments, List<HostAssessment> hostAssessments) {
+ this.clusterAssessments = clusterAssessments;
+ this.hostAssessments = hostAssessments;
+ }
+
+ public List<ClusterAssessment> getClusterAssessments() {
+ return clusterAssessments;
+ }
+
+ public List<HostAssessment> getHostAssessments() {
+ return hostAssessments;
+ }
+ }
+
+ public static class ClusterAssessment {
+ public String app;
+ public String zone;
+ public String cluster;
+ public long clusterImpact;
+ public long clusterSize;
+ public long groupsImpact;
+ public long groupsTotal;
+ public String upgradePolicy;
+ public String suggestedAction;
+ public String impact;
+ }
+
+ public static class HostAssessment {
+ public String hostName;
+ public String switchName;
+ public int numberOfChildren;
+ public int numberOfProblematicChildren;
+ }
+
+ private static class Cluster {
+ private Node.ClusterType clusterType;
+ private String clusterId;
+ private String app;
+ private NodeType nodeType;
+
+ public Cluster(Node.ClusterType clusterType, String clusterId, String app, NodeType nodeType) {
+ this.clusterType = clusterType;
+ this.clusterId = clusterId;
+ this.app = app;
+ this.nodeType = nodeType;
+ }
+
+ public Node.ClusterType getClusterType() {
+ return clusterType;
+ }
+
+ public String getClusterId() {
+ return clusterId;
+ }
+
+ public String getApp() {
+ return app;
+ }
+
+ public NodeType getNodeType() {
+ return nodeType;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Cluster cluster = (Cluster) o;
+ return Objects.equals(clusterType, cluster.clusterType) &&
+ Objects.equals(clusterId, cluster.clusterId) &&
+ Objects.equals(app, cluster.app);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(clusterType, clusterId, app);
+ }
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
new file mode 100644
index 00000000000..ca9ebe132fd
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
@@ -0,0 +1,55 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * @author olaa
+ */
+public class ChangeRequestMaintainer extends ControllerMaintainer {
+
+ private final Logger logger = Logger.getLogger(ChangeRequestMaintainer.class.getName());
+ private final ChangeRequestClient changeRequestClient;
+ private final SystemName system;
+
+ public ChangeRequestMaintainer(Controller controller, Duration interval) {
+ super(controller, interval, null, SystemName.allOf(Predicate.not(SystemName::isPublic)));
+ this.changeRequestClient = controller.serviceRegistry().changeRequestClient();
+ this.system = controller.system();
+ }
+
+
+ @Override
+ protected boolean maintain() {
+ var changeRequests = changeRequestClient.getUpcomingChangeRequests();
+
+ if (!changeRequests.isEmpty()) {
+ logger.info(() -> "Found the following upcoming change requests:");
+ changeRequests.forEach(changeRequest -> logger.info(changeRequest::toString));
+ }
+
+ if (system.equals(SystemName.main))
+ approveChanges(changeRequests);
+
+ // TODO: Store in curator?
+ return true;
+ }
+
+ private void approveChanges(List<ChangeRequest> changeRequests) {
+ var unapprovedRequests = changeRequests
+ .stream()
+ .filter(changeRequest -> changeRequest.getApproval() == ChangeRequest.Approval.REQUESTED)
+ .collect(Collectors.toList());
+
+ changeRequestClient.approveChangeRequests(unapprovedRequests);
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporter.java
index 2e62f8e54df..5ab2ca4a5d6 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporter.java
@@ -2,23 +2,16 @@
package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.aws.AwsEventFetcher;
import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEvent;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Issue;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueHandler;
import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -33,90 +26,56 @@ public class CloudEventReporter extends ControllerMaintainer {
private static final Logger log = Logger.getLogger(CloudEventReporter.class.getName());
- private final IssueHandler issueHandler;
private final AwsEventFetcher eventFetcher;
private final Map<String, List<ZoneApi>> zonesByCloudNativeRegion;
private final NodeRepository nodeRepository;
- private final Metric metric;
- private static final String INFRASTRUCTURE_INSTANCE_EVENTS = "infrastructure_instance_events";
-
- CloudEventReporter(Controller controller, Duration interval, Metric metric) {
+ CloudEventReporter(Controller controller, Duration interval) {
super(controller, interval);
- this.issueHandler = controller.serviceRegistry().issueHandler();
this.eventFetcher = controller.serviceRegistry().eventFetcherService();
this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.zonesByCloudNativeRegion = getZonesByCloudNativeRegion();
- this.metric = metric;
+ this.zonesByCloudNativeRegion = supportedZonesByRegion();
}
@Override
protected boolean maintain() {
- int numberOfInfrastructureEvents = 0;
- for (var awsRegion : zonesByCloudNativeRegion.keySet()) {
- List<CloudEvent> events = eventFetcher.getEvents(awsRegion);
+ for (var region : zonesByCloudNativeRegion.keySet()) {
+ List<CloudEvent> events = eventFetcher.getEvents(region);
for (var event : events) {
log.info(String.format("Retrieved event %s, affecting the following instances: %s",
- event.instanceEventId,
- event.affectedInstances));
- List<Node> needsManualIntervention = handleInstances(awsRegion, event);
- if (!needsManualIntervention.isEmpty()) {
- numberOfInfrastructureEvents += needsManualIntervention.size();
- submitIssue(event);
- }
+ event.instanceEventId,
+ event.affectedInstances));
+ deprovisionAffectedHosts(region, event);
}
}
- metric.set(INFRASTRUCTURE_INSTANCE_EVENTS, numberOfInfrastructureEvents, metric.createContext(Collections.emptyMap()));
return true;
}
- /**
- * Handles affected instances in the following way:
- * 1. Ignore if unknown instance, presumably belongs to different system
- * 2. Retire and deprovision if tenant host
- * 3. Submit issue if infrastructure host, as it requires manual intervention
- */
- private List<Node> handleInstances(String awsRegion, CloudEvent event) {
- List<Node> needsManualIntervention = new ArrayList<>();
- for (var zone : zonesByCloudNativeRegion.get(awsRegion)) {
+ /** Deprovision any host affected by given event */
+ private void deprovisionAffectedHosts(String region, CloudEvent event) {
+ for (var zone : zonesByCloudNativeRegion.get(region)) {
for (var node : nodeRepository.list(zone.getId())) {
- if (!isAffected(node, event)){
- continue;
- }
- if (node.type() == NodeType.host) {
- log.info(String.format("Setting host %s to wantToRetire and wantToDeprovision", node.hostname().value()));
- nodeRepository.retireAndDeprovision(zone.getId(), node.hostname().value());
- }
- else {
- needsManualIntervention.add(node);
- }
+ if (!affects(node, event)) continue;
+ log.info("Retiring and deprovisioning " + node.hostname().value() + " in " + zone.getId() +
+ ": Affected by maintenance event " + event.instanceEventId);
+ nodeRepository.retireAndDeprovision(zone.getId(), node.hostname().value());
}
}
- return needsManualIntervention;
}
- private void submitIssue(CloudEvent event) {
- if (controller().system().isPublic())
- return;
- Issue issue = eventFetcher.createIssue(event);
- if (!issueHandler.issueExists(issue)) {
- issueHandler.file(issue);
- log.log(Level.INFO, String.format("Filed an issue with the title '%s'", issue.summary()));
- }
- }
-
- private boolean isAffected(Node node, CloudEvent event) {
+ private static boolean affects(Node node, CloudEvent event) {
+ if (!node.type().isHost()) return false; // Non-hosts are never affected
return event.affectedInstances.stream()
- .anyMatch(instance -> node.hostname().value().contains(instance));
+ .anyMatch(instance -> node.hostname().value().contains(instance));
}
- private Map<String, List<ZoneApi>> getZonesByCloudNativeRegion() {
+ /** Returns zones supported by this, grouped by their native region name */
+ private Map<String, List<ZoneApi>> supportedZonesByRegion() {
return controller().zoneRegistry().zones()
- .ofCloud(CloudName.from("aws"))
- .reachable()
- .zones().stream()
- .collect(Collectors.groupingBy(
- ZoneApi::getCloudNativeRegionName
- ));
+ .ofCloud(CloudName.from("aws"))
+ .reachable()
+ .zones().stream()
+ .collect(Collectors.groupingBy(ZoneApi::getCloudNativeRegionName));
}
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java
index e19f3b4f9a2..d6733955967 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java
@@ -16,7 +16,7 @@ import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
-import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.FINE;
/**
* Periodically fetch and store contact information for tenants.
@@ -39,13 +39,13 @@ public class ContactInformationMaintainer extends ControllerMaintainer {
TenantController tenants = controller().tenants();
boolean success = true;
for (Tenant tenant : tenants.asList()) {
- log.log(INFO, "Updating contact information for " + tenant);
+ log.log(FINE, "Updating contact information for " + tenant);
try {
switch (tenant.type()) {
case athenz:
tenants.lockIfPresent(tenant.name(), LockedTenant.Athenz.class, lockedTenant -> {
Contact contact = contactRetriever.getContact(lockedTenant.get().propertyId());
- log.log(INFO, "Contact found for " + tenant + " was " +
+ log.log(FINE, "Contact found for " + tenant + " was " +
(Optional.of(contact).equals(tenant.contact()) ? "un" : "") + "changed");
tenants.store(lockedTenant.with(contact));
});
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContainerImageExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContainerImageExpirer.java
index 80a79d004c6..0f976458257 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContainerImageExpirer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContainerImageExpirer.java
@@ -41,7 +41,7 @@ public class ContainerImageExpirer extends ControllerMaintainer {
.filter(image -> canExpire(image, now, versionStatus))
.collect(Collectors.toList());
if (!imagesToExpire.isEmpty()) {
- log.log(Level.INFO, "Expiring container images: " + imagesToExpire);
+ log.log(Level.INFO, "Expiring " + imagesToExpire.size() + " container images: " + imagesToExpire);
controller().serviceRegistry().containerRegistry().deleteAll(imagesToExpire);
}
return true;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java
index 9bf6352813a..03a6268397e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java
@@ -34,7 +34,7 @@ public abstract class ControllerMaintainer extends Maintainer {
public ControllerMaintainer(Controller controller, Duration interval, String name, Set<SystemName> activeSystems) {
super(name, interval, controller.clock().instant(), controller.jobControl(),
- jobMetrics(controller.metric()), controller.curator().cluster());
+ jobMetrics(controller.metric()), controller.curator().cluster(), true);
this.controller = controller;
this.activeSystems = Set.copyOf(Objects.requireNonNull(activeSystems));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
index 979cd9060d9..8433afaf006 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
@@ -50,13 +50,13 @@ public class ControllerMaintenance extends AbstractComponent {
maintainers.add(new ApplicationOwnershipConfirmer(controller, intervals.applicationOwnershipConfirmer, controller.serviceRegistry().ownershipIssues()));
maintainers.add(new SystemUpgrader(controller, intervals.systemUpgrader));
maintainers.add(new JobRunner(controller, intervals.jobRunner));
- maintainers.add(new OsVersionStatusUpdater(controller, intervals.defaultInterval));
+ maintainers.add(new OsVersionStatusUpdater(controller, intervals.osVersionStatusUpdater));
+ maintainers.add(new OsUpgradeScheduler(controller, intervals.osUpgradeScheduler));
maintainers.add(new ContactInformationMaintainer(controller, intervals.contactInformationMaintainer));
maintainers.add(new NameServiceDispatcher(controller, intervals.nameServiceDispatcher));
maintainers.add(new CostReportMaintainer(controller, intervals.costReportMaintainer, controller.serviceRegistry().costReportConsumer()));
maintainers.add(new ResourceMeterMaintainer(controller, intervals.resourceMeterMaintainer, metric, controller.serviceRegistry().meteringService()));
- maintainers.add(new CloudEventReporter(controller, intervals.cloudEventReporter, metric));
- maintainers.add(new RotationStatusUpdater(controller, intervals.defaultInterval));
+ maintainers.add(new CloudEventReporter(controller, intervals.cloudEventReporter));
maintainers.add(new ResourceTagMaintainer(controller, intervals.resourceTagMaintainer, controller.serviceRegistry().resourceTagger()));
maintainers.add(new SystemRoutingPolicyMaintainer(controller, intervals.systemRoutingPolicyMaintainer));
maintainers.add(new ApplicationMetaDataGarbageCollector(controller, intervals.applicationMetaDataGarbageCollector));
@@ -64,6 +64,10 @@ public class ControllerMaintenance extends AbstractComponent {
maintainers.add(new HostSwitchUpdater(controller, intervals.hostSwitchUpdater));
maintainers.add(new ReindexingTriggerer(controller, intervals.reindexingTriggerer));
maintainers.add(new EndpointCertificateMaintainer(controller, intervals.endpointCertificateMaintainer));
+ maintainers.add(new TrafficShareUpdater(controller, intervals.trafficFractionUpdater));
+ maintainers.add(new ArchiveUriUpdater(controller, intervals.archiveUriUpdater));
+ maintainers.add(new TenantRoleMaintainer(controller, intervals.tenantRoleMaintainer));
+ maintainers.add(new ChangeRequestMaintainer(controller, intervals.changeRequestMaintainer));
}
public Upgrader upgrader() { return upgrader; }
@@ -98,7 +102,9 @@ public class ControllerMaintenance extends AbstractComponent {
private final Duration applicationOwnershipConfirmer;
private final Duration systemUpgrader;
private final Duration jobRunner;
+ private final Duration osVersionStatusUpdater;
private final Duration osUpgrader;
+ private final Duration osUpgradeScheduler;
private final Duration contactInformationMaintainer;
private final Duration nameServiceDispatcher;
private final Duration costReportMaintainer;
@@ -111,6 +117,10 @@ public class ControllerMaintenance extends AbstractComponent {
private final Duration hostSwitchUpdater;
private final Duration reindexingTriggerer;
private final Duration endpointCertificateMaintainer;
+ private final Duration trafficFractionUpdater;
+ private final Duration archiveUriUpdater;
+ private final Duration tenantRoleMaintainer;
+ private final Duration changeRequestMaintainer;
public Intervals(SystemName system) {
this.system = Objects.requireNonNull(system);
@@ -120,21 +130,27 @@ public class ControllerMaintenance extends AbstractComponent {
this.readyJobsTrigger = duration(1, MINUTES);
this.deploymentMetricsMaintainer = duration(10, MINUTES);
this.applicationOwnershipConfirmer = duration(12, HOURS);
- this.systemUpgrader = duration(90, SECONDS);
+ this.systemUpgrader = duration(2, MINUTES);
this.jobRunner = duration(90, SECONDS);
+ this.osVersionStatusUpdater = duration(2, MINUTES);
this.osUpgrader = duration(1, MINUTES);
+ this.osUpgradeScheduler = duration(3, HOURS);
this.contactInformationMaintainer = duration(12, HOURS);
- this.nameServiceDispatcher = duration(30, SECONDS);
+ this.nameServiceDispatcher = duration(10, SECONDS);
this.costReportMaintainer = duration(2, HOURS);
- this.resourceMeterMaintainer = duration(1, MINUTES);
+ this.resourceMeterMaintainer = duration(3, MINUTES);
this.cloudEventReporter = duration(30, MINUTES);
this.resourceTagMaintainer = duration(30, MINUTES);
this.systemRoutingPolicyMaintainer = duration(10, MINUTES);
this.applicationMetaDataGarbageCollector = duration(12, HOURS);
- this.containerImageExpirer = duration(2, HOURS);
+ this.containerImageExpirer = duration(12, HOURS);
this.hostSwitchUpdater = duration(12, HOURS);
this.reindexingTriggerer = duration(1, HOURS);
this.endpointCertificateMaintainer = duration(12, HOURS);
+ this.trafficFractionUpdater = duration(5, MINUTES);
+ this.archiveUriUpdater = duration(5, MINUTES);
+ this.tenantRoleMaintainer = duration(5, MINUTES);
+ this.changeRequestMaintainer = duration(12, HOURS);
}
private Duration duration(long amount, TemporalUnit unit) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
index 37de7369452..e5316788802 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
@@ -1,13 +1,18 @@
// 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.maintenance;
+import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.application.Deployment;
+import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.yolean.Exceptions;
import java.time.Duration;
+import java.util.Optional;
import java.util.logging.Level;
/**
@@ -28,7 +33,7 @@ public class DeploymentExpirer extends ControllerMaintainer {
for (Application application : controller().applications().readable()) {
for (Instance instance : application.instances().values())
for (Deployment deployment : instance.deployments().values()) {
- if (!isExpired(deployment)) continue;
+ if (!isExpired(deployment, instance.id())) continue;
try {
log.log(Level.INFO, "Expiring deployment of " + instance.id() + " in " + deployment.zone());
@@ -45,10 +50,19 @@ public class DeploymentExpirer extends ControllerMaintainer {
}
/** Returns whether given deployment has expired according to its TTL */
- private boolean isExpired(Deployment deployment) {
+ private boolean isExpired(Deployment deployment, ApplicationId instance) {
if (deployment.zone().environment().isProduction()) return false; // Never expire production deployments
- return controller().zoneRegistry().getDeploymentTimeToLive(deployment.zone())
- .map(timeToLive -> deployment.at().plus(timeToLive).isBefore(controller().clock().instant()))
+
+ Optional<Duration> ttl = controller().zoneRegistry().getDeploymentTimeToLive(deployment.zone());
+ if (ttl.isEmpty()) return false;
+
+ Optional<JobId> jobId = JobType.from(controller().system(), deployment.zone())
+ .map(type -> new JobId(instance, type));
+ if (jobId.isEmpty()) return false;
+
+ return controller().jobController().last(jobId.get())
+ .flatMap(Run::end)
+ .map(end -> end.plus(ttl.get()).isBefore(controller().clock().instant()))
.orElse(false);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
index e59875e9588..55a957f0247 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
@@ -12,7 +12,6 @@ import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
@@ -92,20 +91,20 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer {
private void deployRefreshedCertificates() {
var now = clock.instant();
curator.readAllEndpointCertificateMetadata().forEach((applicationId, endpointCertificateMetadata) ->
- endpointCertificateMetadata.lastRefreshed().ifPresent(lastRefreshTime -> {
- Instant refreshTime = Instant.ofEpochSecond(lastRefreshTime);
- if (now.isAfter(refreshTime.plus(7, ChronoUnit.DAYS))) {
-
- controller().jobController().jobs(applicationId).forEach(job ->
- controller().jobController().jobStatus(new JobId(applicationId, JobType.fromJobName(job.jobName()))).lastTriggered().ifPresent(run -> {
- if (run.start().isBefore(refreshTime) && job.isProduction() && job.isDeployment()) {
- deploymentTrigger.reTrigger(applicationId, job);
- log.info("Re-triggering deployment job " + job.jobName() + " for instance " +
- applicationId.serializedForm() + " to roll out refreshed endpoint certificate");
- }
- }));
- }
- }));
+ endpointCertificateMetadata.lastRefreshed().ifPresent(lastRefreshTime -> {
+ Instant refreshTime = Instant.ofEpochSecond(lastRefreshTime);
+ if (now.isAfter(refreshTime.plus(7, ChronoUnit.DAYS))) {
+ controller().applications().getInstance(applicationId)
+ .ifPresent(instance -> instance.productionDeployments().forEach((zone, deployment) -> {
+ if (deployment.at().isBefore(refreshTime)) {
+ JobType job = JobType.from(controller().system(), zone).get();
+ deploymentTrigger.reTrigger(applicationId, job);
+ log.info("Re-triggering deployment job " + job.jobName() + " for instance " +
+ applicationId.serializedForm() + " to roll out refreshed endpoint certificate");
+ }
+ }));
+ }
+ }));
}
private OptionalInt latestVersionInSecretStore(EndpointCertificateMetadata originalCertificateMetadata) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java
index 73528977166..b84cfe5af9b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java
@@ -36,7 +36,7 @@ public class JobRunner extends ControllerMaintainer {
private final StepRunner runner;
public JobRunner(Controller controller, Duration duration) {
- this(controller, duration, Executors.newFixedThreadPool(128, new DaemonThreadFactory("job-runner-")), new InternalStepRunner(controller));
+ this(controller, duration, Executors.newFixedThreadPool(32, new DaemonThreadFactory("job-runner-")), new InternalStepRunner(controller));
}
@TestOnly
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java
index e223809a211..e57affdc15d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java
@@ -1,6 +1,7 @@
// 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.maintenance;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
@@ -18,15 +19,13 @@ import java.util.logging.Level;
*/
public class NameServiceDispatcher extends ControllerMaintainer {
- private static final int defaultRequestCount = 1;
-
private final Clock clock;
private final CuratorDb db;
private final NameService nameService;
private final int requestCount;
public NameServiceDispatcher(Controller controller, Duration interval) {
- this(controller, interval, defaultRequestCount);
+ this(controller, interval, requestCount(controller.system()));
}
public NameServiceDispatcher(Controller controller, Duration interval, int requestCount) {
@@ -48,13 +47,19 @@ public class NameServiceDispatcher extends ControllerMaintainer {
var dispatched = queue.first(requestCount);
if (!dispatched.requests().isEmpty()) {
- log.log(Level.FINE, "Dispatched name service request(s) in " +
- Duration.between(instant, clock.instant()) +
- ": " + dispatched.requests());
+ Level logLevel = controller().system().isCd() ? Level.INFO : Level.FINE;
+ log.log(logLevel, "Dispatched name service request(s) in " +
+ Duration.between(instant, clock.instant()) +
+ ": " + dispatched.requests());
}
db.writeNameServiceQueue(remaining);
}
return success;
}
+ private static int requestCount(SystemName system) {
+ // Dispatch more requests at a time in CD as integration tests expect reasonably quick DNS changes
+ return system.isCd() ? 3 : 1;
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java
new file mode 100644
index 00000000000..9fabe9f0e49
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java
@@ -0,0 +1,95 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.CloudName;
+import com.yahoo.config.provision.zone.ZoneApi;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.versions.OsVersion;
+import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Automatically set the OS version target on a schedule.
+ *
+ * This is used in clouds where new OS versions regularly become available.
+ *
+ * @author mpolden
+ */
+public class OsUpgradeScheduler extends ControllerMaintainer {
+
+ /** Trigger a new upgrade when the current target version reaches this age */
+ private static final Duration MAX_VERSION_AGE = Duration.ofDays(30);
+
+ /**
+ * The interval at which new versions become available. We use this to avoid scheduling upgrades to a version that
+ * may not be available yet
+ */
+ private static final Duration AVAILABILITY_INTERVAL = Duration.ofDays(7);
+
+ private static final DateTimeFormatter VERSION_DATE_PATTERN = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+ public OsUpgradeScheduler(Controller controller, Duration interval) {
+ super(controller, interval);
+ }
+
+ @Override
+ protected boolean maintain() {
+ for (var cloud : supportedClouds()) {
+ Optional<Version> newTarget = newTargetIn(cloud);
+ if (newTarget.isEmpty()) continue;
+ controller().upgradeOsIn(cloud, newTarget.get(), Optional.of(upgradeBudget()), false);
+ }
+ return true;
+ }
+
+ /** Returns the new target version for given cloud, if any */
+ private Optional<Version> newTargetIn(CloudName cloud) {
+ Optional<Version> currentTarget = controller().osVersionTarget(cloud)
+ .map(OsVersionTarget::osVersion)
+ .map(OsVersion::version);
+ if (currentTarget.isEmpty()) return Optional.empty();
+ if (!hasExpired(currentTarget.get())) return Optional.empty();
+
+ Instant now = controller().clock().instant();
+ String qualifier = LocalDate.ofInstant(now.minus(AVAILABILITY_INTERVAL), ZoneOffset.UTC)
+ .format(VERSION_DATE_PATTERN);
+ return Optional.of(new Version(currentTarget.get().getMajor(),
+ currentTarget.get().getMinor(),
+ currentTarget.get().getMicro(),
+ qualifier));
+ }
+
+ /** Returns whether we should upgrade from given version */
+ private boolean hasExpired(Version version) {
+ String qualifier = version.getQualifier();
+ if (!qualifier.matches("^\\d{8,}")) return false;
+
+ String dateString = qualifier.substring(0, 8);
+ Instant now = controller().clock().instant();
+ Instant versionDate = LocalDate.parse(dateString, VERSION_DATE_PATTERN)
+ .atStartOfDay(ZoneOffset.UTC)
+ .toInstant();
+ return versionDate.isBefore(now.minus(MAX_VERSION_AGE));
+ }
+
+ /** Returns the clouds where we can safely schedule OS upgrades */
+ private Set<CloudName> supportedClouds() {
+ return controller().zoneRegistry().zones().reprovisionToUpgradeOs().zones().stream()
+ .map(ZoneApi::getCloudName)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ private Duration upgradeBudget() {
+ return controller().system().isCd() ? Duration.ofHours(1) : Duration.ofDays(14);
+ }
+
+}
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 66fb20e8a71..43e9ce51040 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
@@ -16,7 +16,11 @@ import java.util.Set;
import java.util.logging.Logger;
/**
- * Maintenance job that schedules upgrades of OS / kernel on nodes in the system.
+ * Trigger OS upgrade of zones in the system, according to the current OS version target.
+ *
+ * Target OS version is set per cloud, and an instance of this exists per cloud in the system.
+ *
+ * {@link OsUpgradeScheduler} may update the target automatically in supported clouds.
*
* @author mpolden
*/
@@ -52,14 +56,14 @@ public class OsUpgrader extends InfrastructureUpgrader<OsVersionTarget> {
@Override
protected boolean convergedOn(OsVersionTarget target, SystemApplication application, ZoneApi zone) {
- return currentVersion(zone, application, target.osVersion().version()).equals(target.osVersion().version());
+ return !currentVersion(zone, application, target.osVersion().version()).isBefore(target.osVersion().version());
}
@Override
protected boolean expectUpgradeOf(Node node, SystemApplication application, ZoneApi zone) {
- return cloud.equals(zone.getCloudName()) && // Cloud is managed by this upgrader
- application.shouldUpgradeOsIn(zone.getId(), controller()) && // Application should upgrade in this cloud
- canUpgrade(node); // Node is in an upgradable state
+ return cloud.equals(zone.getCloudName()) && // Cloud is managed by this upgrader
+ application.shouldUpgradeOs() && // Application should upgrade in this cloud
+ canUpgrade(node); // Node is in an upgradable state
}
@Override
@@ -72,7 +76,7 @@ public class OsUpgrader extends InfrastructureUpgrader<OsVersionTarget> {
@Override
protected boolean changeTargetTo(OsVersionTarget target, SystemApplication application, ZoneApi zone) {
- if (!application.shouldUpgradeOsIn(zone.getId(), controller())) return false;
+ if (!application.shouldUpgradeOs()) return false;
return controller().serviceRegistry().configServer().nodeRepository()
.targetVersionsOf(zone.getId())
.osVersion(application.nodeType())
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 5efbb3cf31b..8de6bdbb99c 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
@@ -102,6 +102,7 @@ public class ResourceMeterMaintainer extends ControllerMaintainer {
return nodes.stream()
.filter(this::unlessNodeOwnerIsHostedVespa)
.filter(this::isNodeStateMeterable)
+ .filter(this::isNodeTypeMeterable)
.collect(Collectors.groupingBy(node ->
node.owner().get(),
Collectors.collectingAndThen(Collectors.toList(),
@@ -132,6 +133,10 @@ public class ResourceMeterMaintainer extends ControllerMaintainer {
return METERABLE_NODE_STATES.contains(node.state());
}
+ private boolean isNodeTypeMeterable(Node node) {
+ return node.clusterType() != Node.ClusterType.admin; // log servers and shared cluster controllers
+ }
+
private boolean needsRefresh(long lastRefreshTimestamp) {
return clock.instant()
.minusSeconds(METERING_REFRESH_INTERVAL_SECONDS)
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java
deleted file mode 100644
index 935bcbec597..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdater.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// 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.maintenance;
-
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-
-/**
- * Periodically updates the status of assigned global rotations.
- *
- * @author mpolden
- */
-public class RotationStatusUpdater extends ControllerMaintainer {
-
- private static final int applicationsToUpdateInParallel = 10;
-
- private final GlobalRoutingService service;
- private final ApplicationController applications;
-
- public RotationStatusUpdater(Controller controller, Duration interval) {
- super(controller, interval);
- this.service = controller.serviceRegistry().globalRoutingService();
- this.applications = controller.applications();
- }
-
- @Override
- protected boolean maintain() {
- var failures = new AtomicInteger(0);
- var attempts = new AtomicInteger(0);
- var lastException = new AtomicReference<Exception>(null);
- var instancesWithRotations = ApplicationList.from(applications.readable()).hasRotation().asList().stream()
- .flatMap(application -> application.instances().values().stream())
- .filter(instance -> ! instance.rotations().isEmpty());
-
- // Run parallel stream inside a custom ForkJoinPool so that we can control the number of threads used
- var pool = new ForkJoinPool(applicationsToUpdateInParallel);
-
- pool.submit(() -> {
- instancesWithRotations.parallel().forEach(instance -> {
- attempts.incrementAndGet();
- try {
- RotationStatus status = getStatus(instance);
- applications.lockApplicationIfPresent(TenantAndApplicationId.from(instance.id()), app ->
- applications.store(app.with(instance.name(), locked -> locked.with(status))));
- } catch (Exception e) {
- failures.incrementAndGet();
- lastException.set(e);
- }
- });
- });
- pool.shutdown();
- try {
- pool.awaitTermination(30, TimeUnit.SECONDS);
- if (lastException.get() != null) {
- log.log(Level.WARNING, String.format("Failed to get global routing status of %d/%d applications. Retrying in %s. Last error: %s",
- failures.get(),
- attempts.get(),
- interval(),
- Exceptions.toMessageString(lastException.get())));
- }
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- return lastException.get() == null;
- }
-
- private RotationStatus getStatus(Instance instance) {
- var statusMap = new LinkedHashMap<RotationId, RotationStatus.Targets>();
- for (var assignedRotation : instance.rotations()) {
- var rotation = controller().routing().rotations().getRotation(assignedRotation.rotationId());
- if (rotation.isEmpty()) continue;
- var targets = service.getHealthStatus(rotation.get().name()).entrySet().stream()
- .collect(Collectors.toMap(Map.Entry::getKey, (kv) -> from(kv.getValue())));
- var lastUpdated = controller().clock().instant();
- statusMap.put(assignedRotation.rotationId(), new RotationStatus.Targets(targets, lastUpdated));
- }
- return RotationStatus.from(statusMap);
- }
-
- private static RotationState from(com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus status) {
- switch (status) {
- case IN: return RotationState.in;
- case OUT: return RotationState.out;
- case UNKNOWN: return RotationState.unknown;
- default: throw new IllegalArgumentException("Unknown API value for rotation status: " + status);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java
new file mode 100644
index 00000000000..e8b50a6b604
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java
@@ -0,0 +1,35 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.vespa.flags.BooleanFlag;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+
+import java.time.Duration;
+import java.util.stream.Collectors;
+
+public class TenantRoleMaintainer extends ControllerMaintainer {
+
+ private final BooleanFlag provisionTenantRoles;
+
+ public TenantRoleMaintainer(Controller controller, Duration tenantRoleMaintainer) {
+ super(controller, tenantRoleMaintainer);
+ provisionTenantRoles = Flags.PROVISION_TENANT_ROLES.bindTo(controller.flagSource());
+ }
+
+ @Override
+ protected boolean maintain() {
+ var roleService = controller().serviceRegistry().roleService();
+ var tenants = controller().tenants().asList();
+ var tenantsWithRoles = tenants.stream()
+ .map(Tenant::name)
+ // Only maintain a subset of the tenants
+ .filter(name -> provisionTenantRoles.with(FetchVector.Dimension.TENANT_ID, name.value()).value())
+ .collect(Collectors.toList());
+ roleService.maintainRoles(tenantsWithRoles);
+ return true;
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java
new file mode 100644
index 00000000000..fbe9faa9754
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java
@@ -0,0 +1,79 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.Instance;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
+import com.yahoo.vespa.hosted.controller.application.Deployment;
+
+import java.time.Duration;
+import java.util.logging.Level;
+
+/**
+ * This computes, for every application deployment
+ * - the current fraction of the application's global traffic it receives
+ * - the max fraction it can possibly receive, assuming traffic is evenly distributed over regions
+ * and max one region is down at any time. (We can let deployment.xml override these assumptions later).
+ *
+ * These two numbers are sent to a config server of each region where it is ultimately
+ * consumed by autoscaling.
+ *
+ * It depends on the traffic metrics collected by DeploymentMetricsMaintainer.
+ *
+ * @author bratseth
+ */
+public class TrafficShareUpdater extends ControllerMaintainer {
+
+ private final ApplicationController applications;
+ private final NodeRepository nodeRepository;
+
+ public TrafficShareUpdater(Controller controller, Duration duration) {
+ super(controller, duration, TrafficShareUpdater.class.getSimpleName(), SystemName.all());
+ this.applications = controller.applications();
+ this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
+ }
+
+ @Override
+ protected boolean maintain() {
+ boolean success = false;
+ Exception lastException = null;
+ for (var application : applications.asList()) {
+ for (var instance : application.instances().values()) {
+ for (var deployment : instance.deployments().values()) {
+ if ( ! deployment.zone().environment().isProduction()) continue;
+
+ try {
+ success |= updateTrafficFraction(instance, deployment);
+ }
+ catch (Exception e) {
+ // Some failures due to locked applications are expected and benign
+ lastException = e;
+ }
+ }
+ }
+ }
+ if ( ! success && lastException != null) // log on complete failure
+ log.log(Level.WARNING, "Could not update traffic share on any applications", lastException);
+ return success;
+ }
+
+ private boolean updateTrafficFraction(Instance instance, Deployment deployment) {
+ double qpsInZone = deployment.metrics().queriesPerSecond();
+ double totalQps = instance.deployments().values().stream()
+ .filter(i -> i.zone().environment().isProduction())
+ .mapToDouble(i -> i.metrics().queriesPerSecond()).sum();
+ long prodRegions = instance.deployments().values().stream()
+ .filter(i -> i.zone().environment().isProduction())
+ .count();
+ double currentReadShare = totalQps == 0 ? 0 : qpsInZone / totalQps;
+ double maxReadShare = prodRegions < 2 ? 1.0 : 1.0 / ( prodRegions - 1.0);
+ if (currentReadShare > maxReadShare) // This can happen because the assumption of equal traffic
+ maxReadShare = currentReadShare; // distribution can be incorrect
+
+ nodeRepository.patchApplication(deployment.zone(), instance.id(), currentReadShare, maxReadShare);
+ return true;
+ }
+
+}
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 3aac5eb2266..24b553e5153 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
@@ -1,10 +1,6 @@
// 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.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.hash.Hashing;
-import com.google.common.util.concurrent.UncheckedExecutionException;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
@@ -53,7 +49,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.concurrent.ExecutionException;
/**
* Serializes {@link Application}s to/from slime.
@@ -147,13 +142,6 @@ public class ApplicationSerializer {
// Quota usage fields
private static final String quotaUsageRateField = "quotaUsageRate";
- // A cache of deserialized applications.
- //
- // Deserializing an application from slime is expensive, particularly XML fields, such as DeploymentSpec and
- // ValidationOverrides. Applications that have already been deserialized are returned from this cache instead of
- // being deserialized again.
- private final Cache<Long, Application> cache = CacheBuilder.newBuilder().maximumSize(1000).build();
-
// ------------------ Serialization
public Slime toSlime(Application application) {
@@ -297,19 +285,14 @@ public class ApplicationSerializer {
// ------------------ Deserialization
public Application fromSlime(byte[] data) {
- var key = Hashing.sipHash24().hashBytes(data).asLong();
- try {
- return cache.get(key, () -> fromSlime(SlimeUtils.jsonToSlime(data)));
- } catch (ExecutionException e) {
- throw new UncheckedExecutionException(e);
- }
+ return fromSlime(SlimeUtils.jsonToSlime(data));
}
private Application fromSlime(Slime slime) {
Inspector root = slime.get();
TenantAndApplicationId id = TenantAndApplicationId.fromSerialized(root.field(idField).asString());
- Instant createdAt = Instant.ofEpochMilli(root.field(createdAtField).asLong());
+ Instant createdAt = Serializers.instant(root.field(createdAtField));
DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(root.field(deploymentSpecField).asString(), false);
ValidationOverrides validationOverrides = ValidationOverrides.fromXml(root.field(validationOverridesField).asString());
Optional<IssueId> deploymentIssueId = Serializers.optionalString(root.field(deploymentIssueField)).map(IssueId::from);
@@ -368,7 +351,7 @@ public class ApplicationSerializer {
return new Deployment(zoneIdFromSlime(deploymentObject.field(zoneField)),
applicationVersionFromSlime(deploymentObject.field(applicationPackageRevisionField)),
Version.fromString(deploymentObject.field(versionField).asString()),
- Instant.ofEpochMilli(deploymentObject.field(deployTimeField).asLong()),
+ Serializers.instant(deploymentObject.field(deployTimeField)),
deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)),
DeploymentActivity.create(Serializers.optionalInstant(deploymentObject.field(lastQueriedField)),
Serializers.optionalInstant(deploymentObject.field(lastWrittenField)),
@@ -378,9 +361,7 @@ public class ApplicationSerializer {
}
private DeploymentMetrics deploymentMetricsFromSlime(Inspector object) {
- Optional<Instant> instant = object.field(deploymentMetricsUpdateTime).valid() ?
- Optional.of(Instant.ofEpochMilli(object.field(deploymentMetricsUpdateTime).asLong())) :
- Optional.empty();
+ Optional<Instant> instant = Serializers.optionalInstant(object.field(deploymentMetricsUpdateTime));
return new DeploymentMetrics(object.field(deploymentMetricsQPSField).asDouble(),
object.field(deploymentMetricsWPSField).asDouble(),
object.field(deploymentMetricsDocsField).asDouble(),
@@ -403,7 +384,7 @@ public class ApplicationSerializer {
object.traverse((ArrayTraverser) (idx, statusObject) -> statusMap.put(new RotationId(statusObject.field(rotationIdField).asString()),
new RotationStatus.Targets(
singleRotationStatusFromSlime(statusObject.field(statusField)),
- Instant.ofEpochMilli(statusObject.field(lastUpdatedField).asLong()))));
+ Serializers.instant(statusObject.field(lastUpdatedField)))));
return RotationStatus.from(statusMap);
}
@@ -452,7 +433,7 @@ public class ApplicationSerializer {
object.field(jobStatusField).traverse((ArrayTraverser) (__, jobPauseObject) ->
JobType.fromOptionalJobName(jobPauseObject.field(jobTypeField).asString())
.ifPresent(jobType -> jobPauses.put(jobType,
- Instant.ofEpochMilli(jobPauseObject.field(pausedUntilField).asLong()))));
+ Serializers.instant(jobPauseObject.field(pausedUntilField)))));
return jobPauses;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java
index b411f460568..7ea722bf5de 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java
@@ -7,7 +7,6 @@ import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -52,7 +51,7 @@ public class AuditLogSerializer {
Cursor root = slime.get();
root.field(entriesField).traverse((ArrayTraverser) (i, entryObject) -> {
entries.add(new AuditLog.Entry(
- Instant.ofEpochMilli(entryObject.field(atField).asLong()),
+ Serializers.instant(entryObject.field(atField)),
entryObject.field(principalField).asString(),
methodFrom(entryObject.field(methodField)),
entryObject.field(resourceField).asString(),
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
index 5f36499426b..073d1799d3e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
@@ -57,7 +57,7 @@ public class BufferedLogStore {
long lastChunkId = buffer.getLogChunkIds(id, type).max().orElse(0);
long numberOfChunks = Math.max(1, buffer.getLogChunkIds(id, type).count());
if (numberOfChunks > maxLogSize / chunkSize)
- return; // Max size exceeded — store no more.
+ return; // Max size exceeded — store no more.
byte[] emptyChunk = "[]".getBytes();
byte[] lastChunk = buffer.readLog(id, type, lastChunkId).orElse(emptyChunk);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
index 0e8b6087901..30fcc0e40c6 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
@@ -5,8 +5,6 @@ import com.yahoo.component.Version;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.versions.ControllerVersion;
-import java.time.Instant;
-
/**
* Serializer for {@link com.yahoo.vespa.hosted.controller.versions.ControllerVersion}.
*
@@ -38,7 +36,7 @@ public class ControllerVersionSerializer {
var root = slime.get();
var version = Version.fromString(root.field(VERSION_FIELD).asString());
var commitSha = root.field(COMMIT_SHA_FIELD).asString();
- var commitDate = Instant.ofEpochMilli(root.field(COMMIT_DATE_FIELD).asLong());
+ var commitDate = Serializers.instant(root.field(COMMIT_DATE_FIELD));
return new ControllerVersion(version, commitSha, commitDate);
}
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 dc35750f66a..010a2e3f8e4 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
@@ -106,6 +106,10 @@ public class CuratorDb {
private final Curator curator;
private final Duration tryLockTimeout;
+ // For each application id (path), store the ZK node version and its deserialised data - update when version changes.
+ // This will grow to keep all applications in memory, but this should be OK
+ private final Map<Path, Pair<Integer, Application>> cachedApplications = new ConcurrentHashMap<>();
+
// For each job id (path), store the ZK node version and its deserialised data - update when version changes.
private final Map<Path, Pair<Integer, NavigableMap<RunId, Run>>> cachedHistoricRuns = new ConcurrentHashMap<>();
@@ -325,7 +329,12 @@ public class CuratorDb {
}
public Optional<Application> readApplication(TenantAndApplicationId application) {
- return read(applicationPath(application), applicationSerializer::fromSlime);
+ Path path = applicationPath(application);
+ return curator.getStat(path)
+ .map(stat -> cachedApplications.compute(path, (__, old) ->
+ old != null && old.getFirst() == stat.getVersion()
+ ? old
+ : new Pair<>(stat.getVersion(), read(path, applicationSerializer::fromSlime).get())).getSecond());
}
public List<Application> readApplications(boolean canFail) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializer.java
index 19f9542c679..7e4fc285793 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializer.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. 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.yahoo.slime.Cursor;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java
index fffe781e6e1..6416d077ce4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java
@@ -13,7 +13,6 @@ import com.yahoo.vespa.hosted.controller.deployment.Step;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -93,7 +92,7 @@ class LogSerializer {
private LogEntry fromSlime(Inspector entryObject) {
return new LogEntry(entryObject.field(idField).asLong(),
- Instant.ofEpochMilli(entryObject.field(timestampField).asLong()),
+ Serializers.instant(entryObject.field(timestampField)),
typeOf(entryObject.field(typeField).asString()),
entryObject.field(messageField).asString());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
index 1aa229984a8..0ecd86a4a38 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
@@ -121,9 +121,7 @@ class RunSerializer {
// For historical reasons are the step details stored in a separate JSON structure from the step statuses.
Inspector stepDetailsField = detailsField.field(step);
Inspector startTimeValue = stepDetailsField.field(startTimeField);
- Optional<Instant> startTime = startTimeValue.valid() ?
- Optional.of(instantOf(startTimeValue.asLong())) :
- Optional.empty();
+ Optional<Instant> startTime = Serializers.optionalInstant(startTimeValue);
steps.put(typedStep, new StepInfo(typedStep, stepStatusOf(status.asString()), startTime));
});
@@ -132,7 +130,7 @@ class RunSerializer {
runObject.field(numberField).asLong()),
steps,
versionsFromSlime(runObject.field(versionsField)),
- Instant.ofEpochMilli(runObject.field(startField).asLong()),
+ Serializers.instant(runObject.field(startField)),
Serializers.optionalInstant(runObject.field(endField)),
runStatusOf(runObject.field(statusField).asString()),
runObject.field(lastTestRecordField).asLong(),
@@ -259,7 +257,7 @@ class RunSerializer {
applicationVersion.commit().ifPresent(commit -> versionsObject.setString(commitField, commit));
}
- // Don't change this — introduce a separate array with new values if needed.
+ // Don't change this - introduce a separate array with new values if needed.
private void toSlime(ConvergenceSummary summary, Cursor summaryArray) {
summaryArray.addLong(summary.nodes());
summaryArray.addLong(summary.down());
@@ -341,10 +339,6 @@ class RunSerializer {
return instant.toEpochMilli();
}
- static Instant instantOf(Long epochMillis) {
- return Instant.ofEpochMilli(epochMillis);
- }
-
static String valueOf(RunStatus status) {
switch (status) {
case running : return "running";
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/Serializers.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/Serializers.java
index b254732f324..7c8a09e244e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/Serializers.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/Serializers.java
@@ -20,6 +20,10 @@ public class Serializers {
private Serializers() {}
+ public static Instant instant(Inspector field) {
+ return Instant.ofEpochMilli(field.asLong());
+ }
+
public static OptionalLong optionalLong(Inspector field) {
return field.valid() ? OptionalLong.of(field.asLong()) : OptionalLong.empty();
}
@@ -37,13 +41,11 @@ public class Serializers {
}
public static Optional<Instant> optionalInstant(Inspector field) {
- var value = optionalLong(field);
- return value.isPresent() ? Optional.of(Instant.ofEpochMilli(value.getAsLong())) : Optional.empty();
+ return optionalLong(field).stream().mapToObj(Instant::ofEpochMilli).findFirst();
}
public static Optional<Duration> optionalDuration(Inspector field) {
- var value = optionalLong(field);
- return value.isPresent() ? Optional.of(Duration.ofMillis(value.getAsLong())) : Optional.empty();
+ return optionalLong(field).stream().mapToObj(Duration::ofMillis).findFirst();
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
index cc2d7c207e5..8e97368624d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
@@ -13,6 +13,7 @@ import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
@@ -69,6 +70,10 @@ public class TenantSerializer {
private static final String pemDeveloperKeysField = "pemDeveloperKeys";
private static final String tenantInfoField = "info";
private static final String lastLoginInfoField = "lastLoginInfo";
+ private static final String secretStoresField = "secretStores";
+ private static final String archiveAccessRoleField = "archiveAccessRole";
+ private static final String awsIdField = "awsId";
+ private static final String roleField = "role";
public Slime toSlime(Tenant tenant) {
Slime slime = new Slime();
@@ -105,6 +110,8 @@ public class TenantSerializer {
developerKeysToSlime(tenant.developerKeys(), root.setArray(pemDeveloperKeysField));
toSlime(legacyBillingInfo, root.setObject(billingInfoField));
toSlime(tenant.info(), root);
+ toSlime(tenant.tenantSecretStores(), root);
+ tenant.archiveAccessRole().ifPresent(role -> root.setString(archiveAccessRoleField, role));
}
private void developerKeysToSlime(BiMap<PublicKey, Principal> keys, Cursor array) {
@@ -144,19 +151,21 @@ public class TenantSerializer {
Property property = new Property(tenantObject.field(propertyField).asString());
Optional<PropertyId> propertyId = SlimeUtils.optionalString(tenantObject.field(propertyIdField)).map(PropertyId::new);
Optional<Contact> contact = contactFrom(tenantObject.field(contactField));
- Instant createdAt = Instant.ofEpochMilli(tenantObject.field(createdAtField).asLong());
+ Instant createdAt = Serializers.instant(tenantObject.field(createdAtField));
LastLoginInfo lastLoginInfo = lastLoginInfoFromSlime(tenantObject.field(lastLoginInfoField));
return new AthenzTenant(name, domain, property, propertyId, contact, createdAt, lastLoginInfo);
}
private CloudTenant cloudTenantFrom(Inspector tenantObject) {
TenantName name = TenantName.from(tenantObject.field(nameField).asString());
- Instant createdAt = Instant.ofEpochMilli(tenantObject.field(createdAtField).asLong());
+ Instant createdAt = Serializers.instant(tenantObject.field(createdAtField));
LastLoginInfo lastLoginInfo = lastLoginInfoFromSlime(tenantObject.field(lastLoginInfoField));
Optional<Principal> creator = SlimeUtils.optionalString(tenantObject.field(creatorField)).map(SimplePrincipal::new);
BiMap<PublicKey, Principal> developerKeys = developerKeysFromSlime(tenantObject.field(pemDeveloperKeysField));
TenantInfo info = tenantInfoFromSlime(tenantObject.field(tenantInfoField));
- return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info);
+ List<TenantSecretStore> tenantSecretStores = secretStoresFromSlime(tenantObject.field(secretStoresField));
+ Optional<String> archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField));
+ return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole);
}
private BiMap<PublicKey, Principal> developerKeysFromSlime(Inspector array) {
@@ -199,10 +208,26 @@ public class TenantSerializer {
.withAddress(tenantInfoAddressFromSlime(billingObject.field("address")));
}
+ private List<TenantSecretStore> secretStoresFromSlime(Inspector secretStoresObject) {
+ List<TenantSecretStore> secretStores = new ArrayList<>();
+ if (!secretStoresObject.valid()) return secretStores;
+
+ secretStoresObject.traverse((ArrayTraverser) (index, inspector) -> {
+ secretStores.add(
+ new TenantSecretStore(
+ inspector.field(nameField).asString(),
+ inspector.field(awsIdField).asString(),
+ inspector.field(roleField).asString()
+ )
+ );
+ });
+ return secretStores;
+ }
+
private LastLoginInfo lastLoginInfoFromSlime(Inspector lastLoginInfoObject) {
Map<LastLoginInfo.UserLevel, Instant> lastLoginByUserLevel = new HashMap<>();
lastLoginInfoObject.traverse((String name, Inspector value) ->
- lastLoginByUserLevel.put(userLevelOf(name), Instant.ofEpochMilli(value.asLong())));
+ lastLoginByUserLevel.put(userLevelOf(name), Serializers.instant(value)));
return new LastLoginInfo(lastLoginByUserLevel);
}
@@ -240,6 +265,19 @@ public class TenantSerializer {
toSlime(billingContact.address(), addressCursor);
}
+ private void toSlime(List<TenantSecretStore> tenantSecretStores, Cursor parentCursor) {
+ if (tenantSecretStores.isEmpty()) return;
+
+ Cursor secretStoresCursor = parentCursor.setArray(secretStoresField);
+ tenantSecretStores.forEach(tenantSecretStore -> {
+ Cursor secretStoreCursor = secretStoresCursor.addObject();
+ secretStoreCursor.setString(nameField, tenantSecretStore.getName());
+ secretStoreCursor.setString(awsIdField, tenantSecretStore.getAwsId());
+ secretStoreCursor.setString(roleField, tenantSecretStore.getRole());
+ });
+
+ }
+
private Optional<Contact> contactFrom(Inspector object) {
if ( ! object.valid()) return Optional.empty();
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 6eb5b8fadcd..12d15aa7cdd 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
@@ -7,19 +7,14 @@ 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.deployment.Run;
-import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics;
import com.yahoo.vespa.hosted.controller.versions.NodeVersions;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import java.time.Instant;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
-import java.util.stream.Collectors;
/**
* Serializer for {@link VersionStatus}.
@@ -111,7 +106,7 @@ public class VersionStatusSerializer {
var version = Version.fromString(object.field(deploymentStatisticsField).field(versionField).asString());
return new VespaVersion(version,
object.field(releaseCommitField).asString(),
- Instant.ofEpochMilli(object.field(committedAtField).asLong()),
+ Serializers.instant(object.field(committedAtField)),
object.field(isControllerVersionField).asBool(),
object.field(isSystemVersionField).asBool(),
object.field(isReleasedField).asBool(),
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
index f5dcae9c961..858bf857429 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
@@ -1,7 +1,7 @@
// 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.proxy;
-import ai.vespa.util.http.retry.Sleeper;
+import ai.vespa.util.http.hc4.retry.Sleeper;
import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.jdisc.http.HttpRequest.Method;
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 9331e5086cc..30c30d9c5a9 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
@@ -33,15 +33,16 @@ import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.security.KeyUtils;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
+import com.yahoo.slime.JsonParseException;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
+import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.NotExistsException;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ProtonMetrics;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.RefeedAction;
@@ -49,8 +50,8 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbi
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ServiceInfo;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
+import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
@@ -65,6 +66,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartF
import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringData;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
@@ -138,6 +140,7 @@ import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR;
import static com.yahoo.jdisc.Response.Status.NOT_FOUND;
import static java.util.Map.Entry.comparingByKey;
import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toUnmodifiableList;
/**
@@ -152,8 +155,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private static final ObjectMapper jsonMapper = new ObjectMapper();
- private static final String OPTIONAL_PREFIX = "/api";
-
private final Controller controller;
private final AccessControlRequests accessControlRequests;
private final TestConfigSerializer testConfigSerializer;
@@ -176,7 +177,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
try {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
switch (request.getMethod()) {
case GET: return handleGET(path, request);
case PUT: return handlePUT(path, request);
@@ -222,6 +223,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
if (path.matches("/application/v4/tenant")) return tenants(request);
if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/info")) return tenantInfo(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request);
if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/compile-version")) return compileVersion(path.get("tenant"), path.get("application"));
@@ -267,6 +269,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse handlePUT(Path path, HttpRequest request) {
if (path.matches("/application/v4/tenant/{tenant}")) return updateTenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/info")) return updateTenantInfo(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowArchiveAccess(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return addSecretStore(path.get("tenant"), path.get("name"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request);
return ErrorResponse.notFoundError("Nothing at " + path);
@@ -310,6 +314,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse handleDELETE(Path path, HttpRequest request) {
if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return removeArchiveAccess(path.get("tenant"));
+ if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return deleteSecretStore(path.get("tenant"), path.get("name"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return removeAllProdDeployments(path.get("tenant"), path.get("application"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", "all");
@@ -340,8 +346,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse recursiveRoot(HttpRequest request) {
Slime slime = new Slime();
Cursor tenantArray = slime.setArray();
+ List<Application> applications = controller.applications().asList();
for (Tenant tenant : controller.tenants().asList())
- toSlime(tenantArray.addObject(), tenant, request);
+ toSlime(tenantArray.addObject(),
+ tenant,
+ applications.stream().filter(app -> app.id().tenant().equals(tenant.name())).collect(toList()),
+ request);
return new SlimeJsonResponse(slime);
}
@@ -367,7 +377,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse tenant(Tenant tenant, HttpRequest request) {
Slime slime = new Slime();
- toSlime(slime.setObject(), tenant, request);
+ toSlime(slime.setObject(), tenant, controller.applications().asList(tenant.name()), request);
return new SlimeJsonResponse(slime);
}
@@ -477,7 +487,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Slime slime = new Slime();
Cursor applicationArray = slime.setArray();
- for (com.yahoo.vespa.hosted.controller.Application application : controller.applications().asList(tenant)) {
+ for (Application application : controller.applications().asList(tenant)) {
if (applicationName.map(application.id().application().value()::equals).orElse(true)) {
Cursor applicationObject = applicationArray.addObject();
applicationObject.setString("tenant", application.id().tenant().value());
@@ -579,6 +589,41 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(root);
}
+ private HttpResponse validateSecretStore(String tenantName, String secretStoreName, HttpRequest request) {
+
+ var awsRegion = request.getProperty("aws-region");
+ var parameterName = request.getProperty("parameter-name");
+ var applicationId = ApplicationId.fromFullString(request.getProperty("application-id"));
+ var zoneId = ZoneId.from(request.getProperty("zone"));
+ var deploymentId = new DeploymentId(applicationId, zoneId);
+
+ var tenant = (CloudTenant)controller.tenants().require(applicationId.tenant());
+ if (tenant.type() != Tenant.Type.cloud) {
+ return ErrorResponse.badRequest("Tenant '" + applicationId.tenant() + "' is not a cloud tenant");
+ }
+
+ var tenantSecretStore = tenant.tenantSecretStores()
+ .stream()
+ .filter(secretStore -> secretStore.getName().equals(secretStoreName))
+ .findFirst();
+
+ if (tenantSecretStore.isEmpty())
+ return ErrorResponse.notFoundError("No secret store '" + secretStoreName + "' configured for tenant '" + tenantName + "'");
+
+ var response = controller.serviceRegistry().configServer().validateSecretStore(deploymentId, tenantSecretStore.get(), awsRegion, parameterName);
+ try {
+ var responseRoot = new Slime();
+ var responseCursor = responseRoot.setObject();
+ responseCursor.setString("target", deploymentId.toString());
+ var responseResultCursor = responseCursor.setObject("result");
+ var responseSlime = SlimeUtils.jsonToSlime(response);
+ SlimeUtils.copyObject(responseSlime.get(), responseResultCursor);
+ return new SlimeJsonResponse(responseRoot);
+ } catch (JsonParseException e) {
+ return ErrorResponse.internalServerError(response);
+ }
+ }
+
private HttpResponse removeDeveloperKey(String tenantName, HttpRequest request) {
if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
@@ -631,6 +676,94 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(root);
}
+ private HttpResponse addSecretStore(String tenantName, String name, HttpRequest request) {
+ if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
+ throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
+
+ var data = toSlime(request.getData()).get();
+ var awsId = mandatory("awsId", data).asString();
+ var externalId = mandatory("externalId", data).asString();
+ var role = mandatory("role", data).asString();
+
+ var tenant = (CloudTenant) controller.tenants().require(TenantName.from(tenantName));
+ var tenantSecretStore = new TenantSecretStore(name, awsId, role);
+
+ if (!tenantSecretStore.isValid()) {
+ return ErrorResponse.badRequest(String.format("Secret store " + tenantSecretStore + " is invalid"));
+ }
+ if (tenant.tenantSecretStores().contains(tenantSecretStore)) {
+ return ErrorResponse.badRequest(String.format("Secret store " + tenantSecretStore + " is already configured"));
+ }
+
+ controller.serviceRegistry().roleService().createTenantPolicy(TenantName.from(tenantName), name, awsId, role);
+ controller.serviceRegistry().tenantSecretService().addSecretStore(tenant.name(), tenantSecretStore, externalId);
+ // Store changes
+ controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
+ lockedTenant = lockedTenant.withSecretStore(tenantSecretStore);
+ controller.tenants().store(lockedTenant);
+ });
+
+ tenant = (CloudTenant) controller.tenants().require(TenantName.from(tenantName));
+ var slime = new Slime();
+ toSlime(slime.setObject(), tenant.tenantSecretStores());
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse deleteSecretStore(String tenantName, String name, HttpRequest request) {
+ var tenant = (CloudTenant) controller.tenants().require(TenantName.from(tenantName));
+
+ var optionalSecretStore = tenant.tenantSecretStores().stream()
+ .filter(secretStore -> secretStore.getName().equals(name))
+ .findFirst();
+
+ if (optionalSecretStore.isEmpty())
+ return ErrorResponse.notFoundError("Could not delete secret store '" + name + "': Secret store not found");
+
+ var tenantSecretStore = optionalSecretStore.get();
+ controller.serviceRegistry().tenantSecretService().deleteSecretStore(tenant.name(), tenantSecretStore);
+ controller.serviceRegistry().roleService().deleteTenantPolicy(tenant.name(), tenantSecretStore.getName(), tenantSecretStore.getRole());
+ controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
+ lockedTenant = lockedTenant.withoutSecretStore(tenantSecretStore);
+ controller.tenants().store(lockedTenant);
+ });
+
+ tenant = (CloudTenant) controller.tenants().require(TenantName.from(tenantName));
+ var slime = new Slime();
+ toSlime(slime.setObject(), tenant.tenantSecretStores());
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse allowArchiveAccess(String tenantName, HttpRequest request) {
+ if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
+ throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
+
+ var data = toSlime(request.getData()).get();
+ var role = mandatory("role", data).asString();
+
+ if (role.isBlank()) {
+ return ErrorResponse.badRequest("Archive access role can't be whitespace only");
+ }
+
+ controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> {
+ lockedTenant = lockedTenant.withArchiveAccessRole(Optional.of(role));
+ controller.tenants().store(lockedTenant);
+ });
+
+ return new MessageResponse("Archive access role set to '" + role + "' for tenant " + tenantName + ".");
+ }
+
+ private HttpResponse removeArchiveAccess(String tenantName) {
+ if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
+ throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
+
+ controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> {
+ lockedTenant = lockedTenant.withArchiveAccessRole(Optional.empty());
+ controller.tenants().store(lockedTenant);
+ });
+
+ return new MessageResponse("Archive access role removed for tenant " + tenantName + ".");
+ }
+
private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) {
Inspector requestObject = toSlime(request.getData()).get();
StringJoiner messageBuilder = new StringJoiner("\n").setEmptyValue("No applicable changes.");
@@ -656,7 +789,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new MessageResponse(messageBuilder.toString());
}
- private com.yahoo.vespa.hosted.controller.Application getApplication(String tenantName, String applicationName) {
+ private Application getApplication(String tenantName, String applicationName) {
TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenantName, applicationName);
return controller.applications().getApplication(applicationId)
.orElseThrow(() -> new NotExistsException(applicationId + " not found"));
@@ -684,9 +817,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
nodeObject.setString("version", node.currentVersion().toString());
nodeObject.setString("flavor", node.flavor());
toSlime(node.resources(), nodeObject);
- nodeObject.setBool("fastDisk", node.resources().diskSpeed() == NodeResources.DiskSpeed.fast); // TODO: Remove
nodeObject.setString("clusterId", node.clusterId());
nodeObject.setString("clusterType", valueOf(node.clusterType()));
+ nodeObject.setBool("down", node.history().stream().anyMatch(event -> "down".equals(event.getEvent())));
+ nodeObject.setBool("retired", node.retired() || node.wantToRetire());
+ nodeObject.setBool("restarting", node.wantedRestartGeneration() > node.restartGeneration());
+ nodeObject.setBool("rebooting", node.wantedRebootGeneration() > node.rebootGeneration());
}
return new SlimeJsonResponse(slime);
}
@@ -694,12 +830,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse clusters(String tenantName, String applicationName, String instanceName, String environment, String region) {
ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
ZoneId zone = requireZone(environment, region);
- Application application = controller.serviceRegistry().configServer().nodeRepository().getApplication(zone, id);
+ com.yahoo.vespa.hosted.controller.api.integration.configserver.Application application = controller.serviceRegistry().configServer().nodeRepository().getApplication(zone, id);
Slime slime = new Slime();
Cursor clustersObject = slime.setObject().setObject("clusters");
for (Cluster cluster : application.clusters().values()) {
Cursor clusterObject = clustersObject.setObject(cluster.id().value());
+ clusterObject.setString("type", cluster.type().name());
toSlime(cluster.min(), clusterObject.setObject("min"));
toSlime(cluster.max(), clusterObject.setObject("max"));
toSlime(cluster.current(), clusterObject.setObject("current"));
@@ -707,8 +844,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
&& ! cluster.target().get().justNumbers().equals(cluster.current().justNumbers()))
toSlime(cluster.target().get(), clusterObject.setObject("target"));
cluster.suggested().ifPresent(suggested -> toSlime(suggested, clusterObject.setObject("suggested")));
+ utilizationToSlime(cluster.utilization(), clusterObject.setObject("utilization"));
scalingEventsToSlime(cluster.scalingEvents(), clusterObject.setArray("scalingEvents"));
clusterObject.setString("autoscalingStatus", cluster.autoscalingStatus());
+ clusterObject.setLong("scalingDuration", cluster.scalingDuration().toMillis());
+ clusterObject.setDouble("maxQueryGrowthRate", cluster.maxQueryGrowthRate());
+ clusterObject.setDouble("currentQueryFractionOfMax", cluster.currentQueryFractionOfMax());
}
return new SlimeJsonResponse(slime);
}
@@ -830,7 +971,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new MessageResponse(type.jobName() + " for " + id + " resumed");
}
- private void toSlime(Cursor object, com.yahoo.vespa.hosted.controller.Application application, HttpRequest request) {
+ private void toSlime(Cursor object, Application application, HttpRequest request) {
object.setString("tenant", application.id().tenant().value());
object.setString("application", application.id().application().value());
object.setString("deployments", withPath("/application/v4" +
@@ -968,7 +1109,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private void toSlime(Cursor object, Instance instance, DeploymentStatus status, HttpRequest request) {
- com.yahoo.vespa.hosted.controller.Application application = status.application();
+ Application application = status.application();
object.setString("tenant", instance.id().tenant().value());
object.setString("application", instance.id().application().value());
object.setString("instance", instance.id().instance().value());
@@ -1470,7 +1611,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Inspector requestObject = toSlime(request.getData()).get();
TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
Credentials credentials = accessControlRequests.credentials(id.tenant(), requestObject, request.getJDiscRequest());
- com.yahoo.vespa.hosted.controller.Application application = controller.applications().createApplication(id, credentials);
+ Application application = controller.applications().createApplication(id, credentials);
Slime slime = new Slime();
toSlime(id, slime.setObject(), request);
return new SlimeJsonResponse(slime);
@@ -1683,20 +1824,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
controller.jobController().deploy(id, type, version, applicationPackage);
RunId runId = controller.jobController().last(id, type).get().id();
- DeploymentId deploymentId = new DeploymentId(id, type.zone(controller.system()));
-
Slime slime = new Slime();
Cursor rootObject = slime.setObject();
rootObject.setString("message", "Deployment started in " + runId +
". This may take about 15 minutes the first time.");
rootObject.setLong("run", runId.number());
- var endpointArray = rootObject.setArray("endpoints");
- EndpointList zoneEndpoints = controller.routing().endpointsOf(deploymentId)
- .scope(Endpoint.Scope.zone)
- .not().legacy();
- for (var endpoint : controller.routing().directEndpoints(zoneEndpoints, deploymentId.applicationId())) {
- toSlime(endpoint, endpointArray.addObject());
- }
return new SlimeJsonResponse(slime);
}
@@ -1710,99 +1842,30 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return ErrorResponse.badRequest("Missing required form part 'deployOptions'");
Inspector deployOptions = SlimeUtils.jsonToSlime(dataParts.get("deployOptions")).get();
- /*
- * Special handling of the proxy application (the only system application with an application package)
- * Setting any other deployOptions here is not supported for now (e.g. specifying version), but
- * this might be handy later to handle emergency downgrades.
- */
- boolean isZoneApplication = SystemApplication.proxy.id().equals(applicationId);
- if (isZoneApplication) { // TODO jvenstad: Separate out.
- // Make it explicit that version is not yet supported here
- String versionStr = deployOptions.field("vespaVersion").asString();
- boolean versionPresent = !versionStr.isEmpty() && !versionStr.equals("null");
- if (versionPresent) {
- throw new RuntimeException("Version not supported for system applications");
- }
- // To avoid second guessing the orchestrated upgrades of system applications
- // we don't allow to deploy these during an system upgrade (i.e when new vespa is being rolled out)
- VersionStatus versionStatus = controller.readVersionStatus();
- if (versionStatus.isUpgrading()) {
- throw new IllegalArgumentException("Deployment of system applications during a system upgrade is not allowed");
- }
- Optional<VespaVersion> systemVersion = versionStatus.systemVersion();
- if (systemVersion.isEmpty()) {
- throw new IllegalArgumentException("Deployment of system applications is not permitted until system version is determined");
- }
- ActivateResult result = controller.applications()
- .deploySystemApplicationPackage(SystemApplication.proxy, zone, systemVersion.get().versionNumber());
- return new SlimeJsonResponse(toSlime(result));
+ // Resolve system application
+ Optional<SystemApplication> systemApplication = SystemApplication.matching(applicationId);
+ if (systemApplication.isEmpty() || !systemApplication.get().hasApplicationPackage()) {
+ return ErrorResponse.badRequest("Deployment of " + applicationId + " is not supported through this API");
}
- /*
- * Normal applications from here
- */
-
- Optional<ApplicationPackage> applicationPackage = Optional.ofNullable(dataParts.get("applicationZip"))
- .map(ApplicationPackage::new);
- Optional<com.yahoo.vespa.hosted.controller.Application> application = controller.applications().getApplication(TenantAndApplicationId.from(applicationId));
-
- Inspector sourceRevision = deployOptions.field("sourceRevision");
- Inspector buildNumber = deployOptions.field("buildNumber");
- if (sourceRevision.valid() != buildNumber.valid())
- throw new IllegalArgumentException("Source revision and build number must both be provided, or not");
-
- Optional<ApplicationVersion> applicationVersion = Optional.empty();
- if (sourceRevision.valid()) {
- if (applicationPackage.isPresent())
- throw new IllegalArgumentException("Application version and application package can't both be provided.");
-
- applicationVersion = Optional.of(ApplicationVersion.from(toSourceRevision(sourceRevision),
- buildNumber.asLong()));
- applicationPackage = Optional.of(controller.applications().getApplicationPackage(applicationId,
- applicationVersion.get()));
+ // Make it explicit that version is not yet supported here
+ String vespaVersion = deployOptions.field("vespaVersion").asString();
+ if (!vespaVersion.isEmpty() && !vespaVersion.equals("null")) {
+ return ErrorResponse.badRequest("Specifying version for " + applicationId + " is not permitted");
}
- boolean deployDirectly = deployOptions.field("deployDirectly").asBool();
- Optional<Version> vespaVersion = optional("vespaVersion", deployOptions).map(Version::new);
-
- if (deployDirectly && applicationPackage.isEmpty() && applicationVersion.isEmpty() && vespaVersion.isEmpty()) {
-
- // Redeploy the existing deployment with the same versions.
- Optional<Deployment> deployment = controller.applications().getInstance(applicationId)
- .map(Instance::deployments)
- .flatMap(deployments -> Optional.ofNullable(deployments.get(zone)));
-
- if(deployment.isEmpty())
- throw new IllegalArgumentException("Can't redeploy application, no deployment currently exist");
-
- ApplicationVersion version = deployment.get().applicationVersion();
- if(version.isUnknown())
- throw new IllegalArgumentException("Can't redeploy application, application version is unknown");
-
- applicationVersion = Optional.of(version);
- vespaVersion = Optional.of(deployment.get().version());
- applicationPackage = Optional.of(controller.applications().getApplicationPackage(applicationId,
- applicationVersion.get()));
+ // To avoid second guessing the orchestrated upgrades of system applications
+ // we don't allow to deploy these during an system upgrade (i.e when new vespa is being rolled out)
+ VersionStatus versionStatus = controller.readVersionStatus();
+ if (versionStatus.isUpgrading()) {
+ throw new IllegalArgumentException("Deployment of system applications during a system upgrade is not allowed");
}
-
- // TODO: get rid of the json object
- DeployOptions deployOptionsJsonClass = new DeployOptions(deployDirectly,
- vespaVersion,
- deployOptions.field("ignoreValidationErrors").asBool(),
- deployOptions.field("deployCurrentVersion").asBool());
-
- applicationPackage.ifPresent(aPackage -> controller.applications().verifyApplicationIdentityConfiguration(applicationId.tenant(),
- Optional.of(applicationId.instance()),
- Optional.of(zone),
- aPackage,
- Optional.of(requireUserPrincipal(request))));
-
- ActivateResult result = controller.applications().deploy(applicationId,
- zone,
- applicationPackage,
- applicationVersion,
- deployOptionsJsonClass);
-
+ Optional<VespaVersion> systemVersion = versionStatus.systemVersion();
+ if (systemVersion.isEmpty()) {
+ throw new IllegalArgumentException("Deployment of system applications is not permitted until system version is determined");
+ }
+ ActivateResult result = controller.applications()
+ .deploySystemApplicationPackage(systemApplication.get(), zone, systemVersion.get().versionNumber());
return new SlimeJsonResponse(toSlime(result));
}
@@ -1884,10 +1947,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
.orElseThrow(() -> new NotExistsException(new TenantId(tenantName)));
}
- private void toSlime(Cursor object, Tenant tenant, HttpRequest request) {
+ private void toSlime(Cursor object, Tenant tenant, List<Application> applications, HttpRequest request) {
object.setString("tenant", tenant.name().value());
object.setString("type", tenantType(tenant));
- List<com.yahoo.vespa.hosted.controller.Application> applications = controller.applications().asList(tenant.name());
switch (tenant.type()) {
case athenz:
AthenzTenant athenzTenant = (AthenzTenant) tenant;
@@ -1916,29 +1978,41 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
keyObject.setString("user", user.getName());
});
+ // TODO: remove this once console is updated
+ toSlime(object, cloudTenant.tenantSecretStores());
+
+ toSlime(object.setObject("integrations").setObject("aws"),
+ controller.serviceRegistry().roleService().getTenantRole(tenant.name()),
+ cloudTenant.tenantSecretStores());
+
var tenantQuota = controller.serviceRegistry().billingController().getQuota(tenant.name());
var usedQuota = applications.stream()
- .map(com.yahoo.vespa.hosted.controller.Application::quotaUsage)
+ .map(Application::quotaUsage)
.reduce(QuotaUsage.none, QuotaUsage::add);
toSlime(tenantQuota, usedQuota, object.setObject("quota"));
+ cloudTenant.archiveAccessRole().ifPresent(role -> object.setString("archiveAccessRole", role));
+
break;
}
default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
}
// TODO jonmv: This should list applications, not instances.
Cursor applicationArray = object.setArray("applications");
- for (com.yahoo.vespa.hosted.controller.Application application : applications) {
- DeploymentStatus status = controller.jobController().deploymentStatus(application);
+ for (Application application : applications) {
+ DeploymentStatus status = null;
for (Instance instance : showOnlyProductionInstances(request) ? application.productionInstances().values()
: application.instances().values())
- if (recurseOverApplications(request))
+ if (recurseOverApplications(request)) {
+ if (status == null) status = controller.jobController().deploymentStatus(application);
toSlime(applicationArray.addObject(), instance, status, request);
- else
+ }
+ else {
toSlime(instance.id(), applicationArray.addObject(), request);
+ }
}
- tenantMetaDataToSlime(tenant, object.setObject("metaData"));
+ tenantMetaDataToSlime(tenant, applications, object.setObject("metaData"));
}
private void toSlime(Quota quota, QuotaUsage usage, Cursor object) {
@@ -1956,8 +2030,19 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
object.setLong("nodes", resources.nodes());
object.setLong("groups", resources.groups());
toSlime(resources.nodeResources(), object.setObject("nodeResources"));
- if ( ! controller.zoneRegistry().system().isPublic())
- object.setDouble("cost", Math.round(resources.nodes() * resources.nodeResources().cost() * 100.0 / 3.0) / 100.0);
+
+ // Divide cost by 3 in non-public zones to show approx. AWS equivalent cost
+ double costDivisor = controller.zoneRegistry().system().isPublic() ? 1.0 : 3.0;
+ object.setDouble("cost", Math.round(resources.nodes() * resources.nodeResources().cost() * 100.0 / costDivisor) / 100.0);
+ }
+
+ private void utilizationToSlime(Cluster.Utilization utilization, Cursor utilizationObject) {
+ utilizationObject.setDouble("cpu", utilization.cpu());
+ utilizationObject.setDouble("idealCpu", utilization.idealCpu());
+ utilizationObject.setDouble("memory", utilization.memory());
+ utilizationObject.setDouble("idealMemory", utilization.idealMemory());
+ utilizationObject.setDouble("disk", utilization.disk());
+ utilizationObject.setDouble("idealDisk", utilization.idealDisk());
}
private void scalingEventsToSlime(List<Cluster.ScalingEvent> scalingEvents, Cursor scalingEventsArray) {
@@ -1995,18 +2080,23 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
object.setString("url", withPath("/application/v4/tenant/" + tenant.name().value(), requestURI).toString());
}
- private void tenantMetaDataToSlime(Tenant tenant, Cursor object) {
- List<com.yahoo.vespa.hosted.controller.Application> applications = controller.applications().asList(tenant.name());
+ private void tenantMetaDataToSlime(Tenant tenant, List<Application> applications, Cursor object) {
Optional<Instant> lastDev = applications.stream()
- .flatMap(application -> application.instances().values().stream())
- .flatMap(instance -> controller.jobController().jobs(instance.id()).stream()
- .filter(jobType -> jobType.environment() == Environment.dev)
- .flatMap(jobType -> controller.jobController().last(instance.id(), jobType).stream()))
- .map(Run::start)
- .max(Comparator.naturalOrder());
+ .flatMap(application -> application.instances().values().stream())
+ .flatMap(instance -> instance.deployments().values().stream())
+ .filter(deployment -> deployment.zone().environment() == Environment.dev)
+ .map(Deployment::at)
+ .max(Comparator.naturalOrder())
+ .or(() -> applications.stream()
+ .flatMap(application -> application.instances().values().stream())
+ .flatMap(instance -> JobType.allIn(controller.system()).stream()
+ .filter(job -> job.environment() == Environment.dev)
+ .flatMap(jobType -> controller.jobController().last(instance.id(), jobType).stream()))
+ .map(Run::start)
+ .max(Comparator.naturalOrder()));
Optional<Instant> lastSubmission = applications.stream()
- .flatMap(app -> app.latestVersion().flatMap(ApplicationVersion::buildTime).stream())
- .max(Comparator.naturalOrder());
+ .flatMap(app -> app.latestVersion().flatMap(ApplicationVersion::buildTime).stream())
+ .max(Comparator.naturalOrder());
object.setLong("createdAtMillis", tenant.createdAt().toEpochMilli());
lastDev.ifPresent(instant -> object.setLong("lastDeploymentToDevMillis", instant.toEpochMilli()));
lastSubmission.ifPresent(instant -> object.setLong("lastSubmissionToProdMillis", instant.toEpochMilli()));
@@ -2165,6 +2255,27 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
array.addString(string);
}
+ private void toSlime(Cursor object, List<TenantSecretStore> tenantSecretStores) {
+ Cursor secretStore = object.setArray("secretStores");
+ tenantSecretStores.forEach(store -> {
+ toSlime(secretStore.addObject(), store);
+ });
+ }
+
+ private void toSlime(Cursor object, TenantRoles tenantRoles, List<TenantSecretStore> tenantSecretStores) {
+ object.setString("tenantRole", tenantRoles.containerRole());
+ var stores = object.setArray("accounts");
+ tenantSecretStores.forEach(secretStore -> {
+ toSlime(stores.addObject(), secretStore);
+ });
+ }
+
+ private void toSlime(Cursor object, TenantSecretStore secretStore) {
+ object.setString("name", secretStore.getName());
+ object.setString("awsId", secretStore.getAwsId());
+ object.setString("role", secretStore.getRole());
+ }
+
private String readToString(InputStream stream) {
Scanner scanner = new Scanner(stream).useDelimiter("\\A");
if ( ! scanner.hasNext()) return null;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
index d10a4879bf5..26c4bf6292a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
@@ -37,7 +37,6 @@ import java.util.logging.Logger;
public class AthenzApiHandler extends LoggingRequestHandler {
private final static Logger log = Logger.getLogger(AthenzApiHandler.class.getName());
- private static final String OPTIONAL_PREFIX = "/api";
private final AthenzFacade athenz;
private final AthenzDomain sandboxDomain;
@@ -70,7 +69,7 @@ public class AthenzApiHandler extends LoggingRequestHandler {
}
private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
if (path.matches("/athenz/v1")) return root(request);
if (path.matches("/athenz/v1/domains")) return domainList(request);
if (path.matches("/athenz/v1/properties")) return properties();
@@ -80,7 +79,7 @@ public class AthenzApiHandler extends LoggingRequestHandler {
}
private HttpResponse post(HttpRequest request) {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
if (path.matches("/athenz/v1/user")) return signup(request);
return ErrorResponse.notFoundError(String.format("No '%s' handler at '%s'", request.getMethod(),
request.getUri().getPath()));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
index 88191bc836b..c56c2e93f65 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
@@ -5,7 +5,6 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
-import com.yahoo.container.logging.AccessLog;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.JacksonJsonResponse;
import com.yahoo.restapi.MessageResponse;
@@ -55,7 +54,6 @@ import java.util.stream.Collectors;
*/
public class BillingApiHandler extends LoggingRequestHandler {
- private static final String OPTIONAL_PREFIX = "/api";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final BillingController billingController;
@@ -73,7 +71,7 @@ public class BillingApiHandler extends LoggingRequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
try {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
String userId = userIdOrThrow(request);
switch (request.getMethod()) {
case GET:
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
new file mode 100644
index 00000000000..cd9c467582c
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
@@ -0,0 +1,198 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.changemanagement;
+
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.HostName;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.Path;
+import com.yahoo.restapi.SlimeJsonResponse;
+import com.yahoo.slime.ArrayTraverser;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
+import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
+import com.yahoo.vespa.hosted.controller.maintenance.ChangeManagementAssessor;
+import com.yahoo.yolean.Exceptions;
+
+import javax.ws.rs.BadRequestException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
+
+ private final ChangeManagementAssessor assessor;
+ private final Controller controller;
+
+ public ChangeManagementApiHandler(LoggingRequestHandler.Context ctx, Controller controller) {
+ super(ctx, controller.auditLogger());
+ this.assessor = new ChangeManagementAssessor(controller.serviceRegistry().configServer().nodeRepository());
+ this.controller = controller;
+ }
+
+ @Override
+ public HttpResponse auditAndHandle(HttpRequest request) {
+ try {
+ switch (request.getMethod()) {
+ case GET:
+ return get(request);
+ case POST:
+ return post(request);
+ default:
+ return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
+ }
+ } catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse get(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/changemanagement/v1/assessment/{changeRequestId}")) return changeRequestAssessment(path.get("changeRequestId"));
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private HttpResponse post(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/changemanagement/v1/assessment")) return new SlimeJsonResponse(doAssessment(request));
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+ private Inspector inspectorOrThrow(HttpRequest request) {
+ try {
+ return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get();
+ } catch (IOException e) {
+ throw new BadRequestException("Failed to parse request body");
+ }
+ }
+
+ private static Inspector getInspectorFieldOrThrow(Inspector inspector, String field) {
+ if (!inspector.field(field).valid())
+ throw new BadRequestException("Field " + field + " cannot be null");
+ return inspector.field(field);
+ }
+
+ private HttpResponse changeRequestAssessment(String changeRequestId) {
+ var optionalChangeRequest = controller.serviceRegistry().changeRequestClient()
+ .getUpcomingChangeRequests()
+ .stream()
+ .filter(request -> changeRequestId.equals(request.getChangeRequestSource().getId()))
+ .findFirst();
+
+ if (optionalChangeRequest.isEmpty())
+ return ErrorResponse.notFoundError("Could not find any upcoming change requests with id " + changeRequestId);
+
+ var changeRequest = optionalChangeRequest.get();
+ var zone = affectedZone(changeRequest);
+
+ if (zone.isEmpty())
+ return ErrorResponse.notFoundError("Could not find prod zone affected by change request " + changeRequestId);
+
+ var assessment = doAssessment(changeRequest.getImpactedHosts(), zone.get());
+ return new SlimeJsonResponse(assessment);
+ }
+
+ // The structure here should be
+ //
+ // {
+ // zone: string
+ // hosts: string[]
+ // switches: string[]
+ // switchInSequence: boolean
+ // }
+ //
+ // Only zone and host are supported right now
+ private Slime doAssessment(HttpRequest request) {
+
+ Inspector inspector = inspectorOrThrow(request);
+
+ // For now; mandatory fields
+ String zoneStr = getInspectorFieldOrThrow(inspector, "zone").asString();
+ Inspector hostArray = getInspectorFieldOrThrow(inspector, "hosts");
+
+ // The impacted hostnames
+ List<String> hostNames = new ArrayList<>();
+ if (hostArray.valid()) {
+ hostArray.traverse((ArrayTraverser) (i, host) -> hostNames.add(host.asString()));
+ }
+
+ return doAssessment(hostNames, ZoneId.from(zoneStr));
+ }
+
+ private Slime doAssessment(List<String> hostNames, ZoneId zoneId) {
+ ChangeManagementAssessor.Assessment assessments = assessor.assessment(hostNames, zoneId);
+
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+
+ // This is the main structure that might be part of something bigger later
+ Cursor assessmentCursor = root.setObject("assessment");
+
+ // Updated gives clue to if the assessment is old
+ assessmentCursor.setString("updated", "2021-03-12:12:12:12Z");
+
+ // Assessment on the cluster level
+ Cursor clustersCursor = assessmentCursor.setArray("clusters");
+
+ assessments.getClusterAssessments().forEach(assessment -> {
+ Cursor oneCluster = clustersCursor.addObject();
+ oneCluster.setString("app", assessment.app);
+ oneCluster.setString("zone", assessment.zone);
+ oneCluster.setString("cluster", assessment.cluster);
+ oneCluster.setLong("clusterSize", assessment.clusterSize);
+ oneCluster.setLong("clusterImpact", assessment.clusterImpact);
+ oneCluster.setLong("groupsTotal", assessment.groupsTotal);
+ oneCluster.setLong("groupsImpact", assessment.groupsImpact);
+ oneCluster.setString("upgradePolicy", assessment.upgradePolicy);
+ oneCluster.setString("suggestedAction", assessment.suggestedAction);
+ oneCluster.setString("impact", assessment.impact);
+ });
+
+ Cursor hostsCursor = assessmentCursor.setArray("hosts");
+ assessments.getHostAssessments().forEach(assessment -> {
+ Cursor hostObject = hostsCursor.addObject();
+ hostObject.setString("hostname", assessment.hostName);
+ hostObject.setString("switchName", assessment.switchName);
+ hostObject.setLong("numberOfChildren", assessment.numberOfChildren);
+ hostObject.setLong("numberOfProblematicChildren", assessment.numberOfProblematicChildren);
+ });
+
+ return slime;
+ }
+
+ private Optional<ZoneId> affectedZone(ChangeRequest changeRequest) {
+ var affectedHosts = changeRequest.getImpactedHosts()
+ .stream()
+ .map(HostName::from)
+ .collect(Collectors.toList());
+
+ var potentialZones = controller.zoneRegistry()
+ .zones()
+ .reachable()
+ .in(Environment.prod)
+ .ids();
+
+ for (var zone : potentialZones) {
+ var affectedHostsInZone = controller.serviceRegistry().configServer().nodeRepository().list(zone, affectedHosts);
+ if (!affectedHostsInZone.isEmpty())
+ return Optional.of(zone);
+ }
+
+ return Optional.empty();
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
index f65f7534476..1bb3b1c5de8 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
@@ -33,7 +33,6 @@ public class ConfigServerApiHandler extends AuditLoggingRequestHandler {
private static final ZoneId CONTROLLER_ZONE = ZoneId.from("prod", "controller");
private static final URI CONTROLLER_URI = URI.create("https://localhost:4443/");
- private static final String OPTIONAL_PREFIX = "/api";
private static final List<String> WHITELISTED_APIS = List.of("/flags/v1/", "/nodes/v2/", "/orchestrator/v1/");
private final ZoneRegistry zoneRegistry;
@@ -70,7 +69,7 @@ public class ConfigServerApiHandler extends AuditLoggingRequestHandler {
}
private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
if (path.matches("/configserver/v1")) {
return root(request);
}
@@ -78,7 +77,7 @@ public class ConfigServerApiHandler extends AuditLoggingRequestHandler {
}
private HttpResponse proxy(HttpRequest request) {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
if ( ! path.matches("/configserver/v1/{environment}/{region}/{*}")) {
return ErrorResponse.notFoundError("Nothing at " + path);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
index da13a84a3d4..552e22e9a2c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
@@ -49,8 +49,6 @@ import static java.util.stream.Collectors.toUnmodifiableMap;
@SuppressWarnings("unused") // Injected
public class DeploymentApiHandler extends LoggingRequestHandler {
- private static final String OPTIONAL_PREFIX = "/api";
-
private final Controller controller;
public DeploymentApiHandler(LoggingRequestHandler.Context parentCtx, Controller controller) {
@@ -77,7 +75,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
}
private HttpResponse handleGET(HttpRequest request) {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
if (path.matches("/deployment/v1/")) return root(request);
return ErrorResponse.notFoundError("Nothing at " + path);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
index f26477beb3b..828e7e63483 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
@@ -42,7 +42,6 @@ import java.util.stream.Collectors;
*/
public class RoutingApiHandler extends AuditLoggingRequestHandler {
- private static final String OPTIONAL_PREFIX = "/api";
private final Controller controller;
public RoutingApiHandler(Context ctx, Controller controller) {
@@ -53,7 +52,7 @@ public class RoutingApiHandler extends AuditLoggingRequestHandler {
@Override
public HttpResponse auditAndHandle(HttpRequest request) {
try {
- var path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ var path = new Path(request.getUri());
switch (request.getMethod()) {
case GET: return get(path, request);
case POST: return post(path);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java
index 161d3734aae..1709e3cb65f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java
@@ -1,7 +1,7 @@
// 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.restapi.systemflags;
-import ai.vespa.util.http.retry.DelayedConnectionLevelRetryHandler;
+import ai.vespa.util.http.hc4.retry.DelayedConnectionLevelRetryHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java
index 57d47757c5e..d169cd97df7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java
@@ -2,7 +2,9 @@
package com.yahoo.vespa.hosted.controller.restapi.systemflags;
import com.fasterxml.jackson.databind.JsonNode;
+import com.yahoo.vespa.flags.FlagDefinition;
import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireSystemFlagsDeployResult;
@@ -100,6 +102,7 @@ class SystemFlagsDeployResult {
for (FlagDataChange change : flagChanges) {
var wireChange = new WireFlagDataChange();
wireChange.flagId = change.flagId().toString();
+ wireChange.owners = owners(change.flagId());
wireChange.operation = change.operation().asString();
wireChange.targets = change.targets().stream().map(FlagsTarget::asString).collect(toList());
wireChange.data = change.data().map(FlagData::toWire).orElse(null);
@@ -113,6 +116,7 @@ class SystemFlagsDeployResult {
wireError.operation = error.operation().asString();
wireError.targets = error.targets().stream().map(FlagsTarget::asString).collect(toList());
wireError.flagId = error.flagId().map(FlagId::toString).orElse(null);
+ wireError.owners = error.flagId().map(id -> owners(id)).orElse(List.of());
wireError.data = error.flagData().map(FlagData::toWire).orElse(null);
wireResult.errors.add(wireError);
}
@@ -121,12 +125,17 @@ class SystemFlagsDeployResult {
var wireWarning = new WireWarning();
wireWarning.message = warning.message();
wireWarning.flagId = warning.flagId().toString();
+ wireWarning.owners = owners(warning.flagId());
wireWarning.targets = warning.targets().stream().map(FlagsTarget::asString).collect(toList());
wireResult.warnings.add(wireWarning);
}
return wireResult;
}
+ private static List<String> owners(FlagId id) {
+ return Flags.getFlag(id).map(FlagDefinition::getOwners).orElse(List.of());
+ }
+
static class FlagDataChange {
private final FlagId flagId;
@@ -296,7 +305,9 @@ class SystemFlagsDeployResult {
}
static Warning dataForUndefinedFlag(FlagsTarget target, FlagId flagId) {
- return new Warning("Flag data present for undefined flag", Set.of(target), flagId);
+ return new Warning(
+ "Flag data present for undefined flag. Remove flag data files if flag's definition is already removed from Flags/PermanentFlags. " +
+ "Consult ModelContext.FeatureFlags for safe removal of flag used by config-model.", Set.of(target), flagId);
}
String message() { return message; }
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java
index 5ea277f5101..0331bf292a0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.restapi.systemflags;
import com.yahoo.concurrent.DaemonThreadFactory;
import com.yahoo.config.provision.SystemName;
-import java.util.logging.Level;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
import com.yahoo.vespa.flags.FlagId;
import com.yahoo.vespa.flags.Flags;
@@ -25,6 +24,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;
+import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -181,9 +181,11 @@ class SystemFlagsDeployer {
Map<FlagId, FlagData> currentFlagData,
List<FlagId> definedFlags,
List<OperationError> errors) {
+ String errorMessage = "Flag not defined in target zone. If zone/configserver cluster is new, add an empty flag " +
+ "data file for this zone as a temporary measure until the stale flag data files are removed.";
for (FlagId flagId : wantedFlagData.keySet()) {
if (!currentFlagData.containsKey(flagId) && !definedFlags.contains(flagId)) {
- errors.add(OperationError.createFailed("Flag not defined in target zone", target, wantedFlagData.get(flagId)));
+ errors.add(OperationError.createFailed(errorMessage, target, wantedFlagData.get(flagId)));
}
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java
index 129f1e109df..7c39787e295 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java
@@ -33,7 +33,7 @@ public class SystemFlagsHandler extends LoggingRequestHandler {
ServiceIdentityProvider identityProvider,
Executor executor) {
super(executor);
- this.deployer = new SystemFlagsDeployer(identityProvider, zoneRegistry.system(), FlagsTarget.getAllTargetsInSystem(zoneRegistry));
+ this.deployer = new SystemFlagsDeployer(identityProvider, zoneRegistry.system(), FlagsTarget.getAllTargetsInSystem(zoneRegistry, true));
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index e097b82b7d0..039e9b64df7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -64,7 +64,6 @@ import java.util.stream.Collectors;
public class UserApiHandler extends LoggingRequestHandler {
private final static Logger log = Logger.getLogger(UserApiHandler.class.getName());
- private static final String optionalPrefix = "/api";
private final UserManagement users;
private final Controller controller;
@@ -83,7 +82,7 @@ public class UserApiHandler extends LoggingRequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
try {
- Path path = new Path(request.getUri(), optionalPrefix);
+ Path path = new Path(request.getUri());
switch (request.getMethod()) {
case GET: return handleGET(path, request);
case POST: return handlePOST(path, request);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
index abbbbef82c7..fd6dfa61180 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
@@ -29,8 +29,6 @@ import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class ZoneApiHandler extends LoggingRequestHandler {
- private static final String OPTIONAL_PREFIX = "/api";
-
private final ZoneRegistry zoneRegistry;
public ZoneApiHandler(LoggingRequestHandler.Context parentCtx, ServiceRegistry serviceRegistry) {
@@ -57,7 +55,7 @@ public class ZoneApiHandler extends LoggingRequestHandler {
}
private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri(), OPTIONAL_PREFIX);
+ Path path = new Path(request.getUri());
if (path.matches("/zone/v1")) {
return root(request);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
index 5d0bb780c81..4f9702669dd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -4,12 +4,15 @@ package com.yahoo.vespa.hosted.controller.tenant;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import java.security.Principal;
import java.security.PublicKey;
import java.time.Instant;
+import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.regex.Pattern;
/**
* A paying tenant in a Vespa cloud service.
@@ -18,17 +21,27 @@ import java.util.Optional;
*/
public class CloudTenant extends Tenant {
+ private static final Pattern VALID_ARCHIVE_ACCESS_ROLE_PATTERN = Pattern.compile("arn:aws:iam::\\d{12}:.+");
+
private final Optional<Principal> creator;
private final BiMap<PublicKey, Principal> developerKeys;
private final TenantInfo info;
+ private final List<TenantSecretStore> tenantSecretStores;
+ private final Optional<String> archiveAccessRole;
/** Public for the serialization layer — do not use! */
public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator,
- BiMap<PublicKey, Principal> developerKeys, TenantInfo info) {
+ BiMap<PublicKey, Principal> developerKeys, TenantInfo info,
+ List<TenantSecretStore> tenantSecretStores, Optional<String> archiveAccessRole) {
super(name, createdAt, lastLoginInfo, Optional.empty());
this.creator = creator;
this.developerKeys = developerKeys;
this.info = Objects.requireNonNull(info);
+ this.tenantSecretStores = tenantSecretStores;
+ this.archiveAccessRole = archiveAccessRole;
+ if (!archiveAccessRole.map(role -> VALID_ARCHIVE_ACCESS_ROLE_PATTERN.matcher(role).matches()).orElse(true))
+ throw new IllegalArgumentException(String.format("Invalid archive access role '%s': Must match expected pattern: '%s'",
+ archiveAccessRole.get(), VALID_ARCHIVE_ACCESS_ROLE_PATTERN.pattern()));
}
/** Creates a tenant with the given name, provided it passes validation. */
@@ -37,7 +50,7 @@ public class CloudTenant extends Tenant {
createdAt,
LastLoginInfo.EMPTY,
Optional.ofNullable(creator),
- ImmutableBiMap.of(), TenantInfo.EMPTY);
+ ImmutableBiMap.of(), TenantInfo.EMPTY, List.of(), Optional.empty());
}
/** The user that created the tenant */
@@ -50,9 +63,19 @@ public class CloudTenant extends Tenant {
return info;
}
+ /** An iam role which is allowed to access the S3 (log, dump) archive) */
+ public Optional<String> archiveAccessRole() {
+ return archiveAccessRole;
+ }
+
/** Returns the set of developer keys and their corresponding developers for this tenant. */
public BiMap<PublicKey, Principal> developerKeys() { return developerKeys; }
+ /** List of configured secret stores */
+ public List<TenantSecretStore> tenantSecretStores() {
+ return tenantSecretStores;
+ }
+
@Override
public Type type() {
return Type.cloud;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
index 81c08e1083b..a20477d7aab 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
@@ -1,6 +1,10 @@
// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.tenant;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
+
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
/**
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 c505dbfe1c6..226852f1f3d 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
@@ -63,7 +63,7 @@ public class OsVersionStatus {
for (var application : SystemApplication.all()) {
for (var zone : zonesToUpgrade(controller)) {
- if (!application.shouldUpgradeOsIn(zone.getId(), controller)) continue;
+ if (!application.shouldUpgradeOs()) continue;
var targetOsVersion = controller.serviceRegistry().configServer().nodeRepository()
.targetVersionsOf(zone.getId())
.osVersion(application.nodeType())
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 63452f40dbb..362c980a906 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
@@ -13,12 +13,10 @@ import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.path.Path;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
@@ -32,7 +30,6 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
@@ -580,41 +577,6 @@ public class ControllerTest {
}
@Test
- public void testIntegrationTestDeployment() {
- Version six = Version.fromString("6.1");
- tester.controllerTester().zoneRegistry().setSystemName(SystemName.cd);
- tester.controllerTester().zoneRegistry().setZones(ZoneApiMock.fromId("prod.cd-us-central-1"));
- tester.configServer().bootstrap(List.of(ZoneId.from("prod.cd-us-central-1")), SystemApplication.all());
- tester.controllerTester().upgradeSystem(six);
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .majorVersion(6)
- .region("cd-us-central-1")
- .build();
-
- // Create application
- var context = tester.newDeploymentContext();
-
- // Direct deploy is allowed when deployDirectly is true
- ZoneId zone = ZoneId.from("prod", "cd-us-central-1");
- // Same options as used in our integration tests
- DeployOptions options = new DeployOptions(true, Optional.empty(), false,
- false);
- tester.controller().applications().deploy(context.instanceId(), zone, Optional.of(applicationPackage), options);
-
- assertTrue("Application deployed and activated",
- tester.configServer().application(context.instanceId(), zone).get().activated());
-
- assertTrue("No job status added",
- context.instanceJobs().isEmpty());
-
- Version seven = Version.fromString("7.2");
- tester.controllerTester().upgradeSystem(seven);
- tester.upgrader().maintain();
- tester.controller().applications().deploy(context.instanceId(), zone, Optional.of(applicationPackage), options);
- assertEquals(six, context.instance().deployments().get(zone).version());
- }
-
- @Test
public void testDevDeployment() {
ApplicationPackage applicationPackage = new ApplicationPackageBuilder().build();
@@ -625,7 +587,7 @@ public class ControllerTest {
.setRoutingMethod(ZoneApiMock.from(zone), RoutingMethod.shared, RoutingMethod.sharedLayer4);
// Deploy
- tester.controller().applications().deploy(context.instanceId(), zone, Optional.of(applicationPackage), DeployOptions.none());
+ context.runJob(zone, applicationPackage);
assertTrue("Application deployed and activated",
tester.configServer().application(context.instanceId(), zone).get().activated());
assertTrue("No job status added",
@@ -682,10 +644,10 @@ public class ControllerTest {
.region("us-west-1")
.build();
- ZoneId zone = ZoneId.from("prod", "us-west-1");
- tester.controller().applications().deploy(context.instanceId(), zone, Optional.of(applicationPackage), DeployOptions.none());
- tester.controller().applications().deactivate(context.instanceId(), ZoneId.from(Environment.prod, RegionName.from("us-west-1")));
- tester.controller().applications().deactivate(context.instanceId(), ZoneId.from(Environment.prod, RegionName.from("us-west-1")));
+ ZoneId zone = ZoneId.from(Environment.prod, RegionName.from("us-west-1"));
+ context.runJob(zone, applicationPackage);
+ tester.controller().applications().deactivate(context.instanceId(), zone);
+ tester.controller().applications().deactivate(context.instanceId(), zone);
}
@Test
@@ -748,7 +710,7 @@ public class ControllerTest {
var devZone = ZoneId.from("dev", "us-east-1");
// Deploy app2 in a zone with shared routing
- tester.controller().applications().deploy(context2.instanceId(), devZone, Optional.of(applicationPackage), DeployOptions.none());
+ context2.runJob(devZone, applicationPackage);
assertTrue("Application deployed and activated",
tester.configServer().application(context2.instanceId(), devZone).get().activated());
assertTrue("Provisions certificate also in zone with routing layer", certificate.apply(context2.instance()).isPresent());
@@ -930,7 +892,7 @@ public class ControllerTest {
"[endpoint 'default' (cluster foo) -> us-east-3, us-west-1] and add " +
"[endpoint 'default' (cluster bar) -> us-east-3, us-west-1]. To allow this add " +
"<allow until='yyyy-mm-dd'>global-endpoint-change</allow> to validation-overrides.xml, see " +
- "https://docs.vespa.ai/documentation/reference/validation-overrides.html", e.getMessage());
+ "https://docs.vespa.ai/en/reference/validation-overrides.html", e.getMessage());
}
// Redeploy with override succeeds
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index c0244b9ea17..f8645139244 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -16,7 +16,6 @@ import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
import com.yahoo.vespa.athenz.api.OktaIdentityToken;
import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
@@ -27,7 +26,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
@@ -326,6 +324,10 @@ public final class ControllerTester {
}
}
+ public Application createApplication(ApplicationId id) {
+ return createApplication(id.tenant().value(), id.application().value(), id.instance().value());
+ }
+
public Application createApplication(String tenant, String applicationName, String instanceName) {
Application application = createApplication(tenant, applicationName);
controller().applications().createInstance(application.id().instance(instanceName));
@@ -343,21 +345,6 @@ public final class ControllerTester {
return application;
}
- public void deploy(ApplicationId id, ZoneId zone, ApplicationPackage applicationPackage, boolean deployCurrentVersion) {
- deploy(id, zone, Optional.of(applicationPackage), deployCurrentVersion);
- }
-
- public void deploy(ApplicationId id, ZoneId zone, Optional<ApplicationPackage> applicationPackage, boolean deployCurrentVersion) {
- deploy(id, zone, applicationPackage, deployCurrentVersion, Optional.empty());
- }
-
- public void deploy(ApplicationId id, ZoneId zone, Optional<ApplicationPackage> applicationPackage, boolean deployCurrentVersion, Optional<Version> version) {
- controller().applications().deploy(id,
- zone,
- applicationPackage,
- new DeployOptions(false, version, false, deployCurrentVersion));
- }
-
private static Controller createController(CuratorDb curator, RotationsConfig rotationsConfig,
AthenzDbMock athensDb,
ServiceRegistryMock serviceRegistry) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageTest.java
index f626a597d4a..1849be9b6bd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageTest.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. 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.yahoo.config.application.api.DeploymentSpec;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
index 0a262a41138..91d71a1aa25 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
@@ -1,16 +1,30 @@
+// Copyright Verizon Media. 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.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.ApplicationData;
+import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import org.junit.Test;
+
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.Optional;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -38,7 +52,7 @@ public class DeploymentQuotaCalculatorTest {
" </prod>\n" +
" </instance>\n" +
"</deployment>"));
- assertEquals(10d/3, calculated.budget().get().doubleValue(), 1e-5);
+ assertEquals(10d / 3, calculated.budget().orElseThrow().doubleValue(), 1e-5);
}
@Test
@@ -50,7 +64,7 @@ public class DeploymentQuotaCalculatorTest {
@Test
public void zero_quota_remains_zero() {
Quota calculated = DeploymentQuotaCalculator.calculate(Quota.zero(), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.empty);
- assertEquals(calculated.budget().get().doubleValue(), 0, 1e-5);
+ assertEquals(calculated.budget().orElseThrow().doubleValue(), 0, 1e-5);
}
@Test
@@ -59,7 +73,7 @@ public class DeploymentQuotaCalculatorTest {
var mapper = new ObjectMapper();
var application = mapper.readValue(content, ApplicationData.class).toApplication();
var usage = DeploymentQuotaCalculator.calculateQuotaUsage(application);
- assertEquals(1.164, usage.rate(), 0.001);
+ assertEquals(1.068, usage.rate(), 0.001);
}
@Test
@@ -68,4 +82,5 @@ public class DeploymentQuotaCalculatorTest {
var calculated = DeploymentQuotaCalculator.calculate(tenantQuota, List.of(), ApplicationId.defaultId(), ZoneId.from("test", "apac1"), DeploymentSpec.empty);
assertEquals(tenantQuota, calculated);
}
+
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json
index ccfb6af1635..928fabd621e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json
@@ -3,6 +3,7 @@
"id": "vespa.album-recommendation.default",
"clusters": {
"default": {
+ "type": "container",
"min": {
"nodes": 2,
"groups": 1,
@@ -50,9 +51,13 @@
"diskSpeed": "fast",
"storageType": "local"
}
- }
+ },
+ "scalingDuration": 400000,
+ "maxQueryGrowthRate": 0.7,
+ "currentQueryFractionOfMax": 0.3
},
"logserver": {
+ "type": "admin",
"min": {
"nodes": 1,
"groups": 1,
@@ -100,9 +105,13 @@
"diskSpeed": "fast",
"storageType": "local"
}
- }
+ },
+ "scalingDuration": 90000,
+ "maxQueryGrowthRate": 0.7,
+ "currentQueryFractionOfMax": 0.3
},
"music": {
+ "type": "content",
"min": {
"nodes": 2,
"groups": 1,
@@ -150,7 +159,10 @@
"diskSpeed": "fast",
"storageType": "local"
}
- }
+ },
+ "scalingDuration": 1000000,
+ "maxQueryGrowthRate": 0.7,
+ "currentQueryFractionOfMax": 0.3
}
}
} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java
index 0d70fe48a77..e30f044c579 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificateManagerTest.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.certificate;
import com.yahoo.config.application.api.DeploymentSpec;
@@ -16,6 +17,7 @@ import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorImpl;
import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
@@ -47,8 +49,9 @@ public class EndpointCertificateManagerTest {
private final EndpointCertificateMock endpointCertificateMock = new EndpointCertificateMock();
private final InMemoryFlagSource inMemoryFlagSource = new InMemoryFlagSource();
private static final Clock clock = Clock.fixed(Instant.EPOCH, java.time.ZoneId.systemDefault());
+ private final EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
private final EndpointCertificateManager endpointCertificateManager =
- new EndpointCertificateManager(zoneRegistryMock, mockCuratorDb, secretStore, endpointCertificateMock, clock, inMemoryFlagSource);
+ new EndpointCertificateManager(zoneRegistryMock, mockCuratorDb, endpointCertificateMock, endpointCertificateValidator, clock);
private static final List<String> expectedSans = List.of(
"vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
@@ -103,7 +106,6 @@ public class EndpointCertificateManagerTest {
public void setUp() {
zoneRegistryMock.exclusiveRoutingIn(zoneRegistryMock.zones().all().zones());
testZone = zoneRegistryMock.zones().directlyRouted().in(Environment.prod).zones().stream().findFirst().orElseThrow().getId();
- inMemoryFlagSource.withBooleanFlag(Flags.VALIDATE_ENDPOINT_CERTIFICATES.id(), true);
}
@Test
@@ -160,7 +162,7 @@ public class EndpointCertificateManagerTest {
public void reprovisions_certificate_with_added_sans_when_deploying_to_new_zone() {
ZoneId testZone = zoneRegistryMock.zones().directlyRouted().in(Environment.prod).zones().stream().skip(1).findFirst().orElseThrow().getId();
- mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, 0, "uuid", expectedSans, "mockCa", Optional.empty(), Optional.empty()));
+ mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, 0, "original-request-uuid", expectedSans, "mockCa", Optional.empty(), Optional.empty()));
secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), -1);
secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), -1);
@@ -171,6 +173,7 @@ public class EndpointCertificateManagerTest {
assertTrue(endpointCertificateMetadata.isPresent());
assertEquals(0, endpointCertificateMetadata.get().version());
assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id()));
+ assertEquals("original-request-uuid", endpointCertificateMetadata.get().request_id());
assertEquals(Set.copyOf(expectedCombinedSans), Set.copyOf(endpointCertificateMetadata.get().requestedDnsSans()));
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
index 289d48d3241..4a4159180b5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
@@ -328,11 +328,16 @@ public class DeploymentContext {
return runJob(type);
}
- /** Runs a deployment of the given package to the given dev/perf job. */
+ /** Runs a deployment of the given package to the given manually deployable job. */
public DeploymentContext runJob(JobType type, ApplicationPackage applicationPackage) {
return runJob(type, applicationPackage, null);
}
+ /** Runs a deployment of the given package to the given manually deployable zone. */
+ public DeploymentContext runJob(ZoneId zone, ApplicationPackage applicationPackage) {
+ return runJob(JobType.from(tester.controller().system(), zone).get(), applicationPackage, null);
+ }
+
/** Pulls the ready job trigger, and then runs the whole of the given job, successfully. */
public DeploymentContext runJob(JobType type) {
var job = jobId(type);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
index 724ba61da2b..81c9f51278e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
@@ -6,7 +6,6 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
@@ -628,13 +627,8 @@ public class DeploymentTriggerTest {
assertEquals("First deployment gets system version", version1, app1.application().oldestDeployedPlatform().get());
assertEquals(version1, tester.configServer().lastPrepareVersion().get());
- // Unexpected deployment is ignored
- Version version2 = new Version(version1.getMajor(), version1.getMinor() + 1);
- tester.applications().deploy(app1.instanceId(), ZoneId.from("prod", "us-west-1"),
- Optional.empty(), new DeployOptions(false, Optional.of(version2), false, false));
- assertEquals(version1, app1.deployment(ZoneId.from("prod", "us-west-1")).version());
-
// Application change after a new system version, and a region added
+ Version version2 = new Version(version1.getMajor(), version1.getMinor() + 1);
tester.controllerTester().upgradeSystem(version2);
applicationPackage = new ApplicationPackageBuilder()
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 ee3c523a497..a7a99f286df 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
@@ -12,6 +12,7 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
@@ -38,6 +39,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceCon
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.serviceview.bindings.ApplicationView;
@@ -48,6 +50,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
@@ -110,15 +113,20 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
public void provision(ZoneId zone, ApplicationId application, ClusterSpec.Id clusterId) {
var current = new ClusterResources(2, 1, new NodeResources(2, 8, 50, 1, slow, remote));
Cluster cluster = new Cluster(clusterId,
+ ClusterSpec.Type.container,
new ClusterResources(2, 1, new NodeResources(1, 4, 20, 1, slow, remote)),
new ClusterResources(2, 1, new NodeResources(4, 16, 90, 1, slow, remote)),
current,
Optional.of(new ClusterResources(2, 1, new NodeResources(3, 8, 50, 1, slow, remote))),
Optional.empty(),
+ new Cluster.Utilization(0.1, 0.2, 0.3, 0.4, 0.5, 0.6),
List.of(new Cluster.ScalingEvent(new ClusterResources(0, 0, NodeResources.unspecified()),
current,
Instant.ofEpochMilli(1234))),
- "the autoscaling status");
+ "the autoscaling status",
+ Duration.ofMinutes(6),
+ 0.7,
+ 0.3);
nodeRepository.putApplication(zone,
new com.yahoo.vespa.hosted.controller.api.integration.configserver.Application(application,
List.of(cluster)));
@@ -572,6 +580,11 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
return q;
}
+ @Override
+ public String validateSecretStore(DeploymentId deployment, TenantSecretStore tenantSecretStore, String region, String parameterName) {
+ return "{\"settings\":{\"name\":\"foo\",\"role\":\"vespa-secretstore-access\",\"awsId\":\"892075328880\",\"externalId\":\"*****\",\"region\":\"us-east-1\"},\"status\":\"ok\"}";
+ }
+
public static class Application {
private final ApplicationId id;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java
index ca478905893..85e82bb3fcd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java
@@ -2,11 +2,13 @@
package com.yahoo.vespa.hosted.controller.integration;
import com.fasterxml.jackson.databind.JsonNode;
+import com.yahoo.collections.Pair;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
@@ -17,7 +19,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeList
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeState;
+import java.net.URI;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -26,6 +30,7 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
@@ -40,6 +45,12 @@ public class NodeRepositoryMock implements NodeRepository {
private final Map<ZoneId, Map<ApplicationId, Application>> applications = new HashMap<>();
private final Map<ZoneId, TargetVersions> targetVersions = new HashMap<>();
private final Map<Integer, Duration> osUpgradeBudgets = new HashMap<>();
+ private final Map<DeploymentId, Pair<Double, Double>> trafficFractions = new HashMap<>();
+ private final Map<ZoneId, Map<TenantName, URI>> archiveUris = new HashMap<>();
+
+ // A separate/alternative list of NodeRepositoryNode nodes.
+ // Methods operating with Node and NodeRepositoryNode lives separate lives.
+ private final Map<ZoneId, List<NodeRepositoryNode>> nodeRepoNodes = new HashMap<>();
private boolean allowPatching = false;
@@ -55,6 +66,10 @@ public class NodeRepositoryMock implements NodeRepository {
applications.get(zone).put(application.id(), application);
}
+ public Pair<Double, Double> getTrafficFraction(ApplicationId application, ZoneId zone) {
+ return trafficFractions.get(new DeploymentId(application, zone));
+ }
+
/** Add or update given node in zone */
public void putNodes(ZoneId zone, Node node) {
putNodes(zone, Collections.singletonList(node));
@@ -68,6 +83,7 @@ public class NodeRepositoryMock implements NodeRepository {
/** Remove all nodes in all zones */
public void clear() {
nodeRepository.clear();
+ nodeRepoNodes.clear();
}
/** Replace nodes in zone with given nodes */
@@ -122,7 +138,7 @@ public class NodeRepositoryMock implements NodeRepository {
@Override
public void addNodes(ZoneId zone, Collection<NodeRepositoryNode> nodes) {
- throw new UnsupportedOperationException();
+ nodeRepoNodes.put(zone, new ArrayList<>(nodes));
}
@Override
@@ -142,7 +158,7 @@ public class NodeRepositoryMock implements NodeRepository {
@Override
public NodeList listNodes(ZoneId zone) {
- throw new UnsupportedOperationException();
+ return new NodeList(nodeRepoNodes.get(zone));
}
@Override
@@ -180,6 +196,27 @@ public class NodeRepositoryMock implements NodeRepository {
}
@Override
+ public void patchApplication(ZoneId zone, ApplicationId application,
+ double currentReadShare, double maxReadShare) {
+ trafficFractions.put(new DeploymentId(application, zone), new Pair<>(currentReadShare, maxReadShare));
+ }
+
+ @Override
+ public Map<TenantName, URI> getArchiveUris(ZoneId zone) {
+ return Map.copyOf(archiveUris.getOrDefault(zone, Map.of()));
+ }
+
+ @Override
+ public void setArchiveUri(ZoneId zone, TenantName tenantName, URI archiveUri) {
+ archiveUris.computeIfAbsent(zone, z -> new HashMap<>()).put(tenantName, archiveUri);
+ }
+
+ @Override
+ public void removeArchiveUri(ZoneId zone, TenantName tenantName) {
+ Optional.ofNullable(archiveUris.get(zone)).ifPresent(map -> map.remove(tenantName));
+ }
+
+ @Override
public void upgrade(ZoneId zone, NodeType type, Version version) {
this.targetVersions.compute(zone, (ignored, targetVersions) -> {
if (targetVersions == null) {
@@ -247,6 +284,11 @@ public class NodeRepositoryMock implements NodeRepository {
throw new UnsupportedOperationException();
}
+ @Override
+ public boolean isReplaceable(ZoneId zoneId, List<HostName> hostNames) {
+ return false;
+ }
+
public Optional<Duration> osUpgradeBudget(ZoneId zone, NodeType type, Version version) {
return Optional.ofNullable(osUpgradeBudgets.get(Objects.hash(zone, type, version)));
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
index ae1e2c38e6a..326928b9463 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
@@ -7,14 +7,18 @@ import com.yahoo.component.AbstractComponent;
import com.yahoo.config.provision.SystemName;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.ApplicationRoleService;
+import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService;
+import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService;
+import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService;
import com.yahoo.vespa.hosted.controller.api.integration.aws.MockAwsEventFetcher;
import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.NoopApplicationRoleService;
+import com.yahoo.vespa.hosted.controller.api.integration.aws.NoopRoleService;
import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
+import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorMock;
import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService;
import com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService;
import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever;
@@ -22,6 +26,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueH
import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock;
import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopTenantSecretService;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummySystemMonitor;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues;
@@ -29,6 +34,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMeteringClient;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockRunDataStore;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.MockChangeRequestClient;
/**
* A mock implementation of a {@link ServiceRegistry} for testing purposes.
@@ -44,6 +51,7 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
private final MemoryGlobalRoutingService memoryGlobalRoutingService = new MemoryGlobalRoutingService();
private final MockMailer mockMailer = new MockMailer();
private final EndpointCertificateMock endpointCertificateMock = new EndpointCertificateMock();
+ private final EndpointCertificateValidatorMock endpointCertificateValidatorMock = new EndpointCertificateValidatorMock();
private final MockMeteringClient mockMeteringClient = new MockMeteringClient();
private final MockContactRetriever mockContactRetriever = new MockContactRetriever();
private final MockIssueHandler mockIssueHandler = new MockIssueHandler();
@@ -58,9 +66,12 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
private final ApplicationStoreMock applicationStoreMock = new ApplicationStoreMock();
private final MockRunDataStore mockRunDataStore = new MockRunDataStore();
private final MockResourceTagger mockResourceTagger = new MockResourceTagger();
- private final ApplicationRoleService applicationRoleService = new NoopApplicationRoleService();
+ private final RoleService roleService = new NoopRoleService();
private final BillingController billingController = new MockBillingController();
private final ContainerRegistryMock containerRegistry = new ContainerRegistryMock();
+ private final NoopTenantSecretService tenantSecretService = new NoopTenantSecretService();
+ private final ArchiveService archiveService = new MockArchiveService();
+ private final MockChangeRequestClient changeRequestClient = new MockChangeRequestClient();
public ServiceRegistryMock(SystemName system) {
this.zoneRegistryMock = new ZoneRegistryMock(system);
@@ -103,6 +114,11 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
}
@Override
+ public EndpointCertificateValidator endpointCertificateValidator() {
+ return endpointCertificateValidatorMock;
+ }
+
+ @Override
public MockMeteringClient meteringService() {
return mockMeteringClient;
}
@@ -178,8 +194,8 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
}
@Override
- public ApplicationRoleService applicationRoleService() {
- return applicationRoleService;
+ public RoleService roleService() {
+ return roleService;
}
@Override
@@ -197,6 +213,21 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
return containerRegistry;
}
+ @Override
+ public NoopTenantSecretService tenantSecretService() {
+ return tenantSecretService;
+ }
+
+ @Override
+ public ArchiveService archiveService() {
+ return archiveService;
+ }
+
+ @Override
+ public MockChangeRequestClient changeRequestClient() {
+ return changeRequestClient;
+ }
+
public ConfigServerMock configServerMock() {
return configServerMock;
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java
new file mode 100644
index 00000000000..a9666cf9113
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java
@@ -0,0 +1,80 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import org.junit.Test;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author freva
+ */
+public class ArchiveUriUpdaterTest {
+
+ private final DeploymentTester tester = new DeploymentTester();
+
+ @Test
+ public void archive_uri_test() {
+ var updater = new ArchiveUriUpdater(tester.controller(), Duration.ofDays(1));
+
+ var tenant1 = TenantName.from("tenant1");
+ var tenant2 = TenantName.from("tenant2");
+ var tenantInfra = TenantName.from("hosted-vespa");
+ var application = tester.newDeploymentContext(tenant1.value(), "app1", "instance1");
+ ZoneId zone = ZoneId.from("prod", "ap-northeast-1");
+
+ // Initially we should not set any archive URIs as the archive service does not return any
+ updater.maintain();
+ assertArchiveUris(Map.of(), zone);
+
+ // Archive service now has URI for tenant1, but tenant1 is not deployed in zone
+ setArchiveUriInService(Map.of(tenant1, "uri-1"), zone);
+ setArchiveUriInService(Map.of(tenantInfra, "uri-3"), zone);
+ updater.maintain();
+ assertArchiveUris(Map.of(), zone);
+
+ deploy(application, zone);
+ updater.maintain();
+ assertArchiveUris(Map.of(tenant1, "uri-1", tenantInfra, "uri-3"), zone);
+
+ // URI for tenant1 should be updated and removed for tenant2
+ setArchiveUriInNodeRepo(Map.of(tenant1, "wrong-uri", tenant2, "uri-2"), zone);
+ updater.maintain();
+ assertArchiveUris(Map.of(tenant1, "uri-1", tenantInfra, "uri-3"), zone);
+ }
+
+ private void assertArchiveUris(Map<TenantName, String> expectedUris, ZoneId zone) {
+ Map<TenantName, String> actualUris = tester.controller().serviceRegistry().configServer().nodeRepository()
+ .getArchiveUris(zone).entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString()));
+ assertEquals(expectedUris, actualUris);
+ }
+
+ private void setArchiveUriInService(Map<TenantName, String> archiveUris, ZoneId zone) {
+ MockArchiveService archiveService = (MockArchiveService) tester.controller().serviceRegistry().archiveService();
+ archiveUris.forEach((tenant, uri) -> archiveService.setArchiveUri(zone, tenant, URI.create(uri)));
+ }
+
+ private void setArchiveUriInNodeRepo(Map<TenantName, String> archiveUris, ZoneId zone) {
+ NodeRepository nodeRepository = tester.controller().serviceRegistry().configServer().nodeRepository();
+ archiveUris.forEach((tenant, uri) -> nodeRepository.setArchiveUri(zone, tenant, URI.create(uri)));
+ }
+
+ private void deploy(DeploymentContext application, ZoneId zone) {
+ application.runJob(JobType.from(SystemName.main, zone).orElseThrow(), new ApplicationPackage(new byte[0]), Version.fromString("7.1"));
+ }
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java
new file mode 100644
index 00000000000..575a38cd637
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java
@@ -0,0 +1,202 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeMembership;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeOwner;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeState;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeType;
+import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ChangeManagementAssessorTest {
+
+ private ChangeManagementAssessor changeManagementAssessor = new ChangeManagementAssessor(new NodeRepositoryMock());
+
+ @Test
+ public void empty_input_variations() {
+ ZoneId zone = ZoneId.from("prod", "eu-trd");
+ List<String> hostNames = new ArrayList<>();
+ List<NodeRepositoryNode> allNodesInZone = new ArrayList<>();
+
+ // Both zone and hostnames are empty
+ ChangeManagementAssessor.Assessment assessment
+ = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone);
+ assertEquals(0, assessment.getClusterAssessments().size());
+ }
+
+ @Test
+ public void one_host_one_cluster_no_groups() {
+ ZoneId zone = ZoneId.from("prod", "eu-trd");
+ List<String> hostNames = Collections.singletonList("host1");
+ List<NodeRepositoryNode> allNodesInZone = new ArrayList<>();
+ allNodesInZone.add(createNode("node1", "host1", "default", 0 ));
+ allNodesInZone.add(createNode("node2", "host1", "default", 0 ));
+ allNodesInZone.add(createNode("node3", "host1", "default", 0 ));
+
+ // Add an not impacted hosts
+ allNodesInZone.add(createNode("node4", "host2", "default", 0 ));
+
+ // Add tenant hosts
+ allNodesInZone.add(createHost("host1", NodeType.host));
+ allNodesInZone.add(createHost("host2", NodeType.host));
+
+ // Make Assessment
+ List<ChangeManagementAssessor.ClusterAssessment> assessments
+ = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone).getClusterAssessments();
+
+ // Assess the assessment :-o
+ assertEquals(1, assessments.size());
+ assertEquals(3, assessments.get(0).clusterImpact);
+ assertEquals(4, assessments.get(0).clusterSize);
+ assertEquals(1, assessments.get(0).groupsImpact);
+ assertEquals(1, assessments.get(0).groupsTotal);
+ assertEquals("content:default", assessments.get(0).cluster);
+ assertEquals("mytenant:myapp:default", assessments.get(0).app);
+ assertEquals("prod.eu-trd", assessments.get(0).zone);
+ }
+
+ @Test
+ public void one_of_two_groups_in_one_of_two_clusters() {
+ ZoneId zone = ZoneId.from("prod", "eu-trd");
+ List<String> hostNames = Arrays.asList("host1", "host2");
+ List<NodeRepositoryNode> allNodesInZone = new ArrayList<>();
+
+ // Two impacted nodes on host1
+ allNodesInZone.add(createNode("node1", "host1", "default", 0 ));
+ allNodesInZone.add(createNode("node2", "host1", "default", 0 ));
+
+ // One impacted nodes on host2
+ allNodesInZone.add(createNode("node3", "host2", "default", 0 ));
+
+ // Another group on hosts not impacted
+ allNodesInZone.add(createNode("node4", "host3", "default", 1 ));
+ allNodesInZone.add(createNode("node5", "host3", "default", 1 ));
+ allNodesInZone.add(createNode("node6", "host3", "default", 1 ));
+
+ // Another cluster on hosts not impacted - this one also with three different groups (should all be ignored here)
+ allNodesInZone.add(createNode("node4", "host4", "myman", 4 ));
+ allNodesInZone.add(createNode("node5", "host4", "myman", 5 ));
+ allNodesInZone.add(createNode("node6", "host4", "myman", 6 ));
+
+ // Add tenant hosts
+ allNodesInZone.add(createHost("host1", NodeType.host));
+ allNodesInZone.add(createHost("host2", NodeType.host));
+
+
+ // Make Assessment
+ ChangeManagementAssessor.Assessment assessment
+ = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone);
+
+ // Assess the assessment :-o
+ List<ChangeManagementAssessor.ClusterAssessment> clusterAssessments = assessment.getClusterAssessments();
+ assertEquals(1, clusterAssessments.size()); //One cluster is impacted
+ assertEquals(3, clusterAssessments.get(0).clusterImpact);
+ assertEquals(6, clusterAssessments.get(0).clusterSize);
+ assertEquals(1, clusterAssessments.get(0).groupsImpact);
+ assertEquals(2, clusterAssessments.get(0).groupsTotal);
+ assertEquals("content:default", clusterAssessments.get(0).cluster);
+ assertEquals("mytenant:myapp:default", clusterAssessments.get(0).app);
+ assertEquals("prod.eu-trd", clusterAssessments.get(0).zone);
+ assertEquals("Impact not larger than upgrade policy", clusterAssessments.get(0).impact);
+
+ List<ChangeManagementAssessor.HostAssessment> hostAssessments = assessment.getHostAssessments();
+ assertEquals(2, hostAssessments.size());
+ assertTrue(hostAssessments.stream().anyMatch(hostAssessment ->
+ hostAssessment.hostName.equals("host1") &&
+ hostAssessment.switchName.equals("switch1") &&
+ hostAssessment.numberOfChildren == 2 &&
+ hostAssessment.numberOfProblematicChildren == 2
+ ));
+ }
+
+ @Test
+ public void two_config_nodes() {
+ var zone = ZoneId.from("prod", "eu-trd");
+ var hostNames = Arrays.asList("config1", "config2");
+ var allNodesInZone = new ArrayList<NodeRepositoryNode>();
+
+ // Add config nodes and parents
+ allNodesInZone.add(createNode("config1", "confighost1", "config", 0, NodeType.config));
+ allNodesInZone.add(createHost("confighost1", NodeType.confighost));
+ allNodesInZone.add(createNode("config2", "confighost2", "config", 0, NodeType.config));
+ allNodesInZone.add(createHost("confighost2", NodeType.confighost));
+
+ var assessment = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone).getClusterAssessments();
+ var configAssessment = assessment.get(0);
+ assertEquals("Large impact. Consider reprovisioning one or more config servers", configAssessment.impact);
+ assertEquals(2, configAssessment.clusterImpact);
+ }
+
+ @Test
+ public void one_of_three_proxy_nodes() {
+ var zone = ZoneId.from("prod", "eu-trd");
+ var hostNames = Arrays.asList("routing1");
+ var allNodesInZone = new ArrayList<NodeRepositoryNode>();
+
+ // Add routing nodes and parents
+ allNodesInZone.add(createNode("routing1", "parentrouting1", "routing", 0, NodeType.proxy));
+ allNodesInZone.add(createHost("parentrouting1", NodeType.proxyhost));
+ allNodesInZone.add(createNode("routing2", "parentrouting2", "routing", 0, NodeType.proxy));
+ allNodesInZone.add(createHost("parentrouting2", NodeType.proxyhost));
+ allNodesInZone.add(createNode("routing3", "parentrouting3", "routing", 0, NodeType.proxy));
+ allNodesInZone.add(createHost("parentrouting3", NodeType.proxyhost));
+
+ var assessment = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone).getClusterAssessments();
+ assertEquals("33% of routing nodes impacted. Consider reprovisioning if too many", assessment.get(0).impact);
+ }
+
+ private NodeOwner createOwner() {
+ NodeOwner owner = new NodeOwner();
+ owner.tenant = "mytenant";
+ owner.application = "myapp";
+ owner.instance = "default";
+ return owner;
+ }
+
+ private NodeMembership createMembership(String clusterId, int group) {
+ NodeMembership membership = new NodeMembership();
+ membership.group = "" + group;
+ membership.clusterid = clusterId;
+ membership.clustertype = "content";
+ membership.index = 2;
+ membership.retired = false;
+ return membership;
+ }
+
+ private NodeRepositoryNode createNode(String nodename, String hostname, String clusterId, int group) {
+ return createNode(nodename, hostname, clusterId, group, NodeType.tenant);
+ }
+
+ private NodeRepositoryNode createNode(String nodename, String hostname, String clusterId, int group, NodeType nodeType) {
+ NodeRepositoryNode node = new NodeRepositoryNode();
+ node.setHostname(nodename);
+ node.setParentHostname(hostname);
+ node.setState(NodeState.active);
+ node.setOwner(createOwner());
+ node.setMembership(createMembership(clusterId, group));
+ node.setType(nodeType);
+
+ return node;
+ }
+
+ private NodeRepositoryNode createHost(String hostname, NodeType nodeType) {
+ NodeRepositoryNode node = new NodeRepositoryNode();
+ node.setHostname(hostname);
+ node.setSwitchHostname("switch1");
+ node.setType(nodeType);
+ node.setOwner(createOwner());
+ node.setMembership(createMembership(nodeType.name(), 0));
+ return node;
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
new file mode 100644
index 00000000000..1ce59587d6c
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
@@ -0,0 +1,60 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.MockChangeRequestClient;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author olaa
+ */
+public class ChangeRequestMaintainerTest {
+
+ private final ControllerTester tester = new ControllerTester();
+ private final MockChangeRequestClient changeRequestClient = tester.serviceRegistry().changeRequestClient();
+ private final ChangeRequestMaintainer changeRequestMaintainer = new ChangeRequestMaintainer(tester.controller(), Duration.ofMinutes(1));
+
+ @Test
+ public void only_approve_requests_pending_approval() {
+
+ var upcomingChangeRequests = List.of(
+ newChangeRequest("id1", ChangeRequest.Approval.APPROVED),
+ newChangeRequest("id2", ChangeRequest.Approval.REQUESTED)
+ );
+
+ changeRequestClient.setUpcomingChangeRequests(upcomingChangeRequests);
+ changeRequestMaintainer.maintain();
+
+ var approvedChangeRequests = changeRequestClient.getApprovedChangeRequests();
+
+ assertEquals(1, approvedChangeRequests.size());
+ assertEquals("id2", approvedChangeRequests.get(0).getId());
+ }
+
+ private ChangeRequest newChangeRequest(String id, ChangeRequest.Approval approval) {
+ return new ChangeRequest.Builder()
+ .id(id)
+ .approval(approval)
+ .impact(ChangeRequest.Impact.VERY_HIGH)
+ .impactedSwitches(List.of())
+ .impactedHosts(List.of())
+ .changeRequestSource(new ChangeRequestSource.Builder()
+ .plannedStartTime(ZonedDateTime.now())
+ .plannedEndTime(ZonedDateTime.now())
+ .id("some-id")
+ .url("some-url")
+ .system("some-system")
+ .status(ChangeRequestSource.Status.CLOSED)
+ .build())
+ .build();
+
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporterTest.java
index 0749a077a77..d14d4014b48 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventReporterTest.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.HostName;
@@ -7,9 +8,6 @@ import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEvent;
import com.yahoo.vespa.hosted.controller.api.integration.aws.MockAwsEventFetcher;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueHandler;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
import org.junit.Test;
@@ -17,11 +15,10 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
/**
* @author olaa
@@ -29,45 +26,35 @@ import static org.junit.Assert.*;
public class CloudEventReporterTest {
private final ControllerTester tester = new ControllerTester();
- private final MetricsMock metrics = new MetricsMock();
- private final ZoneApiMock nonAwsZone = createZone("prod.zone3", "region-1", "other");
- private final ZoneApiMock awsZone1 = createZone("prod.zone1", "region-1", "aws");
- private final ZoneApiMock awsZone2 = createZone("prod.zone2", "region-2", "aws");
+ private final ZoneApiMock unsupportedZone = createZone("prod.zone3", "region-1", "other");
+ private final ZoneApiMock zone1 = createZone("prod.zone1", "region-1", "aws");
+ private final ZoneApiMock zone2 = createZone("prod.zone2", "region-2", "aws");
/**
- * Test scenario:
- * Consider three zones, two of which are based in AWS
+ * Test scenario: Consider three zones, two of which are supported
+ *
* We want to test the following:
- * 1. Non-AWS zone is completely ignored
- * 2. Tenant hosts affected by cloud event are deprovisioned
- * 3. Infrastructure hosts affected by cloud event are reported by IssueHandler
+ * 1. Unsupported zone is completely ignored
+ * 2. Hosts affected by cloud event are deprovisioned
*/
@Test
public void maintain() {
setUpZones();
- CloudEventReporter cloudEventReporter = new CloudEventReporter(tester.controller(), Duration.ofMinutes(15), metrics);
-
- assertEquals(Set.of("host1.com", "host2.com", "host3.com"), getHostnames(nonAwsZone.getId()));
- assertEquals(Set.of("host1.com", "host2.com", "host3.com"), getHostnames(awsZone1.getId()));
- assertEquals(Set.of("host4.com", "host5.com", "confighost.com"), getHostnames(awsZone2.getId()));
+ CloudEventReporter cloudEventReporter = new CloudEventReporter(tester.controller(), Duration.ofMinutes(15));
+ assertEquals(Set.of("host1.com", "host2.com", "host3.com"), getHostnames(unsupportedZone.getId()));
+ assertEquals(Set.of("host1.com", "host2.com", "host3.com"), getHostnames(zone1.getId()));
+ assertEquals(Set.of("host4.com", "host5.com", "confighost.com"), getHostnames(zone2.getId()));
mockEvents();
cloudEventReporter.maintain();
-
- assertEquals(Set.of("host1.com", "host2.com", "host3.com"), getHostnames(nonAwsZone.getId()));
- assertEquals(Set.of("host3.com"), getHostnames(awsZone1.getId()));
- assertEquals(Set.of("host4.com", "confighost.com"), getHostnames(awsZone2.getId()));
-
- Map<IssueId, MockIssueHandler.MockIssue> createdIssues = tester.serviceRegistry().issueHandler().issues();
- assertEquals(1, createdIssues.size());
- String description = createdIssues.get(IssueId.from("1")).issue().description();
- assertTrue(description.contains("confighost"));
- assertEquals(1, metrics.getMetric("infrastructure_instance_events"));
+ assertEquals(Set.of("host1.com", "host2.com", "host3.com"), getHostnames(unsupportedZone.getId()));
+ assertEquals(Set.of("host3.com"), getHostnames(zone1.getId()));
+ assertEquals(Set.of("host4.com"), getHostnames(zone2.getId()));
}
private void mockEvents() {
- MockAwsEventFetcher mockAwsEventFetcher = (MockAwsEventFetcher)tester.controller().serviceRegistry().eventFetcherService();
+ MockAwsEventFetcher eventFetcher = (MockAwsEventFetcher) tester.controller().serviceRegistry().eventFetcherService();
Date date = new Date();
CloudEvent event1 = new CloudEvent("event 1",
@@ -88,19 +75,18 @@ public class CloudEventReporterTest {
"region-2",
Set.of("host5", "confighost"));
- mockAwsEventFetcher.addEvent("region-1", event1);
- mockAwsEventFetcher.addEvent("region-2", event2);
+ eventFetcher.addEvent("region-1", event1);
+ eventFetcher.addEvent("region-2", event2);
}
private void setUpZones() {
-
tester.zoneRegistry().setZones(
- nonAwsZone,
- awsZone1,
- awsZone2);
+ unsupportedZone,
+ zone1,
+ zone2);
tester.configServer().nodeRepository().putNodes(
- nonAwsZone.getId(),
+ unsupportedZone.getId(),
createNodesWithHostnames(
"host1.com",
"host2.com",
@@ -108,7 +94,7 @@ public class CloudEventReporterTest {
)
);
tester.configServer().nodeRepository().putNodes(
- awsZone1.getId(),
+ zone1.getId(),
createNodesWithHostnames(
"host1.com",
"host2.com",
@@ -116,14 +102,14 @@ public class CloudEventReporterTest {
)
);
tester.configServer().nodeRepository().putNodes(
- awsZone2.getId(),
+ zone2.getId(),
createNodesWithHostnames(
"host4.com",
"host5.com"
)
);
tester.configServer().nodeRepository().putNodes(
- awsZone2.getId(),
+ zone2.getId(),
List.of(createNode("confighost.com", NodeType.confighost))
);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
index d718dc6b9cf..232521c9609 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
@@ -10,13 +10,15 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.deployment.Run;
+import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
import org.junit.Test;
import java.time.Duration;
-import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Optional;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
/**
* @author bratseth
@@ -27,12 +29,9 @@ public class DeploymentExpirerTest {
@Test
public void testDeploymentExpiry() {
- tester.controllerTester().zoneRegistry().setDeploymentTimeToLive(
- ZoneId.from(Environment.dev, RegionName.from("us-east-1")),
- Duration.ofDays(14)
- );
- DeploymentExpirer expirer = new DeploymentExpirer(tester.controller(), Duration.ofDays(10)
- );
+ ZoneId devZone = ZoneId.from(Environment.dev, RegionName.from("us-east-1"));
+ tester.controllerTester().zoneRegistry().setDeploymentTimeToLive(devZone, Duration.ofDays(14));
+ DeploymentExpirer expirer = new DeploymentExpirer(tester.controller(), Duration.ofDays(1));
var devApp = tester.newDeploymentContext("tenant1", "app1", "default");
var prodApp = tester.newDeploymentContext("tenant2", "app2", "default");
@@ -45,27 +44,42 @@ public class DeploymentExpirerTest {
// Deploy prod
prodApp.submit(appPackage).deploy();
-
- assertEquals(1, permanentDeployments(devApp.instance()).size());
- assertEquals(1, permanentDeployments(prodApp.instance()).size());
+ assertEquals(1, permanentDeployments(devApp.instance()));
+ assertEquals(1, permanentDeployments(prodApp.instance()));
// Not expired at first
expirer.maintain();
- assertEquals(1, permanentDeployments(devApp.instance()).size());
- assertEquals(1, permanentDeployments(prodApp.instance()).size());
+ assertEquals(1, permanentDeployments(devApp.instance()));
+ assertEquals(1, permanentDeployments(prodApp.instance()));
+
+ // Deploy dev unsuccessfully a few days before expiry
+ tester.clock().advance(Duration.ofDays(12));
+ tester.configServer().throwOnNextPrepare(new RuntimeException(getClass().getSimpleName()));
+ tester.jobs().deploy(devApp.instanceId(), JobType.devUsEast1, Optional.empty(), appPackage);
+ Run lastRun = tester.jobs().last(devApp.instanceId(), JobType.devUsEast1).get();
+ assertSame(RunStatus.error, lastRun.status());
+ Deployment deployment = tester.applications().requireInstance(devApp.instanceId())
+ .deployments().get(devZone);
+ assertEquals("Time of last run is after time of deployment", Duration.ofDays(12),
+ Duration.between(deployment.at(), lastRun.end().get()));
+
+ // Dev application does not expire based on time of successful deployment
+ tester.clock().advance(Duration.ofDays(2));
+ expirer.maintain();
+ assertEquals(1, permanentDeployments(devApp.instance()));
+ assertEquals(1, permanentDeployments(prodApp.instance()));
- // The dev application is removed
- tester.clock().advance(Duration.ofDays(15));
+ // Dev application expires when enough time has passed since most recent attempt
+ tester.clock().advance(Duration.ofDays(12));
expirer.maintain();
- assertEquals(0, permanentDeployments(devApp.instance()).size());
- assertEquals(1, permanentDeployments(prodApp.instance()).size());
+ assertEquals(0, permanentDeployments(devApp.instance()));
+ assertEquals(1, permanentDeployments(prodApp.instance()));
}
- private List<Deployment> permanentDeployments(Instance instance) {
- return tester.controller().applications().getInstance(instance.id()).get().deployments().values().stream()
- .filter(deployment -> deployment.zone().environment() != Environment.test &&
- deployment.zone().environment() != Environment.staging)
- .collect(Collectors.toList());
+ private long permanentDeployments(Instance instance) {
+ return tester.controller().applications().requireInstance(instance.id()).deployments().values().stream()
+ .filter(deployment -> !deployment.zone().environment().isTest())
+ .count();
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java
new file mode 100644
index 00000000000..4aed9b0bffe
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java
@@ -0,0 +1,66 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.CloudName;
+import com.yahoo.config.provision.zone.ZoneApi;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author mpolden
+ */
+public class OsUpgradeSchedulerTest {
+
+ @Test
+ public void maintain() {
+ ControllerTester tester = new ControllerTester();
+ OsUpgradeScheduler scheduler = new OsUpgradeScheduler(tester.controller(), Duration.ofDays(1));
+ Instant initialTime = Instant.parse("2021-01-23T00:00:00.00Z");
+ tester.clock().setInstant(initialTime);
+
+ CloudName cloud = CloudName.from("cloud");
+ ZoneApi zone = zone("prod.us-west-1", cloud);
+ tester.zoneRegistry().setZones(zone).reprovisionToUpgradeOsIn(zone);
+
+ // Initial run does nothing as the cloud does not have a target
+ scheduler.maintain();
+ assertTrue("No target set", tester.controller().osVersionTarget(cloud).isEmpty());
+
+ // Target is set
+ Version version0 = Version.fromString("7.0.0.20210123190005");
+ tester.controller().upgradeOsIn(cloud, version0, Optional.of(Duration.ofDays(1)), false);
+
+ // Target remains unchanged as it hasn't expired yet
+ for (var interval : List.of(Duration.ZERO, Duration.ofDays(15))) {
+ tester.clock().advance(interval);
+ scheduler.maintain();
+ assertEquals(version0, tester.controller().osVersionTarget(cloud).get().osVersion().version());
+ }
+
+ // Just over 30 days pass, and a new target replaces the expired one
+ Version version1 = Version.fromString("7.0.0.20210215");
+ tester.clock().advance(Duration.ofDays(15).plus(Duration.ofSeconds(1)));
+ scheduler.maintain();
+ assertEquals("New target set", version1, tester.controller().osVersionTarget(cloud).get().osVersion().version());
+
+ // A few days pass and target remains unchanged
+ tester.clock().advance(Duration.ofDays(2));
+ scheduler.maintain();
+ assertEquals(version1, tester.controller().osVersionTarget(cloud).get().osVersion().version());
+ }
+
+ private static ZoneApi zone(String id, CloudName cloud) {
+ return ZoneApiMock.newBuilder().withId(id).with(cloud).build();
+ }
+
+}
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 cb906d61a3b..e3830e274c9 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
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.component.Version;
import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.UpgradePolicy;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.config.provision.zone.ZoneId;
@@ -48,7 +47,7 @@ public class OsUpgraderTest {
.upgradeInParallel(zone2, zone3)
.upgrade(zone5) // Belongs to a different cloud and is ignored by this upgrader
.upgrade(zone4);
- OsUpgrader osUpgrader = osUpgrader(upgradePolicy, SystemName.cd, cloud1, false);
+ OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud1, false);
// Bootstrap system
tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId(), zone3.getId(), zone4.getId(), zone5.getId()),
@@ -118,13 +117,12 @@ public class OsUpgraderTest {
.upgrade(zone1)
.upgradeInParallel(zone2, zone3)
.upgrade(zone4);
- OsUpgrader osUpgrader = osUpgrader(upgradePolicy, SystemName.cd, cloud, true);
+ OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud, true);
// Bootstrap system
+ List<SystemApplication> nodeTypes = List.of(SystemApplication.configServerHost, SystemApplication.tenantHost);
tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId(), zone3.getId(), zone4.getId()),
- List.of(SystemApplication.tenantHost));
- tester.configServer().addNodes(List.of(zone1.getId(), zone2.getId(), zone3.getId(), zone4.getId()),
- List.of(SystemApplication.configServerHost)); // Not supported yet
+ nodeTypes);
// Upgrade without budget fails
Version version = Version.fromString("7.1");
@@ -140,23 +138,28 @@ public class OsUpgraderTest {
osUpgrader.maintain();
// First zone upgrades
- assertWanted(Version.emptyVersion, SystemApplication.configServerHost, zone1.getId());
- assertEquals("Dev zone gets a zero budget", Duration.ZERO, upgradeBudget(zone1.getId(), SystemApplication.tenantHost, version));
- completeUpgrade(version, SystemApplication.tenantHost, zone1.getId());
+ for (var nodeType : nodeTypes) {
+ assertEquals("Dev zone gets a zero budget", Duration.ZERO, upgradeBudget(zone1.getId(), nodeType, version));
+ completeUpgrade(version, nodeType, zone1.getId());
+ }
// Next set of zones upgrade
osUpgrader.maintain();
for (var zone : List.of(zone2.getId(), zone3.getId())) {
- assertEquals("Parallel prod zones share the budget of a single zone", Duration.ofHours(6),
- upgradeBudget(zone, SystemApplication.tenantHost, version));
- completeUpgrade(version, SystemApplication.tenantHost, zone);
+ for (var nodeType : nodeTypes) {
+ assertEquals("Parallel prod zones share the budget of a single zone", Duration.ofHours(6),
+ upgradeBudget(zone, nodeType, version));
+ completeUpgrade(version, nodeType, zone);
+ }
}
// Last zone upgrades
osUpgrader.maintain();
- assertEquals("Last prod zone gets the budget of a single zone", Duration.ofHours(6),
- upgradeBudget(zone4.getId(), SystemApplication.tenantHost, version));
- completeUpgrade(version, SystemApplication.tenantHost, zone4.getId());
+ for (var nodeType : nodeTypes) {
+ assertEquals(nodeType + " in last prod zone gets the budget of a single zone", Duration.ofHours(6),
+ upgradeBudget(zone4.getId(), nodeType, version));
+ completeUpgrade(version, nodeType, zone4.getId());
+ }
// All host applications upgraded
statusUpdater.maintain();
@@ -164,6 +167,48 @@ public class OsUpgraderTest {
.allMatch(node -> node.currentVersion().equals(version)));
}
+ @Test
+ public void upgrade_os_nodes_choose_newer_version() {
+ CloudName cloud = CloudName.from("cloud");
+ ZoneApi zone1 = zone("dev.us-east-1", cloud);
+ ZoneApi zone2 = zone("prod.us-west-1", cloud);
+ UpgradePolicy upgradePolicy = UpgradePolicy.create()
+ .upgrade(zone1)
+ .upgrade(zone2);
+ OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud, false);
+
+ // Bootstrap system
+ tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId()),
+ List.of(SystemApplication.tenantHost));
+
+ // New OS version released
+ Version version = Version.fromString("7.1");
+ tester.controller().upgradeOsIn(cloud, Version.fromString("7.0"), Optional.empty(), false);
+ tester.controller().upgradeOsIn(cloud, version, Optional.empty(), false);
+ statusUpdater.maintain();
+
+ // zone 1 upgrades
+ osUpgrader.maintain();
+ assertWanted(version, SystemApplication.tenantHost, zone1.getId());
+ Version chosenVersion = Version.fromString("7.1.1"); // Upgrade mechanism chooses a slightly newer version
+ completeUpgrade(version, chosenVersion, SystemApplication.tenantHost, zone1.getId());
+ statusUpdater.maintain();
+ assertEquals(3, nodesOn(chosenVersion).size());
+
+ // zone 2 upgrades
+ osUpgrader.maintain();
+ assertWanted(version, SystemApplication.tenantHost, zone2.getId());
+ completeUpgrade(version, chosenVersion, SystemApplication.tenantHost, zone2.getId());
+ statusUpdater.maintain();
+ assertEquals(6, nodesOn(chosenVersion).size());
+
+ // No more upgrades
+ osUpgrader.maintain();
+ assertWanted(version, SystemApplication.tenantHost, zone1.getId(), zone2.getId());
+ assertTrue("All nodes on target version or newer", tester.controller().osVersionStatus().nodesIn(cloud).stream()
+ .noneMatch(node -> node.currentVersion().isBefore(version)));
+ }
+
private Duration upgradeBudget(ZoneId zone, SystemApplication application, Version version) {
var upgradeBudget = tester.configServer().nodeRepository().osUpgradeBudget(zone, application.nodeType(), version);
assertTrue("Expected budget for upgrade to " + version + " of " + application.id() + " in " + zone,
@@ -216,7 +261,11 @@ public class OsUpgraderTest {
/** Simulate OS upgrade of nodes allocated to application. In a real system this is done by the node itself */
private void completeUpgrade(Version version, SystemApplication application, ZoneId... zones) {
- assertWanted(version, application, zones);
+ completeUpgrade(version, version, application, zones);
+ }
+
+ private void completeUpgrade(Version wantedVersion, Version version, SystemApplication application, ZoneId... zones) {
+ assertWanted(wantedVersion, application, zones);
for (ZoneId zone : zones) {
for (Node node : nodesRequiredToUpgrade(zone, application)) {
nodeRepository().putNodes(zone, new Node.Builder(node).wantedOsVersion(version).currentOsVersion(version).build());
@@ -229,11 +278,10 @@ public class OsUpgraderTest {
return tester.configServer().nodeRepository();
}
- private OsUpgrader osUpgrader(UpgradePolicy upgradePolicy, SystemName system, CloudName cloud, boolean reprovisionToUpgradeOs) {
+ private OsUpgrader osUpgrader(UpgradePolicy upgradePolicy, CloudName cloud, boolean reprovisionToUpgradeOs) {
var zones = upgradePolicy.asList().stream().flatMap(Collection::stream).collect(Collectors.toList());
tester.zoneRegistry()
.setZones(zones)
- .setSystemName(system)
.setOsUpgradePolicy(cloud, upgradePolicy);
if (reprovisionToUpgradeOs) {
tester.zoneRegistry().reprovisionToUpgradeOsIn(zones);
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 451b9230f55..0deaa21d13b 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
@@ -19,6 +19,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static org.junit.Assert.*;
@@ -80,35 +81,34 @@ public class ResourceMeterMaintainerTest {
tester.configServer().nodeRepository().setFixedNodes(awsZone2.getId());
tester.configServer().nodeRepository().putNodes(
awsZone1.getId(),
- createNodesInState(
- Node.State.provisioned,
- Node.State.ready,
- Node.State.dirty,
- Node.State.failed,
- Node.State.parked
- )
+ createNodes()
);
}
- private List<Node> createNodesInState(Node.State ...states) {
- return Arrays.stream(states)
- .map(state -> {
- return new Node.Builder()
- .hostname(HostName.from("host" + state))
- .parentHostname(HostName.from("parenthost" + state))
- .state(state)
- .type(NodeType.tenant)
- .owner(ApplicationId.from("tenant1", "app1", "default"))
- .currentVersion(Version.fromString("7.42"))
- .wantedVersion(Version.fromString("7.42"))
- .currentOsVersion(Version.fromString("7.6"))
- .wantedOsVersion(Version.fromString("7.6"))
- .serviceState(Node.ServiceState.expectedUp)
- .resources(new NodeResources(24, 24, 500, 1))
- .clusterId("clusterA")
- .clusterType(Node.ClusterType.container)
- .build();
- })
- .collect(Collectors.toUnmodifiableList());
+ private List<Node> createNodes() {
+ return Stream.of(Node.State.provisioned,
+ Node.State.ready,
+ Node.State.dirty,
+ Node.State.failed,
+ Node.State.parked,
+ Node.State.active)
+ .map(state -> {
+ return new Node.Builder()
+ .hostname(HostName.from("host" + state))
+ .parentHostname(HostName.from("parenthost" + state))
+ .state(state)
+ .type(NodeType.tenant)
+ .owner(ApplicationId.from("tenant1", "app1", "default"))
+ .currentVersion(Version.fromString("7.42"))
+ .wantedVersion(Version.fromString("7.42"))
+ .currentOsVersion(Version.fromString("7.6"))
+ .wantedOsVersion(Version.fromString("7.6"))
+ .serviceState(Node.ServiceState.expectedUp)
+ .resources(new NodeResources(24, 24, 500, 1))
+ .clusterId("clusterA")
+ .clusterType(state == Node.State.active ? Node.ClusterType.admin : Node.ClusterType.container)
+ .build();
+ })
+ .collect(Collectors.toUnmodifiableList());
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java
deleted file mode 100644
index 87c9a4996b9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RotationStatusUpdaterTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// 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.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.rotation.RotationState;
-import org.junit.Test;
-
-import java.time.Duration;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- * @author mpolden
- */
-public class RotationStatusUpdaterTest {
-
- @Test
- public void updates_rotation_status() {
- var tester = new DeploymentTester();
- var globalRotationService = tester.controllerTester().serviceRegistry().globalRoutingServiceMock();
- var updater = new RotationStatusUpdater(tester.controller(), Duration.ofDays(1));
-
- var context = tester.newDeploymentContext(ApplicationId.from("tenant1", "app1", "default"));
- var zone1 = ZoneId.from("prod", "us-west-1");
- var zone2 = ZoneId.from("prod", "us-east-3");
- var zone3 = ZoneId.from("prod", "eu-west-1");
-
- // Deploy application with global rotation
- var applicationPackage = new ApplicationPackageBuilder()
- .globalServiceId("foo")
- .region(zone1.region().value())
- .region(zone2.region().value())
- .build();
- context.submit(applicationPackage)
- .deploy();
-
- // No status gathered yet
- var rotation1 = context.instance().rotations().get(0).rotationId();
- assertEquals(RotationState.unknown, context.instance().rotationStatus().of(rotation1, context.deployment(zone1)));
- assertEquals(RotationState.unknown, context.instance().rotationStatus().of(rotation1, context.deployment(zone2)));
-
- // First rotation: One zone out, one in
- var rotationName1 = "rotation-fqdn-01";
- globalRotationService.setStatus(rotationName1, zone1, RotationStatus.IN)
- .setStatus(rotationName1, zone2, RotationStatus.OUT);
- updater.maintain();
- assertEquals(RotationState.in, context.instance().rotationStatus().of(rotation1, context.deployment(zone1)));
- assertEquals(RotationState.out, context.instance().rotationStatus().of(rotation1, context.deployment(zone2)));
-
- // First rotation: All zones in
- globalRotationService.setStatus(rotationName1, zone2, RotationStatus.IN);
- updater.maintain();
- assertEquals(RotationState.in, context.instance().rotationStatus().of(rotation1, context.deployment(zone1)));
- assertEquals(RotationState.in, context.instance().rotationStatus().of(rotation1, context.deployment(zone2)));
-
- // Another rotation is assigned
- applicationPackage = new ApplicationPackageBuilder()
- .region(zone1.region().value())
- .region(zone2.region().value())
- .region(zone3.region().value())
- .endpoint("default", "foo", "us-east-3", "us-west-1")
- .endpoint("eu", "default", "eu-west-1")
- .build();
- context.submit(applicationPackage)
- .deploy();
- assertEquals(2, context.instance().rotations().size());
-
- // Second rotation: No status gathered yet
- var rotation2 = context.instance().rotations().get(1).rotationId();
- updater.maintain();
- assertEquals(RotationState.unknown, context.instance().rotationStatus().of(rotation2, context.deployment(zone3)));
-
- // Status of third zone is retrieved via second rotation
- var rotationName2 = "rotation-fqdn-02";
- globalRotationService.setStatus(rotationName2, zone3, RotationStatus.IN);
- updater.maintain();
- assertEquals(RotationState.in, context.instance().rotationStatus().of(rotation2, context.deployment(zone3)));
-
- // Each rotation only has status for their configured zones
- assertEquals("Rotation " + rotation1 + " does not know about " + context.deployment(zone3), RotationState.unknown,
- context.instance().rotationStatus().of(rotation1, context.deployment(zone3)));
- assertEquals("Rotation " + rotation2 + " does not know about " + context.deployment(zone1), RotationState.unknown,
- context.instance().rotationStatus().of(rotation2, context.deployment(zone1)));
- assertEquals("Rotation " + rotation2 + " does not know about " + context.deployment(zone2), RotationState.unknown,
- context.instance().rotationStatus().of(rotation2, context.deployment(zone2)));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java
new file mode 100644
index 00000000000..2f24d3e6eee
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java
@@ -0,0 +1,96 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
+import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
+import org.junit.Test;
+
+import java.time.Duration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests the traffic fraction updater. This also tests its dependency on DeploymentMetricsMaintainer.
+ *
+ * @author bratseth
+ */
+public class TrafficShareUpdaterTest {
+
+ @Test
+ public void testTrafficUpdater() {
+ DeploymentTester tester = new DeploymentTester();
+ var application = tester.newDeploymentContext();
+ var deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(tester.controller(), Duration.ofDays(1));
+ var updater = new TrafficShareUpdater(tester.controller(), Duration.ofDays(1));
+ ZoneId prod1 = ZoneId.from("prod", "ap-northeast-1");
+ ZoneId prod2 = ZoneId.from("prod", "us-east-3");
+ ZoneId prod3 = ZoneId.from("prod", "us-west-1");
+ application.runJob(JobType.productionApNortheast1, new ApplicationPackage(new byte[0]), Version.fromString("7.1"));
+
+ // Single zone
+ setQpsMetric(50.0, application.application().id().defaultInstance(), prod1, tester);
+ deploymentMetricsMaintainer.maintain();
+ assertTrue(updater.maintain());
+ assertTrafficFraction(1.0, 1.0, application.instanceId(), prod1, tester);
+
+ // Two zones
+ application.runJob(JobType.productionUsEast3, new ApplicationPackage(new byte[0]), Version.fromString("7.1"));
+ // - one cold
+ setQpsMetric(50.0, application.application().id().defaultInstance(), prod1, tester);
+ setQpsMetric(0.0, application.application().id().defaultInstance(), prod2, tester);
+ deploymentMetricsMaintainer.maintain();
+ assertTrue(updater.maintain());
+ assertTrafficFraction(1.0, 1.0, application.instanceId(), prod1, tester);
+ assertTrafficFraction(0.0, 1.0, application.instanceId(), prod2, tester);
+ // - both hot
+ setQpsMetric(53.0, application.application().id().defaultInstance(), prod1, tester);
+ setQpsMetric(47.0, application.application().id().defaultInstance(), prod2, tester);
+ deploymentMetricsMaintainer.maintain();
+ assertTrue(updater.maintain());
+ assertTrafficFraction(0.53, 1.0, application.instanceId(), prod1, tester);
+ assertTrafficFraction(0.47, 1.0, application.instanceId(), prod2, tester);
+
+ // Three zones
+ application.runJob(JobType.productionUsWest1, new ApplicationPackage(new byte[0]), Version.fromString("7.1"));
+ // - one cold
+ setQpsMetric(53.0, application.application().id().defaultInstance(), prod1, tester);
+ setQpsMetric(47.0, application.application().id().defaultInstance(), prod2, tester);
+ setQpsMetric(0.0, application.application().id().defaultInstance(), prod3, tester);
+ deploymentMetricsMaintainer.maintain();
+ assertTrue(updater.maintain());
+ assertTrafficFraction(0.53, 0.53, application.instanceId(), prod1, tester);
+ assertTrafficFraction(0.47, 0.50, application.instanceId(), prod2, tester);
+ assertTrafficFraction(0.00, 0.50, application.instanceId(), prod3, tester);
+ // - all hot
+ setQpsMetric( 50.0, application.application().id().defaultInstance(), prod1, tester);
+ setQpsMetric(25.0, application.application().id().defaultInstance(), prod2, tester);
+ setQpsMetric(25.0, application.application().id().defaultInstance(), prod3, tester);
+ deploymentMetricsMaintainer.maintain();
+ assertTrue(updater.maintain());
+ assertTrafficFraction(0.50, 0.5, application.instanceId(), prod1, tester);
+ assertTrafficFraction(0.25, 0.5, application.instanceId(), prod2, tester);
+ assertTrafficFraction(0.25, 0.5, application.instanceId(), prod3, tester);
+ }
+
+ private void setQpsMetric(double qps, ApplicationId application, ZoneId zone, DeploymentTester tester) {
+ var clusterMetrics = new ClusterMetrics("default", "container");
+ clusterMetrics = clusterMetrics.addMetric(ClusterMetrics.QUERIES_PER_SECOND, qps);
+ tester.controllerTester().serviceRegistry().configServerMock().setMetrics(new DeploymentId(application, zone), clusterMetrics);
+ }
+
+ private void assertTrafficFraction(double currentReadShare, double maxReadShare,
+ ApplicationId application, ZoneId zone, DeploymentTester tester) {
+ NodeRepositoryMock mock = (NodeRepositoryMock)tester.controller().serviceRegistry().configServer().nodeRepository();
+ assertEquals(currentReadShare, mock.getTrafficFraction(application, zone).getFirst(), 0.00001);
+ assertEquals(maxReadShare, mock.getTrafficFraction(application, zone).getSecond(), 0.00001);
+ }
+
+}
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 16a17d96c03..2dcf012ac6d 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
@@ -95,7 +95,8 @@ public class ApplicationSerializerTest {
.from(new SourceRevision("repo1", "branch1", "commit1"), 32, "a@b",
Version.fromString("6.3.1"), Instant.ofEpochMilli(496));
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(zone1, applicationVersion1, Version.fromString("1.2.3"), Instant.ofEpochMilli(3),
+ DeploymentMetrics.none, DeploymentActivity.none, QuotaUsage.none));
deployments.add(new Deployment(zone2, applicationVersion2, Version.fromString("1.2.3"), Instant.ofEpochMilli(5),
new DeploymentMetrics(2, 3, 4, 5, 6,
Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS)),
@@ -160,14 +161,8 @@ public class ApplicationSerializerTest {
assertEquals(RotationStatus.EMPTY, serialized.require(id3.instance()).rotationStatus());
assertEquals(2, serialized.require(id1.instance()).deployments().size());
- assertEquals(original.require(id1.instance()).deployments().get(zone1).applicationVersion(), serialized.require(id1.instance()).deployments().get(zone1).applicationVersion());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).applicationVersion(), serialized.require(id1.instance()).deployments().get(zone2).applicationVersion());
- assertEquals(original.require(id1.instance()).deployments().get(zone1).version(), serialized.require(id1.instance()).deployments().get(zone1).version());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).version(), serialized.require(id1.instance()).deployments().get(zone2).version());
- assertEquals(original.require(id1.instance()).deployments().get(zone1).at(), serialized.require(id1.instance()).deployments().get(zone1).at());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).at(), serialized.require(id1.instance()).deployments().get(zone2).at());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).activity().lastQueried().get(), serialized.require(id1.instance()).deployments().get(zone2).activity().lastQueried().get());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).activity().lastWritten().get(), serialized.require(id1.instance()).deployments().get(zone2).activity().lastWritten().get());
+ assertEquals(original.require(id1.instance()).deployments().get(zone1), serialized.require(id1.instance()).deployments().get(zone1));
+ assertEquals(original.require(id1.instance()).deployments().get(zone2), serialized.require(id1.instance()).deployments().get(zone2));
assertEquals(original.require(id1.instance()).jobPause(JobType.systemTest),
serialized.require(id1.instance()).jobPause(JobType.systemTest));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializerTest.java
index 00f5335bd82..ff59e14947b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateMetadataSerializerTest.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. 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.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
index 9d187d1d76a..e123c4cca62 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
@@ -10,6 +10,7 @@ import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
@@ -95,7 +96,10 @@ public class TenantSerializerTest {
Optional.of(new SimplePrincipal("foobar-user")),
ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.EMPTY);
+ TenantInfo.EMPTY,
+ List.of(),
+ Optional.empty()
+ );
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(tenant.name(), serialized.name());
assertEquals(tenant.creator(), serialized.creator());
@@ -111,9 +115,16 @@ public class TenantSerializerTest {
Optional.of(new SimplePrincipal("foobar-user")),
ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.EMPTY.withName("Ofni Tnanet"));
+ TenantInfo.EMPTY.withName("Ofni Tnanet"),
+ List.of(
+ new TenantSecretStore("ss1", "123", "role1"),
+ new TenantSecretStore("ss2", "124", "role2")
+ ),
+ Optional.of("arn:aws:iam::123456789012:role/my-role")
+ );
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(tenant.info(), serialized.info());
+ assertEquals(tenant.tenantSecretStores(), serialized.tenantSecretStores());
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
index 2bf6eb39089..10f143a8e96 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
@@ -142,12 +142,16 @@ public class ContainerTester {
expectedStatusCode);
}
- public void assertResponse(Supplier<Request> requestSupplier, Consumer<Response> responseAssertion, int expectedStatusCode) {
+ public void assertResponse(Supplier<Request> requestSupplier, ConsumerThrowingException<Response> responseAssertion, int expectedStatusCode) {
var request = requestSupplier.get();
FilterResult filterResult = invokeSecurityFilters(request);
request = filterResult.request;
Response response = filterResult.response != null ? filterResult.response : container.handleRequest(request);
- responseAssertion.accept(response);
+ try {
+ responseAssertion.accept(response);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
assertEquals("Status code", expectedStatusCode, response.getStatus());
}
@@ -203,5 +207,9 @@ public class ContainerTester {
}
}
+ @FunctionalInterface
+ public interface ConsumerThrowingException<T> {
+ void accept(T t) throws Exception;
+ }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
index b935f8cbbe4..538e98c4c83 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
@@ -36,14 +36,11 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
" <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" +
" <binding>http://*/application/v4/*</binding>\n" +
- " <binding>http://*/api/application/v4/*</binding>\n" +
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>\n" +
" <binding>http://*/zone/v1</binding>\n" +
" <binding>http://*/zone/v1/*</binding>\n" +
- " <binding>http://*/api/zone/v1</binding>\n" +
- " <binding>http://*/api/zone/v1/*</binding>\n" +
" </handler>\n" +
" <http>\n" +
@@ -88,6 +85,7 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
public RequestBuilder principal(String principal) { this.principal = new SimplePrincipal(principal); return this; }
public RequestBuilder user(User user) { this.user = user; return this; }
public RequestBuilder roles(Set<Role> roles) { this.roles = roles; return this; }
+ public RequestBuilder roles(Role... roles) { return roles(Set.of(roles)); }
@Override
public Request get() {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index 4b4a8415d69..e92c229d4bc 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -75,7 +75,6 @@ public class ControllerContainerTest {
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement'/>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>\n" +
" <binding>http://*/deployment/v1/*</binding>\n" +
- " <binding>http://*/api/deployment/v1/*</binding>\n" +
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.BadgeApiHandler'>\n" +
" <binding>http://*/badge/v1/*</binding>\n" +
@@ -93,8 +92,6 @@ public class ControllerContainerTest {
" <handler id='com.yahoo.vespa.hosted.controller.restapi.configserver.ConfigServerApiHandler'>\n" +
" <binding>http://*/configserver/v1</binding>\n" +
" <binding>http://*/configserver/v1/*</binding>\n" +
- " <binding>http://*/api/configserver/v1</binding>\n" +
- " <binding>http://*/api/configserver/v1/*</binding>\n" +
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.flags.AuditedFlagsHandler'>\n" +
" <binding>http://*/flags/v1</binding>\n" +
@@ -102,11 +99,12 @@ public class ControllerContainerTest {
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.user.UserApiHandler'>\n" +
" <binding>http://*/user/v1/*</binding>\n" +
- " <binding>http://*/api/user/v1/*</binding>\n" +
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.routing.RoutingApiHandler'>\n" +
" <binding>http://*/routing/v1/*</binding>\n" +
- " <binding>http://*/api/routing/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.changemanagement.ChangeManagementApiHandler'>\n" +
+ " <binding>http://*/changemanagement/v1/*</binding>\n" +
" </handler>\n" +
variablePartXml() +
"</container>";
@@ -125,7 +123,6 @@ public class ControllerContainerTest {
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.athenz.AthenzApiHandler'>\n" +
" <binding>http://*/athenz/v1/*</binding>\n" +
- " <binding>http://*/api/athenz/v1/*</binding>\n" +
" </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>\n" +
" <binding>http://*/zone/v1</binding>\n" +
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
index 10bb722a605..4fb91639daa 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
@@ -1,12 +1,16 @@
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.application;
+import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.flags.PermanentFlags;
+import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
@@ -15,18 +19,23 @@ import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
import com.yahoo.vespa.hosted.controller.security.Auth0Credentials;
import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
import com.yahoo.vespa.hosted.controller.security.Credentials;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import org.junit.Before;
import org.junit.Test;
import javax.ws.rs.ForbiddenException;
import java.util.Collections;
+import java.util.Optional;
import java.util.Set;
+import static com.yahoo.application.container.handler.Request.Method.DELETE;
import static com.yahoo.application.container.handler.Request.Method.GET;
import static com.yahoo.application.container.handler.Request.Method.POST;
import static com.yahoo.application.container.handler.Request.Method.PUT;
import static com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiTest.createApplicationSubmissionData;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
@@ -114,6 +123,103 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
}
}
+ @Test
+ public void test_secret_store_configuration() {
+ var secretStoreRequest =
+ request("/application/v4/tenant/scoober/secret-store/some-name", PUT)
+ .data("{" +
+ "\"awsId\": \"123\"," +
+ "\"role\": \"role-id\"," +
+ "\"externalId\": \"321\"" +
+ "}")
+ .roles(Set.of(Role.developer(tenantName)));
+ tester.assertResponse(secretStoreRequest, "{\"secretStores\":[{\"name\":\"some-name\",\"awsId\":\"123\",\"role\":\"role-id\"}]}", 200);
+ tester.assertResponse(secretStoreRequest, "{" +
+ "\"error-code\":\"BAD_REQUEST\"," +
+ "\"message\":\"Secret store TenantSecretStore{name='some-name', awsId='123', role='role-id'} is already configured\"" +
+ "}", 400);
+
+ secretStoreRequest =
+ request("/application/v4/tenant/scoober/secret-store/should-fail", PUT)
+ .data("{" +
+ "\"awsId\": \" \"," +
+ "\"role\": \"role-id\"," +
+ "\"externalId\": \"321\"" +
+ "}")
+ .roles(Set.of(Role.developer(tenantName)));
+ tester.assertResponse(secretStoreRequest, "{" +
+ "\"error-code\":\"BAD_REQUEST\"," +
+ "\"message\":\"Secret store TenantSecretStore{name='should-fail', awsId=' ', role='role-id'} is invalid\"" +
+ "}", 400);
+ }
+
+ @Test
+ public void validate_secret_store() {
+ deployApplication();
+ var secretStoreRequest =
+ request("/application/v4/tenant/scoober/secret-store/secret-foo/validate?aws-region=us-west-1&parameter-name=foo&application-id=scoober.albums.default&zone=prod.aws-us-east-1c", GET)
+ .roles(Set.of(Role.developer(tenantName)));
+ tester.assertResponse(secretStoreRequest, "{" +
+ "\"error-code\":\"NOT_FOUND\"," +
+ "\"message\":\"No secret store 'secret-foo' configured for tenant 'scoober'\"" +
+ "}", 404);
+
+ tester.controller().tenants().lockOrThrow(tenantName, LockedTenant.Cloud.class, lockedTenant -> {
+ lockedTenant = lockedTenant.withSecretStore(new TenantSecretStore("secret-foo", "123", "some-role"));
+ tester.controller().tenants().store(lockedTenant);
+ });
+
+ // ConfigServerMock returns message on format deployment.toString() + " - " + tenantSecretStore.toString()
+ secretStoreRequest =
+ request("/application/v4/tenant/scoober/secret-store/secret-foo/validate?aws-region=us-west-1&parameter-name=foo&application-id=scoober.albums.default&zone=prod.aws-us-east-1c", GET)
+ .roles(Set.of(Role.developer(tenantName)));
+ tester.assertResponse(secretStoreRequest, "{\"target\":\"scoober.albums in prod.aws-us-east-1c\",\"result\":{\"settings\":{\"name\":\"foo\",\"role\":\"vespa-secretstore-access\",\"awsId\":\"892075328880\",\"externalId\":\"*****\",\"region\":\"us-east-1\"},\"status\":\"ok\"}}", 200);
+ }
+
+ @Test
+ public void delete_secret_store() {
+ var deleteRequest =
+ request("/application/v4/tenant/scoober/secret-store/secret-foo", DELETE)
+ .roles(Set.of(Role.developer(tenantName)));
+ tester.assertResponse(deleteRequest, "{" +
+ "\"error-code\":\"NOT_FOUND\"," +
+ "\"message\":\"Could not delete secret store 'secret-foo': Secret store not found\"" +
+ "}", 404);
+
+ tester.controller().tenants().lockOrThrow(tenantName, LockedTenant.Cloud.class, lockedTenant -> {
+ lockedTenant = lockedTenant.withSecretStore(new TenantSecretStore("secret-foo", "123", "some-role"));
+ tester.controller().tenants().store(lockedTenant);
+ });
+ var tenant = (CloudTenant) tester.controller().tenants().require(tenantName);
+ assertEquals(1, tenant.tenantSecretStores().size());
+ tester.assertResponse(deleteRequest, "{\"secretStores\":[]}", 200);
+ tenant = (CloudTenant) tester.controller().tenants().require(tenantName);
+ assertEquals(0, tenant.tenantSecretStores().size());
+ }
+
+ @Test
+ public void archive_uri_test() {
+ tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
+ (response) -> assertFalse(response.getBodyAsString().contains("archiveAccessRole")),
+ 200);
+ tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", PUT)
+ .data("{\"role\":\"dummy\"}").roles(Role.administrator(tenantName)),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid archive access role 'dummy': Must match expected pattern: 'arn:aws:iam::\\\\d{12}:.+'\"}", 400);
+
+ tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", PUT)
+ .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)),
+ "{\"message\":\"Archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200);
+ tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
+ (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")),
+ 200);
+
+ tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", DELETE).roles(Role.administrator(tenantName)),
+ "{\"message\":\"Archive access role removed for tenant scoober.\"}", 200);
+ tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
+ (response) -> assertFalse(response.getBodyAsString().contains("archiveAccessRole")),
+ 200);
+ }
+
private ApplicationPackageBuilder prodBuilder() {
return new ApplicationPackageBuilder()
.instances("default")
@@ -135,4 +241,19 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
private static Credentials credentials(String name) {
return new Auth0Credentials(() -> name, Collections.emptySet());
}
+
+ private void deployApplication() {
+ var applicationPackage = new ApplicationPackageBuilder()
+ .instances("default")
+ .globalServiceId("foo")
+ .region("aws-us-east-1c")
+ .build();
+ tester.controller().jobController().deploy(ApplicationId.from("scoober", "albums", "default"),
+ JobType.productionAwsUsEast1c,
+ Optional.empty(),
+ applicationPackage);
+
+
+ }
+
}
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 6626134b69a..c69d2069650 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
@@ -28,7 +28,6 @@ import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.RoutingController;
import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ProtonMetrics;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
@@ -54,12 +53,10 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.maintenance.RotationStatusUpdater;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
@@ -75,7 +72,6 @@ import org.junit.Test;
import java.io.File;
import java.net.URI;
-import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -242,8 +238,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/production-us-east-3/", POST)
.data(entity)
.userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Deployment started in run 1 of production-us-east-3 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1," +
- "\"endpoints\":[{\"cluster\":\"default\",\"tls\":true,\"url\":\"https://instance1--application1--tenant1.us-east-3.vespa.oath.cloud:4443/\",\"scope\":\"zone\",\"routingMethod\":\"shared\"}]}");
+ "{\"message\":\"Deployment started in run 1 of production-us-east-3 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1}");
app1.runJob(JobType.productionUsEast3);
tester.controller().applications().deactivate(app1.instanceId(), ZoneId.from("prod", "us-east-3"));
@@ -251,8 +246,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/dev-us-east-1/", POST)
.data(entity)
.userIdentity(USER_ID),
- "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1," +
- "\"endpoints\":[{\"cluster\":\"default\",\"tls\":true,\"url\":\"https://instance1--application1--tenant1.us-east-1.dev.vespa.oath.cloud:4443/\",\"scope\":\"zone\",\"routingMethod\":\"shared\"}]}");
+ "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1}");
app1.runJob(JobType.devUsEast1);
// GET dev application package
@@ -307,32 +301,6 @@ public class ApplicationApiTest extends ControllerContainerTest {
app1.runJob(JobType.systemTest).runJob(JobType.stagingTest).runJob(JobType.productionUsCentral1);
- // POST an application deployment to a production zone - operator emergency deployment - fails since package is unknown
- entity = createApplicationDeployData(Optional.empty(),
- Optional.of(ApplicationVersion.from(DeploymentContext.defaultSourceRevision, 666)),
- true);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST)
- .data(entity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No application package found for tenant1.application1 with version 1.0.666-commit1\"}",
- 400);
-
- // POST an application deployment to a production zone - operator emergency deployment - works with known package
- entity = createApplicationDeployData(Optional.empty(),
- Optional.of(ApplicationVersion.from(DeploymentContext.defaultSourceRevision, 1)),
- true);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST)
- .data(entity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("deploy-result.json"));
-
- // POST an application deployment to a production zone - operator emergency deployment - chooses latest package without arguments
- entity = createApplicationDeployData(Optional.empty(), true);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST)
- .data(entity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("deploy-result.json"));
-
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.instances("instance1")
.globalServiceId("foo")
@@ -706,14 +674,14 @@ public class ApplicationApiTest extends ControllerContainerTest {
"{\"message\":\"Deactivated tenant1.application1.instance1 in prod.us-central-1\"}");
// Setup for test config tests
- tester.controller().applications().deploy(ApplicationId.from("tenant1", "application1", "default"),
- ZoneId.from("prod", "us-central-1"),
- Optional.of(applicationPackageDefault),
- new DeployOptions(true, Optional.empty(), false, false));
- tester.controller().applications().deploy(ApplicationId.from("tenant1", "application1", "my-user"),
- ZoneId.from("dev", "us-east-1"),
- Optional.of(applicationPackageDefault),
- new DeployOptions(false, Optional.empty(), false, false));
+ tester.controller().jobController().deploy(ApplicationId.from("tenant1", "application1", "default"),
+ JobType.productionUsCentral1,
+ Optional.empty(),
+ applicationPackageDefault);
+ tester.controller().jobController().deploy(ApplicationId.from("tenant1", "application1", "my-user"),
+ JobType.devUsEast1,
+ Optional.empty(),
+ applicationPackageDefault);
// GET test-config for local tests against a dev deployment
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/my-user/job/dev-us-east-1/test-config", GET)
@@ -954,7 +922,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation", GET)
.properties(Map.of("endpointId", "default"))
.userIdentity(USER_ID),
- "{\"bcpStatus\":{\"rotationStatus\":\"IN\"}}",
+ "{\"bcpStatus\":{\"rotationStatus\":\"UNKNOWN\"}}",
200);
// GET global rotation status for us-west-1 in eu endpoint
@@ -968,7 +936,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/eu-west-1/global-rotation", GET)
.properties(Map.of("endpointId", "eu"))
.userIdentity(USER_ID),
- "{\"bcpStatus\":{\"rotationStatus\":\"IN\"}}",
+ "{\"bcpStatus\":{\"rotationStatus\":\"UNKNOWN\"}}",
200);
}
@@ -993,13 +961,6 @@ public class ApplicationApiTest extends ControllerContainerTest {
// Add build service to operator role
addUserToHostedOperatorRole(HostedAthenzIdentities.from(SCREWDRIVER_ID));
- // POST (deploy) an application to a prod zone - allowed when project ID is not specified
- 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),
- new File("deploy-result.json"));
-
// POST (deploy) a system application with an application package
MultiPartStreamer noAppEntity = createApplicationDeployData(Optional.empty(), true);
tester.assertResponse(request("/application/v4/tenant/hosted-vespa/application/routing/environment/prod/region/us-central-1/instance/default/deploy", POST)
@@ -1012,12 +973,6 @@ public class ApplicationApiTest extends ControllerContainerTest {
.data(noAppEntity)
.userIdentity(HOSTED_VESPA_OPERATOR),
new File("deploy-result.json"));
-
- // POST (deploy) a system application without an application package
- tester.assertResponse(request("/application/v4/tenant/hosted-vespa/application/proxy-host/environment/prod/region/us-central-1/instance/instance1/deploy", POST)
- .data(noAppEntity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("deploy-no-deployment.json"), 400);
}
@Test
@@ -1182,33 +1137,12 @@ public class ApplicationApiTest extends ControllerContainerTest {
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid build number: For input string: \\\"foobar\\\"\"}",
400);
- // POST (deploy) an application with an invalid application package
+ // POST (deploy) an application to legacy deploy path
MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1, true);
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1/deploy", POST)
.data(entity)
.userIdentity(USER_ID),
- new File("deploy-failure.json"), 400);
-
- // POST (deploy) an application without available capacity
- configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to prepare application", "Out of capacity", ConfigServerException.ErrorCode.OUT_OF_CAPACITY, null));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1/deploy", POST)
- .data(entity)
- .userIdentity(USER_ID),
- new File("deploy-out-of-capacity.json"), 400);
-
- // POST (deploy) an application where activation fails
- configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to activate application", "Activation conflict", ConfigServerException.ErrorCode.ACTIVATION_CONFLICT, null));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1/deploy", POST)
- .data(entity)
- .userIdentity(USER_ID),
- new File("deploy-activation-conflict.json"), 409);
-
- // POST (deploy) an application where we get an internal server error
- configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to deploy application", "Internal server error", ConfigServerException.ErrorCode.INTERNAL_SERVER_ERROR, null));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1/deploy", POST)
- .data(entity)
- .userIdentity(USER_ID),
- new File("deploy-internal-server-error.json"), 500);
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Deployment of tenant1.application1.instance1 is not supported through this API\"}", 400);
// DELETE tenant which has an application
tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
@@ -1252,7 +1186,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'my-tenant' already exists\"}",
400);
}
-
+
@Test
public void testAuthorization() {
UserId authorizedUser = USER_ID;
@@ -1428,8 +1362,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/new-user/deploy/dev-us-east-1", POST)
.data(entity)
.userIdentity(userId),
- "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.new-user. This may take about 15 minutes the first time.\",\"run\":1," +
- "\"endpoints\":[{\"cluster\":\"default\",\"tls\":true,\"url\":\"https://new-user--application1--tenant1.us-east-1.dev.vespa.oath.cloud:4443/\",\"scope\":\"zone\",\"routingMethod\":\"shared\"}]}");
+ "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.new-user. This may take about 15 minutes the first time.\",\"run\":1}");
}
@Test
@@ -1474,8 +1407,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
.data(entity)
.userIdentity(developer),
- "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":1," +
- "\"endpoints\":[{\"cluster\":\"default\",\"tls\":true,\"url\":\"https://myapp--sandbox.us-east-1.dev.vespa.oath.cloud:4443/\",\"scope\":\"zone\",\"routingMethod\":\"shared\"}]}",
+ "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":1}",
200);
// To add temporary support allowing tenant admins to launch services
@@ -1486,8 +1418,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
.data(entity)
.userIdentity(developer2),
- "{\"message\":\"Deployment started in run 2 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":2," +
- "\"endpoints\":[{\"cluster\":\"default\",\"tls\":true,\"url\":\"https://myapp--sandbox.us-east-1.dev.vespa.oath.cloud:4443/\",\"scope\":\"zone\",\"routingMethod\":\"shared\"}]}",
+ "{\"message\":\"Deployment started in run 2 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":2}",
200);
@@ -1496,8 +1427,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
.data(applicationPackageInstance1.zippedContent())
.contentType("application/zip")
.userIdentity(developer2),
- "{\"message\":\"Deployment started in run 3 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":3," +
- "\"endpoints\":[{\"cluster\":\"default\",\"tls\":true,\"url\":\"https://myapp--sandbox.us-east-1.dev.vespa.oath.cloud:4443/\",\"scope\":\"zone\",\"routingMethod\":\"shared\"}]}");
+ "{\"message\":\"Deployment started in run 3 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":3}");
// POST (deploy) an application package not as content type application/zip — not multipart — is disallowed
tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
@@ -1655,7 +1585,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
private void setZoneInRotation(String rotationName, ZoneId zone) {
tester.serviceRegistry().globalRoutingServiceMock().setStatus(rotationName, zone, com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus.IN);
- new RotationStatusUpdater(tester.controller(), Duration.ofDays(1)).run();
+ //new RotationStatusUpdater(tester.controller(), Duration.ofDays(1)).run();
}
private void updateContactInformation() {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
index c43abf276c5..f574d6bc3f1 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
@@ -5,7 +5,6 @@ import com.yahoo.component.Version;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.test.json.JsonTestHelper;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
@@ -171,10 +170,7 @@ public class JobControllerApiHandlerHelperTest {
var region = "us-west-1";
var applicationPackage = new ApplicationPackageBuilder().region(region).build();
// Deploy directly to production zone, like integration tests.
- tester.controller().applications().deploy(tester.instance().id(), ZoneId.from("prod", region),
- Optional.of(applicationPackage),
- new DeployOptions(true, Optional.empty(),
- false, false));
+ tester.controller().jobController().deploy(tester.instance().id(), productionUsWest1, Optional.empty(), applicationPackage);
assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")),
"jobs-direct-deployment.json");
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
index 817cee7732a..499a425087d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
@@ -1,6 +1,7 @@
{
"clusters": {
"default": {
+ "type":"container",
"min": {
"nodes": 2,
"groups": 1,
@@ -53,6 +54,14 @@
},
"cost": "(ignore)"
},
+ "utilization": {
+ "cpu": 0.1,
+ "idealCpu": 0.2,
+ "memory": 0.3,
+ "idealMemory": 0.4,
+ "disk": 0.5,
+ "idealDisk": 0.6
+ },
"scalingEvents": [
{
"from": {
@@ -84,7 +93,10 @@
"at": 1234
}
],
- "autoscalingStatus": "the autoscaling status"
+ "autoscalingStatus": "the autoscaling status",
+ "scalingDuration": 360000,
+ "maxQueryGrowthRate": 0.7,
+ "currentQueryFractionOfMax":0.3
}
}
} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json
index 0cfb457660c..886a1dec5a5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json
@@ -12,9 +12,12 @@
"bandwidthGbps": 1.0,
"diskSpeed": "slow",
"storageType": "remote",
- "fastDisk": false,
"clusterId": "default",
- "clusterType": "container"
+ "clusterType": "container",
+ "down": false,
+ "retired": false,
+ "restarting": false,
+ "rebooting": false
}
]
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json
deleted file mode 100644
index 8f6dbf17d51..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "month": "2019-09",
- "items": [
- {
- "applicationId":"tenant1:application1:instance1",
- "zoneId":"prod.us-south-1",
- "cpu": {"usage":7.0,"charge":35},
- "memory": {"usage":600.0,"charge":23},
- "disk": {"usage":1000.0,"charge":10}
- },
- {
- "applicationId":"tenant1:application1:instance1",
- "zoneId":"prod.us-north-1",
- "cpu": {"usage":2.0,"charge":10},
- "memory": {"usage":3.0,"charge":20},
- "disk": {"usage":4.0,"charge":30}
- }
- ]
-
-} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json
deleted file mode 100644
index a6130122650..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "message":"Created user 'by-new-user'"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-activation-conflict.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-activation-conflict.json
deleted file mode 100644
index 39d4faa53c9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-activation-conflict.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code":"ACTIVATION_CONFLICT",
- "message":"Failed to activate application: Activation conflict"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-failure.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-failure.json
deleted file mode 100644
index c8802cce57b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-failure.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code":"INVALID_APPLICATION_PACKAGE",
- "message":"Failed to prepare application: Invalid application package"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-internal-server-error.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-internal-server-error.json
deleted file mode 100644
index 9a845e2a7d6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-internal-server-error.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code":"INTERNAL_SERVER_ERROR",
- "message":"Failed to deploy application: Internal server error"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-no-deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-no-deployment.json
deleted file mode 100644
index f90420f28d7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-no-deployment.json
+++ /dev/null
@@ -1 +0,0 @@
-{"error-code":"BAD_REQUEST","message":"Can't redeploy application, no deployment currently exist"} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-out-of-capacity.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-out-of-capacity.json
deleted file mode 100644
index 0bdf5a2653c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-out-of-capacity.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code":"OUT_OF_CAPACITY",
- "message":"Failed to prepare application: Out of capacity"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json
index c53cee8fd97..8ea3f318d1d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json
@@ -1,13 +1,4 @@
{
"message": "Deployment started in run 1 of dev-us-east-1 for tenant1.application1.myuser. This may take about 15 minutes the first time.",
- "run": 1,
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://myuser--application1--tenant1.us-east-1.dev.vespa.oath.cloud:4443/",
- "scope": "zone",
- "routingMethod": "shared"
- }
- ]
+ "run": 1
} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json
index 961d36bd2f3..55be0881ec2 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json
@@ -418,6 +418,7 @@
"targetPlatform": "6.1.0",
"targetApplication": {
"build": 1,
+ "compileVersion": "6.1.0",
"sourceUrl": "repository1/tree/commit1",
"commit": "commit1"
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
index a7755278a39..946593fca00 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
@@ -35,7 +35,7 @@
"endpointId": "default",
"rotationId": "rotation-id-1",
"clusterId": "foo",
- "status": "IN",
+ "status": "UNKNOWN",
"lastUpdated": "(ignore)"
}
],
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json
index 530e21c6c7a..ea8c63cffb6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json
@@ -1,5 +1,5 @@
{
"bcpStatus": {
- "rotationStatus": "IN"
+ "rotationStatus": "UNKNOWN"
}
-} \ No newline at end of file
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json
index 1b2c0b4e237..745d0ad162a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json
@@ -62,14 +62,14 @@
},
{
"bcpStatus": {
- "rotationStatus": "IN"
+ "rotationStatus": "UNKNOWN"
},
"endpointStatus": [
{
"endpointId": "default",
"rotationId": "rotation-id-1",
"clusterId": "foo",
- "status": "IN",
+ "status": "UNKNOWN",
"lastUpdated": "(ignore)"
}
],
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json
index 4d387f626a1..4251ba1ad95 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json
@@ -1,6 +1,6 @@
{
"bcpStatus": {
- "rotationStatus": "IN"
+ "rotationStatus": "UNKNOWN"
},
"tenant": "tenant1",
"application": "application1",
@@ -38,7 +38,7 @@
"endpointId": "default",
"rotationId": "rotation-id-1",
"clusterId": "foo",
- "status": "IN",
+ "status": "UNKNOWN",
"lastUpdated": "(ignore)"
}
],
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list-with-user.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list-with-user.json
deleted file mode 100644
index 774b80f8b0c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list-with-user.json
+++ /dev/null
@@ -1,18 +0,0 @@
-[
- {
- "tenant": "by-myuser",
- "metaData": {
- "type": "USER"
- },
- "url": "http://localhost:8080/application/v4/tenant/by-myuser"
- },
- {
- "tenant": "tenant1",
- "metaData": {
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1"
- },
- "url": "http://localhost:8080/application/v4/tenant/tenant1"
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list.json
deleted file mode 100644
index d7ec9a738f2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-list.json
+++ /dev/null
@@ -1,11 +0,0 @@
-[
- {
- "tenant": "tenant1",
- "metaData": {
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1"
- },
- "url": "http://localhost:8080/application/v4/tenant/tenant1"
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
index 8d3f2d584b8..bf67e06db21 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
@@ -49,7 +49,7 @@ public class AthenzApiTest extends ControllerContainerTest {
new File("property-list.json"));
// POST user signup
- tester.assertResponse(authenticatedRequest("http://localhost:8080/api/athenz/v1/user", "", Request.Method.POST),
+ tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/user", "", Request.Method.POST),
"{\"message\":\"User 'bob' added to admin role of 'vespa.vespa.tenants.sandbox'\"}");
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java
index 5b46df1ad1f..b88715efcc4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.billing;
import com.yahoo.config.provision.SystemName;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
new file mode 100644
index 00000000000..2f2e70e2cf6
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
@@ -0,0 +1,103 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.changemanagement;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.api.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeMembership;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeOwner;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeState;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeType;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.intellij.lang.annotations.Language;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ChangeManagementApiHandlerTest extends ControllerContainerTest {
+
+ private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/";
+ private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser");
+
+ private ContainerTester tester;
+
+ @Before
+ public void before() {
+ tester = new ContainerTester(container, responses);
+ addUserToHostedOperatorRole(operator);
+ tester.serviceRegistry().configServer().nodeRepository().addNodes(ZoneId.from("prod.us-east-3"), createNodes());
+ }
+
+ @Test
+ public void test_api() {
+ assertFile(new Request("http://localhost:8080/changemanagement/v1/assessment", "{\"zone\":\"prod.us-east-3\", \"hosts\": [\"host1\"]}", Request.Method.POST), "initial.json");
+ }
+
+ private void assertResponse(Request request, @Language("JSON") String body, int statusCode) {
+ addIdentityToRequest(request, operator);
+ tester.assertResponse(request, body, statusCode);
+ }
+
+ private void assertFile(Request request, String filename) {
+ addIdentityToRequest(request, operator);
+ tester.assertResponse(request, new File(filename));
+ }
+
+ private List<NodeRepositoryNode> createNodes() {
+ List<NodeRepositoryNode> nodes = new ArrayList<>();
+ nodes.add(createNode("node1", "host1", "default", 0 ));
+ nodes.add(createNode("node2", "host1", "default", 0 ));
+ nodes.add(createNode("node3", "host1", "default", 0 ));
+ nodes.add(createNode("node4", "host2", "default", 0 ));
+ nodes.add(createHost("host1", "switch1"));
+ nodes.add(createHost("host2", "switch2"));
+ return nodes;
+ }
+
+ private NodeOwner createOwner() {
+ NodeOwner owner = new NodeOwner();
+ owner.tenant = "mytenant";
+ owner.application = "myapp";
+ owner.instance = "default";
+ return owner;
+ }
+
+ private NodeMembership createMembership(String clusterId, int group) {
+ NodeMembership membership = new NodeMembership();
+ membership.group = "" + group;
+ membership.clusterid = clusterId;
+ membership.clustertype = "content";
+ membership.index = 2;
+ membership.retired = false;
+ return membership;
+ }
+
+ private NodeRepositoryNode createNode(String nodename, String hostname, String clusterId, int group) {
+ NodeRepositoryNode node = new NodeRepositoryNode();
+ node.setHostname(nodename);
+ node.setParentHostname(hostname);
+ node.setState(NodeState.active);
+ node.setOwner(createOwner());
+ node.setMembership(createMembership(clusterId, group));
+ node.setType(NodeType.tenant);
+
+ return node;
+ }
+
+ private NodeRepositoryNode createHost(String hostname, String switchName) {
+ NodeRepositoryNode node = new NodeRepositoryNode();
+ node.setHostname(hostname);
+ node.setSwitchHostname(switchName);
+ node.setOwner(createOwner());
+ node.setType(NodeType.host);
+ node.setMembership(createMembership("host", 0));
+ return node;
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json
new file mode 100644
index 00000000000..cf349e06cff
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json
@@ -0,0 +1,27 @@
+{
+ "assessment": {
+ "updated": "2021-03-12:12:12:12Z",
+ "clusters": [
+ {
+ "app": "mytenant:myapp:default",
+ "zone": "prod.us-east-3",
+ "cluster": "content:default",
+ "clusterSize": 4,
+ "clusterImpact": 3,
+ "groupsTotal": 1,
+ "groupsImpact": 1,
+ "upgradePolicy": "na",
+ "suggestedAction": "nothing",
+ "impact": "Impact larger than upgrade policy"
+ }
+ ],
+ "hosts": [
+ {
+ "hostname": "host1",
+ "switchName": "switch1",
+ "numberOfChildren": 3,
+ "numberOfProblematicChildren": 3
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java
index c414a3680fc..519c3f56976 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java
@@ -66,7 +66,7 @@ public class ConfigServerApiHandlerTest extends ControllerContainerTest {
assertLastRequest("https://cfg.prod.us-north-1.test.vip:4443/", "PUT");
// DELETE /configserver/v1/prod/us-north-1/nodes/v2/node/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/api/configserver/v1/prod/controller/nodes/v2/node/node1",
+ tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/controller/nodes/v2/node/node1",
"", Request.Method.DELETE), "ok");
assertLastRequest("https://localhost:4443/", "DELETE");
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
index 6f67b0d8aa8..08741e7f38a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
@@ -7,6 +7,12 @@
"name": "ApplicationOwnershipConfirmer"
},
{
+ "name": "ArchiveUriUpdater"
+ },
+ {
+ "name": "ChangeRequestMaintainer"
+ },
+ {
"name": "CloudEventReporter"
},
{
@@ -46,6 +52,9 @@
"name": "NameServiceDispatcher"
},
{
+ "name": "OsUpgradeScheduler"
+ },
+ {
"name": "OsVersionStatusUpdater"
},
{
@@ -64,15 +73,18 @@
"name": "ResourceTagMaintainer"
},
{
- "name": "RotationStatusUpdater"
- },
- {
"name": "SystemRoutingPolicyMaintainer"
},
{
"name": "SystemUpgrader"
},
{
+ "name":"TenantRoleMaintainer"
+ },
+ {
+ "name": "TrafficShareUpdater"
+ },
+ {
"name": "Upgrader"
},
{
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 f3c24458e6e..3d07b5d51d4 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
@@ -74,7 +74,6 @@ public class DeploymentApiTest extends ControllerContainerTest {
tester.controller().updateVersionStatus(censorConfigServers(VersionStatus.compute(tester.controller())));
tester.assertResponse(operatorRequest("http://localhost:8080/deployment/v1/"), new File("root.json"));
- tester.assertResponse(operatorRequest("http://localhost:8080/api/deployment/v1/"), new File("root.json"));
}
private VersionStatus censorConfigServers(VersionStatus versionStatus) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
index df402e8c594..5bf7b03295f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
@@ -1,3 +1,4 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.filter;
import com.yahoo.application.container.handler.Request;
@@ -56,4 +57,4 @@ public class LastLoginUpdateFilterTest {
request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, context);
filter.filter(new ApplicationRequestToDiscFilterRequestWrapper(request));
}
-} \ No newline at end of file
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
index 390823271b4..efca19a61e1 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
@@ -29,6 +29,7 @@ import java.net.http.HttpRequest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.Instant;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -73,7 +74,9 @@ public class SignatureFilterTest {
LastLoginInfo.EMPTY,
Optional.empty(),
ImmutableBiMap.of(),
- TenantInfo.EMPTY));
+ TenantInfo.EMPTY,
+ List.of(),
+ Optional.empty()));
tester.curator().writeApplication(new Application(appId, tester.clock().instant()));
}
@@ -117,7 +120,9 @@ public class SignatureFilterTest {
LastLoginInfo.EMPTY,
Optional.empty(),
ImmutableBiMap.of(publicKey, () -> "user"),
- TenantInfo.EMPTY));
+ TenantInfo.EMPTY,
+ List.of(),
+ Optional.empty()));
verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
new SecurityContext(new SimplePrincipal("user"),
Set.of(Role.reader(id.tenant()),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
index 5834e4cef4a..4350b70f09f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
@@ -104,6 +104,36 @@
"cloud": "cloud2",
"nodes": [
{
+ "hostname": "node-1-configserver-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-2-configserver-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-3-configserver-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-1-proxy-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-3-proxy-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-2-proxy-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
"hostname": "node-1-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
index c8833fea100..dcdecaaa2cc 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
@@ -110,6 +110,36 @@
"cloud": "cloud2",
"nodes": [
{
+ "hostname": "node-1-configserver-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-2-configserver-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-3-configserver-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-1-proxy-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-3-proxy-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
+ "hostname": "node-2-proxy-host-prod.eu-west-1",
+ "environment": "prod",
+ "region": "eu-west-1"
+ },
+ {
"hostname": "node-1-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
index 0a9e2dac49e..b1bd7df059c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
@@ -77,12 +77,6 @@ public class RoutingApiTest extends ControllerContainerTest {
tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/", "",
Request.Method.GET),
new File("discovery/environment.json"));
-
- // GET instance with api prefix (test that the /api prefix works)
- tester.assertResponse(authenticatedRequest("http://localhost:8080/api/routing/v1/status/tenant/t1/application/a1/instance/default/",
- "",
- Request.Method.GET),
- new File("discovery/instance_api.json"));
}
@Test
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance_api.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance_api.json
deleted file mode 100644
index a9e789d9fe9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance_api.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/api/routing/v1/status/tenant/t1/application/a1/instance/default/environment/prod/region/us-east-3/"
- },
- {
- "url": "http://localhost:8080/api/routing/v1/status/tenant/t1/application/a1/instance/default/environment/prod/region/us-west-1/"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java
index 789be26db1f..35a13cdeeec 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java
@@ -147,8 +147,10 @@ public class SystemFlagsDeployerTest {
.build();
SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(prodUsEast3Target));
SystemFlagsDeployResult result = deployer.deployFlags(archive, true);
+ String expectedErrorMessage = "Flag not defined in target zone. If zone/configserver cluster is new, " +
+ "add an empty flag data file for this zone as a temporary measure until the stale flag data files are removed.";
assertThat(result.errors())
- .containsOnly(SystemFlagsDeployResult.OperationError.createFailed("Flag not defined in target zone", prodUsEast3Target, prodUsEast3Data));
+ .containsOnly(SystemFlagsDeployResult.OperationError.createFailed(expectedErrorMessage, prodUsEast3Target, prodUsEast3Data));
}
@Test
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
index 3a63caf52cd..acd481030e2 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
@@ -62,7 +62,7 @@ public class UserApiOnPremTest extends ControllerContainerTest {
}
private Request createUserRequest(User user, AthenzIdentity identity) {
- Request request = new Request("http://localhost:8080/api/user/v1/user");
+ Request request = new Request("http://localhost:8080/user/v1/user");
Map<String, String> userAttributes = new HashMap<>();
userAttributes.put("email", user.email());
if (user.name() != null)
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 3357e5ca8a4..1ad705be0b7 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
@@ -20,6 +20,7 @@ import java.util.Set;
import static com.yahoo.application.container.handler.Request.Method.DELETE;
import static com.yahoo.application.container.handler.Request.Method.POST;
+import static com.yahoo.application.container.handler.Request.Method.PUT;
import static org.junit.Assert.assertEquals;
/**
@@ -56,11 +57,6 @@ public class UserApiTest extends ControllerContainerCloudTest {
.roles(operator),
"[]");
- // GET at application/v4/tenant is available also under the /api prefix.
- tester.assertResponse(request("/api/application/v4/tenant")
- .roles(operator),
- "[]");
-
// POST a tenant is not available to everyone.
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
.data("{\"token\":\"hello\"}"),
@@ -128,11 +124,6 @@ public class UserApiTest extends ControllerContainerCloudTest {
.roles(Set.of(Role.administrator(id.tenant()))),
new File("application-roles.json"));
- // GET application role information is available also under the /api prefix.
- tester.assertResponse(request("/api/user/v1/tenant/my-tenant/application/my-app")
- .roles(Set.of(Role.administrator(id.tenant()))),
- new File("application-roles.json"));
-
// POST a pem deploy key
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST)
.roles(Set.of(Role.developer(id.tenant())))
@@ -172,6 +163,20 @@ public class UserApiTest extends ControllerContainerCloudTest {
.data("{\"key\":\"" + pemPublicKey + "\"}"),
new File("second-developer-key.json"));
+ // PUT in a new secret store for the tenant
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/secret-store/secret-foo", PUT)
+ .principal("developer@tenant")
+ .roles(Set.of(Role.developer(id.tenant())))
+ .data("{\"awsId\":\"123\",\"role\":\"secret-role\",\"externalId\":\"abc\"}"),
+ "{\"secretStores\":[{\"name\":\"secret-foo\",\"awsId\":\"123\",\"role\":\"secret-role\"}]}",
+ 200);
+
+ // GET a tenant with secret stores configured
+ tester.assertResponse(request("/application/v4/tenant/my-tenant")
+ .principal("reader@tenant")
+ .roles(Set.of(Role.reader(id.tenant()))),
+ new File("tenant-with-secrets.json"));
+
// DELETE an application is available to developers.
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", DELETE)
.roles(Set.of(Role.developer(id.tenant()))),
@@ -208,7 +213,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant());
User user = new User("dev@domail", "Joe Developer", "dev", null);
- tester.assertResponse(request("/api/user/v1/user")
+ tester.assertResponse(request("/user/v1/user")
.roles(operator)
.user(user),
new File("user-without-applications.json"));
@@ -231,13 +236,13 @@ public class UserApiTest extends ControllerContainerCloudTest {
controller.createApplication("sandbox", "app2", "dev");
// Should still be empty because none of the roles explicitly refer to any of the applications
- tester.assertResponse(request("/api/user/v1/user")
+ tester.assertResponse(request("/user/v1/user")
.roles(operator)
.user(user),
new File("user-without-applications.json"));
// Empty applications because tenant dummy does not exist
- tester.assertResponse(request("/api/user/v1/user")
+ tester.assertResponse(request("/user/v1/user")
.roles(Set.of(Role.administrator(TenantName.from("tenant1")),
Role.developer(TenantName.from("tenant2")),
Role.developer(TenantName.from("sandbox")),
@@ -259,7 +264,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
controller.createTenant("tenant1", Tenant.Type.cloud);
tester.assertResponse(
- request("/api/user/v1/user").user(user),
+ request("/user/v1/user").user(user),
new File("user-without-trial-capacity-cloud.json"));
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
index 9323067904c..03e5eb2b7a8 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
@@ -10,6 +10,13 @@
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
"user": "developer@tenant"
}],
+ "secretStores": [],
+ "integrations": {
+ "aws": {
+ "tenantRole": "my-tenant-tenant-role",
+ "accounts": []
+ }
+ },
"quota": {
"budget": null,
"budgetUsed": 0.0,
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json
new file mode 100644
index 00000000000..dc717b5cac0
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json
@@ -0,0 +1,38 @@
+{
+ "tenant": "my-tenant",
+ "type": "CLOUD",
+ "pemDeveloperKeys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
+ "user": "developer@tenant"
+ }
+ ],
+ "secretStores": [
+ {
+ "name": "secret-foo",
+ "awsId": "123",
+ "role": "secret-role"
+ }
+ ],
+ "integrations": {
+ "aws": {
+ "tenantRole": "my-tenant-tenant-role",
+ "accounts": [
+ {
+ "name": "secret-foo",
+ "awsId": "123",
+ "role": "secret-role"
+ }
+ ]
+ }
+ },
+ "quota": {
+ "budget": null,
+ "budgetUsed": 0.0,
+ "clusterSize": 5
+ },
+ "applications": [],
+ "metaData": {
+ "createdAtMillis": "(ignore)"
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
index eaabb9fe3e1..14b900caf50 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
@@ -3,6 +3,13 @@
"type": "CLOUD",
"creator": "administrator@tenant",
"pemDeveloperKeys": [],
+ "secretStores": [],
+ "integrations": {
+ "aws": {
+ "tenantRole": "my-tenant-tenant-role",
+ "accounts": []
+ }
+ },
"quota": {
"budget": null,
"budgetUsed": 0.0,
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
index d5031267b27..a8845fa92a3 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
@@ -52,7 +52,7 @@ public class ZoneApiTest extends ControllerContainerCloudTest {
new File("prod.json"));
// GET /zone/v1/environment/dev/default
- tester.assertResponse(request("/api/zone/v1/environment/dev/default")
+ tester.assertResponse(request("/zone/v1/environment/dev/default")
.roles(everyone),
new File("default-for-region.json"));
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
index fab61eeaec3..38f5a60cf8a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
@@ -19,7 +19,6 @@ import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.RoutingController;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
@@ -354,7 +353,7 @@ public class RoutingPoliciesTest {
.exclusiveRoutingIn(zoneApi);
// Deploy to dev
- tester.controllerTester().controller().applications().deploy(context.instanceId(), zone, Optional.of(emptyApplicationPackage), DeployOptions.none());
+ context.runJob(zone, emptyApplicationPackage);
assertEquals("DeploymentSpec is not persisted", DeploymentSpec.empty, context.application().deploymentSpec());
context.flushDnsUpdates();
@@ -378,13 +377,14 @@ public class RoutingPoliciesTest {
assertEquals(prodRecords, tester.recordNames());
// Deploy to dev under different instance
- var devInstance = context.application().id().instance("user");
- tester.controllerTester().controller().applications().deploy(devInstance, zone, Optional.of(applicationPackage), DeployOptions.none());
+ var devContext = tester.newDeploymentContext(context.application().id().instance("user"));
+ devContext.runJob(zone, applicationPackage);
+
assertEquals("DeploymentSpec is persisted", applicationPackage.deploymentSpec(), context.application().deploymentSpec());
context.flushDnsUpdates();
// Routing policy is created and DNS is updated
- assertEquals(1, tester.policiesOf(devInstance).size());
+ assertEquals(1, tester.policiesOf(devContext.instanceId()).size());
assertEquals(Sets.union(prodRecords, Set.of("user.app1.tenant1.us-east-1.dev.vespa.oath.cloud")), tester.recordNames());
}
@@ -732,6 +732,10 @@ public class RoutingPoliciesTest {
return tester.newDeploymentContext(tenant, application, instance);
}
+ public DeploymentContext newDeploymentContext(ApplicationId instance) {
+ return tester.newDeploymentContext(instance);
+ }
+
public ControllerTester controllerTester() {
return tester.controllerTester();
}