summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2017-12-13 15:26:41 +0100
committerJon Bratseth <bratseth@yahoo-inc.com>2017-12-13 15:26:41 +0100
commitd3c1c98b5e67cf8f9bb9f3efdef3161f63c19ffc (patch)
tree05aea3ca495d6acf87ee3817d6f2957a57c39339 /controller-server
parent3783a9b21f8ab7ca3700903d9780a9f7374cf0c5 (diff)
parent540b84751e90d60c13c4da3e1e15d47b720c327e (diff)
Merge with master
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/pom.xml1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java42
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java188
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java90
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java51
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ApplicationAction.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzClientFactory.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPrincipal.java62
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPublicKey.java49
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzService.java55
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzUtils.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/InvalidTokenException.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/NToken.java148
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsClient.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsException.java22
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsKeystore.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsClient.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsException.java22
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzTrustStoreConfigurator.java45
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidator.java42
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzSslContextProviderImpl.java87
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientImpl.java65
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsKeystoreImpl.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZtsClientImpl.java87
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzClientFactoryMock.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java22
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZtsClientMock.java61
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java67
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java33
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java41
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java39
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java109
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java60
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java32
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java49
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java42
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationLock.java25
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java117
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java146
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java54
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java48
-rw-r--r--controller-server/src/main/resources/configdefinitions/athenz.def9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java114
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java61
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/TestIdentities.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidatorTest.java41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MockMetricsService.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java77
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java106
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java282
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponseTest.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/user.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java14
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java14
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java175
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java212
99 files changed, 2042 insertions, 1857 deletions
diff --git a/controller-server/pom.xml b/controller-server/pom.xml
index 0cfcbc40601..b033286b82a 100644
--- a/controller-server/pom.xml
+++ b/controller-server/pom.xml
@@ -9,6 +9,7 @@
<groupId>com.yahoo.vespa</groupId>
<artifactId>parent</artifactId>
<version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>controller-server</artifactId>
<packaging>container-plugin</packaging>
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 e3400d76bce..ae2de96f511 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
@@ -7,14 +7,16 @@ import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Change.VersionChange;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.Collections;
@@ -38,38 +40,44 @@ public class Application {
private final ApplicationId id;
private final DeploymentSpec deploymentSpec;
private final ValidationOverrides validationOverrides;
- private final Map<Zone, Deployment> deployments;
+ private final Map<ZoneId, Deployment> deployments;
private final DeploymentJobs deploymentJobs;
private final Optional<Change> deploying;
private final boolean outstandingChange;
private final Optional<IssueId> ownershipIssueId;
private final ApplicationMetrics metrics;
+ private final Optional<RotationId> rotation;
/** Creates an empty application */
public Application(ApplicationId id) {
- this(id, DeploymentSpec.empty, ValidationOverrides.empty, ImmutableMap.of(),
+ this(id, DeploymentSpec.empty, ValidationOverrides.empty, Collections.emptyMap(),
new DeploymentJobs(Optional.empty(), Collections.emptyList(), Optional.empty()),
- Optional.empty(), false, Optional.empty(), new ApplicationMetrics(0, 0));
+ Optional.empty(), false, Optional.empty(), new ApplicationMetrics(0, 0),
+ Optional.empty());
}
/** Used from persistence layer: Do not use */
public Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
List<Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
- boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics) {
+ boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics,
+ Optional<RotationId> rotation) {
this(id, deploymentSpec, validationOverrides,
deployments.stream().collect(Collectors.toMap(Deployment::zone, d -> d)),
- deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics);
+ deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics, rotation);
}
Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
- Map<Zone, Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
- boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics) {
+ Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
+ boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics,
+ Optional<RotationId> rotation) {
Objects.requireNonNull(id, "id cannot be null");
Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
Objects.requireNonNull(validationOverrides, "validationOverrides cannot be null");
Objects.requireNonNull(deployments, "deployments cannot be null");
Objects.requireNonNull(deploymentJobs, "deploymentJobs cannot be null");
Objects.requireNonNull(deploying, "deploying cannot be null");
+ Objects.requireNonNull(metrics, "metrics cannot be null");
+ Objects.requireNonNull(rotation, "rotation cannot be null");
this.id = id;
this.deploymentSpec = deploymentSpec;
this.validationOverrides = validationOverrides;
@@ -79,6 +87,7 @@ public class Application {
this.outstandingChange = outstandingChange;
this.ownershipIssueId = ownershipIssueId;
this.metrics = metrics;
+ this.rotation = rotation;
}
public ApplicationId id() { return id; }
@@ -97,13 +106,13 @@ public class Application {
public ValidationOverrides validationOverrides() { return validationOverrides; }
/** Returns an immutable map of the current deployments of this */
- public Map<Zone, Deployment> deployments() { return deployments; }
+ public Map<ZoneId, Deployment> deployments() { return deployments; }
/**
* Returns an immutable map of the current *production* deployments of this
* (deployments also includes manually deployed environments)
*/
- public Map<Zone, Deployment> productionDeployments() {
+ public Map<ZoneId, Deployment> productionDeployments() {
return ImmutableMap.copyOf(deployments.values().stream()
.filter(deployment -> deployment.zone().environment() == Environment.prod)
.collect(Collectors.toMap(Deployment::zone, Function.identity())));
@@ -142,7 +151,7 @@ public class Application {
}
/** Returns the version a new deployment to this zone should use for this application */
- public Version deployVersionIn(Zone zone, Controller controller) {
+ public Version deployVersionIn(ZoneId zone, Controller controller) {
if (deploying().isPresent() && deploying().get() instanceof VersionChange)
return ((Change.VersionChange) deploying().get()).version();
@@ -150,13 +159,13 @@ public class Application {
}
/** Returns the current version this application has, or if none; should use, in the given zone */
- public Version versionIn(Zone zone, Controller controller) {
+ public Version versionIn(ZoneId zone, Controller controller) {
return Optional.ofNullable(deployments().get(zone)).map(Deployment::version) // Already deployed in this zone: Use that version
.orElse(oldestDeployedVersion().orElse(controller.systemVersion()));
}
/** Returns the revision a new deployment to this zone should use for this application, or empty if we don't know */
- public Optional<ApplicationRevision> deployRevisionIn(Zone zone) {
+ public Optional<ApplicationRevision> deployRevisionIn(ZoneId zone) {
if (deploying().isPresent() && deploying().get() instanceof Change.ApplicationChange)
return ((Change.ApplicationChange) deploying().get()).revision();
@@ -164,10 +173,15 @@ public class Application {
}
/** Returns the revision this application is or should be deployed with in the given zone, or empty if unknown. */
- public Optional<ApplicationRevision> revisionIn(Zone zone) {
+ public Optional<ApplicationRevision> revisionIn(ZoneId zone) {
return Optional.ofNullable(deployments().get(zone)).map(Deployment::revision);
}
+ /** Returns the global rotation of this, if present */
+ public Optional<ApplicationRotation> rotation() {
+ return rotation.map(rotation -> new ApplicationRotation(id, rotation));
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
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 841b9b4dd9f..fb92109c5df 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
@@ -1,7 +1,6 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;
-import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationId;
@@ -9,9 +8,9 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
-import com.yahoo.vespa.hosted.controller.api.ApplicationAlias;
import com.yahoo.vespa.hosted.controller.api.InstanceEndpoints;
import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
@@ -29,27 +28,33 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.NoInstance
import com.yahoo.vespa.hosted.controller.api.integration.configserver.PrepareResponse;
import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordId;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsClient;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.maintenance.DeploymentExpirer;
import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.rotation.RotationRepository;
+import com.yahoo.vespa.hosted.controller.rotation.Rotation;
+import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
+import com.yahoo.vespa.hosted.controller.rotation.RotationRepository;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
+import com.yahoo.yolean.Exceptions;
import java.io.IOException;
import java.net.URI;
@@ -82,6 +87,7 @@ public class ApplicationController {
/** For permanent storage */
private final ControllerDb db;
+
/** For working memory storage and sharing between controllers */
private final CuratorDb curator;
@@ -93,26 +99,26 @@ public class ApplicationController {
private final Clock clock;
private final DeploymentTrigger deploymentTrigger;
-
+
ApplicationController(Controller controller, ControllerDb db, CuratorDb curator,
- RotationRepository rotationRepository,
- AthenzClientFactory zmsClientFactory,
+ AthenzClientFactory zmsClientFactory, RotationsConfig rotationsConfig,
NameService nameService, ConfigServerClient configserverClient,
RoutingGenerator routingGenerator, Clock clock) {
this.controller = controller;
this.db = db;
this.curator = curator;
- this.rotationRepository = rotationRepository;
this.zmsClientFactory = zmsClientFactory;
this.nameService = nameService;
this.configserverClient = configserverClient;
this.routingGenerator = routingGenerator;
this.clock = clock;
+ this.rotationRepository = new RotationRepository(rotationsConfig, this, curator);
this.deploymentTrigger = new DeploymentTrigger(controller, curator, clock);
- for (Application application : db.listApplications())
- lockedIfPresent(application.id(), this::store);
+ for (Application application : db.listApplications()) {
+ lockIfPresent(application.id(), this::store);
+ }
}
/** Returns the application with the given id, or null if it is not present */
@@ -231,10 +237,6 @@ public class ApplicationController {
if ( ! (id.instance().value().equals("default") || id.instance().value().startsWith("default-pr"))) // TODO: Support instances properly
throw new UnsupportedOperationException("Only the instance names 'default' and names starting with 'default-pr' are supported at the moment");
try (Lock lock = lock(id)) {
- // TODO: Throwing is duplicated below.
- if (get(id).isPresent())
- throw new IllegalArgumentException("An application with id '" + id + "' already exists");
-
com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value());
Optional<Tenant> tenant = controller.tenants().tenant(new TenantId(id.tenant().value()));
@@ -266,7 +268,7 @@ public class ApplicationController {
/** Deploys an application. If the application does not exist it is created. */
// TODO: Get rid of the options arg
- public ActivateResult deployApplication(ApplicationId applicationId, Zone zone,
+ public ActivateResult deployApplication(ApplicationId applicationId, ZoneId zone,
ApplicationPackage applicationPackage, DeployOptions options) {
try (Lock lock = lock(applicationId)) {
// TODO: Shouldn't this go through the above method? Seems you can cheat the checks here ... ?
@@ -328,15 +330,35 @@ public class ApplicationController {
throw new IllegalArgumentException("Rejecting deployment of " + application + " to " + zone +
" as the requested version " + version + " is older than" +
" the current version " + existingDeployment.version());
- }
+ }
+
+ Optional<Rotation> rotation;
+ try (RotationLock rotationLock = rotationRepository.lock()) {
+ rotation = getRotation(application, zone, rotationLock);
+ if (rotation.isPresent()) {
+ application = application.with(rotation.get().id());
+ store(application); // store assigned rotation even if deployment fails
+ registerRotationInDns(rotation.get(), application.rotation().get().dnsName());
+ }
+ }
+
+ // TODO: Improve config server client interface and simplify
+ Set<String> cnames = application.rotation()
+ .map(ApplicationRotation::dnsName)
+ .map(Collections::singleton)
+ .orElseGet(Collections::emptySet);
+
+ Set<com.yahoo.vespa.hosted.controller.api.rotation.Rotation> rotations = rotation
+ .map(r -> new com.yahoo.vespa.hosted.controller.api.rotation.Rotation(
+ new com.yahoo.vespa.hosted.controller.api.identifiers.RotationId(
+ r.id().asString()), r.name()))
+ .map(Collections::singleton)
+ .orElseGet(Collections::emptySet);
// Carry out deployment
- DeploymentId deploymentId = new DeploymentId(applicationId, zone);
- ApplicationRotation rotationInDns = registerRotationInDns(deploymentId, getOrAssignRotation(deploymentId,
- applicationPackage));
- options = withVersion(version, options);
+ options = withVersion(version, options);
ConfigServerClient.PreparedApplication preparedApplication =
- configserverClient.prepare(deploymentId, options, rotationInDns.cnames(), rotationInDns.rotations(),
+ configserverClient.prepare(new DeploymentId(applicationId, zone), options, cnames, rotations,
applicationPackage.zippedContent());
preparedApplication.activate();
application = application.withNewDeployment(zone, revision, version, clock.instant());
@@ -347,7 +369,7 @@ public class ApplicationController {
}
}
- private ActivateResult unexpectedDeployment(ApplicationId applicationId, Zone zone, ApplicationPackage applicationPackage) {
+ private ActivateResult unexpectedDeployment(ApplicationId applicationId, ZoneId zone, ApplicationPackage applicationPackage) {
Log logEntry = new Log();
logEntry.level = "WARNING";
logEntry.time = clock.instant().toEpochMilli();
@@ -384,9 +406,9 @@ public class ApplicationController {
private LockedApplication deleteUnreferencedDeploymentJobs(LockedApplication application) {
for (DeploymentJobs.JobType job : application.deploymentJobs().jobStatus().keySet()) {
- Optional<Zone> zone = job.zone(controller.system());
+ Optional<ZoneId> zone = job.zone(controller.system());
- if ( ! job.isProduction() || (zone.isPresent() && application.deploymentSpec().includes(zone.get().environment(), zone.map(Zone::region))))
+ if ( ! job.isProduction() || (zone.isPresent() && application.deploymentSpec().includes(zone.get().environment(), zone.map(ZoneId::region))))
continue;
application = application.withoutDeploymentJob(job);
}
@@ -430,35 +452,35 @@ public class ApplicationController {
gitRevision.commit.id()));
}
- private ApplicationRotation registerRotationInDns(DeploymentId deploymentId, ApplicationRotation applicationRotation) {
- ApplicationAlias alias = new ApplicationAlias(deploymentId.applicationId());
- if (applicationRotation.rotations().isEmpty()) return applicationRotation;
-
- Rotation rotation = applicationRotation.rotations().iterator().next(); // at this time there should be only one rotation assigned
- String endpointName = alias.toString();
+ /** Register a DNS name for rotation */
+ private void registerRotationInDns(Rotation rotation, String dnsName) {
try {
- Optional<Record> record = nameService.findRecord(Record.Type.CNAME, endpointName);
- if (!record.isPresent()) {
- RecordId recordId = nameService.createCname(endpointName, rotation.rotationName);
- log.info("Registered mapping with record ID " + recordId.id() + ": " +
- endpointName + " -> " + rotation.rotationName);
+ Optional<Record> record = nameService.findRecord(Record.Type.CNAME, RecordName.from(dnsName));
+ RecordData rotationName = RecordData.fqdn(rotation.name());
+ if (record.isPresent()) {
+ // Ensure that the existing record points to the correct rotation
+ if (!record.get().data().equals(rotationName)) {
+ nameService.updateRecord(record.get().id(), rotationName);
+ log.info("Updated mapping for record ID " + record.get().id().asString() + ": '" + dnsName
+ + "' -> '" + rotation.name() + "'");
+ }
+ } else {
+ RecordId id = nameService.createCname(RecordName.from(dnsName), rotationName);
+ log.info("Registered mapping with record ID " + id.asString() + ": '" + dnsName + "' -> '"
+ + rotation.name() + "'");
}
- }
- catch (RuntimeException e) {
+ } catch (RuntimeException e) {
log.log(Level.WARNING, "Failed to register CNAME", e);
}
- return new ApplicationRotation(Collections.singleton(endpointName), Collections.singleton(rotation));
}
- private ApplicationRotation getOrAssignRotation(DeploymentId deploymentId, ApplicationPackage applicationPackage) {
- if (deploymentId.zone().environment().equals(Environment.prod)) {
- return new ApplicationRotation(Collections.emptySet(),
- rotationRepository.getOrAssignRotation(deploymentId.applicationId(),
- applicationPackage.deploymentSpec()));
- } else {
- return new ApplicationRotation(Collections.emptySet(),
- Collections.emptySet());
+ /** Get an available rotation, if deploying to a production zone and a service ID is specified */
+ private Optional<Rotation> getRotation(Application application, ZoneId zone, RotationLock lock) {
+ if (zone.environment() != Environment.prod ||
+ !application.deploymentSpec().globalServiceId().isPresent()) {
+ return Optional.empty();
}
+ return Optional.of(rotationRepository.getRotation(application, lock));
}
/** Returns the endpoints of the deployment, or empty if obtaining them failed */
@@ -476,7 +498,8 @@ public class ApplicationController {
return Optional.of(new InstanceEndpoints(endPointUrls));
}
catch (RuntimeException e) {
- log.log(Level.FINE, "Failed to get endpoint information for " + deploymentId, e);
+ log.log(Level.WARNING, "Failed to get endpoint information for " + deploymentId + ": "
+ + Exceptions.toMessageString(e));
return Optional.empty();
}
}
@@ -491,7 +514,7 @@ public class ApplicationController {
if ( ! controller.applications().get(id).isPresent())
throw new NotExistsException("Could not delete application '" + id + "': Application not found");
- lockedOrThrow(id, application -> {
+ lockOrThrow(id, application -> {
if ( ! application.deployments().isEmpty())
throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments");
@@ -521,24 +544,25 @@ public class ApplicationController {
/**
* Acquire a locked application to modify and store, if there is an application with the given id.
*
- * @param applicationId Id of the application to lock and get.
- * @param actions Things to do with the locked application.
+ * @param applicationId ID of the application to lock and get.
+ * @param action Function which acts on the locked application.
*/
- public void lockedIfPresent(ApplicationId applicationId, Consumer<LockedApplication> actions) {
+ public void lockIfPresent(ApplicationId applicationId, Consumer<LockedApplication> action) {
try (Lock lock = lock(applicationId)) {
- get(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(actions);
+ get(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(action);
}
}
/**
* Acquire a locked application to modify and store, or throw an exception if no application has the given id.
*
- * @param applicationId Id of the application to lock and require.
- * @param actions Things to do with the locked application.
+ * @param applicationId ID of the application to lock and require.
+ * @param action Function which acts on the locked application.
+ * @throws IllegalArgumentException when application does not exist.
*/
- public void lockedOrThrow(ApplicationId applicationId, Consumer<LockedApplication> actions) {
+ public void lockOrThrow(ApplicationId applicationId, Consumer<LockedApplication> action) {
try (Lock lock = lock(applicationId)) {
- actions.accept(new LockedApplication(require(applicationId), lock));
+ action.accept(new LockedApplication(require(applicationId), lock));
}
}
@@ -551,18 +575,14 @@ public class ApplicationController {
deploymentTrigger.triggerFromCompletion(report);
}
- // TODO: Collapse this method and the next
- public void restart(DeploymentId deploymentId) {
- try {
- configserverClient.restart(deploymentId, Optional.empty());
- }
- catch (NoInstanceException e) {
- throw new IllegalArgumentException("Could not restart " + deploymentId + ": No such deployment");
- }
- }
- public void restartHost(DeploymentId deploymentId, Hostname hostname) {
+ /**
+ * Tells config server to schedule a restart of all nodes in this deployment
+ *
+ * @param hostname If non-empty, restart will only be scheduled for this host
+ */
+ public void restart(DeploymentId deploymentId, Optional<Hostname> hostname) {
try {
- configserverClient.restart(deploymentId, Optional.of(hostname));
+ configserverClient.restart(deploymentId, hostname);
}
catch (NoInstanceException e) {
throw new IllegalArgumentException("Could not restart " + deploymentId + ": No such deployment");
@@ -570,7 +590,7 @@ public class ApplicationController {
}
/** Deactivate application in the given zone */
- public void deactivate(Application application, Zone zone) {
+ public void deactivate(Application application, ZoneId zone) {
deactivate(application, zone, Optional.empty(), false);
}
@@ -579,13 +599,13 @@ public class ApplicationController {
deactivate(application, deployment.zone(), Optional.of(deployment), requireThatDeploymentHasExpired);
}
- private void deactivate(Application application, Zone zone, Optional<Deployment> deployment,
+ private void deactivate(Application application, ZoneId zone, Optional<Deployment> deployment,
boolean requireThatDeploymentHasExpired) {
if (requireThatDeploymentHasExpired && deployment.isPresent()
&& ! DeploymentExpirer.hasExpired(controller.zoneRegistry(), deployment.get(), clock.instant()))
return;
- lockedOrThrow(application.id(), lockedApplication ->
+ lockOrThrow(application.id(), lockedApplication ->
store(deactivate(lockedApplication, zone)));
}
@@ -594,7 +614,7 @@ public class ApplicationController {
*
* @return the application with the deployment in the given zone removed
*/
- private LockedApplication deactivate(LockedApplication application, Zone zone) {
+ private LockedApplication deactivate(LockedApplication application, ZoneId zone) {
try {
configserverClient.deactivate(new DeploymentId(application.id(), zone));
}
@@ -624,7 +644,7 @@ public class ApplicationController {
}
/** Returns whether a direct deployment to given zone is allowed */
- private static boolean canDeployDirectlyTo(Zone zone, DeployOptions options) {
+ private static boolean canDeployDirectlyTo(ZoneId zone, DeployOptions options) {
return ! options.screwdriverBuildJob.isPresent() ||
options.screwdriverBuildJob.get().screwdriverId == null ||
zone.environment().isManuallyDeployed();
@@ -640,20 +660,8 @@ public class ApplicationController {
});
}
-
- private static final class ApplicationRotation {
-
- private final ImmutableSet<String> cnames;
- private final ImmutableSet<Rotation> rotations;
-
- public ApplicationRotation(Set<String> cnames, Set<Rotation> rotations) {
- this.cnames = ImmutableSet.copyOf(cnames);
- this.rotations = ImmutableSet.copyOf(rotations);
- }
-
- public Set<String> cnames() { return cnames; }
- public Set<Rotation> rotations() { return rotations; }
-
+ public RotationRepository rotationRepository() {
+ return rotationRepository;
}
}
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 b854ad3f771..71a0a7f6297 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
@@ -6,7 +6,6 @@ import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.component.Version;
import com.yahoo.component.Vtag;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
@@ -25,12 +24,12 @@ import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingSe
import com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.rotation.RotationRepository;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import java.net.URI;
@@ -39,7 +38,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.logging.Logger;
/**
@@ -64,7 +62,6 @@ public class Controller extends AbstractComponent {
private final ApplicationController applicationController;
private final TenantController tenantController;
private final Clock clock;
- private final RotationRepository rotationRepository;
private final GitHub gitHub;
private final EntityService entityService;
private final GlobalRoutingService globalRoutingService;
@@ -82,19 +79,19 @@ public class Controller extends AbstractComponent {
* @param curator the curator instance storing working state shared between controller instances
*/
@Inject
- public Controller(ControllerDb db, CuratorDb curator, RotationRepository rotationRepository,
+ public Controller(ControllerDb db, CuratorDb curator, RotationsConfig rotationsConfig,
GitHub gitHub, EntityService entityService, Organization organization,
GlobalRoutingService globalRoutingService,
ZoneRegistry zoneRegistry, ConfigServerClient configServerClient,
MetricsService metricsService, NameService nameService,
RoutingGenerator routingGenerator, Chef chefClient, AthenzClientFactory athenzClientFactory) {
- this(db, curator, rotationRepository,
+ this(db, curator, rotationsConfig,
gitHub, entityService, organization, globalRoutingService, zoneRegistry,
configServerClient, metricsService, nameService, routingGenerator, chefClient,
Clock.systemUTC(), athenzClientFactory);
}
- public Controller(ControllerDb db, CuratorDb curator, RotationRepository rotationRepository,
+ public Controller(ControllerDb db, CuratorDb curator, RotationsConfig rotationsConfig,
GitHub gitHub, EntityService entityService, Organization organization,
GlobalRoutingService globalRoutingService,
ZoneRegistry zoneRegistry, ConfigServerClient configServerClient,
@@ -103,7 +100,7 @@ public class Controller extends AbstractComponent {
AthenzClientFactory athenzClientFactory) {
Objects.requireNonNull(db, "Controller db cannot be null");
Objects.requireNonNull(curator, "Curator cannot be null");
- Objects.requireNonNull(rotationRepository, "Rotation repository cannot be null");
+ Objects.requireNonNull(rotationsConfig, "RotationsConfig cannot be null");
Objects.requireNonNull(gitHub, "GitHubClient cannot be null");
Objects.requireNonNull(entityService, "EntityService cannot be null");
Objects.requireNonNull(organization, "Organization cannot be null");
@@ -117,7 +114,6 @@ public class Controller extends AbstractComponent {
Objects.requireNonNull(clock, "Clock cannot be null");
Objects.requireNonNull(athenzClientFactory, "Athens cannot be null");
- this.rotationRepository = rotationRepository;
this.curator = curator;
this.gitHub = gitHub;
this.entityService = entityService;
@@ -130,7 +126,8 @@ public class Controller extends AbstractComponent {
this.clock = clock;
this.athenzClientFactory = athenzClientFactory;
- applicationController = new ApplicationController(this, db, curator, rotationRepository, athenzClientFactory,
+ applicationController = new ApplicationController(this, db, curator, athenzClientFactory,
+ rotationsConfig,
nameService, configServerClient, routingGenerator, clock);
tenantController = new TenantController(this, db, curator, entityService, athenzClientFactory);
}
@@ -181,10 +178,6 @@ public class Controller extends AbstractComponent {
return kibanaHost.map(uri -> uri.resolve(kibanaPath)).orElse(null);
}
- public Set<URI> getRotationUris(ApplicationId id) {
- return rotationRepository.getRotationUris(id);
- }
-
public Map<String, RotationStatus> getHealthStatus(String hostname) {
return globalRoutingService.getHealthStatus(hostname);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
index e8c8f8a389c..72ed1a42435 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
@@ -6,10 +6,12 @@ import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -18,7 +20,7 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.LinkedHashMap;
@@ -34,12 +36,6 @@ import java.util.Optional;
*/
public class LockedApplication extends Application {
- private LockedApplication(Builder builder) {
- super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides,
- builder.deployments, builder.deploymentJobs, builder.deploying,
- builder.hasOutstandingChange, builder.ownershipIssueId, builder.metrics);
- }
-
/**
* Used to create a locked application
*
@@ -50,6 +46,12 @@ public class LockedApplication extends Application {
this(new Builder(application));
}
+ private LockedApplication(Builder builder) {
+ super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides,
+ builder.deployments, builder.deploymentJobs, builder.deploying,
+ builder.hasOutstandingChange, builder.ownershipIssueId, builder.metrics, builder.rotation);
+ }
+
public LockedApplication withProjectId(long projectId) {
return new LockedApplication(new Builder(this).with(deploymentJobs().withProjectId(projectId)));
}
@@ -67,7 +69,7 @@ public class LockedApplication extends Application {
return new LockedApplication(new Builder(this).with(deploymentJobs().withTriggering(type, change, version, revision, reason, triggerTime)));
}
- public LockedApplication withNewDeployment(Zone zone, ApplicationRevision revision, Version version, Instant instant) {
+ public LockedApplication withNewDeployment(ZoneId zone, ApplicationRevision revision, Version version, Instant instant) {
// Use info from previous deployment if available, otherwise create a new one.
Deployment previousDeployment = deployments().getOrDefault(zone, new Deployment(zone, revision, version, instant));
Deployment newDeployment = new Deployment(zone, revision, version, instant,
@@ -77,27 +79,27 @@ public class LockedApplication extends Application {
return with(newDeployment);
}
- public LockedApplication withClusterUtilization(Zone zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) {
+ public LockedApplication withClusterUtilization(ZoneId zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) {
Deployment deployment = deployments().get(zone);
if (deployment == null) return this; // No longer deployed in this zone.
return with(deployment.withClusterUtils(clusterUtilization));
}
- public LockedApplication withClusterInfo(Zone zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) {
+ public LockedApplication withClusterInfo(ZoneId zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) {
Deployment deployment = deployments().get(zone);
if (deployment == null) return this; // No longer deployed in this zone.
return with(deployment.withClusterInfo(clusterInfo));
}
- public LockedApplication with(Zone zone, DeploymentMetrics deploymentMetrics) {
+ public LockedApplication with(ZoneId zone, DeploymentMetrics deploymentMetrics) {
Deployment deployment = deployments().get(zone);
if (deployment == null) return this; // No longer deployed in this zone.
return with(deployment.withMetrics(deploymentMetrics));
}
- public LockedApplication withoutDeploymentIn(Zone zone) {
- Map<Zone, Deployment> deployments = new LinkedHashMap<>(deployments());
+ public LockedApplication withoutDeploymentIn(ZoneId zone) {
+ Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(deployments());
deployments.remove(zone);
return new LockedApplication(new Builder(this).with(deployments));
}
@@ -130,6 +132,10 @@ public class LockedApplication extends Application {
return new LockedApplication(new Builder(this).with(metrics));
}
+ public LockedApplication with(RotationId rotation) {
+ return new LockedApplication(new Builder(this).with(rotation));
+ }
+
public Version deployVersionFor(DeploymentJobs.JobType jobType, Controller controller) {
return jobType == JobType.component
? controller.systemVersion()
@@ -144,7 +150,7 @@ public class LockedApplication extends Application {
/** Don't expose non-leaf sub-objects. */
private LockedApplication with(Deployment deployment) {
- Map<Zone, Deployment> deployments = new LinkedHashMap<>(deployments());
+ Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(deployments());
deployments.put(deployment.zone(), deployment);
return new LockedApplication(new Builder(this).with(deployments));
}
@@ -155,12 +161,13 @@ public class LockedApplication extends Application {
private final ApplicationId applicationId;
private DeploymentSpec deploymentSpec;
private ValidationOverrides validationOverrides;
- private Map<Zone, Deployment> deployments;
+ private Map<ZoneId, Deployment> deployments;
private DeploymentJobs deploymentJobs;
private Optional<Change> deploying;
private boolean hasOutstandingChange;
private Optional<IssueId> ownershipIssueId;
private ApplicationMetrics metrics;
+ private Optional<RotationId> rotation;
private Builder(Application application) {
this.applicationId = application.id();
@@ -172,16 +179,53 @@ public class LockedApplication extends Application {
this.hasOutstandingChange = application.hasOutstandingChange();
this.ownershipIssueId = application.ownershipIssueId();
this.metrics = application.metrics();
+ this.rotation = application.rotation().map(ApplicationRotation::id);
+ }
+
+ private Builder with(DeploymentSpec deploymentSpec) {
+ this.deploymentSpec = deploymentSpec;
+ return this;
}
- private Builder with(DeploymentSpec deploymentSpec) { this.deploymentSpec = deploymentSpec; return this; }
- private Builder with(ValidationOverrides validationOverrides) { this.validationOverrides = validationOverrides; return this; }
- private Builder with(Map<Zone, Deployment> deployments) { this.deployments = deployments; return this; }
- private Builder with(DeploymentJobs deploymentJobs) { this.deploymentJobs = deploymentJobs; return this; }
- private Builder withDeploying(Optional<Change> deploying) { this.deploying = deploying; return this; }
- private Builder with(boolean hasOutstandingChange) { this.hasOutstandingChange = hasOutstandingChange; return this; }
- private Builder withOwnershipIssueId(Optional<IssueId> ownershipIssueId) { this.ownershipIssueId = ownershipIssueId; return this; }
- private Builder with(ApplicationMetrics metrics) { this.metrics = metrics; return this; }
+ private Builder with(ValidationOverrides validationOverrides) {
+ this.validationOverrides = validationOverrides;
+ return this;
+ }
+
+ private Builder with(Map<ZoneId, Deployment> deployments) {
+ this.deployments = deployments;
+ return this;
+ }
+
+ private Builder with(DeploymentJobs deploymentJobs) {
+ this.deploymentJobs = deploymentJobs;
+ return this;
+ }
+
+ private Builder withDeploying(Optional<Change> deploying) {
+ this.deploying = deploying;
+ return this;
+ }
+
+ private Builder with(boolean hasOutstandingChange) {
+ this.hasOutstandingChange = hasOutstandingChange;
+ return this;
+ }
+
+ private Builder withOwnershipIssueId(Optional<IssueId> ownershipIssueId) {
+ this.ownershipIssueId = ownershipIssueId;
+ return this;
+ }
+
+ private Builder with(ApplicationMetrics metrics) {
+ this.metrics = metrics;
+ return this;
+ }
+
+ private Builder with(RotationId rotation) {
+ this.rotation = Optional.of(rotation);
+ return this;
+ }
}
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 9530e9a982c..a52098a4a0f 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
@@ -12,11 +12,11 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsClient;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.persistence.PersistenceException;
@@ -67,7 +67,7 @@ public class TenantController {
public List<Tenant> asList(UserId user) {
Set<UserGroup> userGroups = entityService.getUserGroups(user);
Set<AthenzDomain> userDomains = new HashSet<>(athenzClientFactory.createZtsClientWithServicePrincipal()
- .getTenantDomainsForUser(AthenzUtils.createPrincipal(user)));
+ .getTenantDomainsForUser(AthenzUser.fromUserId(user)));
Predicate<Tenant> hasUsersGroup = (tenant) -> tenant.getUserGroup().isPresent() && userGroups.contains(tenant.getUserGroup().get());
Predicate<Tenant> hasUsersDomain = (tenant) -> tenant.getAthensDomain().isPresent() && userDomains.contains(tenant.getAthensDomain().get());
@@ -200,8 +200,7 @@ public class TenantController {
try (Lock lock = lock(tenantId)) {
Tenant existing = tenant(tenantId).orElseThrow(() -> new NotExistsException(tenantId));
if (existing.isAthensTenant()) return existing; // nothing to do
- log.info("Starting migration of " + existing + " to Athenz domain " + tenantDomain.id() +
- " using " + nToken.getPrincipal());
+ log.info("Starting migration of " + existing + " to Athenz domain " + tenantDomain.id());
if (tenantHaving(tenantDomain).isPresent())
throw new IllegalArgumentException("Could not migrate " + existing + " to " + tenantDomain + ": " +
"This domain is already used by " + tenantHaving(tenantDomain).get());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java
deleted file mode 100644
index a9e144a3227..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/ApplicationAlias.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.api;
-
-import com.yahoo.config.provision.ApplicationId;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-/**
- * A DNS alias for an application endpoint.
- *
- * @author smorgrav
- */
-public class ApplicationAlias {
-
- private static final String dnsSuffix = "global.vespa.yahooapis.com";
-
- private final ApplicationId applicationId;
-
- public ApplicationAlias(ApplicationId applicationId) {
- this.applicationId = applicationId;
- }
-
- @Override
- public String toString() {
- return String.format("%s.%s.%s",
- toDns(applicationId.application().value()),
- toDns(applicationId.tenant().value()),
- dnsSuffix);
- }
-
- private String toDns(String id) {
- return id.replace('_', '-');
- }
-
- public URI toHttpUri() {
- try {
- return new URI("http://" + this + ":4080/");
- } catch(URISyntaxException use) {
- throw new RuntimeException("Illegal URI syntax");
- }
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- ApplicationAlias that = (ApplicationAlias) o;
-
- return applicationId.equals(that.applicationId);
- }
-
- @Override
- public int hashCode() { return applicationId.hashCode(); }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java
new file mode 100644
index 00000000000..e4aed04a01c
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationRotation.java
@@ -0,0 +1,51 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
+
+import java.net.URI;
+
+/**
+ * Represents an application's global rotation.
+ *
+ * @author mpolden
+ */
+public class ApplicationRotation {
+
+ public static final String DNS_SUFFIX = "global.vespa.yahooapis.com";
+ private static final int port = 4080;
+
+ private final URI url;
+ private final RotationId id;
+
+ public ApplicationRotation(ApplicationId application, RotationId id) {
+ this.url = URI.create(String.format("http://%s.%s.%s:%d/",
+ sanitize(application.application().value()),
+ sanitize(application.tenant().value()),
+ DNS_SUFFIX,
+ port));
+ this.id = id;
+ }
+
+ /** ID of the rotation */
+ public RotationId id() {
+ return id;
+ }
+
+ /** URL to this rotation */
+ public URI url() {
+ return url;
+ }
+
+ /** DNS name for this rotation */
+ public String dnsName() {
+ return url.getHost();
+ }
+
+ /** Sanitize by translating '_' to '-' as the former is not allowed in a DNS name */
+ private static String sanitize(String s) {
+ return s.replace('_', '-');
+ }
+
+}
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 98ae5ed1762..b9d07249cb2 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
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.application;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ClusterSpec.Id;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import java.time.Instant;
import java.util.HashMap;
@@ -18,7 +19,7 @@ import java.util.Objects;
*/
public class Deployment {
- private final Zone zone;
+ private final ZoneId zone;
private final ApplicationRevision revision;
private final Version version;
private final Instant deployTime;
@@ -26,11 +27,11 @@ public class Deployment {
private final Map<Id, ClusterInfo> clusterInfo;
private final DeploymentMetrics metrics;
- public Deployment(Zone zone, ApplicationRevision revision, Version version, Instant deployTime) {
+ public Deployment(ZoneId zone, ApplicationRevision revision, Version version, Instant deployTime) {
this(zone, revision, version, deployTime, new HashMap<>(), new HashMap<>(), new DeploymentMetrics());
}
- public Deployment(Zone zone, ApplicationRevision revision, Version version, Instant deployTime,
+ public Deployment(ZoneId zone, ApplicationRevision revision, Version version, Instant deployTime,
Map<Id, ClusterUtilization> clusterUtils, Map<Id, ClusterInfo> clusterInfo, DeploymentMetrics metrics) {
Objects.requireNonNull(zone, "zone cannot be null");
Objects.requireNonNull(revision, "revision cannot be null");
@@ -49,7 +50,7 @@ public class Deployment {
}
/** Returns the zone this was deployed to */
- public Zone zone() { return zone; }
+ public ZoneId zone() { return zone; }
/** Returns the revision of the application which was deployed */
public ApplicationRevision revision() { return revision; }
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java
index 98f8c2a3d99..ec8b2d6d019 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java
@@ -7,19 +7,17 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import java.time.Instant;
import java.util.Collection;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -161,34 +159,36 @@ public class DeploymentJobs {
/** Job types that exist in the build system */
public enum JobType {
-
- component ("component" ),
- systemTest ("system-test" , zone("test" , "us-east-1" ), zone(SystemName.cd, "test" , "cd-us-central-1")),
- stagingTest ("staging-test" , zone("staging", "us-east-3" ), zone(SystemName.cd, "staging", "cd-us-central-1")),
- productionCorpUsEast1 ("production-corp-us-east-1" , zone("prod" , "corp-us-east-1")),
- productionUsEast3 ("production-us-east-3" , zone("prod" , "us-east-3" )),
- productionUsWest1 ("production-us-west-1" , zone("prod" , "us-west-1" )),
- productionUsCentral1 ("production-us-central-1" , zone("prod" , "us-central-1" )),
- productionApNortheast1 ("production-ap-northeast-1" , zone("prod" , "ap-northeast-1")),
- productionApNortheast2 ("production-ap-northeast-2" , zone("prod" , "ap-northeast-2")),
- productionApSoutheast1 ("production-ap-southeast-1" , zone("prod" , "ap-southeast-1")),
- productionEuWest1 ("production-eu-west-1" , zone("prod" , "eu-west-1" )),
- productionCdUsCentral1 ("production-cd-us-central-1", zone(SystemName.cd, "prod", "cd-us-central-1")),
- productionCdUsCentral2 ("production-cd-us-central-2", zone(SystemName.cd, "prod", "cd-us-central-2"));
+// | enum name ------------| job name ------------------| Zone in main system ---------------------------------------| Zone in CD system -------------------------------------------
+ component ("component" , null , null ),
+ systemTest ("system-test" , ZoneId.from("test" , "us-east-1") , ZoneId.from("test" , "cd-us-central-1")),
+ stagingTest ("staging-test" , ZoneId.from("staging", "us-east-3") , ZoneId.from("staging", "cd-us-central-1")),
+ productionCorpUsEast1 ("production-corp-us-east-1" , ZoneId.from("prod" , "corp-us-east-1") , null ),
+ productionUsEast3 ("production-us-east-3" , ZoneId.from("prod" , "us-east-3") , null ),
+ productionUsWest1 ("production-us-west-1" , ZoneId.from("prod" , "us-west-1") , null ),
+ productionUsCentral1 ("production-us-central-1" , ZoneId.from("prod" , "us-central-1") , null ),
+ productionApNortheast1 ("production-ap-northeast-1" , ZoneId.from("prod" , "ap-northeast-1") , null ),
+ productionApNortheast2 ("production-ap-northeast-2" , ZoneId.from("prod" , "ap-northeast-2") , null ),
+ productionApSoutheast1 ("production-ap-southeast-1" , ZoneId.from("prod" , "ap-southeast-1") , null ),
+ productionEuWest1 ("production-eu-west-1" , ZoneId.from("prod" , "eu-west-1") , null ),
+ productionCdUsCentral1 ("production-cd-us-central-1", null , ZoneId.from("prod" , "cd-us-central-1")),
+ productionCdUsCentral2 ("production-cd-us-central-2", null , ZoneId.from("prod" , "cd-us-central-2"));
private final String jobName;
- private final ImmutableMap<SystemName, Zone> zones;
+ private final ImmutableMap<SystemName, ZoneId> zones;
- JobType(String jobName, Zone... zones) {
+ JobType(String jobName, ZoneId mainZone, ZoneId cdZone) {
this.jobName = jobName;
- this.zones = ImmutableMap.copyOf(Stream.of(zones).collect(Collectors.toMap(zone -> zone.system(),
- zone -> zone)));
+ ImmutableMap.Builder<SystemName, ZoneId> builder = ImmutableMap.builder();
+ if (mainZone != null) builder.put(SystemName.main, mainZone);
+ if (cdZone != null) builder.put(SystemName.cd, cdZone);
+ this.zones = builder.build();
}
public String jobName() { return jobName; }
/** Returns the zone for this job in the given system, or empty if this job does not have a zone */
- public Optional<Zone> zone(SystemName system) {
+ public Optional<ZoneId> zone(SystemName system) {
return Optional.ofNullable(zones.get(system));
}
@@ -207,7 +207,7 @@ public class DeploymentJobs {
/** Returns the region of this job type, or null if it does not have a region */
public Optional<RegionName> region(SystemName system) {
- return zone(system).map(Zone::region);
+ return zone(system).map(ZoneId::region);
}
public static JobType fromJobName(String jobName) {
@@ -217,7 +217,7 @@ public class DeploymentJobs {
}
/** Returns the job type for the given zone */
- public static Optional<JobType> from(SystemName system, Zone zone) {
+ public static Optional<JobType> from(SystemName system, ZoneId zone) {
return Stream.of(values())
.filter(job -> job.zone(system).filter(zone::equals).isPresent())
.findAny();
@@ -229,16 +229,9 @@ public class DeploymentJobs {
case test: return Optional.of(systemTest);
case staging: return Optional.of(stagingTest);
}
- return from(system, new Zone(environment, region));
- }
-
- private static Zone zone(SystemName system, String environment, String region) {
- return new Zone(system, Environment.from(environment), RegionName.from(region));
+ return from(system, ZoneId.from(environment, region));
}
- private static Zone zone(String environment, String region) {
- return new Zone(Environment.from(environment), RegionName.from(region));
- }
}
/** A job report. This class is immutable. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ApplicationAction.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ApplicationAction.java
deleted file mode 100644
index 8614414dc95..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ApplicationAction.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// 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.athenz;
-
-/**
- * @author bjorncs
- */
-public enum ApplicationAction {
- deploy("deployer"),
- read("reader"),
- write("writer");
-
- public final String roleName;
-
- ApplicationAction(String roleName) {
- this.roleName = roleName;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzClientFactory.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzClientFactory.java
deleted file mode 100644
index b6a21f94f74..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzClientFactory.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.athenz;
-
-/**
- * @author bjorncs
- */
-public interface AthenzClientFactory {
-
- ZmsClient createZmsClientWithServicePrincipal();
-
- ZtsClient createZtsClientWithServicePrincipal();
-
- ZmsClient createZmsClientWithAuthorizedServiceToken(NToken authorizedServiceToken);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPrincipal.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPrincipal.java
deleted file mode 100644
index 1e4952a39c5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPrincipal.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-
-import java.security.Principal;
-import java.util.Objects;
-
-/**
- * @author bjorncs
- */
-public class AthenzPrincipal implements Principal {
-
- private final AthenzDomain domain;
- private final UserId userId;
-
- public AthenzPrincipal(AthenzDomain domain, UserId userId) {
- this.domain = domain;
- this.userId = userId;
- }
-
- public UserId getUserId() {
- return userId;
- }
-
- public AthenzDomain getDomain() {
- return domain;
- }
-
- public String toYRN() {
- return domain.id() + "." + userId.id();
- }
-
- @Override
- public String getName() {
- return userId.id();
- }
-
- @Override
- public String toString() {
- return "AthenzPrincipal{" +
- "domain=" + domain +
- ", userId=" + userId +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AthenzPrincipal that = (AthenzPrincipal) o;
- return Objects.equals(domain, that.domain) &&
- Objects.equals(userId, that.userId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(domain, userId);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPublicKey.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPublicKey.java
deleted file mode 100644
index 01596ead0f4..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzPublicKey.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// 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.athenz;
-
-import java.security.PublicKey;
-import java.util.Objects;
-
-/**
- * @author bjorncs
- */
-public class AthenzPublicKey {
-
- private final PublicKey publicKey;
- private final String keyId;
-
- public AthenzPublicKey(PublicKey publicKey, String keyId) {
- this.publicKey = publicKey;
- this.keyId = keyId;
- }
-
- public PublicKey getPublicKey() {
- return publicKey;
- }
-
- public String getKeyId() {
- return keyId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AthenzPublicKey that = (AthenzPublicKey) o;
- return Objects.equals(publicKey, that.publicKey) &&
- Objects.equals(keyId, that.keyId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(publicKey, keyId);
- }
-
- @Override
- public String toString() {
- return "AthenzPublicKey{" +
- "publicKey=" + publicKey +
- ", keyId='" + keyId + '\'' +
- '}';
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzService.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzService.java
deleted file mode 100644
index 37c6459b687..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzService.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-
-import java.util.Objects;
-
-/**
- * @author bjorncs
- */
-public class AthenzService {
-
- private final AthenzDomain domain;
- private final String serviceName;
-
- public AthenzService(AthenzDomain domain, String serviceName) {
- this.domain = domain;
- this.serviceName = serviceName;
- }
-
- public AthenzService(String domain, String serviceName) {
- this(new AthenzDomain(domain), serviceName);
- }
-
- public String toFullServiceName() {
- return domain.id() + "." + serviceName;
- }
-
- public AthenzDomain getDomain() {
- return domain;
- }
-
- public String getServiceName() {
- return serviceName;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AthenzService that = (AthenzService) o;
- return Objects.equals(domain, that.domain) &&
- Objects.equals(serviceName, that.serviceName);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(domain, serviceName);
- }
-
- @Override
- public String toString() {
- return String.format("AthenzService(%s)", toFullServiceName());
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzUtils.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzUtils.java
deleted file mode 100644
index 18bd626369d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/AthenzUtils.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-
-/**
- * @author bjorncs
- */
-public class AthenzUtils {
-
- private AthenzUtils() {}
-
- public static final AthenzDomain USER_PRINCIPAL_DOMAIN = new AthenzDomain("user");
- public static final AthenzDomain SCREWDRIVER_DOMAIN = new AthenzDomain("cd.screwdriver.project");
- public static final AthenzService ZMS_ATHENZ_SERVICE = new AthenzService("sys.auth", "zms");
-
- public static AthenzPrincipal createPrincipal(UserId userId) {
- return new AthenzPrincipal(USER_PRINCIPAL_DOMAIN, userId);
- }
-
- public static AthenzPrincipal createPrincipal(ScrewdriverId screwdriverId) {
- return new AthenzPrincipal(SCREWDRIVER_DOMAIN, new UserId("sd" + screwdriverId.id()));
- }
-
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/InvalidTokenException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/InvalidTokenException.java
deleted file mode 100644
index e41bd8d4283..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/InvalidTokenException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// 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.athenz;
-
-/**
- * @author bjorncs
- */
-public class InvalidTokenException extends Exception {
- public InvalidTokenException(String message) {
- super(message);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/NToken.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/NToken.java
deleted file mode 100644
index 7e3abeb77d9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/NToken.java
+++ /dev/null
@@ -1,148 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.athenz.auth.token.PrincipalToken;
-import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalLong;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Represents an Athenz NToken (principal token)
- *
- * @author bjorncs
- */
-// TODO Split out encoding/decoding of token into separate class. Move NToken to controller-api.
-public class NToken {
-
- // Max allowed skew in token timestamp (only for creation, not expiry timestamp)
- private static final int ALLOWED_TIMESTAMP_OFFSET = (int) TimeUnit.SECONDS.toSeconds(300);
-
- private final PrincipalToken token;
-
- // Note: PrincipalToken does not provide any way of constructing an instance from a unsigned token string
- public NToken(String signedToken) {
- try {
- this.token = new PrincipalToken(signedToken);
- if (this.token.getSignature() == null) {
- throw new IllegalArgumentException("Signature missing (unsigned token)");
- }
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException("Malformed NToken: " + e.getMessage());
- }
- }
-
- public AthenzPrincipal getPrincipal() {
- return new AthenzPrincipal(getDomain(), getUser());
- }
-
- public UserId getUser() {
- return new UserId(token.getName());
- }
-
- public AthenzDomain getDomain() {
- return new AthenzDomain(token.getDomain());
- }
-
- public String getToken() {
- return token.getSignedToken();
- }
-
- public String getKeyId() {
- return token.getKeyId();
- }
-
- public void validateSignatureAndExpiration(PublicKey publicKey) throws InvalidTokenException {
- StringBuilder errorMessageBuilder = new StringBuilder();
- if (!token.validate(publicKey, ALLOWED_TIMESTAMP_OFFSET, true, errorMessageBuilder)) {
- throw new InvalidTokenException("NToken is expired or has invalid signature: " + errorMessageBuilder.toString());
- }
- }
-
- @Override
- public String toString() {
- return String.format("NToken(%s)", getToken());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- NToken nToken = (NToken) o;
- return Objects.equals(getToken(), nToken.getToken()); // PrincipalToken does not implement equals()
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(getToken()); // PrincipalToken does not implement hashcode()
- }
-
- public static class Builder {
-
- private final String version;
- private final AthenzPrincipal principal;
- private final PrivateKey privateKey;
- private final String keyId;
- private Optional<String> salt = Optional.empty();
- private Optional<String> hostname = Optional.empty();
- private Optional<String> ip = Optional.empty();
- private OptionalLong issueTime = OptionalLong.empty();
- private OptionalLong expirationWindow = OptionalLong.empty();
-
- /**
- * NOTE: We must have some signature, else we might end up with problems later on as
- * {@link PrincipalToken#PrincipalToken(String)} only accepts signed token
- * (supplying an unsigned token to the constructor will result in inconsistent state)
- */
- public Builder(String version, AthenzPrincipal principal, PrivateKey privateKey, String keyId) {
- this.version = version;
- this.principal = principal;
- this.privateKey = privateKey;
- this.keyId = keyId;
- }
-
- public Builder salt(String salt) {
- this.salt = Optional.of(salt);
- return this;
- }
-
- public Builder hostname(String hostname) {
- this.hostname = Optional.of(hostname);
- return this;
- }
-
- public Builder ip(String ip) {
- this.ip = Optional.of(ip);
- return this;
- }
-
- public Builder issueTime(long issueTime) {
- this.issueTime = OptionalLong.of(issueTime);
- return this;
- }
-
- public Builder expirationWindow(long expirationWindow) {
- this.expirationWindow = OptionalLong.of(expirationWindow);
- return this;
- }
-
- public NToken build() {
- PrincipalToken token = new PrincipalToken.Builder(version, principal.getDomain().id(), principal.getName())
- .keyId(this.keyId)
- .salt(this.salt.orElse(null))
- .host(this.hostname.orElse(null))
- .ip(this.ip.orElse(null))
- .issueTime(this.issueTime.orElse(0))
- .expirationWindow(this.expirationWindow.orElse(0))
- .build();
- token.sign(this.privateKey);
- return new NToken(token.getSignedToken());
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsClient.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsClient.java
deleted file mode 100644
index 407bce05c6e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsClient.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-
-import java.util.List;
-
-/**
- * @author bjorncs
- */
-public interface ZmsClient {
-
- void createTenant(AthenzDomain tenantDomain);
-
- void deleteTenant(AthenzDomain tenantDomain);
-
- void addApplication(AthenzDomain tenantDomain, ApplicationId applicationName);
-
- void deleteApplication(AthenzDomain tenantDomain, ApplicationId applicationName);
-
- boolean hasApplicationAccess(AthenzPrincipal principal, ApplicationAction action, AthenzDomain tenantDomain, ApplicationId applicationName);
-
- boolean hasTenantAdminAccess(AthenzPrincipal principal, AthenzDomain tenantDomain);
-
- // Used before vespa tenancy is established for the domain.
- boolean isDomainAdmin(AthenzPrincipal principal, AthenzDomain domain);
-
- List<AthenzDomain> getDomainList(String prefix);
-
- AthenzPublicKey getPublicKey(AthenzService service, String keyId);
-
- List<AthenzPublicKey> getPublicKeys(AthenzService service);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsException.java
deleted file mode 100644
index 59548339d11..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsException.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.athenz.zms.ZMSClientException;
-
-/**
- * @author bjorncs
- */
-public class ZmsException extends RuntimeException {
-
- private final int code;
-
- public ZmsException(ZMSClientException e) {
- super(e.getMessage(), e);
- this.code = e.getCode();
- }
-
-
- public int getCode() {
- return code;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsKeystore.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsKeystore.java
deleted file mode 100644
index 93fed95c768..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZmsKeystore.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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.athenz;
-
-import java.security.PublicKey;
-import java.util.Optional;
-
-/**
- * @author bjorncs
- */
-public interface ZmsKeystore {
-
- Optional<PublicKey> getPublicKey(AthenzService service, String keyId);
-
- default void preloadKeys(AthenzService service) { /* Default implementation is noop */ }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsClient.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsClient.java
deleted file mode 100644
index f400ba2eb99..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsClient.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-
-import java.util.List;
-
-/**
- * @author bjorncs
- */
-public interface ZtsClient {
-
- List<AthenzDomain> getTenantDomainsForUser(AthenzPrincipal principal);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsException.java
deleted file mode 100644
index cb0b21ba459..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/ZtsException.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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.athenz;
-
-import com.yahoo.athenz.zts.ZTSClientException;
-
-/**
- * @author bjorncs
- */
-public class ZtsException extends RuntimeException {
-
- private final int code;
-
- public ZtsException(ZTSClientException e) {
- super(e.getMessage(), e);
- this.code = e.getCode();
- }
-
-
- public int getCode() {
- return code;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java
index 51865be04fa..328461355db 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java
@@ -6,10 +6,10 @@ import com.yahoo.jdisc.Response;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.SecurityRequestFilter;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.InvalidTokenException;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.InvalidTokenException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsKeystore;
import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
import java.util.concurrent.Executor;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzTrustStoreConfigurator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzTrustStoreConfigurator.java
new file mode 100644
index 00000000000..939a5667a36
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzTrustStoreConfigurator.java
@@ -0,0 +1,45 @@
+// 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.athenz.filter;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.http.ssl.SslTrustStoreConfigurator;
+import com.yahoo.jdisc.http.ssl.SslTrustStoreContext;
+import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+
+/**
+ * Load trust store with Athenz CA certificates
+ *
+ * @author bjorncs
+ */
+public class AthenzTrustStoreConfigurator implements SslTrustStoreConfigurator {
+
+ private final KeyStore trustStore;
+
+ @Inject
+ public AthenzTrustStoreConfigurator(AthenzConfig config) {
+ this.trustStore = createTrustStore(new File(config.athenzCaTrustStore()));
+ }
+
+ private static KeyStore createTrustStore(File trustStoreFile) {
+ try (FileInputStream in = new FileInputStream(trustStoreFile)) {
+ KeyStore trustStore = KeyStore.getInstance("JKS");
+ trustStore.load(in, "changeit".toCharArray());
+ return trustStore;
+ } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void configure(SslTrustStoreContext context) {
+ context.updateTrustStore(trustStore);
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidator.java
index f43d2d8e80e..69f59ebabe2 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidator.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidator.java
@@ -1,17 +1,21 @@
// 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.athenz.filter;
+import com.yahoo.athenz.auth.token.PrincipalToken;
import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.InvalidTokenException;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
+import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.InvalidTokenException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsKeystore;
import java.security.PublicKey;
+import java.time.Duration;
import java.util.Optional;
import java.util.logging.Logger;
-import static com.yahoo.vespa.hosted.controller.athenz.AthenzUtils.ZMS_ATHENZ_SERVICE;
+import static com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils.ZMS_ATHENZ_SERVICE;
/**
* Validates the content of an NToken:
@@ -22,6 +26,9 @@ import static com.yahoo.vespa.hosted.controller.athenz.AthenzUtils.ZMS_ATHENZ_SE
*/
class NTokenValidator {
+ // Max allowed skew in token timestamp (only for creation, not expiry timestamp)
+ private static final long ALLOWED_TIMESTAMP_OFFSET = Duration.ofMinutes(5).getSeconds();
+
private static final Logger log = Logger.getLogger(NTokenValidator.class.getName());
private final ZmsKeystore keystore;
@@ -35,10 +42,15 @@ class NTokenValidator {
}
AthenzPrincipal validate(NToken token) throws InvalidTokenException {
- PublicKey zmsPublicKey = getPublicKey(token.getKeyId())
+ PrincipalToken principalToken = new PrincipalToken(token.getRawToken());
+ PublicKey zmsPublicKey = getPublicKey(principalToken.getKeyId())
.orElseThrow(() -> new InvalidTokenException("NToken has an unknown keyId"));
- validateSignatureAndExpiration(token, zmsPublicKey);
- return token.getPrincipal();
+ validateSignatureAndExpiration(principalToken, zmsPublicKey);
+ return new AthenzPrincipal(
+ AthenzUtils.createAthenzIdentity(
+ new AthenzDomain(principalToken.getDomain()),
+ principalToken.getName()),
+ token);
}
private Optional<PublicKey> getPublicKey(String keyId) throws InvalidTokenException {
@@ -50,13 +62,13 @@ class NTokenValidator {
}
}
- private static void validateSignatureAndExpiration(NToken token, PublicKey zmsPublicKey) throws InvalidTokenException {
- try {
- token.validateSignatureAndExpiration(zmsPublicKey);
- } catch (InvalidTokenException e) {
- // The underlying error message is not user friendly
- logDebug(e.getMessage());
- throw new InvalidTokenException("NToken is expired or has invalid signature");
+ private static void validateSignatureAndExpiration(PrincipalToken token,
+ PublicKey zmsPublicKey) throws InvalidTokenException {
+ StringBuilder errorMessageBuilder = new StringBuilder();
+ if (!token.validate(zmsPublicKey, (int) ALLOWED_TIMESTAMP_OFFSET, true, errorMessageBuilder)) {
+ String message = "NToken is expired or has invalid signature: " + errorMessageBuilder.toString();
+ logDebug(message);
+ throw new InvalidTokenException(message);
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java
index bfa543f160a..b4859220667 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java
@@ -8,13 +8,15 @@ import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsKeystore;
import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
import com.yahoo.vespa.hosted.controller.restapi.application.Authorizer;
import java.security.Principal;
+import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import java.util.stream.Stream;
@@ -34,11 +36,13 @@ public class UserAuthWithAthenzPrincipalFilter extends AthenzPrincipalFilter {
private static final Logger log = Logger.getLogger(UserAuthWithAthenzPrincipalFilter.class.getName());
private final String userAuthenticationPassThruAttribute;
+ private final String principalHeaderName;
@Inject
public UserAuthWithAthenzPrincipalFilter(ZmsKeystore zmsKeystore, Executor executor, AthenzConfig config) {
super(zmsKeystore, executor, config);
this.userAuthenticationPassThruAttribute = config.userAuthenticationPassThruAttribute();
+ this.principalHeaderName = config.principalHeaderName();
}
@Override
@@ -81,13 +85,14 @@ public class UserAuthWithAthenzPrincipalFilter extends AthenzPrincipalFilter {
* NOTE: The Bouncer user roles ({@link DiscFilterRequest#roles} are still intact as they are required
* for {@link Authorizer#isMemberOfVespaBouncerGroup(HttpRequest)}.
*/
- private static void rewriteUserPrincipalToAthenz(DiscFilterRequest request) {
+ private void rewriteUserPrincipalToAthenz(DiscFilterRequest request) {
Principal userPrincipal = request.getUserPrincipal();
log.log(LogLevel.DEBUG, () -> "Original user principal: " + userPrincipal.toString());
UserId userId = new UserId(userPrincipal.getName());
- AthenzPrincipal athenzPrincipal = AthenzUtils.createPrincipal(userId);
- request.setUserPrincipal(athenzPrincipal);
- request.setRemoteUser(athenzPrincipal.toYRN());
+ AthenzUser athenzIdentity = AthenzUser.fromUserId(userId);
+ request.setRemoteUser(athenzIdentity.getFullName());
+ NToken nToken = Optional.ofNullable(request.getHeader(principalHeaderName)).map(NToken::new).orElse(null);
+ request.setUserPrincipal(new AthenzPrincipal(athenzIdentity, nToken));
}
private enum UserAuthenticationResult {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java
index 1c32b35f599..a91604f937b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java
@@ -10,17 +10,17 @@ import com.yahoo.athenz.auth.token.PrincipalToken;
import com.yahoo.athenz.auth.util.Crypto;
import com.yahoo.athenz.zms.ZMSClient;
import com.yahoo.athenz.zts.ZTSClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZtsClient;
import com.yahoo.vespa.hosted.controller.api.integration.security.KeyService;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsClient;
-import com.yahoo.vespa.hosted.controller.athenz.ZtsClient;
import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
import java.security.PrivateKey;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
-import static com.yahoo.vespa.hosted.controller.athenz.AthenzUtils.USER_PRINCIPAL_DOMAIN;
+import static com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils.USER_PRINCIPAL_DOMAIN;
/**
* @author bjorncs
@@ -51,7 +51,7 @@ public class AthenzClientFactoryImpl implements AthenzClientFactory {
*/
@Override
public ZtsClient createZtsClientWithServicePrincipal() {
- return new ZtsClientImpl(new ZTSClient(config.ztsUrl(), createServicePrincipal()), config);
+ return new ZtsClientImpl(new ZTSClient(config.ztsUrl(), createServicePrincipal()), getServicePrivateKey(), config);
}
/**
@@ -59,7 +59,7 @@ public class AthenzClientFactoryImpl implements AthenzClientFactory {
*/
@Override
public ZmsClient createZmsClientWithAuthorizedServiceToken(NToken authorizedServiceToken) {
- PrincipalToken signedToken = new PrincipalToken(authorizedServiceToken.getToken());
+ PrincipalToken signedToken = new PrincipalToken(authorizedServiceToken.getRawToken());
AthenzConfig.Service service = config.service();
signedToken.signForAuthorizedService(
config.domain() + "." + service.name(), service.publicKeyId(), getServicePrivateKey());
@@ -75,8 +75,12 @@ public class AthenzClientFactoryImpl implements AthenzClientFactory {
// TODO bjorncs: Cache principal token
SimpleServiceIdentityProvider identityProvider =
new SimpleServiceIdentityProvider(
- athenzPrincipalAuthority, config.domain(), service.name(),
- getServicePrivateKey(), service.publicKeyId(), /*tokenTimeout*/TimeUnit.HOURS.toSeconds(1));
+ athenzPrincipalAuthority,
+ config.domain(),
+ service.name(),
+ getServicePrivateKey(),
+ service.publicKeyId(),
+ Duration.ofMinutes(service.credentialsExpiryMinutes()).getSeconds());
return identityProvider.getIdentity(config.domain(), service.name());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzSslContextProviderImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzSslContextProviderImpl.java
new file mode 100644
index 00000000000..3a7a72ac8ae
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzSslContextProviderImpl.java
@@ -0,0 +1,87 @@
+// 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.athenz.impl;
+
+import com.google.inject.Inject;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentityCertificate;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzSslContextProvider;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZtsClient;
+import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+
+/**
+ * @author bjorncs
+ */
+public class AthenzSslContextProviderImpl implements AthenzSslContextProvider {
+
+ private final AthenzClientFactory clientFactory;
+ private final AthenzConfig config;
+
+ @Inject
+ public AthenzSslContextProviderImpl(AthenzClientFactory clientFactory, AthenzConfig config) {
+ this.clientFactory = clientFactory;
+ this.config = config;
+ }
+
+ @Override
+ public SSLContext get() {
+ return createSslContext();
+ }
+
+ private SSLContext createSslContext() {
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(createKeyManagersWithServiceCertificate(clientFactory.createZtsClientWithServicePrincipal()),
+ createTrustManagersWithAthenzCa(config),
+ null);
+ return sslContext;
+ } catch (NoSuchAlgorithmException | KeyManagementException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static KeyManager[] createKeyManagersWithServiceCertificate(ZtsClient ztsClient) {
+ try {
+ AthenzIdentityCertificate identityCertificate = ztsClient.getIdentityCertificate();
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(null);
+ keyStore.setKeyEntry("athenz-controller-key",
+ identityCertificate.getPrivateKey(),
+ new char[0],
+ new Certificate[]{identityCertificate.getCertificate()});
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, new char[0]);
+ return keyManagerFactory.getKeyManagers();
+ } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static TrustManager[] createTrustManagersWithAthenzCa(AthenzConfig config) {
+ try {
+ KeyStore trustStore = KeyStore.getInstance("JKS");
+ try (FileInputStream in = new FileInputStream(config.athenzCaTrustStore())) {
+ trustStore.load(in, "changeit".toCharArray());
+ }
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore);
+ return trustManagerFactory.getTrustManagers();
+ } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientImpl.java
index 110e06b767c..d3fac257583 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientImpl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientImpl.java
@@ -13,12 +13,12 @@ import com.yahoo.athenz.zms.ZMSClientException;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPublicKey;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzService;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsClient;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPublicKey;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzService;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
import java.util.Arrays;
@@ -49,16 +49,16 @@ public class ZmsClientImpl implements ZmsClient {
runOrThrow(() -> {
Tenancy tenancy = new Tenancy()
.setDomain(tenantDomain.id())
- .setService(service.toFullServiceName())
+ .setService(service.getFullName())
.setResourceGroups(Collections.emptyList());
- zmsClient.putTenancy(tenantDomain.id(), service.toFullServiceName(), /*auditref*/null, tenancy);
+ zmsClient.putTenancy(tenantDomain.id(), service.getFullName(), /*auditref*/null, tenancy);
});
}
@Override
public void deleteTenant(AthenzDomain tenantDomain) {
log("deleteTenancy(tenantDomain=%s, service=%s)", tenantDomain, service);
- runOrThrow(() -> zmsClient.deleteTenancy(tenantDomain.id(), service.toFullServiceName(), /*auditref*/null));
+ runOrThrow(() -> zmsClient.deleteTenancy(tenantDomain.id(), service.getFullName(), /*auditref*/null));
}
@Override
@@ -66,16 +66,16 @@ public class ZmsClientImpl implements ZmsClient {
List<TenantRoleAction> tenantRoleActions = createTenantRoleActions();
log("putProviderResourceGroupRoles(" +
"tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s, roleActions=%s)",
- tenantDomain, service.getDomain().id(), service.getServiceName(), applicationName, tenantRoleActions);
+ tenantDomain, service.getDomain().id(), service.getName(), applicationName, tenantRoleActions);
runOrThrow(() -> {
ProviderResourceGroupRoles resourceGroupRoles = new ProviderResourceGroupRoles()
.setDomain(service.getDomain().id())
- .setService(service.getServiceName())
+ .setService(service.getName())
.setTenant(tenantDomain.id())
.setResourceGroup(applicationName.id())
.setRoles(tenantRoleActions);
zmsClient.putProviderResourceGroupRoles(
- tenantDomain.id(), service.getDomain().id(), service.getServiceName(),
+ tenantDomain.id(), service.getDomain().id(), service.getName(),
applicationName.id(), /*auditref*/null, resourceGroupRoles);
});
}
@@ -83,34 +83,34 @@ public class ZmsClientImpl implements ZmsClient {
@Override
public void deleteApplication(AthenzDomain tenantDomain, ApplicationId applicationName) {
log("deleteProviderResourceGroupRoles(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
- tenantDomain, service.getDomain().id(), service.getServiceName(), applicationName);
+ tenantDomain, service.getDomain().id(), service.getName(), applicationName);
runOrThrow(() -> {
zmsClient.deleteProviderResourceGroupRoles(
- tenantDomain.id(), service.getDomain().id(), service.getServiceName(), applicationName.id(), /*auditref*/null);
+ tenantDomain.id(), service.getDomain().id(), service.getName(), applicationName.id(), /*auditref*/null);
});
}
@Override
public boolean hasApplicationAccess(
- AthenzPrincipal principal, ApplicationAction action, AthenzDomain tenantDomain, ApplicationId applicationName) {
+ AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationId applicationName) {
return hasAccess(
- action.name(), applicationResourceString(tenantDomain, applicationName), principal);
+ action.name(), applicationResourceString(tenantDomain, applicationName), identity);
}
@Override
- public boolean hasTenantAdminAccess(AthenzPrincipal principal, AthenzDomain tenantDomain) {
- return hasAccess(TenantAction._modify_.name(), tenantResourceString(tenantDomain), principal);
+ public boolean hasTenantAdminAccess(AthenzIdentity identity, AthenzDomain tenantDomain) {
+ return hasAccess(TenantAction._modify_.name(), tenantResourceString(tenantDomain), identity);
}
/**
* Used when creating tenancies. As there are no tenancy policies at this point,
- * we cannot use {@link #hasTenantAdminAccess(AthenzPrincipal, AthenzDomain)}
+ * we cannot use {@link #hasTenantAdminAccess(AthenzIdentity, AthenzDomain)}
*/
@Override
- public boolean isDomainAdmin(AthenzPrincipal principal, AthenzDomain domain) {
- log("getMembership(domain=%s, role=%s, principal=%s)", domain, "admin", principal);
+ public boolean isDomainAdmin(AthenzIdentity identity, AthenzDomain domain) {
+ log("getMembership(domain=%s, role=%s, principal=%s)", domain, "admin", identity);
return getOrThrow(
- () -> zmsClient.getMembership(domain.id(), "admin", principal.toYRN()).getIsMember());
+ () -> zmsClient.getMembership(domain.id(), "admin", identity.getFullName()).getIsMember());
}
@Override
@@ -127,18 +127,18 @@ public class ZmsClientImpl implements ZmsClient {
@Override
public AthenzPublicKey getPublicKey(AthenzService service, String keyId) {
- log("getPublicKeyEntry(domain=%s, service=%s, keyId=%s)", service.getDomain().id(), service.getServiceName(), keyId);
+ log("getPublicKeyEntry(domain=%s, service=%s, keyId=%s)", service.getDomain().id(), service.getName(), keyId);
return getOrThrow(() -> {
- PublicKeyEntry entry = zmsClient.getPublicKeyEntry(service.getDomain().id(), service.getServiceName(), keyId);
+ PublicKeyEntry entry = zmsClient.getPublicKeyEntry(service.getDomain().id(), service.getName(), keyId);
return fromYbase64EncodedKey(entry.getKey(), keyId);
});
}
@Override
public List<AthenzPublicKey> getPublicKeys(AthenzService service) {
- log("getServiceIdentity(domain=%s, service=%s)", service.getDomain().id(), service.getServiceName());
+ log("getServiceIdentity(domain=%s, service=%s)", service.getDomain().id(), service.getName());
return getOrThrow(() -> {
- ServiceIdentity serviceIdentity = zmsClient.getServiceIdentity(service.getDomain().id(), service.getServiceName());
+ ServiceIdentity serviceIdentity = zmsClient.getServiceIdentity(service.getDomain().id(), service.getName());
return toAthenzPublicKeys(serviceIdentity.getPublicKeys());
});
}
@@ -163,10 +163,11 @@ public class ZmsClientImpl implements ZmsClient {
.collect(toList());
}
- private boolean hasAccess(String action, String resource, AthenzPrincipal principal) {
- log("getAccess(action=%s, resource=%s, principal=%s)", action, resource, principal);
+ private boolean hasAccess(String action, String resource, AthenzIdentity identity) {
+ log("getAccess(action=%s, resource=%s, principal=%s)", action, resource, identity);
return getOrThrow(
- () -> zmsClient.getAccess(action, resource, /*trustDomain*/null, principal.toYRN()).getGranted());
+ () -> zmsClient.getAccess(action, resource, /*trustDomain*/null, identity.getFullName())
+ .getGranted());
}
private static void log(String format, Object... args) {
@@ -178,7 +179,7 @@ public class ZmsClientImpl implements ZmsClient {
wrappedCode.run();
} catch (ZMSClientException e) {
logWarning(e);
- throw new ZmsException(e);
+ throw new ZmsException(e.getCode(), e);
}
}
@@ -187,7 +188,7 @@ public class ZmsClientImpl implements ZmsClient {
return wrappedCode.get();
} catch (ZMSClientException e) {
logWarning(e);
- throw new ZmsException(e);
+ throw new ZmsException(e.getCode(), e);
}
}
@@ -197,7 +198,7 @@ public class ZmsClientImpl implements ZmsClient {
private String resourceStringPrefix(AthenzDomain tenantDomain) {
return String.format("%s:service.%s.tenant.%s",
- service.getDomain().id(), service.getServiceName(), tenantDomain.id());
+ service.getDomain().id(), service.getName(), tenantDomain.id());
}
private String tenantResourceString(AthenzDomain tenantDomain) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsKeystoreImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsKeystoreImpl.java
index fd58a3daba7..513434f7273 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsKeystoreImpl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsKeystoreImpl.java
@@ -3,11 +3,11 @@ package com.yahoo.vespa.hosted.controller.athenz.impl;
import com.google.inject.Inject;
import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPublicKey;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzService;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPublicKey;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzService;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsKeystore;
import java.security.PublicKey;
import java.util.List;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZtsClientImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZtsClientImpl.java
index 1111e56c742..a29f2e81fba 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZtsClientImpl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZtsClientImpl.java
@@ -1,18 +1,27 @@
// 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.athenz.impl;
+import com.yahoo.athenz.auth.util.Crypto;
+import com.yahoo.athenz.zts.InstanceRefreshRequest;
+import com.yahoo.athenz.zts.RoleCertificateRequest;
import com.yahoo.athenz.zts.TenantDomains;
import com.yahoo.athenz.zts.ZTSClient;
import com.yahoo.athenz.zts.ZTSClientException;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzService;
-import com.yahoo.vespa.hosted.controller.athenz.ZtsClient;
-import com.yahoo.vespa.hosted.controller.athenz.ZtsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentityCertificate;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzRoleCertificate;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzService;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZtsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZtsException;
import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
import java.util.List;
+import java.util.function.Supplier;
import java.util.logging.Logger;
import static java.util.stream.Collectors.toList;
@@ -26,25 +35,79 @@ public class ZtsClientImpl implements ZtsClient {
private final ZTSClient ztsClient;
private final AthenzService service;
+ private final PrivateKey privateKey;
+ private final String certificateDnsDomain;
+ private final Duration certExpiry;
- public ZtsClientImpl(ZTSClient ztsClient, AthenzConfig config) {
+ public ZtsClientImpl(ZTSClient ztsClient, PrivateKey privateKey, AthenzConfig config) {
this.ztsClient = ztsClient;
this.service = new AthenzService(config.domain(), config.service().name());
+ this.privateKey = privateKey;
+ this.certificateDnsDomain = config.certDnsDomain();
+ this.certExpiry = Duration.ofMinutes(config.service().credentialsExpiryMinutes());
}
@Override
- public List<AthenzDomain> getTenantDomainsForUser(AthenzPrincipal principal) {
- log.log(LogLevel.DEBUG, String.format(
- "getTenantDomains(domain=%s, username=%s, rolename=admin, service=%s)",
- service.getDomain().id(), principal, service.getServiceName()));
- try {
+ public List<AthenzDomain> getTenantDomainsForUser(AthenzIdentity identity) {
+ return getOrThrow(() -> {
+ log.log(LogLevel.DEBUG, String.format(
+ "getTenantDomains(domain=%s, identity=%s, rolename=admin, service=%s)",
+ service.getDomain().id(), identity.getFullName(), service.getFullName()));
TenantDomains domains = ztsClient.getTenantDomains(
- service.getDomain().id(), principal.toYRN(), "admin", service.getServiceName());
+ service.getDomain().id(), identity.getFullName(), "admin", service.getName());
return domains.getTenantDomainNames().stream()
.map(AthenzDomain::new)
.collect(toList());
+ });
+ }
+
+ @Override
+ public AthenzIdentityCertificate getIdentityCertificate() {
+ return getOrThrow(() -> {
+ log.log(LogLevel.DEBUG,
+ String.format("postInstanceRefreshRequest(service=%s)", service.getFullName()));
+ InstanceRefreshRequest req =
+ ZTSClient.generateInstanceRefreshRequest(
+ service.getDomain().id(),
+ service.getName(),
+ privateKey,
+ certificateDnsDomain,
+ (int) certExpiry.getSeconds());
+ X509Certificate certificate = Crypto.loadX509Certificate(
+ ztsClient.postInstanceRefreshRequest(service.getDomain().id(), service.getName(), req)
+ .getCertificate());
+ return new AthenzIdentityCertificate(certificate, privateKey);
+ });
+ }
+
+ @Override
+ public AthenzRoleCertificate getRoleCertificate(AthenzDomain roleDomain, String roleName) {
+ return getOrThrow(() -> {
+ log.log(LogLevel.DEBUG,
+ String.format("postRoleCertificateRequest(service=%s, roleDomain=%s, roleName=%s)",
+ service.getFullName(), roleDomain.id(), roleName));
+ RoleCertificateRequest req =
+ ZTSClient.generateRoleCertificateRequest(
+ service.getDomain().id(),
+ service.getName(),
+ roleDomain.id(),
+ roleName,
+ privateKey,
+ certificateDnsDomain,
+ (int)certExpiry.getSeconds());
+ X509Certificate roleCertificate = Crypto.loadX509Certificate(
+ ztsClient.postRoleCertificateRequest(roleDomain.id(), roleName, req)
+ .getToken());
+ return new AthenzRoleCertificate(roleCertificate, privateKey);
+ });
+ }
+
+ private static <T> T getOrThrow(Supplier<T> wrappedCode) {
+ try {
+ return wrappedCode.get();
} catch (ZTSClientException e) {
- throw new ZtsException(e);
+ log.warning("Error from Athenz: " + e.getMessage());
+ throw new ZtsException(e.getCode(), e);
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzClientFactoryMock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzClientFactoryMock.java
index d4a2d77c115..52a1f2d477d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzClientFactoryMock.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzClientFactoryMock.java
@@ -3,10 +3,10 @@ package com.yahoo.vespa.hosted.controller.athenz.mock;
import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsClient;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.ZtsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZtsClient;
import java.util.logging.Level;
import java.util.logging.Logger;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java
index 017e8c7be44..c633d780e30 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java
@@ -3,8 +3,8 @@ package com.yahoo.vespa.hosted.controller.athenz.mock;
import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
import java.util.HashMap;
import java.util.HashSet;
@@ -26,8 +26,8 @@ public class AthenzDbMock {
public static class Domain {
public final AthenzDomain name;
- public final Set<AthenzPrincipal> admins = new HashSet<>();
- public final Set<AthenzPrincipal> tenantAdmins = new HashSet<>();
+ public final Set<AthenzIdentity> admins = new HashSet<>();
+ public final Set<AthenzIdentity> tenantAdmins = new HashSet<>();
public final Map<ApplicationId, Application> applications = new HashMap<>();
public boolean isVespaTenant = false;
@@ -35,13 +35,13 @@ public class AthenzDbMock {
this.name = name;
}
- public Domain admin(AthenzPrincipal user) {
- admins.add(user);
+ public Domain admin(AthenzIdentity identity) {
+ admins.add(identity);
return this;
}
- public Domain tenantAdmin(AthenzPrincipal user) {
- tenantAdmins.add(user);
+ public Domain tenantAdmin(AthenzIdentity identity) {
+ tenantAdmins.add(identity);
return this;
}
@@ -56,7 +56,7 @@ public class AthenzDbMock {
public static class Application {
- public final Map<ApplicationAction, Set<AthenzPrincipal>> acl = new HashMap<>();
+ public final Map<ApplicationAction, Set<AthenzIdentity>> acl = new HashMap<>();
public Application() {
acl.put(ApplicationAction.deploy, new HashSet<>());
@@ -64,8 +64,8 @@ public class AthenzDbMock {
acl.put(ApplicationAction.write, new HashSet<>());
}
- public Application addRoleMember(ApplicationAction action, AthenzPrincipal user) {
- acl.get(action).add(user);
+ public Application addRoleMember(ApplicationAction action, AthenzIdentity identity) {
+ acl.get(action).add(identity);
return this;
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java
index b2e657eae09..4b50a34094a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java
@@ -1,15 +1,14 @@
// 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.athenz.mock;
-import com.yahoo.athenz.zms.ZMSClientException;
import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPublicKey;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzService;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsClient;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPublicKey;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzService;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import java.util.ArrayList;
import java.util.List;
@@ -61,28 +60,28 @@ public class ZmsClientMock implements ZmsClient {
}
@Override
- public boolean hasApplicationAccess(AthenzPrincipal principal, ApplicationAction action, AthenzDomain tenantDomain, ApplicationId applicationName) {
+ public boolean hasApplicationAccess(AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationId applicationName) {
log("hasApplicationAccess(principal='%s', action='%s', tenantDomain='%s', applicationName='%s')",
- principal, action, tenantDomain, applicationName);
+ identity, action, tenantDomain, applicationName);
AthenzDbMock.Domain domain = getDomainOrThrow(tenantDomain, true);
AthenzDbMock.Application application = domain.applications.get(applicationName);
if (application == null) {
throw zmsException(400, "Application '%s' not found", applicationName);
}
- return domain.admins.contains(principal) || application.acl.get(action).contains(principal);
+ return domain.admins.contains(identity) || application.acl.get(action).contains(identity);
}
@Override
- public boolean hasTenantAdminAccess(AthenzPrincipal principal, AthenzDomain tenantDomain) {
- log("hasTenantAdminAccess(principal='%s', tenantDomain='%s')", principal, tenantDomain);
- return isDomainAdmin(principal, tenantDomain) ||
- getDomainOrThrow(tenantDomain, true).tenantAdmins.contains(principal);
+ public boolean hasTenantAdminAccess(AthenzIdentity identity, AthenzDomain tenantDomain) {
+ log("hasTenantAdminAccess(principal='%s', tenantDomain='%s')", identity, tenantDomain);
+ return isDomainAdmin(identity, tenantDomain) ||
+ getDomainOrThrow(tenantDomain, true).tenantAdmins.contains(identity);
}
@Override
- public boolean isDomainAdmin(AthenzPrincipal principal, AthenzDomain domain) {
- log("isDomainAdmin(principal='%s', domain='%s')", principal, domain);
- return getDomainOrThrow(domain, false).admins.contains(principal);
+ public boolean isDomainAdmin(AthenzIdentity identity, AthenzDomain domain) {
+ log("isDomainAdmin(principal='%s', domain='%s')", identity, domain);
+ return getDomainOrThrow(domain, false).admins.contains(identity);
}
@Override
@@ -111,7 +110,7 @@ public class ZmsClientMock implements ZmsClient {
}
private static ZmsException zmsException(int code, String message, Object... args) {
- return new ZmsException(new ZMSClientException(code, String.format(message, args)));
+ return new ZmsException(code, String.format(message, args));
}
private static void log(String format, Object... args) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZtsClientMock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZtsClientMock.java
index f21bc011273..d778fb550ed 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZtsClientMock.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZtsClientMock.java
@@ -1,10 +1,21 @@
// 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.athenz.mock;
+import com.yahoo.athenz.auth.util.Crypto;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.ZtsClient;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentityCertificate;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzRoleCertificate;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZtsClient;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.pkcs.PKCS10CertificationRequest;
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -24,11 +35,51 @@ public class ZtsClientMock implements ZtsClient {
}
@Override
- public List<AthenzDomain> getTenantDomainsForUser(AthenzPrincipal principal) {
- log.log(Level.INFO, "getTenantDomainsForUser(principal='%s')", principal);
+ public List<AthenzDomain> getTenantDomainsForUser(AthenzIdentity identity) {
+ log.log(Level.INFO, "getTenantDomainsForUser(principal='%s')", identity);
return athenz.domains.values().stream()
- .filter(domain -> domain.tenantAdmins.contains(principal) || domain.admins.contains(principal))
+ .filter(domain -> domain.tenantAdmins.contains(identity) || domain.admins.contains(identity))
.map(domain -> domain.name)
.collect(toList());
}
+
+ @Override
+ public AthenzIdentityCertificate getIdentityCertificate() {
+ log.log(Level.INFO, "getIdentityCertificate()");
+ try {
+ KeyPair keyPair = createKeyPair();
+ String subject = "CN=controller";
+ return new AthenzIdentityCertificate(createCertificate(keyPair, subject), keyPair.getPrivate());
+ } catch (NoSuchAlgorithmException | OperatorCreationException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public AthenzRoleCertificate getRoleCertificate(AthenzDomain roleDomain, String roleName) {
+ log.log(Level.INFO,
+ String.format("getRoleCertificate(roleDomain=%s, roleName=%s)", roleDomain.id(), roleDomain));
+ try {
+ KeyPair keyPair = createKeyPair();
+ String subject = String.format("CN=%s:role.%s", roleDomain.id(), roleName);
+ return new AthenzRoleCertificate(createCertificate(keyPair, subject), keyPair.getPrivate());
+ } catch (NoSuchAlgorithmException | OperatorCreationException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static X509Certificate createCertificate(KeyPair keyPair, String subject) throws
+ OperatorCreationException, IOException {
+ PKCS10CertificationRequest csr =
+ Crypto.getPKCS10CertRequest(
+ Crypto.generateX509CSR(keyPair.getPrivate(), subject, null));
+ return Crypto.generateX509Certificate(csr, keyPair.getPrivate(), new X500Name(subject), 3600, false);
+ }
+
+ private static KeyPair createKeyPair() throws NoSuchAlgorithmException {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(512);
+ return keyGen.genKeyPair();
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
index 7c06ef27ce9..2bf64571bdf 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
@@ -2,7 +2,7 @@
package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.LockedApplication;
@@ -46,7 +46,6 @@ public class DeploymentOrder {
/** Returns a list of jobs to trigger after the given job */
// TODO: This does too much - should just tell us the order, as advertised
- // TODO: You're next!
public List<JobType> nextAfter(JobType job, LockedApplication application) {
if ( ! application.deploying().isPresent()) { // Change was cancelled
return Collections.emptyList();
@@ -106,9 +105,9 @@ public class DeploymentOrder {
/** Returns deployments sorted according to declared zones */
public List<Deployment> sortBy(List<DeploymentSpec.DeclaredZone> zones, Collection<Deployment> deployments) {
- List<Zone> productionZones = zones.stream()
+ List<ZoneId> productionZones = zones.stream()
.filter(z -> z.region().isPresent())
- .map(z -> new Zone(z.environment(), z.region().get()))
+ .map(z -> ZoneId.from(z.environment(), z.region().get()))
.collect(toList());
return deployments.stream()
.sorted(comparingInt(deployment -> productionZones.indexOf(deployment.zone())))
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
index 1eee727214b..f0c950b024b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
@@ -4,8 +4,7 @@ package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.Zone;
-import com.yahoo.vespa.curator.Lock;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -28,7 +27,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.function.Consumer;
import java.util.logging.Logger;
/**
@@ -79,7 +77,7 @@ public class DeploymentTrigger {
* @param report information about the job that just completed
*/
public void triggerFromCompletion(JobReport report) {
- applications().lockedOrThrow(report.applicationId(), application -> {
+ applications().lockOrThrow(report.applicationId(), application -> {
application = application.withJobCompletion(report, clock.instant(), controller);
// Handle successful starting and ending
@@ -132,7 +130,7 @@ public class DeploymentTrigger {
ApplicationList applications = ApplicationList.from(applications().asList());
applications = applications.notPullRequest();
for (Application application : applications.asList())
- applications().lockedIfPresent(application.id(), this::triggerReadyJobs);
+ applications().lockIfPresent(application.id(), this::triggerReadyJobs);
}
/** Find the next step to trigger if any, and triggers it */
@@ -219,7 +217,7 @@ public class DeploymentTrigger {
* @throws IllegalArgumentException if this application already have an ongoing change
*/
public void triggerChange(ApplicationId applicationId, Change change) {
- applications().lockedOrThrow(applicationId, application -> {
+ applications().lockOrThrow(applicationId, application -> {
if (application.deploying().isPresent() && ! application.deploymentJobs().hasFailures())
throw new IllegalArgumentException("Could not start " + change + " on " + application + ": " +
application.deploying().get() + " is already in progress");
@@ -238,7 +236,7 @@ public class DeploymentTrigger {
* @param applicationId the application to trigger
*/
public void cancelChange(ApplicationId applicationId) {
- applications().lockedOrThrow(applicationId, application -> {
+ applications().lockOrThrow(applicationId, application -> {
buildSystem.removeJobs(application.id());
applications().store(application.withDeploying(Optional.empty()));
});
@@ -360,7 +358,7 @@ public class DeploymentTrigger {
*/
private boolean isOnNewerVersionInProductionThan(Version version, Application application, JobType job) {
if ( ! job.isProduction()) return false;
- Optional<Zone> zone = job.zone(controller.system());
+ Optional<ZoneId> zone = job.zone(controller.system());
if ( ! zone.isPresent()) return false;
Deployment existingDeployment = application.deployments().get(zone.get());
if (existingDeployment == null) return false;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
index 09f8df58205..2b7260d5ffa 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
@@ -1,7 +1,6 @@
package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.Tenant;
@@ -86,7 +85,7 @@ public class ApplicationOwnershipConfirmer extends Maintainer {
}
protected void store(IssueId issueId, ApplicationId applicationId) {
- controller().applications().lockedIfPresent(applicationId, application ->
+ controller().applications().lockIfPresent(applicationId, application ->
controller().applications().store(application.withOwnershipIssueId(issueId)));
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java
index ae617f87be6..ad7fa90967b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java
@@ -4,10 +4,9 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.Zone;
-import com.yahoo.vespa.curator.Lock;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeList;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
@@ -46,7 +45,7 @@ public class ClusterInfoMaintainer extends Maintainer {
return node.membership.clusterId;
}
- private Map<ClusterSpec.Id, ClusterInfo> getClusterInfo(NodeList nodes, Zone zone) {
+ private Map<ClusterSpec.Id, ClusterInfo> getClusterInfo(NodeList nodes, ZoneId zone) {
Map<ClusterSpec.Id, ClusterInfo> infoMap = new HashMap<>();
// Group nodes by clusterid
@@ -65,7 +64,8 @@ public class ClusterInfoMaintainer extends Maintainer {
double cpu = 0;
double mem = 0;
double disk = 0;
- if (zone.nodeFlavors().isPresent()) {
+ // TODO: This code was never run. Reenable when flavours are available from a FlavorRegistry or something, or remove.
+ /*if (zone.nodeFlavors().isPresent()) {
Optional<Flavor> flavorOptional = zone.nodeFlavors().get().getFlavor(node.flavor);
if ((flavorOptional.isPresent())) {
Flavor flavor = flavorOptional.get();
@@ -73,7 +73,7 @@ public class ClusterInfoMaintainer extends Maintainer {
mem = flavor.getMinMainMemoryAvailableGb();
disk = flavor.getMinMainMemoryAvailableGb();
}
- }
+ }*/
// Add to map
List<String> hostnames = clusterNodes.stream().map(node1 -> node1.hostname).collect(Collectors.toList());
@@ -93,7 +93,7 @@ public class ClusterInfoMaintainer extends Maintainer {
try {
NodeList nodes = controller().applications().configserverClient().getNodeList(deploymentId);
Map<ClusterSpec.Id, ClusterInfo> clusterInfo = getClusterInfo(nodes, deployment.zone());
- controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().lockIfPresent(application.id(), lockedApplication ->
controller.applications().store(lockedApplication.withClusterInfo(deployment.zone(), clusterInfo)));
}
catch (IOException | IllegalArgumentException e) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java
index 3744be67135..58e32344372 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java
@@ -3,8 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Zone;
-import com.yahoo.vespa.curator.Lock;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
@@ -30,7 +29,7 @@ public class ClusterUtilizationMaintainer extends Maintainer {
this.controller = controller;
}
- private Map<ClusterSpec.Id, ClusterUtilization> getUpdatedClusterUtilizations(ApplicationId app, Zone zone) {
+ private Map<ClusterSpec.Id, ClusterUtilization> getUpdatedClusterUtilizations(ApplicationId app, ZoneId zone) {
Map<String, MetricsService.SystemMetrics> systemMetrics = controller.metricsService().getSystemMetrics(app, zone);
Map<ClusterSpec.Id, ClusterUtilization> utilizationMap = new HashMap<>();
@@ -50,7 +49,7 @@ public class ClusterUtilizationMaintainer extends Maintainer {
Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization = getUpdatedClusterUtilizations(application.id(), deployment.zone());
- controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().lockIfPresent(application.id(), lockedApplication ->
controller().applications().store(lockedApplication.withClusterUtilization(deployment.zone(), clusterUtilization)));
}
}
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 bc2112ac0ca..4e9dd94d8e5 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
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.component.AbstractComponent;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues;
import com.yahoo.vespa.hosted.controller.api.integration.chef.Chef;
@@ -34,11 +35,13 @@ public class ControllerMaintenance extends AbstractComponent {
private final ClusterUtilizationMaintainer clusterUtilizationMaintainer;
private final DeploymentMetricsMaintainer deploymentMetricsMaintainer;
private final ApplicationOwnershipConfirmer applicationOwnershipConfirmer;
+ private final DnsMaintainer dnsMaintainer;
@SuppressWarnings("unused") // instantiated by Dependency Injection
public ControllerMaintenance(MaintainerConfig maintainerConfig, Controller controller, CuratorDb curator,
JobControl jobControl, Metric metric, Chef chefClient,
- DeploymentIssues deploymentIssues, OwnershipIssues ownershipIssues) {
+ DeploymentIssues deploymentIssues, OwnershipIssues ownershipIssues,
+ NameService nameService) {
Duration maintenanceInterval = Duration.ofMinutes(maintainerConfig.intervalMinutes());
this.jobControl = jobControl;
deploymentExpirer = new DeploymentExpirer(controller, maintenanceInterval, jobControl);
@@ -52,6 +55,7 @@ public class ControllerMaintenance extends AbstractComponent {
clusterUtilizationMaintainer = new ClusterUtilizationMaintainer(controller, Duration.ofHours(2), jobControl);
deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(controller, Duration.ofMinutes(10), jobControl);
applicationOwnershipConfirmer = new ApplicationOwnershipConfirmer(controller, Duration.ofHours(12), jobControl, ownershipIssues);
+ dnsMaintainer = new DnsMaintainer(controller, Duration.ofHours(1), jobControl, nameService);
}
public Upgrader upgrader() { return upgrader; }
@@ -72,6 +76,7 @@ public class ControllerMaintenance extends AbstractComponent {
clusterInfoMaintainer.deconstruct();
deploymentMetricsMaintainer.deconstruct();
applicationOwnershipConfirmer.deconstruct();
+ dnsMaintainer.deconstruct();
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
index ae6ba364d25..324868878af 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
@@ -2,7 +2,6 @@
package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.Tenant;
@@ -131,7 +130,7 @@ public class DeploymentIssueReporter extends Maintainer {
}
private void store(ApplicationId id, IssueId issueId) {
- controller().applications().lockedIfPresent(id, application ->
+ controller().applications().lockIfPresent(id, application ->
controller().applications().store(application.withDeploymentIssueId(issueId)));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
index 13eb5075f34..821efba013d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
@@ -1,7 +1,6 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
@@ -34,7 +33,7 @@ public class DeploymentMetricsMaintainer extends Maintainer {
boolean hasWarned = false;
for (Application application : ApplicationList.from(controller().applications().asList()).notPullRequest().asList()) {
try {
- controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().lockIfPresent(application.id(), lockedApplication ->
controller().applications().store(lockedApplication.with(controller().metricsService().getApplicationMetrics(application.id()))));
for (Deployment deployment : application.deployments().values()) {
@@ -46,7 +45,7 @@ public class DeploymentMetricsMaintainer extends Maintainer {
deploymentMetrics.queryLatencyMillis(),
deploymentMetrics.writeLatencyMillis());
- controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().lockIfPresent(application.id(), lockedApplication ->
controller().applications().store(lockedApplication.with(deployment.zone(), appMetrics)));
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java
new file mode 100644
index 00000000000..89394bf4dd9
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java
@@ -0,0 +1,67 @@
+// 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.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
+import com.yahoo.vespa.hosted.controller.rotation.Rotation;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
+import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
+import com.yahoo.vespa.hosted.controller.rotation.RotationRepository;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * Performs DNS maintenance tasks such as removing DNS aliases for unassigned rotations.
+ *
+ * @author mpolden
+ */
+public class DnsMaintainer extends Maintainer {
+
+ private static final Logger log = Logger.getLogger(DnsMaintainer.class.getName());
+
+ private final NameService nameService;
+
+ public DnsMaintainer(Controller controller, Duration interval, JobControl jobControl,
+ NameService nameService) {
+ super(controller, interval, jobControl);
+ this.nameService = nameService;
+ }
+
+ private RotationRepository rotationRepository() {
+ return controller().applications().rotationRepository();
+ }
+
+ @Override
+ protected void maintain() {
+ try (RotationLock lock = rotationRepository().lock()) {
+ Map<RotationId, Rotation> unassignedRotations = rotationRepository().availableRotations(lock);
+ unassignedRotations.values().forEach(this::removeDnsAlias);
+ }
+ }
+
+ /** Remove DNS alias for unassigned rotation */
+ private void removeDnsAlias(Rotation rotation) {
+ // When looking up CNAME by data, the data must be a FQDN
+ Optional<Record> record = nameService.findRecord(Record.Type.CNAME, RecordData.fqdn(rotation.name()));
+ record.filter(this::canUpdate)
+ .ifPresent(r -> {
+ log.warning(String.format("Want to remove DNS record %s (%s) because it points to the unassigned " +
+ "rotation %s (%s)", record.get().id().asString(),
+ record.get().name().asString(), rotation.id().asString(), rotation.name()));
+ // TODO: Actually remove the record
+ //nameService.removeRecord(r.id());
+ });
+ }
+
+ /** Returns whether we can update the given record */
+ private boolean canUpdate(Record record) {
+ return record.name().asString().endsWith(ApplicationRotation.DNS_SUFFIX);
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
index 01e53ce4f79..ab388ca9a9f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
@@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.chef.Chef;
import com.yahoo.vespa.hosted.controller.api.integration.chef.rest.PartialNode;
import com.yahoo.vespa.hosted.controller.api.integration.chef.rest.PartialNodeResult;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
+import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
import java.time.Clock;
import java.time.Duration;
@@ -23,11 +24,14 @@ import java.util.Optional;
/**
* @author mortent
+ * @author mpolden
*/
public class MetricsReporter extends Maintainer {
public static final String convergeMetric = "seconds.since.last.chef.convergence";
public static final String deploymentFailMetric = "deployment.failurePercentage";
+ public static final String remainingRotations = "remaining_rotations";
+
private final Metric metric;
private final Chef chefClient;
private final Clock clock;
@@ -51,6 +55,14 @@ public class MetricsReporter extends Maintainer {
public void maintain() {
reportChefMetrics();
reportDeploymentMetrics();
+ reportRemainingRotations();
+ }
+
+ private void reportRemainingRotations() {
+ try (RotationLock lock = controller().applications().rotationRepository().lock()) {
+ int availableRotations = controller().applications().rotationRepository().availableRotations(lock).size();
+ metric.set(remainingRotations, availableRotations, metric.createContext(Collections.emptyMap()));
+ }
}
private void reportChefMetrics() {
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 23316a74aae..762f12c3e8a 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
@@ -8,7 +8,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
@@ -27,6 +27,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.ArrayList;
@@ -55,6 +56,7 @@ public class ApplicationSerializer {
private final String ownershipIssueIdField = "ownershipIssueId";
private final String writeQualityField = "writeQuality";
private final String queryQualityField = "queryQuality";
+ private final String rotationField = "rotation";
// Deployment fields
private final String zoneField = "zone";
@@ -114,7 +116,7 @@ public class ApplicationSerializer {
private final String deploymentMetricsQueryLatencyField = "queryLatencyMillis";
private final String deploymentMetricsWriteLatencyField = "writeLatencyMillis";
-
+
// ------------------ Serialization
public Slime toSlime(Application application) {
@@ -130,6 +132,7 @@ public class ApplicationSerializer {
application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value()));
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
+ application.rotation().ifPresent(rotation -> root.setString(rotationField, rotation.id().asString()));
return slime;
}
@@ -139,7 +142,7 @@ public class ApplicationSerializer {
}
private void deploymentToSlime(Deployment deployment, Cursor object) {
- zoneToSlime(deployment.zone(), object.setObject(zoneField));
+ zoneIdToSlime(deployment.zone(), object.setObject(zoneField));
object.setString(versionField, deployment.version().toString());
object.setLong(deployTimeField, deployment.at().toEpochMilli());
toSlime(deployment.revision(), object.setObject(applicationPackageRevisionField));
@@ -191,7 +194,7 @@ public class ApplicationSerializer {
object.setDouble(clusterUtilsDiskBusyField, utils.getDiskBusy());
}
- private void zoneToSlime(Zone zone, Cursor object) {
+ private void zoneIdToSlime(ZoneId zone, Cursor object) {
object.setString(environmentField, zone.environment().value());
object.setString(regionField, zone.region().value());
}
@@ -267,9 +270,10 @@ public class ApplicationSerializer {
Optional<IssueId> ownershipIssueId = optionalString(root.field(ownershipIssueIdField)).map(IssueId::from);
ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
root.field(writeQualityField).asDouble());
+ Optional<RotationId> rotation = rotationFromSlime(root.field(rotationField));
- return new Application(id, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics);
+ return new Application(id, deploymentSpec, validationOverrides, deployments, deploymentJobs, deploying,
+ outstandingChange, ownershipIssueId, metrics, rotation);
}
private List<Deployment> deploymentsFromSlime(Inspector array) {
@@ -279,13 +283,13 @@ public class ApplicationSerializer {
}
private Deployment deploymentFromSlime(Inspector deploymentObject) {
- return new Deployment(zoneFromSlime(deploymentObject.field(zoneField)),
+ return new Deployment(zoneIdFromSlime(deploymentObject.field(zoneField)),
applicationRevisionFromSlime(deploymentObject.field(applicationPackageRevisionField)).get(),
Version.fromString(deploymentObject.field(versionField).asString()),
Instant.ofEpochMilli(deploymentObject.field(deployTimeField).asLong()),
- clusterUtilsMapFromSlime(deploymentObject.field(clusterUtilsField)),
- clusterInfoMapFromSlime(deploymentObject.field(clusterInfoField)),
- deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)));
+ clusterUtilsMapFromSlime(deploymentObject.field(clusterUtilsField)),
+ clusterInfoMapFromSlime(deploymentObject.field(clusterInfoField)),
+ deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)));
}
private DeploymentMetrics deploymentMetricsFromSlime(Inspector object) {
@@ -334,9 +338,8 @@ public class ApplicationSerializer {
return new ClusterInfo(flavor, cost, flavorCpu, flavorMem, flavorDisk, ClusterSpec.Type.from(type), hostnames);
}
- private Zone zoneFromSlime(Inspector object) {
- return new Zone(Environment.from(object.field(environmentField).asString()),
- RegionName.from(object.field(regionField).asString()));
+ private ZoneId zoneIdFromSlime(Inspector object) {
+ return ZoneId.from(object.field(environmentField).asString(), object.field(regionField).asString());
}
private Optional<ApplicationRevision> applicationRevisionFromSlime(Inspector object) {
@@ -403,6 +406,10 @@ public class ApplicationSerializer {
Instant.ofEpochMilli(object.field(atField).asLong())));
}
+ private Optional<RotationId> rotationFromSlime(Inspector field) {
+ return field.valid() ? optionalString(field).map(RotationId::new) : Optional.empty();
+ }
+
private Optional<Long> optionalLong(Inspector field) {
return field.valid() ? Optional.of(field.asLong()) : Optional.empty();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
index 3fbfdd31808..fb6608ea643 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
@@ -6,12 +6,10 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.identifiers.Identifier;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import java.util.List;
import java.util.Optional;
-import java.util.Set;
/**
* Used to store the permanent data of the controller.
@@ -19,55 +17,46 @@ import java.util.Set;
* @author Stian Kristoffersen
* @author bratseth
*/
-public abstract class ControllerDb {
+public interface ControllerDb {
// --------- Tenants
- public abstract void createTenant(Tenant tenant);
+ void createTenant(Tenant tenant);
- public abstract void updateTenant(Tenant tenant) throws PersistenceException;
+ // TODO: Remove exception from all signatures
+ void updateTenant(Tenant tenant) throws PersistenceException;
- public abstract void deleteTenant(TenantId tenantId) throws PersistenceException;
+ void deleteTenant(TenantId tenantId) throws PersistenceException;
- public abstract Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException;
+ Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException;
- public abstract List<Tenant> listTenants();
+ List<Tenant> listTenants();
// --------- Applications
// ONLY call this from ApplicationController.store()
- public abstract void store(Application application);
+ void store(Application application);
- public abstract void deleteApplication(ApplicationId applicationId);
+ void deleteApplication(ApplicationId applicationId);
- public abstract Optional<Application> getApplication(ApplicationId applicationId);
+ Optional<Application> getApplication(ApplicationId applicationId);
/** Returns all applications */
- public abstract List<Application> listApplications();
+ List<Application> listApplications();
/** Returns all applications of a tenant */
- public abstract List<Application> listApplications(TenantId tenantId);
-
- // --------- Rotations
-
- public abstract Set<RotationId> getRotations();
-
- public abstract Set<RotationId> getRotations(ApplicationId applicationId);
-
- public abstract boolean assignRotation(RotationId rotationId, ApplicationId applicationId);
-
- public abstract Set<RotationId> deleteRotations(ApplicationId applicationId);
+ List<Application> listApplications(TenantId tenantId);
/** Returns the given elements joined by dot "." */
- protected String path(Identifier... elements) {
+ default String path(Identifier... elements) {
return Joiner.on(".").join(elements);
}
- protected String path(String... elements) {
+ default String path(String... elements) {
return Joiner.on(".").join(elements);
}
- protected String path(ApplicationId applicationId) {
+ default String path(ApplicationId applicationId) {
return applicationId.tenant().value() + "." + applicationId.application().value() + "." + applicationId.instance().value();
}
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 e5616f025ce..a3bb191fc38 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
@@ -39,6 +39,8 @@ public class CuratorDb {
private static final Path root = Path.fromString("/controller/v1");
+ private static final Path lockRoot = root.append("locks");
+
private static final Duration defaultLockTimeout = Duration.ofMinutes(5);
private final StringSetSerializer stringSetSerializer = new StringSetSerializer();
@@ -67,6 +69,10 @@ public class CuratorDb {
return lock(lockPath(id), timeout);
}
+ public Lock lockRotations() {
+ return lock(lockRoot.append("rotations"), defaultLockTimeout);
+ }
+
/** Create a reentrant lock */
private Lock lock(Path path, Duration timeout) {
Lock lock = locks.computeIfAbsent(path, (pathArg) -> new Lock(pathArg.getAbsolute(), curator));
@@ -75,18 +81,18 @@ public class CuratorDb {
}
public Lock lockInactiveJobs() {
- return lock(root.append("locks").append("inactiveJobsLock"), defaultLockTimeout);
+ return lock(lockRoot.append("inactiveJobsLock"), defaultLockTimeout);
}
public Lock lockJobQueues() {
- return lock(root.append("locks").append("jobQueuesLock"), defaultLockTimeout);
+ return lock(lockRoot.append("jobQueuesLock"), defaultLockTimeout);
}
public Lock lockMaintenanceJob(String jobName) {
// Use a short timeout such that if maintenance jobs are started at about the same time on different nodes
// and the maintenance job takes a long time to complete, only one of the nodes will run the job
// in each maintenance interval
- return lock(root.append("locks").append("maintenanceJobLocks").append(jobName), Duration.ofSeconds(1));
+ return lock(lockRoot.append("maintenanceJobLocks").append(jobName), Duration.ofSeconds(1));
}
public Lock lockProvisionState(String provisionStateId) {
@@ -94,11 +100,11 @@ public class CuratorDb {
}
public Lock lockVespaServerPool() {
- return lock(root.append("locks").append("vespaServerPoolLock"), Duration.ofSeconds(1));
+ return lock(lockRoot.append("vespaServerPoolLock"), Duration.ofSeconds(1));
}
public Lock lockOpenStackServerPool() {
- return lock(root.append("locks").append("openStackServerPoolLock"), Duration.ofSeconds(1));
+ return lock(lockRoot.append("openStackServerPoolLock"), Duration.ofSeconds(1));
}
// -------------- Read and write --------------------------------------------------
@@ -222,19 +228,15 @@ public class CuratorDb {
// -------------- Paths --------------------------------------------------
- private Path systemVersionPath() {
- return root.append("systemVersion");
- }
-
private Path lockPath(TenantId tenant) {
- Path lockPath = root.append("locks")
+ Path lockPath = lockRoot
.append(tenant.id());
curator.create(lockPath);
return lockPath;
}
private Path lockPath(ApplicationId application) {
- Path lockPath = root.append("locks")
+ Path lockPath = lockRoot
.append(application.tenant().value())
.append(application.application().value())
.append(application.instance().value());
@@ -243,7 +245,7 @@ public class CuratorDb {
}
private Path lockPath(String provisionId) {
- Path lockPath = root.append("locks")
+ Path lockPath = lockRoot
.append(provisionStatePath())
.append(provisionId);
curator.create(lockPath);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
index ab240b9dea9..2c5d77c7773 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
@@ -6,7 +6,6 @@ import com.yahoo.vespa.hosted.controller.AlreadyExistsException;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.NotExistsException;
import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import java.util.ArrayList;
@@ -14,7 +13,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -22,11 +20,10 @@ import java.util.stream.Collectors;
*
* @author Stian Kristoffersen
*/
-public class MemoryControllerDb extends ControllerDb {
+public class MemoryControllerDb implements ControllerDb {
private final Map<TenantId, Tenant> tenants = new HashMap<>();
private final Map<String, Application> applications = new HashMap<>();
- private final Map<RotationId, ApplicationId> rotationAssignments = new HashMap<>();
@Override
public void createTenant(Tenant tenant) {
@@ -52,7 +49,7 @@ public class MemoryControllerDb extends ControllerDb {
}
@Override
- public Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException {
+ public Optional<Tenant> getTenant(TenantId tenantId) {
return Optional.ofNullable(tenants.get(tenantId));
}
@@ -88,36 +85,4 @@ public class MemoryControllerDb extends ControllerDb {
.collect(Collectors.toList());
}
- @Override
- public Set<RotationId> getRotations() {
- return rotationAssignments.keySet();
- }
-
- @Override
- public Set<RotationId> getRotations(ApplicationId applicationId) {
- return rotationAssignments.entrySet().stream()
- .filter(entry -> entry.getValue().equals(applicationId))
- .map(Map.Entry::getKey)
- .collect(Collectors.toSet());
- }
-
- @Override
- public boolean assignRotation(RotationId rotationId, ApplicationId applicationId) {
- if (rotationAssignments.containsKey(rotationId)) {
- return false;
- } else {
- rotationAssignments.put(rotationId, applicationId);
- return true;
- }
- }
-
- @Override
- public Set<RotationId> deleteRotations(ApplicationId applicationId) {
- Set<RotationId> rotations = getRotations(applicationId);
- for (RotationId rotation : rotations) {
- rotationAssignments.remove(rotation);
- }
- return rotations;
- }
-
}
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 e8b68d0c55a..c9ce0b76520 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
@@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.io.IOUtils;
import com.yahoo.jdisc.http.HttpRequest.Method;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
@@ -83,8 +83,8 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
ObjectMapper mapper = new ObjectMapper();
DiscoveryResponseStructure responseStructure = new DiscoveryResponseStructure();
- List<Zone> zones = zoneRegistry.zones();
- for (Zone zone : zones) {
+ List<ZoneId> zones = zoneRegistry.zones();
+ for (ZoneId zone : zones) {
if (!"".equals(proxyRequest.getEnvironment()) &&
!proxyRequest.getEnvironment().equals(zone.environment().value())) {
continue;
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 d7324450d4c..4e3b96b40df 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
@@ -10,7 +10,7 @@ import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
@@ -21,7 +21,6 @@ import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.AlreadyExistsException;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -53,7 +52,6 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
@@ -66,9 +64,12 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentCost;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
import com.yahoo.vespa.hosted.controller.restapi.MessageResponse;
import com.yahoo.vespa.hosted.controller.restapi.Path;
@@ -93,7 +94,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
-import java.util.Set;
import java.util.concurrent.Executor;
import java.util.logging.Level;
@@ -241,7 +241,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
String userIdString = request.getProperty("userOverride");
if (userIdString == null)
userIdString = userFrom(request)
- .orElseThrow(() -> new ForbiddenException("You must be authenticated or specify userOverride"));
+ .map(UserId::id)
+ .orElseThrow(() -> new ForbiddenException("You must be authenticated or specify userOverride"));
UserId userId = new UserId(userIdString);
List<Tenant> tenants = controller.tenants().asList(userId);
@@ -376,13 +377,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
// Compile version. The version that should be used when building an application
object.setString("compileVersion", application.oldestDeployedVersion().orElse(controller.systemVersion()).toFullString());
- // Rotations
+ // Rotation
Cursor globalRotationsArray = object.setArray("globalRotations");
- Set<URI> rotations = controller.getRotationUris(application.id());
- Map<String, RotationStatus> rotationHealthStatus =
- rotations.isEmpty() ? Collections.emptyMap() : controller.getHealthStatus(rotations.iterator().next().getHost());
- for (URI rotation : rotations)
- globalRotationsArray.addString(rotation.toString());
+ Map<String, RotationStatus> rotationHealthStatus = application.rotation()
+ .map(rotation -> controller.getHealthStatus(rotation.dnsName()))
+ .orElse(Collections.emptyMap());
+ application.rotation().ifPresent(rotation -> globalRotationsArray.addString(rotation.url().toString()));
// Deployments sorted according to deployment spec
List<Deployment> deployments = controller.applications().deploymentTrigger()
@@ -395,7 +395,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
deploymentObject.setString("environment", deployment.zone().environment().value());
deploymentObject.setString("region", deployment.zone().region().value());
deploymentObject.setString("instance", application.id().instance().value()); // pointless
- if ( ! rotations.isEmpty())
+ if (application.rotation().isPresent())
setRotationStatus(deployment, rotationHealthStatus, deploymentObject);
if (recurseOverDeployments(request)) // List full deployment information when recursive.
@@ -423,7 +423,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
.orElseThrow(() -> new NotExistsException(id + " not found"));
DeploymentId deploymentId = new DeploymentId(application.id(),
- new Zone(Environment.from(environment), RegionName.from(region)));
+ ZoneId.from(environment, region));
Deployment deployment = application.deployments().get(deploymentId.zone());
if (deployment == null)
@@ -507,14 +507,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Inspector requestData = toSlime(request.getData()).get();
String reason = mandatory("reason", requestData).asString();
- String agent = authorizer.getUserId(request).toString();
+ String agent = authorizer.getIdentity(request).getFullName();
long timestamp = controller.clock().instant().getEpochSecond();
EndpointStatus.Status status = inService ? EndpointStatus.Status.in : EndpointStatus.Status.out;
EndpointStatus endPointStatus = new EndpointStatus(status, reason, agent, timestamp);
// DeploymentId identifies the zone and application we are dealing with
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- new Zone(Environment.from(environment), RegionName.from(region)));
+ ZoneId.from(environment, region));
try {
List<String> rotations = controller.applications().setGlobalRotationStatus(deploymentId, endPointStatus);
return new MessageResponse(String.format("Rotations %s successfully set to %s service", rotations.toString(), inService ? "in" : "out of"));
@@ -526,7 +526,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse getGlobalRotationOverride(String tenantName, String applicationName, String instanceName, String environment, String region) {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- new Zone(Environment.from(environment), RegionName.from(region)));
+ ZoneId.from(environment, region));
Slime slime = new Slime();
Cursor c1 = slime.setObject().setArray("globalrotationoverride");
@@ -549,17 +549,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse rotationStatus(String tenantName, String applicationName, String instanceName, String environment, String region) {
-
ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName);
- Set<URI> rotations = controller.getRotationUris(applicationId);
- if (rotations.isEmpty())
+ Application application = controller.applications().require(applicationId);
+ if (!application.rotation().isPresent()) {
throw new NotExistsException("global rotation does not exist for '" + environment + "." + region + "'");
+ }
Slime slime = new Slime();
Cursor response = slime.setObject();
- Map<String, RotationStatus> rotationHealthStatus = controller.getHealthStatus(rotations.iterator().next().getHost());
-
+ Map<String, RotationStatus> rotationHealthStatus = controller.getHealthStatus(application.rotation().get().dnsName());
for (String rotationEndpoint : rotationHealthStatus.keySet()) {
if (rotationEndpoint.contains(toDns(environment)) && rotationEndpoint.contains(toDns(region))) {
Cursor bcpStatusObject = response.setObject("bcpStatus");
@@ -572,13 +571,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse waitForConvergence(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
return new JacksonJsonResponse(controller.waitForConfigConvergence(new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- new Zone(Environment.from(environment), RegionName.from(region))),
+ ZoneId.from(environment, region)),
asLong(request.getProperty("timeout"), 1000)));
}
private HttpResponse services(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
ApplicationView applicationView = controller.getApplicationView(tenantName, applicationName, instanceName, environment, region);
- ServiceApiResponse response = new ServiceApiResponse(new Zone(Environment.from(environment), RegionName.from(region)),
+ ServiceApiResponse response = new ServiceApiResponse(ZoneId.from(environment, region),
new ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
controller.getConfigServerUris(Environment.from(environment), RegionName.from(region)),
request.getUri());
@@ -588,7 +587,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse service(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String restPath, HttpRequest request) {
Map<?,?> result = controller.getServiceApiResponse(tenantName, applicationName, instanceName, environment, region, serviceName, restPath);
- ServiceApiResponse response = new ServiceApiResponse(new Zone(Environment.from(environment), RegionName.from(region)),
+ ServiceApiResponse response = new ServiceApiResponse(ZoneId.from(environment, region),
new ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
controller.getConfigServerUris(Environment.from(environment), RegionName.from(region)),
request.getUri());
@@ -597,15 +596,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse createUser(HttpRequest request) {
- Optional<String> username = userFrom(request);
- if ( ! username.isPresent() ) throw new ForbiddenException("Not authenticated.");
+ Optional<UserId> user = userFrom(request);
+ if ( ! user.isPresent() ) throw new ForbiddenException("Not authenticated.");
try {
- controller.tenants().createUserTenant(username.get());
- return new MessageResponse("Created user '" + username.get() + "'");
+ controller.tenants().createUserTenant(user.get().id());
+ return new MessageResponse("Created user '" + user.get() + "'");
} catch (AlreadyExistsException e) {
// Ok
- return new MessageResponse("User '" + username + "' already exists");
+ return new MessageResponse("User '" + user + "' already exists");
}
}
@@ -711,7 +710,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
"Active versions: " + controller.versionStatus().versions());
ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
- controller.applications().lockedOrThrow(id, application -> {
+ controller.applications().lockOrThrow(id, application -> {
if (application.deploying().isPresent())
throw new IllegalArgumentException("Can not start a deployment of " + application + " at this time: " +
application.deploying().get() + " is in progress");
@@ -729,7 +728,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
if ( ! change.isPresent())
return new MessageResponse("No deployment in progress for " + application + " at this time");
- controller.applications().lockedOrThrow(id, lockedApplication ->
+ controller.applications().lockOrThrow(id, lockedApplication ->
controller.applications().deploymentTrigger().cancelChange(id));
return new MessageResponse("Cancelled " + change.get() + " for " + application);
@@ -738,12 +737,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
/** Schedule restart of deployment, or specific host in a deployment */
private HttpResponse restart(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- new Zone(Environment.from(environment), RegionName.from(region)));
+ ZoneId.from(environment, region));
// TODO: Propagate all filters
- if (request.getProperty("hostname") != null)
- controller.applications().restartHost(deploymentId, new Hostname(request.getProperty("hostname")));
- else
- controller.applications().restart(deploymentId);
+ Optional<Hostname> hostname = Optional.ofNullable(request.getProperty("hostname")).map(Hostname::new);
+ controller.applications().restart(deploymentId, hostname);
// TODO: Change to return JSON
return new StringResponse("Requested restart of " + path(TenantResource.API_PATH, tenantName,
@@ -761,7 +758,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse log(String tenantName, String applicationName, String instanceName, String environment, String region) {
try {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- new Zone(Environment.from(environment), RegionName.from(region)));
+ ZoneId.from(environment, region));
return new JacksonJsonResponse(controller.grabLog(deploymentId));
}
catch (RuntimeException e) {
@@ -773,7 +770,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse deploy(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName);
- Zone zone = new Zone(Environment.from(environment), RegionName.from(region));
+ ZoneId zone = ZoneId.from(environment, region);
Map<String, byte[]> dataParts = new MultipartParser().parse(request);
if ( ! dataParts.containsKey("deployOptions"))
@@ -783,17 +780,17 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Inspector deployOptions = SlimeUtils.jsonToSlime(dataParts.get("deployOptions")).get();
+ ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get("applicationZip"));
DeployAuthorizer deployAuthorizer = new DeployAuthorizer(controller.zoneRegistry(), athenzClientFactory);
Tenant tenant = controller.tenants().tenant(new TenantId(tenantName)).orElseThrow(() -> new NotExistsException(new TenantId(tenantName)));
Principal principal = authorizer.getPrincipal(request);
- deployAuthorizer.throwIfUnauthorizedForDeploy(principal, Environment.from(environment), tenant, applicationId);
+ deployAuthorizer.throwIfUnauthorizedForDeploy(principal, Environment.from(environment), tenant, applicationId, applicationPackage);
// TODO: get rid of the json object
DeployOptions deployOptionsJsonClass = new DeployOptions(screwdriverBuildJobFromSlime(deployOptions.field("screwdriverBuildJob")),
optional("vespaVersion", deployOptions).map(Version::new),
deployOptions.field("ignoreValidationErrors").asBool(),
deployOptions.field("deployCurrentVersion").asBool());
- ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get("applicationZip"));
controller.applications().validate(applicationPackage.deploymentSpec());
ActivateResult result = controller.applications().deployApplication(applicationId,
zone,
@@ -824,7 +821,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse deactivate(String tenantName, String applicationName, String instanceName, String environment, String region) {
Application application = controller.applications().require(ApplicationId.from(tenantName, applicationName, instanceName));
- Zone zone = new Zone(Environment.from(environment), RegionName.from(region));
+ ZoneId zone = ZoneId.from(environment, region);
Deployment deployment = application.deployments().get(zone);
if (deployment == null) {
// Attempt to deactivate application even if the deployment is not known by the controller
@@ -873,8 +870,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
}
- private Optional<String> userFrom(HttpRequest request) {
- return authorizer.getPrincipalIfAny(request).map(Principal::getName);
+ private Optional<UserId> userFrom(HttpRequest request) {
+ return authorizer.getPrincipalIfAny(request)
+ .map(AthenzPrincipal::getIdentity)
+ .filter(AthenzUser.class::isInstance)
+ .map(AthenzUser.class::cast)
+ .map(AthenzUser::getUserId);
}
private void toSlime(Cursor object, Tenant tenant, HttpRequest request, boolean listApplications) {
@@ -985,18 +986,22 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private void throwIfNotSuperUserOrPartOfOpsDbGroup(UserGroup userGroup, HttpRequest request) {
- UserId userId = authorizer.getUserId(request);
- if (!authorizer.isSuperUser(request) && !authorizer.isGroupMember(userId, userGroup) ) {
+ AthenzIdentity identity = authorizer.getIdentity(request);
+ if (!(identity instanceof AthenzUser)) {
+ throw new ForbiddenException("Identity not an user: " + identity.getFullName());
+ }
+ AthenzUser user = (AthenzUser) identity;
+ if (!authorizer.isSuperUser(request) && !authorizer.isGroupMember(user.getUserId(), userGroup) ) {
throw new ForbiddenException(String.format("User '%s' is not super user or part of the OpsDB user group '%s'",
- userId.id(), userGroup.id()));
+ user.getUserId().id(), userGroup.id()));
}
}
private void throwIfNotAthenzDomainAdmin(AthenzDomain tenantDomain, HttpRequest request) {
- UserId userId = authorizer.getUserId(request);
- if ( ! authorizer.isAthenzDomainAdmin(userId, tenantDomain)) {
+ AthenzIdentity identity = authorizer.getIdentity(request);
+ if ( ! authorizer.isAthenzDomainAdmin(identity, tenantDomain)) {
throw new ForbiddenException(
- String.format("The user '%s' is not admin in Athenz domain '%s'", userId.id(), tenantDomain.id()));
+ String.format("The user '%s' is not admin in Athenz domain '%s'", identity.getFullName(), tenantDomain.id()));
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java
index 0c808e30c2a..b7080a763f0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java
@@ -10,16 +10,17 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
import com.yahoo.vespa.hosted.controller.common.ContextAttributes;
import com.yahoo.vespa.hosted.controller.restapi.filter.NTokenRequestFilter;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.SecurityContext;
-import java.security.Principal;
import java.util.Optional;
import java.util.logging.Logger;
@@ -54,27 +55,26 @@ public class Authorizer {
Optional<Tenant> tenant = controller.tenants().tenant(tenantId);
if ( ! tenant.isPresent()) return;
- UserId userId = getUserId(request);
- if (isTenantAdmin(userId, tenant.get())) return;
+ AthenzIdentity identity = getIdentity(request);
+ if (isTenantAdmin(identity, tenant.get())) return;
- throw loggedForbiddenException("User " + userId + " does not have write access to tenant " + tenantId);
+ throw loggedForbiddenException("User " + identity.getFullName() + " does not have write access to tenant " + tenantId);
}
- public UserId getUserId(HttpRequest request) {
- String name = getPrincipal(request).getName();
- if (name == null)
- throw loggedForbiddenException("Not authorized: User name is null");
- return new UserId(name);
+ public AthenzIdentity getIdentity(HttpRequest request) {
+ return getPrincipal(request).getIdentity();
}
/** Returns the principal or throws forbidden */ // TODO: Avoid REST exceptions
- public Principal getPrincipal(HttpRequest request) {
+ public AthenzPrincipal getPrincipal(HttpRequest request) {
return getPrincipalIfAny(request).orElseThrow(() -> Authorizer.loggedForbiddenException("User is not authenticated"));
}
/** Returns the principal if there is any */
- public Optional<Principal> getPrincipalIfAny(HttpRequest request) {
- return securityContextOf(request).map(SecurityContext::getUserPrincipal);
+ public Optional<AthenzPrincipal> getPrincipalIfAny(HttpRequest request) {
+ return securityContextOf(request)
+ .map(SecurityContext::getUserPrincipal)
+ .map(AthenzPrincipal.class::cast);
}
public Optional<NToken> getNToken(HttpRequest request) {
@@ -93,26 +93,36 @@ public class Authorizer {
return new ForbiddenException(formattedMessage);
}
- private boolean isTenantAdmin(UserId userId, Tenant tenant) {
+ private boolean isTenantAdmin(AthenzIdentity identity, Tenant tenant) {
switch (tenant.tenantType()) {
case ATHENS:
- return isAthenzTenantAdmin(userId, tenant.getAthensDomain().get());
- case OPSDB:
- return isGroupMember(userId, tenant.getUserGroup().get());
- case USER:
- return isUserTenantOwner(tenant.getId(), userId);
+ return isAthenzTenantAdmin(identity, tenant.getAthensDomain().get());
+ case OPSDB: {
+ if (!(identity instanceof AthenzUser)) {
+ return false;
+ }
+ AthenzUser user = (AthenzUser) identity;
+ return isGroupMember(user.getUserId(), tenant.getUserGroup().get());
+ }
+ case USER: {
+ if (!(identity instanceof AthenzUser)) {
+ return false;
+ }
+ AthenzUser user = (AthenzUser) identity;
+ return isUserTenantOwner(tenant.getId(), user.getUserId());
+ }
}
throw new IllegalArgumentException("Unknown tenant type: " + tenant.tenantType());
}
- private boolean isAthenzTenantAdmin(UserId userId, AthenzDomain tenantDomain) {
+ private boolean isAthenzTenantAdmin(AthenzIdentity athenzIdentity, AthenzDomain tenantDomain) {
return athenzClientFactory.createZmsClientWithServicePrincipal()
- .hasTenantAdminAccess(AthenzUtils.createPrincipal(userId), tenantDomain);
+ .hasTenantAdminAccess(athenzIdentity, tenantDomain);
}
- public boolean isAthenzDomainAdmin(UserId userId, AthenzDomain tenantDomain) {
+ public boolean isAthenzDomainAdmin(AthenzIdentity identity, AthenzDomain tenantDomain) {
return athenzClientFactory.createZmsClientWithServicePrincipal()
- .isDomainAdmin(AthenzUtils.createPrincipal(userId), tenantDomain);
+ .isDomainAdmin(identity, tenantDomain);
}
public boolean isGroupMember(UserId userId, UserGroup userGroup) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java
index 71126259417..c7e03048ec8 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java
@@ -6,15 +6,17 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsException;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import java.security.Principal;
+import java.util.Objects;
import java.util.logging.Logger;
import static com.yahoo.vespa.hosted.controller.restapi.application.Authorizer.environmentRequiresAuthorization;
@@ -38,7 +40,21 @@ public class DeployAuthorizer {
public void throwIfUnauthorizedForDeploy(Principal principal,
Environment environment,
Tenant tenant,
- ApplicationId applicationId) {
+ ApplicationId applicationId,
+ ApplicationPackage applicationPackage) {
+ // Validate that domain in identity configuration (deployment.xml) is same as tenant domain
+ applicationPackage.deploymentSpec().athenzDomain().ifPresent(identityDomain -> {
+ AthenzDomain tenantDomain = tenant.getAthensDomain().orElseThrow(() -> new IllegalArgumentException("Identity provider only available to Athenz onboarded tenants"));
+ if (! Objects.equals(tenantDomain.id(), identityDomain.value())) {
+ throw new ForbiddenException(
+ String.format(
+ "Athenz domain in deployment.xml: [%s] must match tenant domain: [%s]",
+ identityDomain.value(),
+ tenantDomain.id()
+ ));
+ }
+ });
+
if (!environmentRequiresAuthorization(environment)) {
return;
}
@@ -70,7 +86,7 @@ public class DeployAuthorizer {
"Screwdriver principal '%1$s' does not have deploy access to '%2$s'. " +
"Either the application has not been created at " + zoneRegistry.getDashboardUri() + " or " +
"'%1$s' is not added to the application's deployer role in Athenz domain '%3$s'.",
- athenzPrincipal.toYRN(), applicationId, tenantDomain.id());
+ athenzPrincipal.getIdentity().getFullName(), applicationId, tenantDomain.id());
}
}
}
@@ -91,7 +107,7 @@ public class DeployAuthorizer {
try {
return athenzClientFactory.createZmsClientWithServicePrincipal()
.hasApplicationAccess(
- principal,
+ principal.getIdentity(),
ApplicationAction.deploy,
domain,
new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(applicationId.application().value()));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java
index 6a448e475c5..0b0a2c3ad52 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java
@@ -2,7 +2,7 @@
package com.yahoo.vespa.hosted.controller.restapi.application;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.JsonFormat;
@@ -30,7 +30,7 @@ import java.util.regex.Pattern;
*/
class ServiceApiResponse extends HttpResponse {
- private final Zone zone;
+ private final ZoneId zone;
private final ApplicationId application;
private final List<URI> configServerURIs;
private final Slime slime;
@@ -40,7 +40,7 @@ class ServiceApiResponse extends HttpResponse {
private String serviceName = null;
private String restPath = null;
- public ServiceApiResponse(Zone zone, ApplicationId application, List<URI> configServerURIs, URI requestUri) {
+ public ServiceApiResponse(ZoneId zone, ApplicationId application, List<URI> configServerURIs, URI requestUri) {
super(200);
this.zone = zone;
this.application = application;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
index e350b98adb9..4d4f01bc1a6 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
@@ -12,9 +12,7 @@ import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService.BuildJob;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport;
@@ -109,7 +107,7 @@ public class ScrewdriverApiHandler extends LoggingRequestHandler {
.orElse(JobType.component);
ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default");
- controller.applications().lockedOrThrow(applicationId, application -> {
+ controller.applications().lockOrThrow(applicationId, application -> {
// Since this is a manual operation we likely want it to trigger as soon as possible so we add it at to the
// front of the queue
application = controller.applications().deploymentTrigger().triggerAllowParallel(
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 3a3fd445bcf..aecd3847653 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
@@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
@@ -70,7 +70,7 @@ public class ZoneApiHandler extends LoggingRequestHandler {
private HttpResponse root(HttpRequest request) {
List<Environment> environments = zoneRegistry.zones().stream()
- .map(Zone::environment)
+ .map(ZoneId::environment)
.distinct()
.sorted(Comparator.comparing(Environment::value))
.collect(Collectors.toList());
@@ -89,7 +89,7 @@ public class ZoneApiHandler extends LoggingRequestHandler {
}
private HttpResponse environment(HttpRequest request, Environment environment) {
- List<Zone> zones = zoneRegistry.zones().stream()
+ List<ZoneId> zones = zoneRegistry.zones().stream()
.filter(zone -> zone.environment() == environment)
.collect(Collectors.toList());
Slime slime = new Slime();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
index 529b2b25785..3f85b0116ad 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
@@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
@@ -81,7 +81,7 @@ public class ZoneApiHandler extends LoggingRequestHandler {
}
Environment environment = Environment.from(path.get("environment"));
RegionName region = RegionName.from(path.get("region"));
- Optional<Zone> zone = zoneRegistry.getZone(environment, region);
+ Optional<ZoneId> zone = zoneRegistry.getZone(environment, region);
if (!zone.isPresent()) {
throw new IllegalArgumentException("No such zone: " + environment.value() + "." + region.value());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java
new file mode 100644
index 00000000000..a638756a600
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/Rotation.java
@@ -0,0 +1,49 @@
+// 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.rotation;
+
+import java.util.Objects;
+
+/**
+ * Represents a global routing rotation.
+ *
+ * @author mpolden
+ */
+public class Rotation {
+
+ private final RotationId id;
+ private final String name;
+
+ public Rotation(RotationId id, String name) {
+ this.id = Objects.requireNonNull(id);
+ this.name = Objects.requireNonNull(name);
+ }
+
+ /** The ID of the allocated rotation. This value is generated by global routing system */
+ public RotationId id() {
+ return id;
+ }
+
+ /** The global rotation FQDN */
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Rotation)) return false;
+ final Rotation rotation = (Rotation) o;
+ return id().equals(rotation.id()) && name().equals(rotation.name());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id(), name());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("rotation %s -> %s", id().asString(), name());
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java
new file mode 100644
index 00000000000..10b15488f6e
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationId.java
@@ -0,0 +1,42 @@
+// 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.rotation;
+
+import java.util.Objects;
+
+/**
+ * ID of a global rotation.
+ *
+ * @author mpolden
+ */
+public class RotationId {
+
+ private final String id;
+
+ public RotationId(String id) {
+ this.id = id;
+ }
+
+ /** Rotation ID, e.g. rotation-42.vespa.global.routing */
+ public String asString() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RotationId that = (RotationId) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return "rotation ID " + id;
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationLock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationLock.java
new file mode 100644
index 00000000000..508df263837
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationLock.java
@@ -0,0 +1,25 @@
+// 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.rotation;
+
+import com.yahoo.vespa.curator.Lock;
+
+import java.util.Objects;
+
+/**
+ * A lock for the rotation repository. This is a type-safe wrapper for a curator lock.
+ *
+ * @author mpolden
+ */
+public class RotationLock implements AutoCloseable {
+
+ private final Lock lock;
+
+ RotationLock(Lock lock) {
+ this.lock = Objects.requireNonNull(lock, "lock cannot be null");
+ }
+
+ @Override
+ public void close() {
+ lock.close();
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java
new file mode 100644
index 00000000000..c0d3fd4758e
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java
@@ -0,0 +1,117 @@
+// 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.rotation;
+
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
+import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.collectingAndThen;
+
+/**
+ * The rotation repository offers global rotations to Vespa applications.
+ *
+ * The list of rotations comes from RotationsConfig, which is set in the controller's services.xml.
+ *
+ * @author Oyvind Gronnesby
+ * @author mpolden
+ */
+public class RotationRepository {
+
+ private static final Logger log = Logger.getLogger(RotationRepository.class.getName());
+
+ private final Map<RotationId, Rotation> allRotations;
+ private final ApplicationController applications;
+ private final CuratorDb curator;
+
+ public RotationRepository(RotationsConfig rotationsConfig, ApplicationController applications, CuratorDb curator) {
+ this.allRotations = from(rotationsConfig);
+ this.applications = applications;
+ this.curator = curator;
+ }
+
+ /** Acquire a exclusive lock for this */
+ public RotationLock lock() {
+ return new RotationLock(curator.lockRotations());
+ }
+
+ /**
+ * Returns a rotation for the given application
+ *
+ * If a rotation is already assigned to the application, that rotation will be returned.
+ * If no rotation is assigned, return an available rotation. The caller is responsible for assigning the rotation.
+ *
+ * @param application The application requesting a rotation
+ * @param lock Lock which must be acquired by the caller
+ */
+ public Rotation getRotation(Application application, RotationLock lock) {
+ if (application.rotation().isPresent()) {
+ return allRotations.get(application.rotation().get().id());
+ }
+ if (!application.deploymentSpec().globalServiceId().isPresent()) {
+ throw new IllegalArgumentException("global-service-id is not set in deployment spec");
+ }
+ long productionZones = application.deploymentSpec().zones().stream()
+ .filter(zone -> zone.deploysTo(Environment.prod))
+ // Global rotations don't work for nodes in corp network
+ .filter(zone -> !isCorp(zone))
+ .count();
+ if (productionZones < 2) {
+ throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined");
+ }
+ return findAvailableRotation(application, lock);
+ }
+
+ /**
+ * Returns all unassigned rotations
+ * @param lock Lock which must be acquired by the caller
+ */
+ public Map<RotationId, Rotation> availableRotations(@SuppressWarnings("unused") RotationLock lock) {
+ List<RotationId> assignedRotations = applications.asList().stream()
+ .filter(application -> application.rotation().isPresent())
+ .map(application -> application.rotation().get().id())
+ .collect(Collectors.toList());
+ Map<RotationId, Rotation> unassignedRotations = new LinkedHashMap<>(this.allRotations);
+ assignedRotations.forEach(unassignedRotations::remove);
+ return Collections.unmodifiableMap(unassignedRotations);
+ }
+
+ private Rotation findAvailableRotation(Application application, RotationLock lock) {
+ Map<RotationId, Rotation> availableRotations = availableRotations(lock);
+ if (availableRotations.isEmpty()) {
+ throw new IllegalStateException("Unable to assign global rotation to " + application.id()
+ + " - no rotations available");
+ }
+ // Return first available rotation
+ RotationId rotation = availableRotations.keySet().iterator().next();
+ log.info(String.format("Offering %s to application %s", rotation, application.id()));
+ return allRotations.get(rotation);
+ }
+
+ private static boolean isCorp(DeploymentSpec.DeclaredZone zone) {
+ return zone.region().isPresent() && zone.region().get().value().contains("corp");
+ }
+
+ /** Returns a immutable map of rotation ID to rotation sorted by rotation ID */
+ private static Map<RotationId, Rotation> from(RotationsConfig rotationConfig) {
+ return rotationConfig.rotations().entrySet().stream()
+ .map(entry -> new Rotation(new RotationId(entry.getKey()), entry.getValue().trim()))
+ .sorted(Comparator.comparing(rotation -> rotation.id().asString()))
+ .collect(collectingAndThen(Collectors.toMap(Rotation::id,
+ rotation -> rotation,
+ (k, v) -> v,
+ LinkedHashMap::new),
+ Collections::unmodifiableMap));
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
index d152cf80472..0e07f7b7589 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
@@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList;
import com.yahoo.collections.ListMap;
import com.yahoo.component.Version;
import com.yahoo.component.Vtag;
+import com.yahoo.config.provision.RegionName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.github.GitSha;
@@ -119,15 +120,16 @@ public class VersionStatus {
private static ListMap<Version, String> findConfigServerVersions(Controller controller) {
List<URI> configServers = controller.zoneRegistry().zones().stream()
- .flatMap(zone -> controller.getConfigServerUris(zone.environment(), zone.region()).stream())
- .collect(Collectors.toList());
+ .filter(zone -> ! zone.region().equals(RegionName.from("us-east-2a")))
+ .flatMap(zone -> controller.getConfigServerUris(zone.environment(), zone.region()).stream())
+ .collect(Collectors.toList());
ListMap<Version, String> versions = new ListMap<>();
for (URI configServer : configServers)
versions.put(controller.applications().configserverClient().version(configServer), configServer.getHost());
return versions;
}
-
+
private static Collection<DeploymentStatistics> computeDeploymentStatistics(Set<Version> infrastructureVersions,
List<Application> applications,
Instant jobTimeoutLimit) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
deleted file mode 100644
index 363a2ea19cd..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// 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.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.hosted.controller.api.ApplicationAlias;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * A rotation repository.
- *
- * @author Oyvind Gronnesby
- */
-// TODO: Fold this into ApplicationController+Application
-public class ControllerRotationRepository implements RotationRepository {
-
- private static final Logger log = Logger.getLogger(ControllerRotationRepository.class.getName());
- public static final String REMAINING_ROTATIONS_METRIC_NAME = "remaining_rotations";
-
- private final ControllerDb controllerDb;
- private final Map<RotationId, Rotation> rotationsMap;
- private final Metric metric;
-
- public ControllerRotationRepository(RotationsConfig rotationConfig, ControllerDb controllerDb, Metric metric) {
- this.controllerDb = controllerDb;
- this.rotationsMap = buildRotationsMap(rotationConfig);
- this.metric = metric;
- }
-
- private static Map<RotationId, Rotation> buildRotationsMap(RotationsConfig rotationConfig) {
- return rotationConfig.rotations().entrySet().stream()
- .map(entry -> {
- RotationId rotationId = new RotationId(entry.getKey());
- return new Rotation(rotationId, entry.getValue().trim());
- })
- .collect(Collectors.toMap(
- rotation -> rotation.rotationId,
- rotation -> rotation
- ));
- }
-
- @Override
- @NotNull
- public Set<Rotation> getOrAssignRotation(ApplicationId applicationId, DeploymentSpec deploymentSpec) {
- reportRemainingRotations();
-
- Set<RotationId> rotations = controllerDb.getRotations(applicationId);
-
- if (rotations.size() > 1) {
- log.warning(String.format("Application %s has %d > 1 rotation", applicationId, rotations.size()));
- }
-
- if (!rotations.isEmpty()) {
- return rotations.stream()
- .map(rotationsMap::get)
- .collect(Collectors.toSet());
- }
-
- if (!deploymentSpec.globalServiceId().isPresent()) {
- return Collections.emptySet();
- }
-
- long productionZoneCount = deploymentSpec.zones().stream()
- .filter(zone -> zone.deploysTo(Environment.prod))
- .filter(zone -> ! isCorp(zone)) // Global rotations don't work for nodes in corp network
- .count();
-
- if (productionZoneCount >= 2) {
- return assignRotation(applicationId);
- } else {
- throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined");
- }
- }
-
- private static boolean isCorp(DeploymentSpec.DeclaredZone zone) {
- return zone.region().isPresent() && zone.region().get().value().contains("corp");
- }
-
- @Override
- @NotNull
- public Set<URI> getRotationUris(ApplicationId applicationId) {
- Set<RotationId> rotations = controllerDb.getRotations(applicationId);
- if (rotations.isEmpty()) {
- return Collections.emptySet();
- }
- else {
- ApplicationAlias applicationAlias = new ApplicationAlias(applicationId);
- return Collections.singleton(applicationAlias.toHttpUri());
- }
- }
-
- private Set<Rotation> assignRotation(ApplicationId applicationId) {
- Set<RotationId> availableRotations = availableRotations();
- if (availableRotations.isEmpty()) {
- String message = "Unable to assign global rotation to "
- + applicationId + " - no rotations available";
- log.info(message);
- throw new RuntimeException(message);
- }
-
- for (RotationId rotationId : availableRotations) {
- if (controllerDb.assignRotation(rotationId, applicationId)) {
- log.info(String.format("Assigned rotation %s to application %s", rotationId, applicationId));
- Rotation rotation = this.rotationsMap.get(rotationId);
- return Collections.singleton(rotation);
- }
- }
-
- log.info(String.format("Rotation: No rotations assigned with %s rotations available", availableRotations.size()));
- return Collections.emptySet();
- }
-
- private Set<RotationId> availableRotations() {
- Set<RotationId> assignedRotations = controllerDb.getRotations();
- Set<RotationId> allRotations = new HashSet<>(rotationsMap.keySet());
- allRotations.removeAll(assignedRotations);
- return allRotations;
- }
-
- private void reportRemainingRotations() {
- try {
- int freeRotationsCount = availableRotations().size();
- log.log(LogLevel.INFO, "Rotation: {0} global rotations remaining", freeRotationsCount);
- metric.set(REMAINING_ROTATIONS_METRIC_NAME, freeRotationsCount,
- metric.createContext(Collections.emptyMap()));
- } catch (Exception e) {
- log.log(LogLevel.INFO, "Failed to report rotations metric", e);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java
deleted file mode 100644
index 4e333f0268b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/MemoryRotationRepository.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// 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.rotation;
-
-import com.google.common.collect.ImmutableSet;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-/**
- * A rotation repository backed by in-memory data structures
- *
- * @author bratseth
- */
-public class MemoryRotationRepository implements RotationRepository {
-
- private final Map<ApplicationId, Set<Rotation>> rotations = new HashMap<>();
-
- @NotNull
- @Override
- public Set<Rotation> getOrAssignRotation(ApplicationId application, DeploymentSpec deploymentSpec) {
- if (rotations.containsKey(application)) {
- return rotations.get(application);
- }
- Set<Rotation> rotations = ImmutableSet.of(new Rotation(
- new RotationId("generated-by-routing-service-" + UUID.randomUUID().toString()),
- "fake-global-rotation-" + application.toShortString())
- );
- this.rotations.put(application, rotations);
- return rotations;
- }
-
- @NotNull
- @Override
- public Set<URI> getRotationUris(ApplicationId applicationId) {
- Set<Rotation> rotations = this.rotations.get(applicationId);
- if (rotations == null) {
- return Collections.emptySet();
- }
- return rotations.stream()
- .map(rotation -> URI.create("http://" + rotation.rotationName))
- .collect(Collectors.toSet());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java
deleted file mode 100644
index b1f7b33e58e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/RotationRepository.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.URI;
-import java.util.Set;
-
-/**
- * A rotation repository assigns global rotations to Vespa applications. It does not take into account
- * whether an application qualifies or not, but it assumes that each application should get only
- * one.
- *
- * The list of rotations comes from the RotationsConfig, set in the controller's services.xml.
- * Assignments are persisted with the RotationId as the primary key. When we assign the
- * rotation to an application we try to put the mapping RotationId -&gt; Application. If a
- * mapping already exists for that RotationId, the assignment will fail.
- *
- * @author Oyvind Gronnesby
- */
-public interface RotationRepository {
-
- // TODO: Change to use provision.ApplicationId
- // TODO: Move the persistence into ControllerDb (done), and then collapse the 2 implementations and the interface into one
-
- /**
- * If any rotations are assigned to the application, these will be returned.
- * If no rotations are assigned, assign one rotation to the application and return that.
- *
- * @param applicationId ID of the application to get or assign rotation for
- * @param deploymentSpec Spec of current application being deployed
- * @return Set of rotations assigned (may be empty)
- */
- @NotNull
- Set<Rotation> getOrAssignRotation(ApplicationId applicationId, DeploymentSpec deploymentSpec);
-
- /**
- * Get the external visible rotation URIs for this application.
- *
- * @param applicationId ID of the application to get or assign rotation for
- */
- @NotNull
- Set<URI> getRotationUris(ApplicationId applicationId);
-
-}
diff --git a/controller-server/src/main/resources/configdefinitions/athenz.def b/controller-server/src/main/resources/configdefinitions/athenz.def
index 6d10f3dee28..068b1d353ba 100644
--- a/controller-server/src/main/resources/configdefinitions/athenz.def
+++ b/controller-server/src/main/resources/configdefinitions/athenz.def
@@ -17,6 +17,12 @@ domain string
userAuthenticationPassThruAttribute string
# TODO Remove once migrated to Okta
+# Path to Athenz CA JKS trust store
+athenzCaTrustStore string
+
+# Certificate DNS domain
+certDnsDomain string
+
# Athenz service name for controller identity
service.name string
@@ -28,3 +34,6 @@ service.privateKeyVersion int
# Name of Athenz service private key secret
service.privateKeySecretName string
+
+# Expiry of service principal token and certificate
+service.credentialsExpiryMinutes int default=43200 # 30 days
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java
index 9228e83bbc6..bf7f19a996c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerClientMock.java
@@ -22,7 +22,6 @@ import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import com.yahoo.vespa.serviceview.bindings.ClusterView;
import com.yahoo.vespa.serviceview.bindings.ServiceView;
-import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
@@ -43,16 +42,14 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
private final Map<ApplicationId, Boolean> applicationActivated = new HashMap<>();
private final Map<String, EndpointStatus> endpoints = new HashMap<>();
private final Map<URI, Version> versions = new HashMap<>();
- private Version defaultVersion = new Version(6, 1, 0);
- /** The exception to throw on the next prepare run, or null to continue normally */
+ private Version defaultVersion = new Version(6, 1, 0);
private RuntimeException prepareException = null;
-
- private Optional<Version> lastPrepareVersion = Optional.empty();
+ private Version lastPrepareVersion = null;
/** The version given in the previous prepare call, or empty if no call has been made */
public Optional<Version> lastPrepareVersion() {
- return lastPrepareVersion;
+ return Optional.ofNullable(lastPrepareVersion);
}
/** Return map of applications that may have been activated */
@@ -60,6 +57,7 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
return Collections.unmodifiableMap(applicationActivated);
}
+ /** The exception to throw on the next prepare run, or null to continue normally */
public void throwOnNextPrepare(RuntimeException prepareException) {
this.prepareException = prepareException;
}
@@ -71,10 +69,16 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
public Map<URI, Version> versions() {
return versions;
}
+
+ /** Set the default config server version */
+ public void setDefaultVersion(Version version) {
+ this.defaultVersion = version;
+ }
@Override
- public PreparedApplication prepare(DeploymentId deployment, DeployOptions deployOptions, Set<String> rotationCnames, Set<Rotation> rotations, byte[] content) {
- lastPrepareVersion = deployOptions.vespaVersion.map(Version::new);
+ public PreparedApplication prepare(DeploymentId deployment, DeployOptions deployOptions, Set<String> rotationCnames,
+ Set<Rotation> rotations, byte[] content) {
+ lastPrepareVersion = deployOptions.vespaVersion.map(Version::new).orElse(null);
if (prepareException != null) {
RuntimeException prepareException = this.prepareException;
this.prepareException = null;
@@ -108,23 +112,20 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
public PrepareResponse prepareResponse() {
PrepareResponse prepareResponse = new PrepareResponse();
prepareResponse.message = "foo";
- prepareResponse.configChangeActions = new ConfigChangeActions(Collections.emptyList(), Collections.emptyList());
+ prepareResponse.configChangeActions = new ConfigChangeActions(Collections.emptyList(),
+ Collections.emptyList());
prepareResponse.tenant = new TenantId("tenant");
return prepareResponse;
}
};
}
-
- /** Set the default config server version */
- public void setDefaultVersion(Version version) { this.defaultVersion = version; }
@Override
public List<String> getNodeQueryHost(DeploymentId deployment, String type) {
if (applicationInstances.containsKey(deployment.applicationId())) {
return Collections.singletonList(applicationInstances.get(deployment.applicationId()));
- } else {
- return Collections.emptyList();
}
+ return Collections.emptyList();
}
@Override
@@ -151,7 +152,8 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
// Returns a canned example response
@Override
- public ApplicationView getApplicationView(String tenantName, String applicationName, String instanceName, String environment, String region) {
+ public ApplicationView getApplicationView(String tenantName, String applicationName, String instanceName,
+ String environment, String region) {
ApplicationView applicationView = new ApplicationView();
ClusterView cluster = new ClusterView();
cluster.name = "cluster1";
@@ -172,7 +174,8 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
// Returns a canned example response
@Override
- public Map<?,?> getServiceApiResponse(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String restPath) {
+ public Map<?,?> getServiceApiResponse(String tenantName, String applicationName, String instanceName,
+ String environment, String region, String serviceName, String restPath) {
Map<String,List<?>> root = new HashMap<>();
List<Map<?,?>> resources = new ArrayList<>();
Map<String,String> resource = new HashMap<>();
@@ -199,7 +202,7 @@ public class ConfigServerClientMock extends AbstractComponent implements ConfigS
}
@Override
- public NodeList getNodeList(DeploymentId deployment) throws IOException {
+ public NodeList getNodeList(DeploymentId deployment) {
NodeList list = new NodeList();
list.nodes = new ArrayList<>();
NodeList.Node hostA = new NodeList.Node();
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
index cc915d4d9a1..02b33e4640a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
@@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.component.AbstractComponent;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
-import com.yahoo.vespa.hosted.controller.proxy.ProxyException;
import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
import com.yahoo.vespa.hosted.controller.restapi.StringResponse;
@@ -21,7 +20,7 @@ public class ConfigServerProxyMock extends AbstractComponent implements ConfigSe
private volatile String requestBody = null;
@Override
- public HttpResponse handle(ProxyRequest proxyRequest) throws ProxyException {
+ public HttpResponse handle(ProxyRequest proxyRequest) {
lastReceived = proxyRequest;
// Copy request body as the input stream is drained once the request completes
requestBody = asString(proxyRequest.getData());
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 d0c1fd95427..4a78c97750f 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
@@ -11,9 +11,8 @@ import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
-import com.yahoo.slime.Slime;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
@@ -25,6 +24,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService.BuildJob;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
@@ -32,12 +32,14 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.BuildSystem;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.persistence.ApplicationSerializer;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
+import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
@@ -494,7 +496,7 @@ public class ControllerTest {
public void testGlobalRotations() throws IOException {
// Setup tester and app def
ControllerTester tester = new ControllerTester();
- Zone zone = Zone.defaultZone();
+ ZoneId zone = ZoneId.from(Environment.defaultEnvironment(), RegionName.defaultName());
ApplicationId appId = tester.applicationId("tenant", "app1", "default");
DeploymentId deployId = new DeploymentId(appId, zone);
@@ -523,11 +525,11 @@ public class ControllerTest {
TenantId tenant = tester.createTenant("tenant1", "domain1", 11L);
Application app = tester.createApplication(tenant, "app1", "default", 1);
- tester.controller().applications().lockedOrThrow(app.id(), application -> {
+ tester.controller().applications().lockOrThrow(app.id(), application -> {
application = application.withDeploying(Optional.of(new Change.VersionChange(Version.fromString("6.3"))));
applications.store(application);
try {
- tester.deploy(app, new Zone(Environment.prod, RegionName.from("us-east-3")));
+ tester.deploy(app, ZoneId.from("prod", "us-east-3"));
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Rejecting deployment of application 'tenant1.app1' to zone prod.us-east-3 as version change to 6.3 is not tested", e.getMessage());
@@ -601,16 +603,108 @@ public class ControllerTest {
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
+ .globalServiceId("foo")
.region("us-west-1")
.region("us-central-1") // Two deployments should result in DNS alias being registered once
.build();
tester.deployCompletely(application, applicationPackage);
assertEquals(1, tester.controllerTester().nameService().records().size());
- Optional<Record> record = tester.controllerTester().nameService().findRecord(Record.Type.CNAME, "app1.tenant1.global.vespa.yahooapis.com");
+ Optional<Record> record = tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app1.tenant1.global.vespa.yahooapis.com")
+ );
assertTrue(record.isPresent());
- assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name());
- assertEquals("fake-global-rotation-tenant1.app1", record.get().value());
+ assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString());
+ assertEquals("rotation-fqdn-01.", record.get().data().asString());
+ }
+
+ @Test
+ public void testUpdatesExistingDnsAlias() {
+ DeploymentTester tester = new DeploymentTester();
+
+ // Application 1 is deployed and deleted
+ {
+ Application app1 = tester.createApplication("app1", "tenant1", 1, 1L);
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .globalServiceId("foo")
+ .region("us-west-1")
+ .region("us-central-1") // Two deployments should result in DNS alias being registered once
+ .build();
+
+ tester.deployCompletely(app1, applicationPackage);
+ assertEquals(1, tester.controllerTester().nameService().records().size());
+ Optional<Record> record = tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app1.tenant1.global.vespa.yahooapis.com")
+ );
+ assertTrue(record.isPresent());
+ assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString());
+ assertEquals("rotation-fqdn-01.", record.get().data().asString());
+
+ // Application is deleted and rotation is unassigned
+ applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .allow(ValidationId.deploymentRemoval)
+ .build();
+ tester.notifyJobCompletion(component, app1, true);
+ tester.deployAndNotify(app1, applicationPackage, true, systemTest);
+ tester.applications().deactivate(app1, ZoneId.from(Environment.test, RegionName.from("us-east-1")));
+ tester.applications().deactivate(app1, ZoneId.from(Environment.staging, RegionName.from("us-east-3")));
+ tester.applications().deleteApplication(app1.id(), Optional.of(new NToken("ntoken")));
+ try (RotationLock lock = tester.applications().rotationRepository().lock()) {
+ assertTrue("Rotation is unassigned",
+ tester.applications().rotationRepository().availableRotations(lock)
+ .containsKey(new RotationId("rotation-id-01")));
+ }
+
+ // Record remains
+ record = tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app1.tenant1.global.vespa.yahooapis.com")
+ );
+ assertTrue(record.isPresent());
+ }
+
+ // Application 2 is deployed and assigned same rotation as application 1 had before deletion
+ {
+ Application app2 = tester.createApplication("app2", "tenant2", 1, 1L);
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .globalServiceId("foo")
+ .region("us-west-1")
+ .region("us-central-1")
+ .build();
+ tester.deployCompletely(app2, applicationPackage);
+ assertEquals(2, tester.controllerTester().nameService().records().size());
+ Optional<Record> record = tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app2.tenant2.global.vespa.yahooapis.com")
+ );
+ assertTrue(record.isPresent());
+ assertEquals("app2.tenant2.global.vespa.yahooapis.com", record.get().name().asString());
+ assertEquals("rotation-fqdn-01.", record.get().data().asString());
+ }
+
+ // Application 1 is recreated, deployed and assigned a new rotation
+ {
+ Application app1 = tester.createApplication("app1", "tenant1", 1, 1L);
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .globalServiceId("foo")
+ .region("us-west-1")
+ .region("us-central-1")
+ .build();
+ tester.deployCompletely(app1, applicationPackage);
+ app1 = tester.applications().require(app1.id());
+ assertEquals("rotation-id-02", app1.rotation().get().id().asString());
+
+ // Existing DNS record is updated to point to the newly assigned rotation
+ assertEquals(2, tester.controllerTester().nameService().records().size());
+ Optional<Record> record = tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app1.tenant1.global.vespa.yahooapis.com")
+ );
+ assertTrue(record.isPresent());
+ assertEquals("rotation-fqdn-02.", record.get().data().asString());
+ }
+
}
@Test
@@ -626,7 +720,7 @@ public class ControllerTest {
Application app = tester.createApplication("app1", "tenant1", 1, 2L);
// Direct deploy is allowed when project ID is missing
- Zone zone = new Zone(Environment.prod, RegionName.from("cd-us-central-1"));
+ ZoneId zone = ZoneId.from("prod", "cd-us-central-1");
// Same options as used in our integration tests
DeployOptions options = new DeployOptions(Optional.empty(), Optional.empty(), false,
false);
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 8f9c22f8b81..52e1b3ae400 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
@@ -7,7 +7,7 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.slime.Slime;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.curator.Lock;
@@ -41,7 +41,7 @@ import com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.rotation.MemoryRotationRepository;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import java.util.Optional;
@@ -63,22 +63,31 @@ public final class ControllerTester {
private final GitHubMock gitHub;
private final CuratorDb curator;
private final MemoryNameService nameService;
+ private final RotationsConfig rotationsConfig;
private Controller controller;
public ControllerTester() {
this(new MemoryControllerDb(), new AthenzDbMock(), new ManualClock(), new ConfigServerClientMock(),
- new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), new MemoryNameService());
+ new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), defaultRotationsConfig(),
+ new MemoryNameService());
}
public ControllerTester(ManualClock clock) {
this(new MemoryControllerDb(), new AthenzDbMock(), clock, new ConfigServerClientMock(),
- new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), new MemoryNameService());
+ new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), defaultRotationsConfig(),
+ new MemoryNameService());
+ }
+
+ public ControllerTester(RotationsConfig rotationsConfig) {
+ this(new MemoryControllerDb(), new AthenzDbMock(), new ManualClock(), new ConfigServerClientMock(),
+ new ZoneRegistryMock(), new GitHubMock(), new MockCuratorDb(), rotationsConfig, new MemoryNameService());
}
private ControllerTester(ControllerDb db, AthenzDbMock athenzDb, ManualClock clock,
ConfigServerClientMock configServer, ZoneRegistryMock zoneRegistry,
- GitHubMock gitHub, CuratorDb curator, MemoryNameService nameService) {
+ GitHubMock gitHub, CuratorDb curator, RotationsConfig rotationsConfig,
+ MemoryNameService nameService) {
this.db = db;
this.athenzDb = athenzDb;
this.clock = clock;
@@ -87,7 +96,8 @@ public final class ControllerTester {
this.gitHub = gitHub;
this.curator = curator;
this.nameService = nameService;
- this.controller = createController(db, curator, configServer, clock, gitHub, zoneRegistry,
+ this.rotationsConfig = rotationsConfig;
+ this.controller = createController(db, curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry,
athenzDb, nameService);
}
@@ -109,7 +119,8 @@ public final class ControllerTester {
/** Create a new controller instance. Useful to verify that controller state is rebuilt from persistence */
public final void createNewController() {
- controller = createController(db, curator, configServer, clock, gitHub, zoneRegistry, athenzDb, nameService);
+ controller = createController(db, curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry, athenzDb,
+ nameService);
}
/** Creates the given tenant and application and deploys it */
@@ -119,7 +130,7 @@ public final class ControllerTester {
/** Creates the given tenant and application and deploys it */
public Application createAndDeploy(String tenantName, String domainName, String applicationName,
- String instanceName, Zone zone, long projectId, Long propertyId) {
+ String instanceName, ZoneId zone, long projectId, Long propertyId) {
TenantId tenant = createTenant(tenantName, domainName, propertyId);
Application application = createApplication(tenant, applicationName, instanceName, projectId);
deploy(application, zone);
@@ -133,7 +144,7 @@ public final class ControllerTester {
}
/** Creates the given tenant and application and deploys it */
- public Application createAndDeploy(String tenantName, String domainName, String applicationName, Zone zone, long projectId, Long propertyId) {
+ public Application createAndDeploy(String tenantName, String domainName, String applicationName, ZoneId zone, long projectId, Long propertyId) {
return createAndDeploy(tenantName, domainName, applicationName, "default", zone, projectId, propertyId);
}
@@ -152,11 +163,14 @@ public final class ControllerTester {
return application;
}
- public Zone toZone(Environment environment) {
+ public ZoneId toZone(Environment environment) {
switch (environment) {
- case dev: case test: return new Zone(environment, RegionName.from("us-east-1"));
- case staging: return new Zone(environment, RegionName.from("us-east-3"));
- default: return new Zone(environment, RegionName.from("us-west-1"));
+ case dev: case test:
+ return ZoneId.from(environment, RegionName.from("us-east-1"));
+ case staging:
+ return ZoneId.from(environment, RegionName.from("us-east-3"));
+ default:
+ return ZoneId.from(environment, RegionName.from("us-west-1"));
}
}
@@ -181,20 +195,20 @@ public final class ControllerTester {
public Application createApplication(TenantId tenant, String applicationName, String instanceName, long projectId) {
ApplicationId applicationId = applicationId(tenant.id(), applicationName, instanceName);
controller().applications().createApplication(applicationId, Optional.of(TestIdentities.userNToken));
- controller().applications().lockedOrThrow(applicationId, lockedApplication ->
+ controller().applications().lockOrThrow(applicationId, lockedApplication ->
controller().applications().store(lockedApplication.withProjectId(projectId)));
return controller().applications().require(applicationId);
}
- public void deploy(Application application, Zone zone) {
+ public void deploy(Application application, ZoneId zone) {
deploy(application, zone, new ApplicationPackage(new byte[0]));
}
- public void deploy(Application application, Zone zone, ApplicationPackage applicationPackage) {
+ public void deploy(Application application, ZoneId zone, ApplicationPackage applicationPackage) {
deploy(application, zone, applicationPackage, false);
}
- public void deploy(Application application, Zone zone, ApplicationPackage applicationPackage, boolean deployCurrentVersion) {
+ public void deploy(Application application, ZoneId zone, ApplicationPackage applicationPackage, boolean deployCurrentVersion) {
ScrewdriverId app1ScrewdriverId = new ScrewdriverId(String.valueOf(application.deploymentJobs().projectId().get()));
GitRevision app1RevisionId = new GitRevision(new GitRepository("repo"), new GitBranch("master"), new GitCommit("commit1"));
controller().applications().deployApplication(application.id(),
@@ -214,13 +228,13 @@ public final class ControllerTester {
return new LockedApplication(application, new Lock("/test", new MockCurator()));
}
- private static Controller createController(ControllerDb db, CuratorDb curator,
+ private static Controller createController(ControllerDb db, CuratorDb curator, RotationsConfig rotationsConfig,
ConfigServerClientMock configServerClientMock, ManualClock clock,
GitHubMock gitHubClientMock, ZoneRegistryMock zoneRegistryMock,
AthenzDbMock athensDb, MemoryNameService nameService) {
Controller controller = new Controller(db,
curator,
- new MemoryRotationRepository(),
+ rotationsConfig,
gitHubClientMock,
new MemoryEntityService(),
new MockOrganization(clock),
@@ -237,4 +251,13 @@ public final class ControllerTester {
return controller;
}
+ private static RotationsConfig defaultRotationsConfig() {
+ RotationsConfig.Builder builder = new RotationsConfig.Builder();
+ for (int i = 1; i <= 10; i++) {
+ String id = String.format("%02d", i);
+ builder = builder.rotations("rotation-id-" + id, "rotation-fqdn-" + id);
+ }
+ return new RotationsConfig(builder);
+ }
+
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/TestIdentities.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/TestIdentities.java
index 355b63335c0..085819b433d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/TestIdentities.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/TestIdentities.java
@@ -9,9 +9,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.RegionId;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.filter.AthenzTestUtils;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
/**
* @author Tony Vaagenes
@@ -34,8 +32,6 @@ public class TestIdentities {
public static Tenant tenant = Tenant.createOpsDbTenant(tenantId, userGroup1, property);
- public static NToken userNToken = new NToken.Builder(
- "U1", AthenzUtils.createPrincipal(userId), AthenzTestUtils.generateRsaKeypair().getPrivate(), "0")
- .build();
+ public static NToken userNToken = new NToken("dummy");
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java
index 18332942c24..53af74bf542 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java
@@ -7,7 +7,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import java.net.URI;
@@ -24,19 +24,19 @@ import java.util.Optional;
*/
public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry {
- private final Map<Zone, Duration> deploymentTimeToLive = new HashMap<>();
+ private final Map<ZoneId, Duration> deploymentTimeToLive = new HashMap<>();
private final Map<Environment, RegionName> defaultRegionForEnvironment = new HashMap<>();
- private List<Zone> zones = new ArrayList<>();
+ private List<ZoneId> zones = new ArrayList<>();
private SystemName system = SystemName.main;
@Inject
public ZoneRegistryMock() {
- this.zones.add(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("corp-us-east-1")));
- this.zones.add(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("us-east-3")));
- this.zones.add(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("us-west-1")));
+ this.zones.add(ZoneId.from("prod", "corp-us-east-1"));
+ this.zones.add(ZoneId.from("prod", "us-east-3"));
+ this.zones.add(ZoneId.from("prod", "us-west-1"));
}
- public ZoneRegistryMock setDeploymentTimeToLive(Zone zone, Duration duration) {
+ public ZoneRegistryMock setDeploymentTimeToLive(ZoneId zone, Duration duration) {
deploymentTimeToLive.put(zone, duration);
return this;
}
@@ -46,7 +46,7 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry
return this;
}
- public ZoneRegistryMock setZones(List<Zone> zones) {
+ public ZoneRegistryMock setZones(List<ZoneId> zones) {
this.zones = zones;
return this;
}
@@ -62,12 +62,12 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry
}
@Override
- public List<Zone> zones() {
+ public List<ZoneId> zones() {
return Collections.unmodifiableList(zones);
}
@Override
- public Optional<Zone> getZone(Environment environment, RegionName region) {
+ public Optional<ZoneId> getZone(Environment environment, RegionName region) {
return zones().stream().filter(z -> z.environment().equals(environment) &&
z.region().equals(region)).findFirst();
}
@@ -88,7 +88,7 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry
@Override
public Optional<Duration> getDeploymentTimeToLive(Environment environment, RegionName region) {
- return Optional.ofNullable(deploymentTimeToLive.get(new Zone(environment, region)));
+ return Optional.ofNullable(deploymentTimeToLive.get(ZoneId.from(environment, region)));
}
@Override
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java
index 20db038485d..ffb78b7342a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilterTest.java
@@ -7,10 +7,10 @@ import com.yahoo.jdisc.handler.ReadableContentChannel;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.InvalidTokenException;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.InvalidTokenException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
import org.junit.Before;
import org.junit.Test;
@@ -35,7 +35,7 @@ import static org.mockito.Mockito.when;
*/
public class AthenzPrincipalFilterTest {
- private static final NToken NTOKEN = createDummyToken();
+ private static final NToken NTOKEN = new NToken("dummy");
private static final String ATHENZ_PRINCIPAL_HEADER = "Athenz-Principal-Auth";
private NTokenValidator validator;
@@ -44,13 +44,13 @@ public class AthenzPrincipalFilterTest {
@Before
public void before() {
validator = mock(NTokenValidator.class);
- principal = AthenzUtils.createPrincipal(new UserId("bob"));
+ principal = new AthenzPrincipal(AthenzUser.fromUserId(new UserId("bob")), NTOKEN);
}
@Test
public void valid_ntoken_is_accepted() throws Exception {
DiscFilterRequest request = mock(DiscFilterRequest.class);
- when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getToken());
+ when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getRawToken());
when(validator.validate(NTOKEN)).thenReturn(principal);
@@ -78,7 +78,7 @@ public class AthenzPrincipalFilterTest {
@Test
public void invalid_token_is_unauthorized() throws Exception {
DiscFilterRequest request = mock(DiscFilterRequest.class);
- when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getToken());
+ when(request.getHeader(ATHENZ_PRINCIPAL_HEADER)).thenReturn(NTOKEN.getRawToken());
when(validator.validate(NTOKEN)).thenThrow(new InvalidTokenException("Invalid token"));
@@ -92,12 +92,6 @@ public class AthenzPrincipalFilterTest {
assertThat(responseHandler.getResponseContent(), containsString("Invalid token"));
}
- private static NToken createDummyToken() {
- return new NToken.Builder(
- "U1", AthenzUtils.createPrincipal(new UserId("bob")), AthenzTestUtils.generateRsaKeypair().getPrivate(), "0")
- .build();
- }
-
private static class ResponseHandlerMock implements ResponseHandler {
public Response response;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidatorTest.java
index 70ba504df03..907fabe9d75 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidatorTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/athenz/filter/NTokenValidatorTest.java
@@ -1,21 +1,25 @@
// 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.athenz.filter;
+import com.yahoo.athenz.auth.token.PrincipalToken;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.InvalidTokenException;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
-import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.InvalidTokenException;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsKeystore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.time.Instant;
import java.util.Optional;
-import static com.yahoo.vespa.hosted.controller.athenz.AthenzUtils.ZMS_ATHENZ_SERVICE;
-import static com.yahoo.vespa.hosted.controller.athenz.AthenzUtils.createPrincipal;
+import static com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils.ZMS_ATHENZ_SERVICE;
import static org.junit.Assert.assertEquals;
/**
@@ -25,7 +29,7 @@ public class NTokenValidatorTest {
private static final KeyPair TRUSTED_KEY = AthenzTestUtils.generateRsaKeypair();
private static final KeyPair UNKNOWN_KEY = AthenzTestUtils.generateRsaKeypair();
- private static final AthenzPrincipal PRINCIPAL = createPrincipal(new UserId("myuser"));
+ private static final AthenzIdentity IDENTITY = AthenzUser.fromUserId(new UserId("myuser"));
@Rule
public ExpectedException exceptionRule = ExpectedException.none();
@@ -33,15 +37,15 @@ public class NTokenValidatorTest {
@Test
public void valid_token_is_accepted() throws NoSuchAlgorithmException, InvalidTokenException {
NTokenValidator validator = new NTokenValidator(createKeystore());
- NToken token = createNToken(PRINCIPAL, System.currentTimeMillis(), TRUSTED_KEY, "0");
+ NToken token = createNToken(IDENTITY, Instant.now(), TRUSTED_KEY.getPrivate(), "0");
AthenzPrincipal principal = validator.validate(token);
- assertEquals("user.myuser", principal.toYRN());
+ assertEquals("user.myuser", principal.getIdentity().getFullName());
}
@Test
public void invalid_signature_is_not_accepted() throws InvalidTokenException {
NTokenValidator validator = new NTokenValidator(createKeystore());
- NToken token = createNToken(PRINCIPAL, System.currentTimeMillis(), UNKNOWN_KEY, "0");
+ NToken token = createNToken(IDENTITY, Instant.now(), UNKNOWN_KEY.getPrivate(), "0");
exceptionRule.expect(InvalidTokenException.class);
exceptionRule.expectMessage("NToken is expired or has invalid signature");
validator.validate(token);
@@ -50,7 +54,7 @@ public class NTokenValidatorTest {
@Test
public void expired_token_is_not_accepted() throws InvalidTokenException {
NTokenValidator validator = new NTokenValidator(createKeystore());
- NToken token = createNToken(PRINCIPAL, 1234 /*long time ago*/, TRUSTED_KEY, "0");
+ NToken token = createNToken(IDENTITY, Instant.ofEpochMilli(1234) /*long time ago*/, TRUSTED_KEY.getPrivate(), "0");
exceptionRule.expect(InvalidTokenException.class);
exceptionRule.expectMessage("NToken is expired or has invalid signature");
validator.validate(token);
@@ -59,7 +63,7 @@ public class NTokenValidatorTest {
@Test
public void unknown_keyId_is_not_accepted() throws InvalidTokenException {
NTokenValidator validator = new NTokenValidator(createKeystore());
- NToken token = createNToken(PRINCIPAL, System.currentTimeMillis(), TRUSTED_KEY, "unknown-key-id");
+ NToken token = createNToken(IDENTITY, Instant.now(), TRUSTED_KEY.getPrivate(), "unknown-key-id");
exceptionRule.expect(InvalidTokenException.class);
exceptionRule.expectMessage("NToken has an unknown keyId");
validator.validate(token);
@@ -69,7 +73,7 @@ public class NTokenValidatorTest {
public void failing_to_find_key_should_throw_exception() throws InvalidTokenException {
ZmsKeystore keystore = (athensService, keyId) -> { throw new RuntimeException(); };
NTokenValidator validator = new NTokenValidator(keystore);
- NToken token = createNToken(PRINCIPAL, System.currentTimeMillis(), TRUSTED_KEY, "0");
+ NToken token = createNToken(IDENTITY, Instant.now(), TRUSTED_KEY.getPrivate(), "0");
exceptionRule.expect(InvalidTokenException.class);
exceptionRule.expectMessage("Failed to retrieve public key");
validator.validate(token);
@@ -82,14 +86,17 @@ public class NTokenValidatorTest {
: Optional.empty();
}
- private static NToken createNToken(AthenzPrincipal principal, long issueTime, KeyPair keyPair, String keyId) {
- return new NToken.Builder("U1", principal, keyPair.getPrivate(), keyId)
+ private static NToken createNToken(AthenzIdentity identity, Instant issueTime, PrivateKey privateKey, String keyId) {
+ PrincipalToken token = new PrincipalToken.Builder("U1", identity.getDomain().id(), identity.getName())
+ .keyId(keyId)
.salt("1234")
- .hostname("host")
+ .host("host")
.ip("1.2.3.4")
- .issueTime(issueTime / 1000)
+ .issueTime(issueTime.getEpochSecond())
.expirationWindow(1000)
.build();
+ token.sign(privateKey);
+ return new NToken(token.getSignedToken());
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
index 72bfa238094..3311cffa078 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
@@ -2,6 +2,8 @@
package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.config.application.api.ValidationId;
+import com.yahoo.config.provision.AthenzDomain;
+import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.Environment;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
@@ -26,9 +28,11 @@ public class ApplicationPackageBuilder {
private String upgradePolicy = null;
private Environment environment = Environment.prod;
+ private String globalServiceId = null;
private final StringBuilder environmentBody = new StringBuilder();
private final StringBuilder validationOverridesBody = new StringBuilder();
private final StringBuilder blockChange = new StringBuilder();
+ private String athenzIdentityAttributes = null;
private String searchDefinition = "search test { }";
public ApplicationPackageBuilder upgradePolicy(String upgradePolicy) {
@@ -41,6 +45,11 @@ public class ApplicationPackageBuilder {
return this;
}
+ public ApplicationPackageBuilder globalServiceId(String globalServiceId) {
+ this.globalServiceId = globalServiceId;
+ return this;
+ }
+
public ApplicationPackageBuilder region(String regionName) {
environmentBody.append(" <region active='true'>");
environmentBody.append(regionName);
@@ -83,6 +92,11 @@ public class ApplicationPackageBuilder {
return this;
}
+ public ApplicationPackageBuilder athenzIdentity(AthenzDomain domain, AthenzService service) {
+ this.athenzIdentityAttributes = String.format("athenz-domain='%s' athenz-service='%s'", domain.value(), service.value());
+ return this;
+ }
+
/** Sets the content of the search definition test.sd */
public ApplicationPackageBuilder searchDefinition(String testSearchDefinition) {
this.searchDefinition = testSearchDefinition;
@@ -90,7 +104,12 @@ public class ApplicationPackageBuilder {
}
private byte[] deploymentSpec() {
- StringBuilder xml = new StringBuilder("<deployment version='1.0'>\n");
+ StringBuilder xml = new StringBuilder();
+ xml.append("<deployment version='1.0' ");
+ if(athenzIdentityAttributes != null) {
+ xml.append(athenzIdentityAttributes);
+ }
+ xml.append(">\n");
if (upgradePolicy != null) {
xml.append("<upgrade policy='");
xml.append(upgradePolicy);
@@ -99,6 +118,11 @@ public class ApplicationPackageBuilder {
xml.append(blockChange);
xml.append(" <");
xml.append(environment.value());
+ if (globalServiceId != null) {
+ xml.append(" global-service-id='");
+ xml.append(globalServiceId);
+ xml.append("'");
+ }
xml.append(">\n");
xml.append(environmentBody);
xml.append(" </");
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MockMetricsService.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MockMetricsService.java
index 2dc6471effb..a58d2d0fa39 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MockMetricsService.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MockMetricsService.java
@@ -2,7 +2,7 @@
package com.yahoo.vespa.hosted.controller.integration;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import java.util.HashMap;
import java.util.Map;
@@ -18,12 +18,12 @@ public class MockMetricsService implements com.yahoo.vespa.hosted.controller.api
}
@Override
- public DeploymentMetrics getDeploymentMetrics(ApplicationId application, Zone zone) {
+ public DeploymentMetrics getDeploymentMetrics(ApplicationId application, ZoneId zone) {
return new DeploymentMetrics(1, 2, 3, 4, 5);
}
@Override
- public Map<String, SystemMetrics> getSystemMetrics(ApplicationId application, Zone zone) {
+ public Map<String, SystemMetrics> getSystemMetrics(ApplicationId application, ZoneId zone) {
Map<String, SystemMetrics> result = new HashMap<>();
SystemMetrics system = new SystemMetrics(55.54, 69.90, 34.59);
result.put("default", system);
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 ef0b05f9bb2..47d62f93def 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
@@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -35,7 +35,7 @@ public class DeploymentExpirerTest {
@Test
public void testDeploymentExpiry() throws IOException, InterruptedException {
tester.controllerTester().zoneRegistry().setDeploymentTimeToLive(
- new Zone(Environment.dev, RegionName.from("us-east-1")),
+ ZoneId.from(Environment.dev, RegionName.from("us-east-1")),
Duration.ofDays(14)
);
DeploymentExpirer expirer = new DeploymentExpirer(tester.controller(), Duration.ofDays(10),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java
new file mode 100644
index 00000000000..8647b87133e
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java
@@ -0,0 +1,77 @@
+// 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.application.api.ValidationId;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType.component;
+import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType.systemTest;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author mpolden
+ */
+public class DnsMaintainerTest {
+
+ @Test
+ @Ignore // TODO: Enable once DnsMaintainer actually removes records
+ public void removes_record_for_unassigned_rotation() {
+ DeploymentTester tester = new DeploymentTester();
+ Application application = tester.createApplication("app1", "tenant1", 1, 1L);
+
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .globalServiceId("foo")
+ .region("us-west-1")
+ .region("us-central-1")
+ .build();
+
+ // Deploy application
+ tester.deployCompletely(application, applicationPackage);
+ assertEquals(1, tester.controllerTester().nameService().records().size());
+ Optional<Record> record = tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app1.tenant1.global.vespa.yahooapis.com")
+ );
+ assertTrue(record.isPresent());
+ assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString());
+ assertEquals("rotation-fqdn-01.", record.get().data().asString());
+
+ // Remove application
+ applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .allow(ValidationId.deploymentRemoval)
+ .build();
+ tester.notifyJobCompletion(component, application, true);
+ tester.deployAndNotify(application, applicationPackage, true, systemTest);
+ tester.applications().deactivate(application, ZoneId.from(Environment.test, RegionName.from("us-east-1")));
+ tester.applications().deactivate(application, ZoneId.from(Environment.staging, RegionName.from("us-east-3")));
+ tester.applications().deleteApplication(application.id(), Optional.of(new NToken("ntoken")));
+
+ // DnsMaintainer removes record
+ DnsMaintainer dnsMaintainer = new DnsMaintainer(tester.controller(), Duration.ofHours(12),
+ new JobControl(new MockCuratorDb()),
+ tester.controllerTester().nameService());
+ dnsMaintainer.maintain();
+ assertFalse("DNS record removed", tester.controllerTester().nameService().findRecord(
+ Record.Type.CNAME, RecordName.from("app1.tenant1.global.vespa.yahooapis.com")).isPresent());
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
index 8839f6a5a18..ac282422c89 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
@@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.component.Version;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ControllerTester;
@@ -692,7 +692,7 @@ public class UpgraderTest {
// Dev deployment which should be ignored
Application dev0 = tester.createApplication("dev0", "tenant1", 7, 1L);
- tester.controllerTester().deploy(dev0, new Zone(Environment.dev, RegionName.from("dev-region")));
+ tester.controllerTester().deploy(dev0, ZoneId.from(Environment.dev, RegionName.from("dev-region")));
// New version is released and canaries upgrade
version = Version.fromString("5.1");
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 2c1471b29b6..b281b513f4a 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
@@ -8,7 +8,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
@@ -25,6 +25,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
+import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import org.junit.Test;
import java.io.IOException;
@@ -47,8 +48,8 @@ public class ApplicationSerializerTest {
private static final ApplicationSerializer applicationSerializer = new ApplicationSerializer();
- private static final Zone zone1 = new Zone(Environment.from("prod"), RegionName.from("us-west-1"));
- private static final Zone zone2 = new Zone(Environment.from("prod"), RegionName.from("us-east-3"));
+ private static final ZoneId zone1 = ZoneId.from("prod", "us-west-1");
+ private static final ZoneId zone2 = ZoneId.from("prod", "us-east-3");
@Test
public void testSerialization() {
@@ -86,7 +87,8 @@ public class ApplicationSerializerTest {
Optional.of(new Change.VersionChange(Version.fromString("6.7"))),
true,
Optional.of(IssueId.from("1234")),
- new MetricsService.ApplicationMetrics(0.5, 0.9));
+ new MetricsService.ApplicationMetrics(0.5, 0.9),
+ Optional.of(new RotationId("my-rotation")));
Application serialized = applicationSerializer.fromSlime(applicationSerializer.toSlime(original));
@@ -115,6 +117,7 @@ public class ApplicationSerializerTest {
assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId());
assertEquals(original.deploying(), serialized.deploying());
+ assertEquals(original.rotation().get().id(), serialized.rotation().get().id());
// Test cluster utilization
assertEquals(0, serialized.deployments().get(zone1).clusterUtils().size());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
index 189b3a97a80..c668bde0d40 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
@@ -21,7 +21,7 @@ import static org.junit.Assert.assertEquals;
public class VersionStatusSerializerTest {
@Test
- public void testSerialization() throws Exception {
+ public void testSerialization() {
List<VespaVersion> vespaVersions = new ArrayList<>();
DeploymentStatistics statistics = new DeploymentStatistics(
Version.fromString("5.0"),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
index 6c5120df515..f3fa1e21eda 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
@@ -5,6 +5,7 @@ import com.yahoo.application.container.JDisc;
import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.TestIdentities;
@@ -20,12 +21,13 @@ 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.identifiers.ScrewdriverId;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzService;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock;
+import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.maintenance.JobControl;
import com.yahoo.vespa.hosted.controller.maintenance.Upgrader;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
@@ -44,18 +46,16 @@ import java.util.Optional;
public class ContainerControllerTester {
private final ContainerTester containerTester;
- private final Controller controller;
private final Upgrader upgrader;
public ContainerControllerTester(JDisc container, String responseFilePath) {
containerTester = new ContainerTester(container, responseFilePath);
- controller = (Controller)container.components().getComponent("com.yahoo.vespa.hosted.controller.Controller");
CuratorDb curatorDb = new MockCuratorDb();
curatorDb.writeUpgradesPerMinute(100);
- upgrader = new Upgrader(controller, Duration.ofDays(1), new JobControl(curatorDb), curatorDb);
+ upgrader = new Upgrader(controller(), Duration.ofDays(1), new JobControl(curatorDb), curatorDb);
}
- public Controller controller() { return controller; }
+ public Controller controller() { return containerTester.controller(); }
public Upgrader upgrader() { return upgrader; }
@@ -69,18 +69,18 @@ public class ContainerControllerTester {
public Application createApplication(String athensDomain, String tenant, String application) {
AthenzDomain domain1 = addTenantAthenzDomain(athensDomain, "mytenant");
- controller.tenants().addTenant(Tenant.createAthensTenant(new TenantId(tenant), domain1,
+ controller().tenants().addTenant(Tenant.createAthensTenant(new TenantId(tenant), domain1,
new Property("property1"),
Optional.of(new PropertyId("1234"))),
Optional.of(TestIdentities.userNToken));
ApplicationId app = ApplicationId.from(tenant, application, "default");
- return controller.applications().createApplication(app, Optional.of(TestIdentities.userNToken));
+ return controller().applications().createApplication(app, Optional.of(TestIdentities.userNToken));
}
- public Application deploy(Application application, ApplicationPackage applicationPackage, Zone zone, long projectId) {
+ public Application deploy(Application application, ApplicationPackage applicationPackage, ZoneId zone, long projectId) {
ScrewdriverId app1ScrewdriverId = new ScrewdriverId(String.valueOf(projectId));
GitRevision app1RevisionId = new GitRevision(new GitRepository("repo"), new GitBranch("master"), new GitCommit("commit1"));
- controller.applications().deployApplication(application.id(),
+ controller().applications().deployApplication(application.id(),
zone,
applicationPackage,
new DeployOptions(Optional.of(new ScrewdriverBuildJob(app1ScrewdriverId, app1RevisionId)), Optional.empty(), false, false));
@@ -106,7 +106,7 @@ public class ContainerControllerTester {
AthenzDomain athensDomain = new AthenzDomain(domainName);
AthenzDbMock.Domain domain = new AthenzDbMock.Domain(athensDomain);
domain.markAsVespaTenant();
- domain.admin(new AthenzPrincipal(new AthenzDomain("domain"), new UserId(userName)));
+ domain.admin(AthenzUtils.createAthenzIdentity(new AthenzDomain("domain"), userName));
mock.getSetup().addDomain(domain);
return athensDomain;
}
@@ -121,4 +121,17 @@ public class ContainerControllerTester {
containerTester.assertResponse(request, expectedResponse, expectedStatusCode);
}
+ /*
+ * Authorize action on tenantDomain/application for a given screwdriverId
+ */
+ public void authorize(AthenzDomain tenantDomain, ScrewdriverId screwdriverId, ApplicationAction action, Application application) {
+ AthenzClientFactoryMock mock = (AthenzClientFactoryMock) containerTester.container().components()
+ .getComponent(AthenzClientFactoryMock.class.getName());
+
+ mock.getSetup()
+ .domains.get(tenantDomain)
+ .applications.get(new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(application.id().application().value()))
+ .addRoleMember(action, AthenzService.fromScrewdriverId(screwdriverId));
+ }
+
}
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 c0e8b48f821..95810e90cdb 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
@@ -43,14 +43,16 @@ public class ContainerTester {
public JDisc container() { return container; }
+ public Controller controller() {
+ return (Controller) container.components().getComponent(Controller.class.getName());
+ }
+
public void updateSystemVersion() {
- Controller controller = (Controller)container.components().getComponent("com.yahoo.vespa.hosted.controller.Controller");
- controller.updateVersionStatus(VersionStatus.compute(controller));
+ controller().updateVersionStatus(VersionStatus.compute(controller()));
}
public void updateSystemVersion(Version version) {
- Controller controller = (Controller)container.components().getComponent("com.yahoo.vespa.hosted.controller.Controller");
- controller.updateVersionStatus(VersionStatus.compute(controller, version));
+ controller().updateVersionStatus(VersionStatus.compute(controller(), version));
}
public void assertResponse(Supplier<Request> request, File responseFile) throws IOException {
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 044c5d75d12..631ceab98a5 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
@@ -25,60 +25,70 @@ import static org.junit.Assert.assertEquals;
public class ControllerContainerTest {
protected JDisc container;
+
@Before
public void startContainer() { container = JDisc.fromServicesXml(controllerServicesXml, Networking.disable); }
+
@After
public void stopContainer() { container.close(); }
private final String controllerServicesXml =
- "<jdisc version='1.0'>" +
- " <config name='vespa.hosted.zone.config.zone'>" +
- " <system>main</system>" +
- " </config>" +
- " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.chef.ChefMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.github.GitHubMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrganization'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.ConfigServerClientMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.ZoneRegistryMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.Controller'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.ConfigServerProxyMock'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.integration.MockMetricsService'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.restapi.application.MockAuthorizer'/>" +
- " <component id='com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator'/>" +
- " <component id='com.yahoo.vespa.hosted.rotation.MemoryRotationRepository'/>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.RootHandler'>" +
- " <binding>http://*/</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>" +
- " <binding>http://*/application/v4/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>" +
- " <binding>http://*/deployment/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.controller.ControllerApiHandler'>" +
- " <binding>http://*/controller/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.screwdriver.ScrewdriverApiHandler'>" +
- " <binding>http://*/screwdriver/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>" +
- " <binding>http://*/zone/v1</binding>" +
- " <binding>http://*/zone/v1/*</binding>" +
- " </handler>" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>" +
- " <binding>http://*/zone/v2</binding>" +
- " <binding>http://*/zone/v2/*</binding>" +
- " </handler>" +
+ "<jdisc version='1.0'>\n" +
+ " <config name='vespa.hosted.zone.config.zone'>\n" +
+ " <system>main</system>\n" +
+ " </config>\n" +
+ " <config name=\"vespa.hosted.rotation.config.rotations\">\n" +
+ " <rotations>\n" +
+ " <item key=\"rotation-id-1\">rotation-fqdn-1</item>\n" +
+ " <item key=\"rotation-id-2\">rotation-fqdn-2</item>\n" +
+ " <item key=\"rotation-id-3\">rotation-fqdn-3</item>\n" +
+ " <item key=\"rotation-id-4\">rotation-fqdn-4</item>\n" +
+ " <item key=\"rotation-id-5\">rotation-fqdn-5</item>\n" +
+ " </rotations>\n" +
+ " </config>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.chef.ChefMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.github.GitHubMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrganization'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.ConfigServerClientMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.ZoneRegistryMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.Controller'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.ConfigServerProxyMock'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.integration.MockMetricsService'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.restapi.application.MockAuthorizer'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator'/>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.RootHandler'>\n" +
+ " <binding>http://*/</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" +
+ " <binding>http://*/application/v4/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>\n" +
+ " <binding>http://*/deployment/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.controller.ControllerApiHandler'>\n" +
+ " <binding>http://*/controller/v1/*</binding>\n" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.screwdriver.ScrewdriverApiHandler'>\n" +
+ " <binding>http://*/screwdriver/v1/*</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" +
+ " </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>\n" +
+ " <binding>http://*/zone/v2</binding>\n" +
+ " <binding>http://*/zone/v2/*</binding>\n" +
+ " </handler>\n" +
"</jdisc>";
protected void assertResponse(Request request, int responseStatus, String responseMessage) throws IOException {
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 bf4586f9fd0..f48f6b02bd2 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
@@ -9,6 +9,7 @@ import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ConfigServerClientMock;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
@@ -21,10 +22,12 @@ import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
-import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzIdentity;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzService;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUser;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock;
+import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
@@ -57,17 +60,23 @@ import static com.yahoo.application.container.handler.Request.Method.PUT;
/**
* @author bratseth
* @author mpolden
+ * @author bjorncs
*/
public class ApplicationApiTest extends ControllerContainerTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/";
+
private static final ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
+ .globalServiceId("foo")
.region("corp-us-east-1")
+ .region("us-east-3")
+ .region("us-west-1")
.build();
- private static final String athenzUserDomain = "domain1";
- private static final String athenzScrewdriverDomain = AthenzUtils.SCREWDRIVER_DOMAIN.id();
+ private static final AthenzDomain ATHENZ_TENANT_DOMAIN = new AthenzDomain("domain1");
+ private static final ScrewdriverId SCREWDRIVER_ID = new ScrewdriverId("12345");
+ private static final UserId USER_ID = new UserId("myuser");
@Test
public void testApplicationApi() throws Exception {
@@ -75,7 +84,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
ContainerTester tester = controllerTester.containerTester();
tester.updateSystemVersion();
- addTenantAthenzDomain(athenzUserDomain, "mytenant"); // (Necessary but not provided in this API)
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); // (Necessary but not provided in this API)
// GET API root
tester.assertResponse(request("/application/v4/", GET),
@@ -91,14 +100,16 @@ public class ApplicationApiTest extends ControllerContainerTest {
new File("cookiefreshness.json"));
// POST (add) a tenant without property ID
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// PUT (modify) a tenant
tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// GET the authenticated user (with associated tenants)
- tester.assertResponse(request("/application/v4/user", GET),
+ tester.assertResponse(request("/application/v4/user", GET).userIdentity(USER_ID),
new File("user.json"));
// GET all tenants
tester.assertResponse(request("/application/v4/tenant/", GET),
@@ -106,15 +117,17 @@ public class ApplicationApiTest extends ControllerContainerTest {
// Add another Athens domain, so we can try to create more tenants
- addTenantAthenzDomain("domain2", "mytenant"); // New domain to test tenant w/property ID
+ createAthenzDomainWithAdmin(new AthenzDomain("domain2"), USER_ID); // New domain to test tenant w/property ID
// Add property info for that property id, as well, in the mock organization.
addPropertyData((MockOrganization) controllerTester.controller().organization(), "1234");
// POST (add) a tenant with property ID
tester.assertResponse(request("/application/v4/tenant/tenant2", POST)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"),
new File("tenant-without-applications-with-id.json"));
// PUT (modify) a tenant with property ID
tester.assertResponse(request("/application/v4/tenant/tenant2", PUT)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"),
new File("tenant-without-applications-with-id.json"));
// GET a tenant with property ID
@@ -124,15 +137,18 @@ public class ApplicationApiTest extends ControllerContainerTest {
// Test legacy OpsDB tenants
// POST (add) an OpsDB tenant with property ID
tester.assertResponse(request("/application/v4/tenant/tenant3", POST)
+ .userIdentity(USER_ID)
.data("{\"userGroup\":\"group1\",\"property\":\"property1\",\"propertyId\":\"1234\"}"),
new File("opsdb-tenant-with-id-without-applications.json"));
// PUT (modify) the OpsDB tenant to set another property
tester.assertResponse(request("/application/v4/tenant/tenant3", PUT)
+ .userIdentity(USER_ID)
.data("{\"userGroup\":\"group1\",\"property\":\"property2\",\"propertyId\":\"4321\"}"),
new File("opsdb-tenant-with-new-id-without-applications.json"));
// POST (create) an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .userIdentity(USER_ID),
new File("application-reference.json"));
// GET a tenant
tester.assertResponse(request("/application/v4/tenant/tenant1", GET),
@@ -143,11 +159,13 @@ public class ApplicationApiTest extends ControllerContainerTest {
new File("application-list.json"));
// POST triggering of a full deployment to an application (if version is omitted, current system version is used)
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST)
+ .userIdentity(USER_ID)
.data("6.1.0"),
new File("application-deployment.json"));
// DELETE (cancel) ongoing change
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE)
+ .userIdentity(USER_ID),
new File("application-deployment-cancelled.json"));
// DELETE (cancel) again is a no-op
@@ -158,14 +176,16 @@ public class ApplicationApiTest extends ControllerContainerTest {
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzUserDomain).user("mytenant"),
+ .userIdentity(USER_ID),
new File("deploy-result.json"));
// POST (deploy) an application to a zone. This simulates calls done by our tenant pipeline.
ApplicationId id = ApplicationId.from("tenant1", "application1", "default");
long screwdriverProjectId = 123;
- addScrewdriverUserToDomain("screwdriveruser1", "domain1"); // (Necessary but not provided in this API)
+ addScrewdriverUserToDeployRole(SCREWDRIVER_ID,
+ ATHENZ_TENANT_DOMAIN,
+ new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value())); // (Necessary but not provided in this API)
// Trigger deployment
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST)
@@ -175,7 +195,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// ... systemtest
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST)
.data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default");
@@ -184,7 +204,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// ... staging
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default/", POST)
.data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default");
@@ -193,7 +213,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// ... prod zone
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/", POST)
.data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, screwdriverProjectId, false, DeploymentJobs.JobType.productionCorpUsEast1);
@@ -211,22 +231,22 @@ public class ApplicationApiTest extends ControllerContainerTest {
addIssues(controllerTester, ApplicationId.from("tenant1", "application1", "default"));
// GET at root, with "&recursive=deployment", returns info about all tenants, their applications and their deployments
tester.assertResponse(request("/application/v4/", GET)
- .domain("domain1").user("mytenant")
+ .userIdentity(USER_ID)
.recursive("deployment"),
new File("recursive-root.json"));
// GET at root, with "&recursive=tenant", returns info about all tenants, with limmited info about their applications.
tester.assertResponse(request("/application/v4/", GET)
- .domain("domain1").user("mytenant")
+ .userIdentity(USER_ID)
.recursive("tenant"),
new File("recursive-until-tenant-root.json"));
// GET at a tenant, with "&recursive=true", returns full info about their applications and their deployments
tester.assertResponse(request("/application/v4/tenant/tenant1/", GET)
- .domain("domain1").user("mytenant")
+ .userIdentity(USER_ID)
.recursive("true"),
new File("tenant1-recursive.json"));
// GET at an application, with "&recursive=true", returns full info about its deployments
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/", GET)
- .domain("domain1").user("mytenant")
+ .userIdentity(USER_ID)
.recursive("true"),
new File("application1-recursive.json"));
@@ -260,18 +280,11 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default");
- // DELETE an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
- "");
- // DELETE a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
- new File("tenant-without-applications.json"));
-
// PUT (create) the authenticated user
byte[] data = new byte[0];
tester.assertResponse(request("/application/v4/user?user=newuser&domain=by", PUT)
.data(data)
- .domain(athenzUserDomain).user("newuser"),
+ .userIdentity(new UserId("newuser")),
new File("create-user-response.json"));
// OPTIONS return 200 OK
tester.assertResponse(request("/application/v4/", Request.Method.OPTIONS),
@@ -287,11 +300,13 @@ public class ApplicationApiTest extends ControllerContainerTest {
// SET global rotation override status
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation/override", PUT)
+ .userIdentity(USER_ID)
.data("{\"reason\":\"because i can\"}"),
new File("global-rotation-put.json"));
// DELETE global rotation override status
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation/override", DELETE)
+ .userIdentity(USER_ID)
.data("{\"reason\":\"because i can\"}"),
new File("global-rotation-delete.json"));
@@ -300,11 +315,18 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/promote", POST),
"{\"message\":\"Successfully copied environment hosted-instance_tenant1_application1_placeholder_component_default to hosted-instance_tenant1_application1_us-west-1_prod_default\"}");
+ // DELETE an application
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE).userIdentity(USER_ID),
+ "");
+ // DELETE a tenant
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).userIdentity(USER_ID),
+ new File("tenant-without-applications.json"));
+
controllerTester.controller().deconstruct();
}
private void addIssues(ContainerControllerTester tester, ApplicationId id) {
- tester.controller().applications().lockedOrThrow(id, application ->
+ tester.controller().applications().lockOrThrow(id, application ->
tester.controller().applications().store(application
.withDeploymentIssueId(IssueId.from("123"))
.withOwnershipIssueId(IssueId.from("321"))));
@@ -316,23 +338,28 @@ public class ApplicationApiTest extends ControllerContainerTest {
ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles);
ContainerTester tester = controllerTester.containerTester();
tester.updateSystemVersion();
- addTenantAthenzDomain(athenzUserDomain, "mytenant");
- addScrewdriverUserToDomain("screwdriveruser1", "domain1");
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
// Create tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST).userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// Create application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .userIdentity(USER_ID),
new File("application-reference.json"));
+ // Grant deploy access
+ addScrewdriverUserToDeployRole(SCREWDRIVER_ID,
+ ATHENZ_TENANT_DOMAIN,
+ new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId("application1"));
+
// POST (deploy) an application to a prod zone - allowed when project ID is not specified
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
}
@@ -342,18 +369,22 @@ public class ApplicationApiTest extends ControllerContainerTest {
ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles);
ContainerTester tester = controllerTester.containerTester();
tester.updateSystemVersion();
- addTenantAthenzDomain(athenzUserDomain, "mytenant");
- addScrewdriverUserToDomain("screwdriveruser1", "domain1");
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
// Create tenant
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// Create application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .userIdentity(USER_ID),
new File("application-reference.json"));
+ // Give Screwdriver project deploy access
+ addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId("application1"));
+
// Deploy
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.region("us-east-3")
@@ -367,12 +398,13 @@ public class ApplicationApiTest extends ControllerContainerTest {
// us-east-3
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east-3/instance/default/deploy", POST)
.data(deployData)
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsEast3);
// New zone is added before us-east-3
applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
// These decides the ordering of deploymentJobs and instances in the response
.region("us-west-1")
.region("us-east-3")
@@ -383,13 +415,13 @@ public class ApplicationApiTest extends ControllerContainerTest {
// us-west-1
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST)
.data(deployData)
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsWest1);
// us-east-3
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east-3/instance/default/deploy", POST)
- .data(deployData).domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .data(deployData).screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsEast3);
@@ -402,7 +434,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
public void testErrorResponses() throws Exception {
ContainerTester tester = new ContainerTester(container, responseFiles);
tester.updateSystemVersion();
- addTenantAthenzDomain("domain1", "mytenant");
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
// PUT (update) non-existing tenant
tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
@@ -427,28 +459,33 @@ public class ApplicationApiTest extends ControllerContainerTest {
// POST (add) a tenant
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// POST (add) another tenant under the same domain
tester.assertResponse(request("/application/v4/tenant/tenant2", POST)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create tenant 'tenant2': The Athens domain 'domain1' is already connected to tenant 'tenant1'\"}",
400);
// Add the same tenant again
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .userIdentity(USER_ID)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'tenant1' already exists\"}",
400);
// POST (create) an (empty) application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .userIdentity(USER_ID),
new File("application-reference.json"));
// Create the same application again
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"An application with id 'tenant1.application1' already exists\"}",
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .userIdentity(USER_ID),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create 'tenant1.application1': Application already exists\"}",
400);
ConfigServerClientMock configServer = (ConfigServerClientMock)container.components().getComponent("com.yahoo.vespa.hosted.controller.ConfigServerClientMock");
@@ -458,44 +495,48 @@ public class ApplicationApiTest extends ControllerContainerTest {
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzUserDomain).user("mytenant"),
+ .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", ConfigServerException.ErrorCode.OUT_OF_CAPACITY, null));
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzUserDomain).user("mytenant"),
+ .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", ConfigServerException.ErrorCode.ACTIVATION_CONFLICT, null));
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzUserDomain).user("mytenant"),
+ .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"), "Internal server error", ConfigServerException.ErrorCode.INTERNAL_SERVER_ERROR, null));
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzUserDomain).user("mytenant"),
+ .userIdentity(USER_ID),
new File("deploy-internal-server-error.json"), 500);
// DELETE tenant which has an application
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
+ .userIdentity(USER_ID),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not delete tenant 'tenant1': This tenant has active applications\"}",
400);
// DELETE application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
+ .userIdentity(USER_ID),
"");
// DELETE application again - should produce 404
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
+ .userIdentity(USER_ID),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete application 'tenant1.application1': Application not found\"}",
404);
// DELETE tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
+ .userIdentity(USER_ID),
new File("tenant-without-applications.json"));
// DELETE tenant again - should produce 404
tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
@@ -511,48 +552,46 @@ public class ApplicationApiTest extends ControllerContainerTest {
@Test
public void testAuthorization() throws Exception {
ContainerTester tester = new ContainerTester(container, responseFiles);
- String authorizedUser = "mytenant";
- String unauthorizedUser = "othertenant";
+ UserId authorizedUser = USER_ID;
+ UserId unauthorizedUser = new UserId("othertenant");
// Mutation without an authorized user is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .domain("domain1").user(null),
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"User is not authenticated\"}",
403);
// ... but read methods are allowed
tester.assertResponse(request("/application/v4/tenant/", GET)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .domain("domain1").user(null),
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"[]",
200);
- addTenantAthenzDomain("domain1", "mytenant");
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
// Creating a tenant for an Athens domain the user is not admin for is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .domain("domain1").user(unauthorizedUser),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"The user 'othertenant' is not admin in Athenz domain 'domain1'\"}",
+ .userIdentity(unauthorizedUser),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"The user 'user.othertenant' is not admin in Athenz domain 'domain1'\"}",
403);
// (Create it with the right tenant id)
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .domain("domain1").user(authorizedUser),
+ .userIdentity(authorizedUser),
new File("tenant-without-applications.json"),
200);
// Creating an application for an Athens domain the user is not admin for is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
- .domain("domain1").user(unauthorizedUser),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
+ .userIdentity(unauthorizedUser),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}",
403);
// (Create it with the right tenant id)
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
- .domain("domain1").user(authorizedUser),
+ .userIdentity(authorizedUser),
new File("application-reference.json"),
200);
@@ -560,44 +599,96 @@ public class ApplicationApiTest extends ControllerContainerTest {
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST)
.data(entity)
- .domain(athenzUserDomain).user("mytenant"),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"Principal 'mytenant' is not a Screwdriver principal. Excepted principal with Athenz domain 'cd.screwdriver.project', got 'domain1'.\"}",
+ .userIdentity(USER_ID),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"Principal 'user.myuser' is not a Screwdriver principal. Excepted principal with Athenz domain 'cd.screwdriver.project', got 'user'.\"}",
403);
// Deleting an application for an Athens domain the user is not admin for is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
- .domain("domain1").user(unauthorizedUser),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
+ .userIdentity(unauthorizedUser),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}",
403);
// (Deleting it with the right tenant id)
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
- .domain("domain1").user(authorizedUser),
+ .userIdentity(authorizedUser),
"",
200);
// Updating a tenant for an Athens domain the user is not admin for is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .domain("domain1").user(unauthorizedUser),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
+ .userIdentity(unauthorizedUser),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}",
403);
// Change Athens domain
- addTenantAthenzDomain("domain2", "mytenant");
+ createAthenzDomainWithAdmin(new AthenzDomain("domain2"), USER_ID);
tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
.data("{\"athensDomain\":\"domain2\", \"property\":\"property1\"}")
- .domain("domain1").user(authorizedUser),
+ .userIdentity(authorizedUser),
"{\"tenant\":\"tenant1\",\"type\":\"ATHENS\",\"athensDomain\":\"domain2\",\"property\":\"property1\",\"applications\":[]}",
200);
// Deleting a tenant for an Athens domain the user is not admin for is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
- .domain("domain1").user(unauthorizedUser),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
+ .userIdentity(unauthorizedUser),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}",
403);
}
+ @Test
+ public void deployment_fails_on_illegal_domain_in_deployment_spec() throws IOException {
+ ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles);
+ ContainerTester tester = controllerTester.containerTester();
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .upgradePolicy("default")
+ .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("invalid.domain"), com.yahoo.config.provision.AthenzService.from("service"))
+ .environment(Environment.prod)
+ .region("us-west-1")
+ .build();
+ long screwdriverProjectId = 123;
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
+
+ Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.id(), "tenant1", "application1");
+ ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId));
+ controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application);
+
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST)
+ .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
+ .screwdriverIdentity(screwdriverId),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"Athenz domain in deployment.xml: [invalid.domain] must match tenant domain: [domain1]\"}",
+ 403);
+
+ }
+
+ @Test
+ public void deployment_succeeds_when_correct_domain_is_used() throws IOException {
+ ContainerControllerTester controllerTester = new ContainerControllerTester(container, responseFiles);
+ ContainerTester tester = controllerTester.containerTester();
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .upgradePolicy("default")
+ .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service"))
+ .environment(Environment.prod)
+ .region("us-west-1")
+ .build();
+ long screwdriverProjectId = 123;
+ ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId));
+
+ createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
+
+ Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.id(), "tenant1", "application1");
+ controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application);
+
+ // Allow systemtest to succeed by notifying completion of system test
+ controllerTester.notifyJobCompletion(application.id(), screwdriverProjectId, true, DeploymentJobs.JobType.component);
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST)
+ .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
+ .screwdriverIdentity(screwdriverId),
+ new File("deploy-result.json"));
+
+ }
+
private HttpEntity createApplicationDeployData(ApplicationPackage applicationPackage, Optional<Long> screwdriverJobId) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.addTextBody("deployOptions", deployOptions(screwdriverJobId), ContentType.APPLICATION_JSON);
@@ -634,8 +725,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
private final String path;
private final Request.Method method;
private byte[] data = new byte[0];
- private String domain = "domain1";
- private String user = "mytenant";
+ private AthenzIdentity identity;
private String contentType = "application/json";
private String recursive;
@@ -655,8 +745,8 @@ public class ApplicationApiTest extends ControllerContainerTest {
}
return data(out.toByteArray()).contentType(data.getContentType().getValue());
}
- private RequestBuilder domain(String domain) { this.domain = domain; return this; }
- private RequestBuilder user(String user) { this.user = user; return this; }
+ private RequestBuilder userIdentity(UserId userId) { this.identity = AthenzUser.fromUserId(userId); return this; }
+ private RequestBuilder screwdriverIdentity(ScrewdriverId screwdriverId) { this.identity = AthenzService.fromScrewdriverId(screwdriverId); return this; }
private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; }
private RequestBuilder recursive(String recursive) { this.recursive = recursive; return this; }
@@ -664,10 +754,13 @@ public class ApplicationApiTest extends ControllerContainerTest {
public Request get() {
Request request = new Request("http://localhost:8080" + path +
// user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters
- "?domain=" + domain + (user == null ? "" : "&user=" + user) +
- (recursive == null ? "" : "&recursive=" + recursive),
+ (recursive == null ? "" : "?recursive=" + recursive),
data, method);
request.getHeaders().put("Content-Type", contentType);
+ if (identity != null) {
+ request.getHeaders().put("Athenz-Identity-Domain", identity.getDomain().id());
+ request.getHeaders().put("Athenz-Identity-Name", identity.getName());
+ }
return request;
}
}
@@ -681,26 +774,27 @@ public class ApplicationApiTest extends ControllerContainerTest {
* In production this happens outside hosted Vespa, so there is no API for it and we need to reach down into the
* mock setup to replicate the action.
*/
- private AthenzDomain addTenantAthenzDomain(String domainName, String userName) {
+ private void createAthenzDomainWithAdmin(AthenzDomain domain, UserId userId) {
AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components()
.getComponent(AthenzClientFactoryMock.class.getName());
- AthenzDomain athensDomain = new AthenzDomain(domainName);
- AthenzDbMock.Domain domain = new AthenzDbMock.Domain(athensDomain);
- domain.markAsVespaTenant();
- domain.admin(AthenzUtils.createPrincipal(new UserId(userName)));
- mock.getSetup().addDomain(domain);
- return athensDomain;
+ AthenzDbMock.Domain domainMock = new AthenzDbMock.Domain(domain);
+ domainMock.markAsVespaTenant();
+ domainMock.admin(AthenzUser.fromUserId(userId));
+ mock.getSetup().addDomain(domainMock);
}
/**
* In production this happens outside hosted Vespa, so there is no API for it and we need to reach down into the
* mock setup to replicate the action.
*/
- private void addScrewdriverUserToDomain(String screwdriverUserId, String domainName) {
+ private void addScrewdriverUserToDeployRole(ScrewdriverId screwdriverId,
+ AthenzDomain domain,
+ com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId applicationId) {
AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components()
.getComponent(AthenzClientFactoryMock.class.getName());
- AthenzDbMock.Domain domain = mock.getSetup().domains.get(new AthenzDomain(domainName));
- domain.admin(new AthenzPrincipal(new AthenzDomain(athenzScrewdriverDomain), new UserId(screwdriverUserId)));
+ AthenzIdentity screwdriverIdentity = AthenzService.fromScrewdriverId(screwdriverId);
+ AthenzDbMock.Application athenzApplication = mock.getSetup().domains.get(domain).applications.get(applicationId);
+ athenzApplication.addRoleMember(ApplicationAction.deploy, screwdriverIdentity);
}
private void startAndTestChange(ContainerControllerTester controllerTester, ApplicationId application, long projectId,
@@ -715,7 +809,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
application.tenant().value(), application.application().value());
tester.assertResponse(request(testPath, POST)
.data(deployData)
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
tester.assertResponse(request(testPath, DELETE),
"Deactivated " + testPath.replaceFirst("/application/v4/", ""));
@@ -726,7 +820,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
application.tenant().value(), application.application().value());
tester.assertResponse(request(stagingPath, POST)
.data(deployData)
- .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ .screwdriverIdentity(SCREWDRIVER_ID),
new File("deploy-result.json"));
tester.assertResponse(request(stagingPath, DELETE),
"Deactivated " + stagingPath.replaceFirst("/application/v4/", ""));
@@ -742,7 +836,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
*/
private void setDeploymentMaintainedInfo(ContainerControllerTester controllerTester) {
for (Application application : controllerTester.controller().applications().asList()) {
- controllerTester.controller().applications().lockedOrThrow(application.id(), lockedApplication -> {
+ controllerTester.controller().applications().lockOrThrow(application.id(), lockedApplication -> {
lockedApplication = lockedApplication.with(new ApplicationMetrics(0.5, 0.7));
for (Deployment deployment : application.deployments().values()) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java
index e5898b7a593..988304be600 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java
@@ -5,11 +5,11 @@ import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.TestIdentities;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
-import com.yahoo.vespa.hosted.controller.athenz.NToken;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzUtils;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.NToken;
import javax.ws.rs.core.SecurityContext;
import java.security.Principal;
@@ -20,6 +20,7 @@ import java.util.Optional;
* This is necessary because filters are not currently executed when executing requests with Application.
*
* @author bratseth
+ * @author bjorncs
*/
@SuppressWarnings("unused") // injected
public class MockAuthorizer extends Authorizer {
@@ -30,10 +31,14 @@ public class MockAuthorizer extends Authorizer {
/** Returns a principal given by the request parameters 'domain' and 'user' */
@Override
- public Optional<Principal> getPrincipalIfAny(HttpRequest request) {
- if (request.getProperty("user") == null) return Optional.empty();
- return Optional.of(new AthenzPrincipal(new AthenzDomain(request.getProperty("domain")),
- new UserId(request.getProperty("user"))));
+ public Optional<AthenzPrincipal> getPrincipalIfAny(HttpRequest request) {
+ String domain = request.getHeader("Athenz-Identity-Domain");
+ String name = request.getHeader("Athenz-Identity-Name");
+ if (domain == null || name == null) return Optional.empty();
+ return Optional.of(
+ new AthenzPrincipal(
+ AthenzUtils.createAthenzIdentity(new AthenzDomain(domain), name),
+ new NToken("dummy")));
}
/** Returns the hardcoded NToken of {@link TestIdentities#userId} */
@@ -42,12 +47,6 @@ public class MockAuthorizer extends Authorizer {
return Optional.of(TestIdentities.userNToken);
}
- private static class MockPrincipal implements Principal {
-
- @Override
- public String getName() { return TestIdentities.userId.id(); }
-
- }
@Override
protected Optional<SecurityContext> securityContextOf(HttpRequest request) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponseTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponseTest.java
index 6cf90905679..4c25bf6fe61 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponseTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponseTest.java
@@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.controller.restapi.application;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.io.IOUtils;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
@@ -32,10 +32,10 @@ public class ServiceApiResponseTest {
@Test
public void testServiceViewResponse() throws URISyntaxException, IOException {
- ServiceApiResponse response = new ServiceApiResponse(new Zone(Environment.prod, RegionName.from("us-west-1")),
- ApplicationId.from("tenant1", "application1", "default"),
- Collections.singletonList(new URI("config-server1")),
- new URI("http://server1:4080/request/path?foo=bar"));
+ ServiceApiResponse response = new ServiceApiResponse(ZoneId.from(Environment.prod, RegionName.from("us-west-1")),
+ ApplicationId.from("tenant1", "application1", "default"),
+ Collections.singletonList(new URI("config-server1")),
+ new URI("http://server1:4080/request/path?foo=bar"));
ApplicationView applicationView = new ApplicationView();
ClusterView clusterView = new ClusterView();
clusterView.type = "container";
@@ -63,7 +63,7 @@ public class ServiceApiResponseTest {
@Test
public void testServiceViewResponseWithURLs() throws URISyntaxException, IOException {
- ServiceApiResponse response = new ServiceApiResponse(new Zone(Environment.prod, RegionName.from("us-west-1")),
+ ServiceApiResponse response = new ServiceApiResponse(ZoneId.from(Environment.prod, RegionName.from("us-west-1")),
ApplicationId.from("tenant2", "application2", "default"),
Collections.singletonList(new URI("http://cfg1.test/")),
new URI("http://cfg1.test/serviceview/v1/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/service/searchnode-9dujk1pa0vufxrj6n4yvmi8uc/state/v1"));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
index 6442ddf5c02..961e005bfbd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
@@ -205,7 +205,7 @@
],
"compileVersion": "(ignore)",
"globalRotations": [
- "http://fake-global-rotation-tenant1.application1"
+ "http://application1.tenant1.global.vespa.yahooapis.com:4080/"
],
"instances": [
{
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
index fdd3dcc4d5c..3924cf51ca9 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
@@ -146,7 +146,7 @@
],
"compileVersion": "(ignore)",
"globalRotations": [
- "http://fake-global-rotation-tenant1.application1"
+ "http://application1.tenant1.global.vespa.yahooapis.com:4080/"
],
"instances": [
{
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
index 41556c04209..5030fc7d0a6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
@@ -146,7 +146,7 @@
],
"compileVersion": "6.1.0",
"globalRotations": [
- "http://fake-global-rotation-tenant1.application1"
+ "http://application1.tenant1.global.vespa.yahooapis.com:4080/"
],
"instances": [
@include(dev-us-west-1.json),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/user.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/user.json
index d3927cbcfcf..79b9a785801 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/user.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/user.json
@@ -1,5 +1,5 @@
{
- "user": "mytenant",
+ "user": "myuser",
"tenants": @include(tenant-list.json),
"tenantExists": false
} \ No newline at end of file
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 354bab4379c..8f8b76c83c6 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
@@ -19,6 +19,9 @@
"name": "DeploymentMetricsMaintainer"
},
{
+ "name": "DnsMaintainer"
+ },
+ {
"name": "MetricsReporter"
},
{
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 55a4b46f4a7..d16a0222e4a 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
@@ -6,7 +6,7 @@ import com.yahoo.application.container.handler.Request;
import com.yahoo.component.Version;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
@@ -95,14 +95,12 @@ public class DeploymentApiTest extends ControllerContainerTest {
private void deployCompletely(Application application, ApplicationPackage applicationPackage, long projectId,
boolean success) {
tester.notifyJobCompletion(application.id(), projectId, true, component);
- tester.deploy(application, applicationPackage, new Zone(Environment.test,
- RegionName.from("us-east-1")), projectId);
+ tester.deploy(application, applicationPackage, ZoneId.from(Environment.test, RegionName.from("us-east-1")), projectId);
tester.notifyJobCompletion(application.id(), projectId, true, systemTest);
- tester.deploy(application, applicationPackage, new Zone(Environment.staging,
- RegionName.from("us-east-3")), projectId);
+ tester.deploy(application, applicationPackage, ZoneId.from(Environment.staging, RegionName.from("us-east-3")), projectId);
tester.notifyJobCompletion(application.id(), projectId, success, stagingTest);
if (success) {
- tester.deploy(application, applicationPackage, new Zone(Environment.prod,RegionName.from("corp-us-east-1")),
+ tester.deploy(application, applicationPackage, ZoneId.from(Environment.prod, RegionName.from("corp-us-east-1")),
projectId);
tester.notifyJobCompletion(application.id(), projectId, true, productionCorpUsEast1);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java
index e6b3eacd44e..1269bb23105 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java
@@ -7,9 +7,8 @@ import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
@@ -40,8 +39,8 @@ import static org.junit.Assert.assertTrue;
public class ScrewdriverApiTest extends ControllerContainerTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/";
- private static final Zone testZone = new Zone(Environment.test, RegionName.from("us-east-1"));
- private static final Zone stagingZone = new Zone(Environment.staging, RegionName.from("us-east-3"));
+ private static final ZoneId testZone = ZoneId.from(Environment.test, RegionName.from("us-east-1"));
+ private static final ZoneId stagingZone = ZoneId.from(Environment.staging, RegionName.from("us-east-3"));
@Test
public void testGetReleaseStatus() throws Exception {
@@ -148,7 +147,7 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
tester.containerTester().updateSystemVersion();
Application app = tester.createApplication();
- tester.controller().applications().lockedOrThrow(app.id(), application ->
+ tester.controller().applications().lockOrThrow(app.id(), application ->
tester.controller().applications().store(application.withProjectId(1)));
// Unknown application
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 a00665b77cb..2d92d10b661 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
@@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.vespa.hosted.controller.ZoneRegistryMock;
import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
@@ -21,12 +21,12 @@ import java.util.List;
public class ZoneApiTest extends ControllerContainerTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/";
- private static final List<Zone> zones = Arrays.asList(
- new Zone(Environment.prod, RegionName.from("us-north-1")),
- new Zone(Environment.dev, RegionName.from("us-north-2")),
- new Zone(Environment.test, RegionName.from("us-north-3")),
- new Zone(Environment.staging, RegionName.from("us-north-4"))
- );
+ private static final List<ZoneId> zones = Arrays.asList(
+ ZoneId.from(Environment.prod, RegionName.from("us-north-1")),
+ ZoneId.from(Environment.dev, RegionName.from("us-north-2")),
+ ZoneId.from(Environment.test, RegionName.from("us-north-3")),
+ ZoneId.from(Environment.staging, RegionName.from("us-north-4"))
+ );
private ContainerControllerTester tester;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
index 63899d808f9..9c20c470cf8 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
@@ -4,7 +4,7 @@ import com.yahoo.application.container.handler.Request;
import com.yahoo.application.container.handler.Request.Method;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneId;
import com.yahoo.text.Utf8;
import com.yahoo.vespa.hosted.controller.ConfigServerProxyMock;
import com.yahoo.vespa.hosted.controller.ZoneRegistryMock;
@@ -26,12 +26,12 @@ import static org.junit.Assert.assertFalse;
public class ZoneApiTest extends ControllerContainerTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/";
- private static final List<Zone> zones = Arrays.asList(
- new Zone(Environment.prod, RegionName.from("us-north-1")),
- new Zone(Environment.dev, RegionName.from("us-north-2")),
- new Zone(Environment.test, RegionName.from("us-north-3")),
- new Zone(Environment.staging, RegionName.from("us-north-4"))
- );
+ private static final List<ZoneId> zones = Arrays.asList(
+ ZoneId.from(Environment.prod, RegionName.from("us-north-1")),
+ ZoneId.from(Environment.dev, RegionName.from("us-north-2")),
+ ZoneId.from(Environment.test, RegionName.from("us-north-3")),
+ ZoneId.from(Environment.staging, RegionName.from("us-north-4"))
+ );
private ContainerControllerTester tester;
private ConfigServerProxyMock proxy;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java
new file mode 100644
index 00000000000..c259ae0ca60
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationTest.java
@@ -0,0 +1,175 @@
+// 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.rotation;
+
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRotation;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.net.URI;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * @author Oyvind Gronnesby
+ * @author mpolden
+ */
+public class RotationTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private final RotationsConfig rotationsConfig = new RotationsConfig(
+ new RotationsConfig.Builder()
+ .rotations("foo-1", "foo-1.com")
+ .rotations("foo-2", "foo-2.com")
+ );
+
+ private final RotationsConfig rotationsConfigWhitespaces = new RotationsConfig(
+ new RotationsConfig.Builder()
+ .rotations("foo-1", "\n foo-1.com \n")
+ .rotations("foo-2", "foo-2.com")
+ );
+
+ private final ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("us-west-1")
+ .build();
+
+ private DeploymentTester tester;
+ private RotationRepository repository;
+ private Application application;
+
+ @Before
+ public void before() {
+ tester = new DeploymentTester(new ControllerTester(rotationsConfig));
+ repository = tester.controller().applications().rotationRepository();
+ application = tester.createApplication("app1", "tenant1", 11L,1L);
+ }
+
+ @Test
+ public void assigns_and_reuses_rotation() {
+ // Deploying assigns a rotation
+ tester.deployCompletely(application, applicationPackage);
+ Rotation expected = new Rotation(new RotationId("foo-1"), "foo-1.com");
+
+ application = tester.applications().require(application.id());
+ assertEquals(expected.id(), application.rotation().get().id());
+ assertEquals(URI.create("http://app1.tenant1.global.vespa.yahooapis.com:4080/"),
+ application.rotation().get().url());
+ try (RotationLock lock = repository.lock()) {
+ Rotation rotation = repository.getRotation(tester.applications().require(application.id()), lock);
+ assertEquals(expected, rotation);
+ }
+
+ // Deploying once more assigns same rotation
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("us-west-1")
+ .searchDefinition("search foo { }") // Update application package so there is something to deploy
+ .build();
+ tester.deployCompletely(application, applicationPackage);
+ assertEquals(expected.id(), tester.applications().require(application.id()).rotation().get().id());
+ }
+
+ @Test
+ public void strips_whitespace_in_rotation_fqdn() {
+ DeploymentTester tester = new DeploymentTester(new ControllerTester(rotationsConfigWhitespaces));
+ RotationRepository repository = tester.controller().applications().rotationRepository();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ tester.deployCompletely(application, applicationPackage);
+ application = tester.applications().require(application.id());
+
+ try (RotationLock lock = repository.lock()) {
+ Rotation rotation = repository.getRotation(application, lock);
+ Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
+ assertEquals(assignedRotation, rotation);
+ }
+ }
+
+
+ @Test
+ public void out_of_rotations() {
+ // Assigns 1 rotation
+ tester.deployCompletely(application, applicationPackage);
+
+ // Assigns 1 more
+ Application application2 = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ tester.deployCompletely(application2, applicationPackage);
+
+ // We're now out of rotations
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("no rotations available");
+ Application application3 = tester.createApplication("app3", "tenant3", 33L,
+ 3L);
+ tester.deployCompletely(application3, applicationPackage);
+ }
+
+ @Test
+ public void too_few_zones() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .build();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("less than 2 prod zones are defined");
+ tester.deployCompletely(application, applicationPackage);
+ }
+
+ @Test
+ public void no_rotation_assigned_for_application_without_service_id() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .region("us-east-3")
+ .region("us-west-1")
+ .build();
+ tester.deployCompletely(application, applicationPackage);
+ Application app = tester.applications().require(application.id());
+ Optional<ApplicationRotation> rotation = app.rotation();
+ assertFalse(rotation.isPresent());
+ }
+
+ @Test
+ public void application_with_only_one_non_corp_region() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("corp-us-east-1")
+ .build();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("less than 2 prod zones are defined");
+ tester.deployCompletely(application, applicationPackage);
+ }
+
+ @Test
+ public void application_with_corp_region_and_two_non_corp_region() {
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .globalServiceId("foo")
+ .region("us-east-3")
+ .region("corp-us-east-1")
+ .region("us-west-1")
+ .build();
+ Application application = tester.createApplication("app2", "tenant2", 22L,
+ 2L);
+ tester.deployCompletely(application, applicationPackage);
+ assertEquals(new RotationId("foo-1"), tester.applications().require(application.id())
+ .rotation().get().id());
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
deleted file mode 100644
index b4074fc1944..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
+++ /dev/null
@@ -1,212 +0,0 @@
-// 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.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
-import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
-import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
-import com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import java.io.StringReader;
-import java.net.URI;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-/**
- * @author Oyvind Gronnesby
- */
-public class ControllerRotationRepositoryTest {
-
- private final RotationsConfig rotationsConfig = new RotationsConfig(
- new RotationsConfig.Builder()
- .rotations("foo-1", "foo-1.com")
- .rotations("foo-2", "foo-2.com")
- );
- private final RotationsConfig rotationsConfigWhitespaces = new RotationsConfig(
- new RotationsConfig.Builder()
- .rotations("foo-1", "\n foo-1.com \n")
- .rotations("foo-2", "foo-2.com")
- );
- private final ControllerDb controllerDb = new MemoryControllerDb();
- private final ApplicationId applicationId = ApplicationId.from("msbe", "tumblr-search", "default");
-
- @Rule public ExpectedException thrown = ExpectedException.none();
-
- private final DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='foo'>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecOneRegion = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='nalle'>" +
- " <region active='true'>us-east</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecNoServiceId = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecOnlyOneNonCorpRegion = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='nalle'>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>corp-us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private final DeploymentSpec deploymentSpecWithAdditionalCorpZone = DeploymentSpec.fromXml(
- new StringReader(
- "<deployment>" +
- " <prod global-service-id='nalle'>" +
- " <region active='true'>us-east</region>" +
- " <region active='true'>corp-us-west</region>" +
- " <region active='true'>us-west</region>" +
- " </prod>" +
- "</deployment>"
- )
- );
-
- private ControllerRotationRepository repository;
- private ControllerRotationRepository repositoryWhitespaces;
- private Metric metric;
-
- @Before
- public void setup_repository() {
- metric = mock(Metric.class);
- repository = new ControllerRotationRepository(rotationsConfig, controllerDb, metric);
- repositoryWhitespaces = new ControllerRotationRepository(rotationsConfigWhitespaces, controllerDb, metric);
- controllerDb.assignRotation(new RotationId("foo-1"), applicationId);
- }
-
- @Test
- public void application_with_rotation_reused() {
- Set<Rotation> rotations = repository.getOrAssignRotation(applicationId, deploymentSpec);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertContainsOnly(assignedRotation, rotations);
- }
-
- @Test
- public void names_stripped() {
- Set<Rotation> rotations = repositoryWhitespaces.getOrAssignRotation(applicationId, deploymentSpec);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertContainsOnly(assignedRotation, rotations);
- }
-
- @Test
- public void application_without_rotation() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpec);
- Rotation assignedRotation = new Rotation(new RotationId("foo-2"), "foo-2.com");
- assertContainsOnly(assignedRotation, rotations);
- verify(metric).set(eq(ControllerRotationRepository.REMAINING_ROTATIONS_METRIC_NAME), eq(1), any());
- }
-
- @Test
- public void application_without_rotation_but_none_left() {
- application_without_rotation(); // run this test to assign last rotation
- ApplicationId third = ApplicationId.from("thirdtenant", "thirdapplication", "default");
-
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("no rotations available");
-
- repository.getOrAssignRotation(third, deploymentSpec);
- verify(metric).set(eq(ControllerRotationRepository.REMAINING_ROTATIONS_METRIC_NAME), eq(0), any());
- }
-
- @Test
- public void application_without_rotation_but_does_not_qualify() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
-
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("less than 2 prod zones are defined");
-
- repository.getOrAssignRotation(other, deploymentSpecOneRegion);
- }
-
- @Test
- public void application_with_rotation_but_does_not_qualify() {
- Set<Rotation> rotations = repository.getOrAssignRotation(applicationId, deploymentSpecOneRegion);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertContainsOnly(assignedRotation, rotations);
- }
-
- @Test
- public void application_with_rotation_is_listed() {
- repository.getOrAssignRotation(applicationId, deploymentSpec);
- Set<URI> uris = repository.getRotationUris(applicationId);
- assertEquals(Collections.singleton(URI.create("http://tumblr-search.msbe.global.vespa.yahooapis.com:4080/")), uris);
- }
-
- @Test
- public void application_without_rotation_is_empty() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<URI> uris = repository.getRotationUris(other);
- assertTrue(uris.isEmpty());
- }
-
- @Test
- public void application_without_serviceid_and_two_regions() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpecNoServiceId);
- assertTrue(rotations.isEmpty());
- }
-
- @Test
- public void application_with_only_one_non_corp_region() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
-
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("less than 2 prod zones are defined");
-
- repository.getOrAssignRotation(other, deploymentSpecOnlyOneNonCorpRegion);
- }
-
- @Test
- public void application_with_corp_region_and_two_non_corp_region() {
- ApplicationId other = ApplicationId.from("othertenant", "otherapplication", "default");
- Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpecWithAdditionalCorpZone);
- assertContainsOnly(new Rotation(new RotationId("foo-2"), "foo-2.com"), rotations);
- }
-
- private static <T> void assertContainsOnly(T item, Collection<T> items) {
- assertTrue("Collection contains only " + item.toString(),
- items.size() == 1 && items.contains(item));
- }
-
-}