aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Marius Venstad <jonmv@users.noreply.github.com>2019-10-02 10:56:52 +0200
committerGitHub <noreply@github.com>2019-10-02 10:56:52 +0200
commit0f8dece7a6a38ccd86d90cb7db1a45a430429246 (patch)
tree98e5979daab6dcfb012373d59a790e9afb899ecf
parent7074971cb3e696576c83d6e46e89626fafe87fd4 (diff)
parentb31cd8f92591af5fda79328d6dc551fa34c6eac1 (diff)
Merge pull request #10829 from vespa-engine/jvenstad/remove-old-instance-parts-2
Jvenstad/remove old instance parts 2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java4
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java42
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java54
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java167
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java79
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java32
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java100
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java48
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializer.java574
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java80
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java117
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java223
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldCuratorDb.java687
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldMockCuratorDb.java22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json534
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-with-routing-policy.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java42
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json2
43 files changed, 490 insertions, 2616 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java
index 5ebea6c8d87..03eda33233d 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java
@@ -43,7 +43,7 @@ public class MockUserManagement implements UserManagement {
@Override
public void removeUsers(Role role, Collection<UserId> users) {
- memberships.get(role).removeAll(users);
+ memberships.get(role).removeIf(user -> users.contains(new UserId(user.email())));
}
@Override
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
index 08702027264..958ded06c78 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
@@ -46,6 +46,15 @@ enum PathGroup {
Optional.of("/api"),
"/application/v4/tenant/{tenant}/application/"),
+ tenantKeys(Matcher.tenant,
+ Optional.of("/api"),
+ "/application/v4/tenant/{tenant}/key/"),
+
+ applicationKeys(Matcher.tenant,
+ Matcher.application,
+ Optional.of("/api"),
+ "/application/v4/tenant/{tenant}/application/{application}/key/"),
+
/** Path for the base application resource. */
application(Matcher.tenant,
Matcher.application,
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
index 290382c6e6c..db7dd5909b3 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
@@ -83,6 +83,11 @@ enum Policy {
.on(PathGroup.applicationInfo, PathGroup.productionRestart)
.in(SystemName.all())),
+ /** Access to create and delete developer and deploy keys under a tenant. */
+ keyManagement(Privilege.grant(Action.write())
+ .on(PathGroup.tenantKeys, PathGroup.applicationKeys)
+ .in(SystemName.all())),
+
/** Full access to application development deployments. */
developmentDeployment(Privilege.grant(Action.all())
.on(PathGroup.developmentDeployment, PathGroup.developmentRestart)
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
index 980b8bd316f..7bbd89404c7 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
@@ -56,7 +56,8 @@ public enum RoleDefinition {
/** Tenant operator with access to create application under a tenant, and to read the tenant's and public data. */
tenantOperator(everyone,
Policy.tenantRead,
- Policy.applicationCreate),
+ Policy.applicationCreate,
+ Policy.keyManagement),
/** Tenant admin with full access to all tenant resources, except deleting the tenant. */
tenantAdmin(tenantOperator,
@@ -84,6 +85,7 @@ public enum RoleDefinition {
Policy.applicationUpdate,
Policy.applicationDelete,
Policy.applicationOperations,
+ Policy.keyManagement,
Policy.developmentDeployment);
private final Set<RoleDefinition> parents;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java
index 3378f9e0061..92f902dc0f7 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java
@@ -49,4 +49,5 @@ public class SecurityContext {
", roles=" + roles +
'}';
}
+
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java
new file mode 100644
index 00000000000..11e4552fcb5
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java
@@ -0,0 +1,42 @@
+package com.yahoo.vespa.hosted.controller.api.role;
+
+import java.security.Principal;
+
+/**
+ * A principal wrapper of a single String entry.
+ *
+ * @author jonmv
+ */
+public class SimplePrincipal implements Principal {
+
+ private final String name;
+
+ public SimplePrincipal(String name) {
+ if (name.isBlank())
+ throw new IllegalArgumentException("Name cannot be blank");
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ return name.equals(((SimplePrincipal) o).name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+}
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 83fb71422cb..c17ac044136 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
@@ -1,11 +1,10 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
@@ -13,7 +12,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.application.ApplicationActivity;
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.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
@@ -27,6 +25,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
+import java.util.Set;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -51,21 +51,21 @@ public class Application {
private final Optional<User> owner;
private final OptionalInt majorVersion;
private final ApplicationMetrics metrics;
- private final Optional<String> pemDeployKey;
+ private final Set<String> pemDeployKeys;
private final Map<InstanceName, Instance> instances;
/** Creates an empty application. */
public Application(TenantAndApplicationId id, Instant now) {
this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Change.empty(), Change.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(),
- new ApplicationMetrics(0, 0), Optional.empty(), OptionalLong.empty(), true, List.of());
+ new ApplicationMetrics(0, 0), Set.of(), OptionalLong.empty(), false, List.of());
}
// DO NOT USE! For serialization purposes, only.
public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
- Change change, Change outstandingChange, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey, OptionalLong projectId,
- boolean internal, Collection<Instance> instances) {
+ Change change, Change outstandingChange, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
+ OptionalInt majorVersion, ApplicationMetrics metrics, Set<String> pemDeployKeys,
+ OptionalLong projectId, boolean internal, Collection<Instance> instances) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
@@ -77,41 +77,10 @@ public class Application {
this.owner = Objects.requireNonNull(owner, "owner cannot be null");
this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null");
this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
- this.pemDeployKey = Objects.requireNonNull(pemDeployKey, "pemDeployKey cannot be null");
+ this.pemDeployKeys = Objects.requireNonNull(pemDeployKeys, "pemDeployKeys cannot be null");
this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null");
this.internal = internal;
- this.instances = ImmutableMap.copyOf((Iterable<Map.Entry<InstanceName, Instance>>)
- instances.stream()
- .map(instance -> Map.entry(instance.name(), instance))
- .sorted(Comparator.comparing(Map.Entry::getKey))
- ::iterator);
- }
-
- /** Returns an aggregate application, from the given instances, if at least one. */
- public static Optional<Application> aggregate(List<Instance> instances) {
- if (instances.isEmpty())
- return Optional.empty();
-
- Instance base = instances.stream()
- .filter(instance -> instance.id().instance().isDefault())
- .findFirst()
- .orElse(instances.iterator().next());
-
- return Optional.of(new Application(TenantAndApplicationId.from(base.id()), base.createdAt(), base.deploymentSpec(),
- base.validationOverrides(), base.change(), base.outstandingChange(),
- base.deploymentJobs().issueId(), base.ownershipIssueId(), base.owner(),
- base.majorVersion(), base.metrics(), base.pemDeployKey(),
- base.deploymentJobs().projectId(), base.deploymentJobs().deployedInternally(), instances));
- }
-
- /** Returns an old Instance representation of this and the given instance, for serialisation. */
- public Instance legacy(InstanceName instance) {
- Instance base = require(instance);
-
- return new Instance(base.id(), createdAt, deploymentSpec, validationOverrides, base.deployments(),
- new DeploymentJobs(projectId, base.deploymentJobs().jobStatus().values(), deploymentIssueId, internal),
- change, outstandingChange, ownershipIssueId, owner,
- majorVersion, metrics, pemDeployKey, base.rotations(), base.rotationStatus());
+ this.instances = ImmutableSortedMap.copyOf(instances.stream().collect(Collectors.toMap(Instance::name, Function.identity())));
}
public TenantAndApplicationId id() { return id; }
@@ -221,7 +190,8 @@ public class Application {
.min(Comparator.naturalOrder());
}
- public Optional<String> pemDeployKey() { return pemDeployKey; }
+ /** Returns the set of deploy keys for this application. */
+ public Set<String> pemDeployKeys() { return pemDeployKeys; }
@Override
public boolean equals(Object o) {
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 ba9f8e809c8..c012abff670 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
@@ -149,6 +149,7 @@ public class ApplicationController {
// Update serialization format of all applications
Once.after(Duration.ofMinutes(1), () -> {
+ curator.deleteOldApplicationData();
Instant start = clock.instant();
int count = 0;
for (Application application : curator.readApplications()) {
@@ -285,14 +286,12 @@ public class ApplicationController {
if ( ! id.instance().isTester()) // Only store the application permits for non-user applications.
accessControl.createApplication(id, credentials.get());
}
- List<Instance> instances = getApplication(TenantAndApplicationId.from(id)).map(application -> application.instances().values())
- .map(ArrayList::new)
- .orElse(new ArrayList<>());
- instances.add(new Instance(id, clock.instant()));
- LockedApplication application = new LockedApplication(Application.aggregate(instances).get(), lock);
- store(application);
- log.info("Created " + application);
- return application.get();
+ Application application = getApplication(TenantAndApplicationId.from(id)).orElse(new Application(TenantAndApplicationId.from(id),
+ clock.instant()));
+ LockedApplication locked = new LockedApplication(application, lock).withNewInstance(id.instance());
+ store(locked);
+ log.info("Created " + locked);
+ return locked.get();
}
}
@@ -754,7 +753,7 @@ public class ApplicationController {
controller.nameServiceForwarder().removeRecords(Record.Type.CNAME, RecordName.from(name), Priority.normal);
});
});
- curator.storeWithoutInstance(application.without(applicationId.instance()).get(), applicationId);
+ curator.storeWithoutInstance(application.without(applicationId.instance()).get());
log.info("Deleted " + application);
});
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
index 3f1bebfed48..f885b7a146e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
@@ -3,8 +3,6 @@ package com.yahoo.vespa.hosted.controller;
import com.google.common.collect.ImmutableMap;
import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
@@ -13,11 +11,7 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.ApplicationActivity;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -26,20 +20,17 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.EndpointList;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
import java.time.Instant;
-import java.util.Collections;
-import java.util.Comparator;
+import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.OptionalInt;
import java.util.OptionalLong;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -53,95 +44,40 @@ import java.util.stream.Collectors;
public class Instance {
private final ApplicationId id;
- private final Instant createdAt;
- private final DeploymentSpec deploymentSpec;
- private final ValidationOverrides validationOverrides;
private final Map<ZoneId, Deployment> deployments;
private final DeploymentJobs deploymentJobs;
- private final Change change;
- private final Change outstandingChange;
- private final Optional<IssueId> ownershipIssueId;
- private final Optional<User> owner;
- private final OptionalInt majorVersion;
- private final ApplicationMetrics metrics;
- private final Optional<String> pemDeployKey;
private final List<AssignedRotation> rotations;
private final RotationStatus rotationStatus;
- /** Creates an empty instance*/
- public Instance(ApplicationId id, Instant now) {
- this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Collections.emptyMap(),
- new DeploymentJobs(OptionalLong.empty(), Collections.emptyList(), Optional.empty(), false),
- Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(),
- new ApplicationMetrics(0, 0),
- Optional.empty(), Collections.emptyList(), RotationStatus.EMPTY);
+ /** Creates an empty instance */
+ public Instance(ApplicationId id) {
+ this(id, Set.of(), new DeploymentJobs(List.of()),
+ List.of(), RotationStatus.EMPTY);
}
/** Creates an empty instance*/
- public Instance(ApplicationId id, List<Deployment> deployments, DeploymentJobs deploymentJobs,
+ public Instance(ApplicationId id, Collection<Deployment> deployments, DeploymentJobs deploymentJobs,
List<AssignedRotation> rotations, RotationStatus rotationStatus) {
- this(id,
- Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty,
- deployments.stream().collect(Collectors.toMap(Deployment::zone, Function.identity())),
- deploymentJobs,
- Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(),
- new ApplicationMetrics(0, 0), Optional.empty(),
- rotations, rotationStatus);
- }
-
- /** Used from persistence layer: Do not use */
- public Instance(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
- List<Deployment> deployments, DeploymentJobs deploymentJobs, Change change,
- Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey,
- List<AssignedRotation> rotations, RotationStatus rotationStatus) {
- this(id, createdAt, deploymentSpec, validationOverrides,
- deployments.stream().collect(Collectors.toMap(Deployment::zone, Function.identity())),
- deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion,
- metrics, pemDeployKey, rotations, rotationStatus);
- }
-
- Instance(ApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
- Map<ZoneId, Deployment> deployments, DeploymentJobs deploymentJobs, Change change,
- Change outstandingChange, Optional<IssueId> ownershipIssueId, Optional<User> owner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey,
- List<AssignedRotation> rotations, RotationStatus rotationStatus) {
this.id = Objects.requireNonNull(id, "id cannot be null");
- this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
- this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
- this.validationOverrides = Objects.requireNonNull(validationOverrides, "validationOverrides cannot be null");
- this.deployments = ImmutableMap.copyOf(Objects.requireNonNull(deployments, "deployments cannot be null"));
+ this.deployments = ImmutableMap.copyOf(Objects.requireNonNull(deployments, "deployments cannot be null").stream()
+ .collect(Collectors.toMap(Deployment::zone, Function.identity())));
this.deploymentJobs = Objects.requireNonNull(deploymentJobs, "deploymentJobs cannot be null");
- this.change = Objects.requireNonNull(change, "change cannot be null");
- this.outstandingChange = Objects.requireNonNull(outstandingChange, "outstandingChange cannot be null");
- this.ownershipIssueId = Objects.requireNonNull(ownershipIssueId, "ownershipIssueId cannot be null");
- this.owner = Objects.requireNonNull(owner, "owner cannot be null");
- this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null");
- this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
- this.pemDeployKey = pemDeployKey;
this.rotations = List.copyOf(Objects.requireNonNull(rotations, "rotations cannot be null"));
this.rotationStatus = Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null");
}
public Instance withJobPause(JobType jobType, OptionalLong pausedUntil) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs.withPause(jobType, pausedUntil), change, outstandingChange,
- ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ return new Instance(id, deployments.values(), deploymentJobs.withPause(jobType, pausedUntil),
rotations, rotationStatus);
}
- public Instance withJobCompletion(JobType jobType, JobStatus.JobRun completion,
- Optional<DeploymentJobs.JobError> jobError) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs.withCompletion(jobType, completion, jobError),
- change, outstandingChange, ownershipIssueId, owner, majorVersion, metrics,
- pemDeployKey, rotations, rotationStatus);
+ public Instance withJobCompletion(JobType jobType, JobStatus.JobRun completion, Optional<DeploymentJobs.JobError> jobError) {
+ return new Instance(id, deployments.values(), deploymentJobs.withCompletion(jobType, completion, jobError),
+ rotations, rotationStatus);
}
public Instance withJobTriggering(JobType jobType, JobStatus.JobRun job) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs.withTriggering(jobType, job), change, outstandingChange,
- ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ return new Instance(id, deployments.values(), deploymentJobs.withTriggering(jobType, job),
rotations, rotationStatus);
}
@@ -168,7 +104,6 @@ public class Instance {
Deployment deployment = deployments.get(zone);
if (deployment == null) return this; // No longer deployed in this zone.
return with(deployment.withClusterInfo(clusterInfo));
-
}
public Instance recordActivityAt(Instant instant, ZoneId zone) {
@@ -190,28 +125,18 @@ public class Instance {
}
public Instance withoutDeploymentJob(JobType jobType) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs.without(jobType), change, outstandingChange,
- ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ return new Instance(id, deployments.values(), deploymentJobs.without(jobType),
rotations, rotationStatus);
}
- public Instance with(ApplicationMetrics metrics) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion,
- metrics, pemDeployKey, rotations, rotationStatus);
- }
-
public Instance with(List<AssignedRotation> assignedRotations) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion,
- metrics, pemDeployKey, assignedRotations, rotationStatus);
+ return new Instance(id, deployments.values(), deploymentJobs,
+ assignedRotations, rotationStatus);
}
public Instance with(RotationStatus rotationStatus) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion,
- metrics, pemDeployKey, rotations, rotationStatus);
+ return new Instance(id, deployments.values(), deploymentJobs,
+ rotations, rotationStatus);
}
private Instance with(Deployment deployment) {
@@ -221,30 +146,14 @@ public class Instance {
}
private Instance with(Map<ZoneId, Deployment> deployments) {
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, change, outstandingChange, ownershipIssueId, owner, majorVersion,
- metrics, pemDeployKey, rotations, rotationStatus);
+ return new Instance(id, deployments.values(), deploymentJobs,
+ rotations, rotationStatus);
}
public ApplicationId id() { return id; }
public InstanceName name() { return id.instance(); }
- public Instant createdAt() { return createdAt; }
-
- /**
- * Returns the last deployed deployment spec of this application,
- * or the empty deployment spec if it has never been deployed
- */
- public DeploymentSpec deploymentSpec() { return deploymentSpec; }
-
- /**
- * Returns the last deployed validation overrides of this application,
- * or the empty validation overrides if it has never been deployed
- * (or was deployed with an empty/missing validation overrides)
- */
- public ValidationOverrides validationOverrides() { return validationOverrides; }
-
/** Returns an immutable map of the current deployments of this */
public Map<ZoneId, Deployment> deployments() { return deployments; }
@@ -260,38 +169,6 @@ public class Instance {
public DeploymentJobs deploymentJobs() { return deploymentJobs; }
- /**
- * Returns base change for this application, i.e., the change that is deployed outside block windows.
- * This is empty when no change is currently under deployment.
- */
- public Change change() { return change; }
-
- /**
- * Returns whether this has an outstanding change (in the source repository), which
- * has currently not started deploying (because a deployment is (or was) already in progress
- */
- public Change outstandingChange() { return outstandingChange; }
-
- /** Returns ID of the last ownership issue filed for this */
- public Optional<IssueId> ownershipIssueId() {
- return ownershipIssueId;
- }
-
- public Optional<User> owner() {
- return owner;
- }
-
- /**
- * Overrides the system major version for this application. This override takes effect if the deployment
- * spec does not specify a major version.
- */
- public OptionalInt majorVersion() { return majorVersion; }
-
- /** Returns metrics for this */
- public ApplicationMetrics metrics() {
- return metrics;
- }
-
/** Returns all rotations assigned to this */
public List<AssignedRotation> rotations() {
return rotations;
@@ -311,8 +188,6 @@ public class Instance {
return EndpointList.of(endpointStream);
}
- public Optional<String> pemDeployKey() { return pemDeployKey; }
-
/** Returns the status of the global rotation(s) assigned to this */
public RotationStatus rotationStatus() {
return rotationStatus;
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 b8912a848fc..5aa5a8e13de 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
@@ -1,41 +1,27 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;
-import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.searchlib.rankingexpression.rule.Function;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
-import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
-import java.util.function.BinaryOperator;
+import java.util.Set;
import java.util.function.UnaryOperator;
/**
@@ -57,7 +43,7 @@ public class LockedApplication {
private final Optional<User> owner;
private final OptionalInt majorVersion;
private final ApplicationMetrics metrics;
- private final Optional<String> pemDeployKey;
+ private final Set<String> pemDeployKeys;
private final OptionalLong projectId;
private final boolean internal;
private final Map<InstanceName, Instance> instances;
@@ -72,15 +58,16 @@ public class LockedApplication {
this(Objects.requireNonNull(lock, "lock cannot be null"), application.id(), application.createdAt(),
application.deploymentSpec(), application.validationOverrides(), application.change(),
application.outstandingChange(), application.deploymentIssueId(), application.ownershipIssueId(),
- application.owner(), application.majorVersion(), application.metrics(), application.pemDeployKey(),
+ application.owner(), application.majorVersion(), application.metrics(), application.pemDeployKeys(),
application.projectId(), application.internal(), application.instances());
}
private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec,
ValidationOverrides validationOverrides, Change change, Change outstandingChange,
Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Optional<String> pemDeployKey,
- OptionalLong projectId, boolean internal, Map<InstanceName, Instance> instances) {
+ OptionalInt majorVersion, ApplicationMetrics metrics, Set<String> pemDeployKeys,
+ OptionalLong projectId, boolean internal,
+ Map<InstanceName, Instance> instances) {
this.lock = lock;
this.id = id;
this.createdAt = createdAt;
@@ -93,7 +80,7 @@ public class LockedApplication {
this.owner = owner;
this.majorVersion = majorVersion;
this.metrics = metrics;
- this.pemDeployKey = pemDeployKey;
+ this.pemDeployKeys = pemDeployKeys;
this.projectId = projectId;
this.internal = internal;
this.instances = Map.copyOf(instances);
@@ -102,15 +89,23 @@ public class LockedApplication {
/** Returns a read-only copy of this */
public Application get() {
return new Application(id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances.values());
}
+ public LockedApplication withNewInstance(InstanceName instance) {
+ var instances = new HashMap<>(this.instances);
+ instances.put(instance, new Instance(id.instance(instance)));
+ return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ projectId, internal, instances);
+ }
+
public LockedApplication with(InstanceName instance, UnaryOperator<Instance> modification) {
var instances = new HashMap<>(this.instances);
instances.put(instance, modification.apply(instances.get(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
@@ -118,61 +113,61 @@ public class LockedApplication {
var instances = new HashMap<>(this.instances);
instances.remove(instance);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withBuiltInternally(boolean builtInternally) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, builtInternally, instances);
}
public LockedApplication withProjectId(OptionalLong projectId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withDeploymentIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication with(DeploymentSpec deploymentSpec) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication with(ValidationOverrides validationOverrides) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withChange(Change change) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withOutstandingChange(Change outstandingChange) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withOwnershipIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withOwner(User owner) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
@@ -181,18 +176,28 @@ public class LockedApplication {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
deploymentIssueId, ownershipIssueId, owner,
majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion),
- metrics, pemDeployKey, projectId, internal, instances);
+ metrics, pemDeployKeys, projectId, internal, instances);
}
public LockedApplication with(ApplicationMetrics metrics) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKey,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
projectId, internal, instances);
}
public LockedApplication withPemDeployKey(String pemDeployKey) {
+ Set<String> keys = new LinkedHashSet<>(pemDeployKeys);
+ keys.add(pemDeployKey);
+ return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
+ projectId, internal, instances);
+ }
+
+ public LockedApplication withoutPemDeployKey(String pemDeployKey) {
+ Set<String> keys = new LinkedHashSet<>(pemDeployKeys);
+ keys.remove(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, Optional.ofNullable(pemDeployKey),
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
projectId, internal, instances);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
index f6ce63ca2ab..ecc8bd65b72 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
@@ -1,6 +1,8 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.curator.Lock;
@@ -13,6 +15,7 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
+import java.security.Principal;
import java.util.Optional;
import static java.util.Objects.requireNonNull;
@@ -123,23 +126,44 @@ public abstract class LockedTenant {
public static class Cloud extends LockedTenant {
private final BillingInfo billingInfo;
+ private final BiMap<String, Principal> pemDeveloperKeys;
- private Cloud(TenantName name, BillingInfo billingInfo) {
+ private Cloud(TenantName name, BillingInfo billingInfo, BiMap<String, Principal> pemDeveloperKeys) {
super(name);
this.billingInfo = billingInfo;
+ this.pemDeveloperKeys = pemDeveloperKeys;
}
private Cloud(CloudTenant tenant) {
- this(tenant.name(), tenant.billingInfo());
+ this(tenant.name(), tenant.billingInfo(), tenant.pemDeveloperKeys());
}
@Override
public CloudTenant get() {
- return new CloudTenant(name, billingInfo);
+ return new CloudTenant(name, billingInfo, pemDeveloperKeys);
}
public Cloud with(BillingInfo billingInfo) {
- return new Cloud(name, billingInfo);
+ return new Cloud(name, billingInfo, pemDeveloperKeys);
+ }
+
+ public Cloud withPemDeveloperKey(String pemKey, Principal principal) {
+ ImmutableBiMap.Builder<String, Principal> keys = ImmutableBiMap.builder();
+ pemDeveloperKeys.forEach((key, user) -> {
+ if ( ! user.equals(principal))
+ keys.put(key, user);
+ });
+ keys.put(pemKey, principal);
+ return new Cloud(name, billingInfo, keys.build());
+ }
+
+ public Cloud withoutPemDeveloperKey(String pemKey) {
+ ImmutableBiMap.Builder<String, Principal> keys = ImmutableBiMap.builder();
+ pemDeveloperKeys.forEach((key, user) -> {
+ if ( ! key.equals(pemKey))
+ keys.put(key, user);
+ });
+ return new Cloud(name, billingInfo, keys.build());
}
}
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 066ba0fbda5..da168345c18 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
@@ -5,9 +5,8 @@ import com.google.common.collect.ImmutableMap;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import java.util.Collection;
import java.util.HashMap;
@@ -16,6 +15,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import static java.util.stream.Collectors.toMap;
/**
* Information about which deployment jobs an application should run and their current status.
@@ -26,78 +29,41 @@ import java.util.OptionalLong;
*/
public class DeploymentJobs {
- private final OptionalLong projectId;
private final ImmutableMap<JobType, JobStatus> status;
- private final Optional<IssueId> issueId;
- private final boolean builtInternally;
-
- public DeploymentJobs(OptionalLong projectId, Collection<JobStatus> jobStatusEntries,
- Optional<IssueId> issueId, boolean builtInternally) {
- this(projectId, asMap(jobStatusEntries), issueId, builtInternally);
- }
- private DeploymentJobs(OptionalLong projectId, Map<JobType, JobStatus> status, Optional<IssueId> issueId,
- boolean builtInternally) {
- requireId(projectId, "projectId must be a positive integer");
- Objects.requireNonNull(status, "status cannot be null");
- Objects.requireNonNull(issueId, "issueId cannot be null");
- this.projectId = projectId;
- this.status = ImmutableMap.copyOf(status);
- this.issueId = issueId;
- this.builtInternally = builtInternally;
+ public DeploymentJobs(Collection<JobStatus> jobStatusEntries) {
+ this.status = ImmutableMap.copyOf((Iterable<Map.Entry<JobType, JobStatus>>)
+ jobStatusEntries.stream()
+ .map(job -> Map.entry(job.type(), job))::iterator);
}
- private static Map<JobType, JobStatus> asMap(Collection<JobStatus> jobStatusEntries) {
- ImmutableMap.Builder<JobType, JobStatus> b = new ImmutableMap.Builder<>();
- for (JobStatus jobStatusEntry : jobStatusEntries)
- b.put(jobStatusEntry.type(), jobStatusEntry);
- return b.build();
- }
-
- /** Return a new instance with the given completion */
- public DeploymentJobs withCompletion(JobType jobType, JobStatus.JobRun completion, Optional<JobError> jobError) {
+ /** Return a new instance with the given job update applied. */
+ public DeploymentJobs withUpdate(JobType jobType, UnaryOperator<JobStatus> update) {
Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status);
status.compute(jobType, (type, job) -> {
if (job == null) job = JobStatus.initial(jobType);
- return job.withCompletion(completion, jobError);
+ return update.apply(job);
});
- return new DeploymentJobs(projectId, status, issueId, builtInternally);
+ return new DeploymentJobs(status.values());
}
- public DeploymentJobs withTriggering(JobType jobType, JobStatus.JobRun jobRun) {
- Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status);
- status.compute(jobType, (__, job) -> {
- if (job == null) job = JobStatus.initial(jobType);
- return job.withTriggering(jobRun);
- });
- return new DeploymentJobs(projectId, status, issueId, builtInternally);
- }
-
- public DeploymentJobs withPause(JobType jobType, OptionalLong pausedUntil) {
- Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status);
- status.compute(jobType, (__, job) -> {
- if (job == null) job = JobStatus.initial(jobType);
- return job.withPause(pausedUntil);
- });
- return new DeploymentJobs(projectId, status, issueId, builtInternally);
+ /** Return a new instance with the given completion */
+ public DeploymentJobs withCompletion(JobType jobType, JobStatus.JobRun completion, Optional<JobError> jobError) {
+ return withUpdate(jobType, job -> job.withCompletion(completion, jobError));
}
- public DeploymentJobs withProjectId(OptionalLong projectId) {
- return new DeploymentJobs(projectId, status, issueId, builtInternally);
+ public DeploymentJobs withTriggering(JobType jobType, JobStatus.JobRun jobRun) {
+ return withUpdate(jobType, job -> job.withTriggering(jobRun));
}
- public DeploymentJobs with(IssueId issueId) {
- return new DeploymentJobs(projectId, status, Optional.ofNullable(issueId), builtInternally);
+ public DeploymentJobs withPause(JobType jobType, OptionalLong pausedUntil) {
+ return withUpdate(jobType, job -> job.withPause(pausedUntil));
}
public DeploymentJobs without(JobType job) {
- Map<JobType, JobStatus> status = new HashMap<>(this.status);
+ Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status);
status.remove(job);
- return new DeploymentJobs(projectId, status, issueId, builtInternally);
- }
-
- public DeploymentJobs withBuiltInternally(boolean builtInternally) {
- return new DeploymentJobs(projectId, status, issueId, builtInternally);
+ return new DeploymentJobs(status.values());
}
/** Returns an immutable map of the status entries in this */
@@ -116,28 +82,6 @@ public class DeploymentJobs {
return Optional.ofNullable(jobStatus().get(jobType));
}
- /**
- * Returns the id of the Screwdriver project running these deployment jobs
- * - or empty when this is not known or does not exist.
- * It is not known until the jobs have run once and reported back to the controller.
- */
- public OptionalLong projectId() { return projectId; }
-
- public Optional<IssueId> issueId() { return issueId; }
-
- public boolean deployedInternally() { return builtInternally; }
-
- private static OptionalLong requireId(OptionalLong id, String message) {
- Objects.requireNonNull(id, message);
- if ( ! id.isPresent()) {
- return id;
- }
- if (id.getAsLong() <= 0) {
- throw new IllegalArgumentException(message);
- }
- return id;
- }
-
/** A job report. This class is immutable. */
public static class JobReport {
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 1bba1baa91b..376048143d9 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,17 +4,17 @@ package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.DeploymentSpec.Step;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -43,7 +43,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.BuildJob;
-import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.disabled;
import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.idle;
import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.queued;
import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.running;
@@ -71,6 +70,14 @@ import static java.util.stream.Collectors.toList;
*/
public class DeploymentTrigger {
+ /*
+ * Instance orchestration TODO jonmv.
+ * Store new production application packages under non-instance path
+ * Read production packages from non-instance path, with fallback
+ * Deprecate and redirect some instance qualified paths in application/v4
+ * Orchestrate deployment across instances.
+ */
+
public static final Duration maxPause = Duration.ofDays(3);
private final static Logger log = Logger.getLogger(DeploymentTrigger.class.getName());
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 1a2524fa05e..08b3355587f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
@@ -1,10 +1,11 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.zone.ZoneId;
@@ -20,6 +21,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
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.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -37,17 +39,20 @@ import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.rotation.RotationState;
import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
+import java.security.Principal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
+import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -83,7 +88,7 @@ public class ApplicationSerializer {
private static final String majorVersionField = "majorVersion";
private static final String writeQualityField = "writeQuality";
private static final String queryQualityField = "queryQuality";
- private static final String pemDeployKeyField = "pemDeployKeys";
+ private static final String pemDeployKeysField = "pemDeployKeys";
private static final String assignedRotationClusterField = "clusterId";
private static final String assignedRotationRotationField = "rotationId";
private static final String applicationCertificateField = "applicationCertificate";
@@ -187,7 +192,7 @@ public class ApplicationSerializer {
application.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion));
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
- deployKeysToSlime(application.pemDeployKey().stream(), root.setArray(pemDeployKeyField));
+ deployKeysToSlime(application.pemDeployKeys().stream(), root.setArray(pemDeployKeysField));
instancesToSlime(application, root.setArray(instancesField));
return slime;
}
@@ -206,6 +211,7 @@ public class ApplicationSerializer {
private void deployKeysToSlime(Stream<String> pemDeployKeys, Cursor array) {
pemDeployKeys.forEach(array::addString);
}
+
private void deploymentsToSlime(Collection<Deployment> deployments, Cursor array) {
for (Deployment deployment : deployments)
deploymentToSlime(deployment, array.addObject());
@@ -378,14 +384,14 @@ public class ApplicationSerializer {
OptionalInt majorVersion = Serializers.optionalInteger(root.field(majorVersionField));
ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
root.field(writeQualityField).asDouble());
- List<String> pemDeployKeys = pemDeployKeysFromSlime(root.field(pemDeployKeyField));
+ Set<String> pemDeployKeys = pemDeployKeysFromSlime(root.field(pemDeployKeysField));
List<Instance> instances = instancesFromSlime(id, deploymentSpec, root.field(instancesField));
OptionalLong projectId = Serializers.optionalLong(root.field(projectIdField));
boolean builtInternally = root.field(builtInternallyField).asBool();
return new Application(id, createdAt, deploymentSpec, validationOverrides, deploying, outstandingChange,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics,
- pemDeployKeys.stream().findFirst(), projectId, builtInternally, instances);
+ pemDeployKeys, projectId, builtInternally, instances);
}
private List<Instance> instancesFromSlime(TenantAndApplicationId id, DeploymentSpec deploymentSpec, Inspector field) {
@@ -405,11 +411,12 @@ public class ApplicationSerializer {
return instances;
}
- private List<String> pemDeployKeysFromSlime(Inspector array) {
- List<String> keys = new ArrayList<>();
+ private Set<String> pemDeployKeysFromSlime(Inspector array) {
+ Set<String> keys = new LinkedHashSet<>();
array.traverse((ArrayTraverser) (__, key) -> keys.add(key.asString()));
return keys;
}
+
private List<Deployment> deploymentsFromSlime(Inspector array) {
List<Deployment> deployments = new ArrayList<>();
array.traverse((ArrayTraverser) (int i, Inspector item) -> deployments.add(deploymentFromSlime(item)));
@@ -539,8 +546,7 @@ public class ApplicationSerializer {
private DeploymentJobs deploymentJobsFromSlime(Inspector object) {
List<JobStatus> jobStatusList = jobStatusListFromSlime(object.field(jobStatusField));
-
- return new DeploymentJobs(OptionalLong.empty(), jobStatusList, Optional.empty(), false); // WARNING: Unused variables.
+ return new DeploymentJobs(jobStatusList);
}
private Change changeFromSlime(Inspector object) {
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 76a146fb1e6..9501ac5a7f9 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
@@ -87,7 +87,6 @@ public class CuratorDb {
private final ConfidenceOverrideSerializer confidenceOverrideSerializer = new ConfidenceOverrideSerializer();
private final TenantSerializer tenantSerializer = new TenantSerializer();
private final ApplicationSerializer applicationSerializer = new ApplicationSerializer();
- private final InstanceSerializer instanceSerializer = new InstanceSerializer();
private final RunSerializer runSerializer = new RunSerializer();
private final OsVersionSerializer osVersionSerializer = new OsVersionSerializer();
private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer);
@@ -337,10 +336,6 @@ public class CuratorDb {
public void writeApplication(Application application) {
curator.set(applicationPath(application.id()), asJson(applicationSerializer.toSlime(application)));
- for (InstanceName name : application.instances().keySet()) {
- curator.set(oldApplicationPath(application.id().instance(name)),
- asJson(instanceSerializer.toSlime(application.legacy(name))));
- }
}
public Optional<Application> readApplication(TenantAndApplicationId application) {
@@ -363,51 +358,26 @@ public class CuratorDb {
.collect(Collectors.toUnmodifiableList());
}
- // TODO jonmv: Clear out old instance data here
private Stream<TenantAndApplicationId> readApplicationIds() {
return curator.getChildren(applicationRoot).stream()
.filter(id -> id.split(":").length == 2)
.map(TenantAndApplicationId::fromSerialized);
}
+ public void deleteOldApplicationData() {
+ curator.getChildren(applicationRoot).stream()
+ .filter(id -> id.split(":").length == 3)
+ .forEach(id -> curator.delete(applicationRoot.append(id)));
+ }
+
// TODO jonmv: Refactor when instance split operation is done
- public void storeWithoutInstance(Application application, ApplicationId instanceId) {
- curator.delete(oldApplicationPath(instanceId));
+ public void storeWithoutInstance(Application application) {
if (application.instances().isEmpty())
curator.delete(applicationPath(application.id()));
else
writeApplication(application);
}
- /**
- * Migration plan:
- *
- * Add filter for reading only Instance from old application path RELEASED
- * Write Instance to instance and old application path RELEASED
- *
- * Lock on application level for instance mutations MERGED
- *
- * Write Instance to instance and application and old application paths DONE TO CHANGE DONE
- * Read Instance from instance path DONE TO REMOVE DONE
- * Duplicate Application from Instance, with helper classes DONE
- * Write Application to instance and application and old application paths DONE TO CHANGE DONE
- * Read Application from instance path DONE TO REMOVE DONE
- * Use Application where applicable DONE !!!
- * Lock instances and application on same level: tenant + application DONE TO CHANGE DONE
- * When reading an application, read all instances, and aggregate them DONE
- * Write application with instances to application path DONE
- * Write all instances of an application to old application path DONE
- * Remove everything under instance root DONE
- * Stop locking applications on instance level DONE
- *
- * Read Application with instances from application path (with filter) DONE
- *
- * Stop writing Instance to old application path
- * Remove unused parts of Instance (Used only for legacy serialization)
- * Store new production application packages under non-instance path
- * Read production packages from non-instance path, with fallback
- */
-
// -------------- Job Runs ------------------------------------------------
public void writeLastRun(Run run) {
@@ -631,10 +601,6 @@ public class CuratorDb {
return applicationRoot.append(id.serialized());
}
- private static Path oldApplicationPath(ApplicationId application) {
- return applicationRoot.append(application.serializedForm());
- }
-
private static Path runsPath(ApplicationId id, JobType type) {
return jobRoot.append(id.serializedForm()).append(type.jobName());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializer.java
deleted file mode 100644
index 3cd594a277d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializer.java
+++ /dev/null
@@ -1,574 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.ObjectTraverser;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-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.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
-import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.JobStatus;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.stream.Collectors;
-
-/**
- * Serializes {@link Instance} to/from slime.
- * This class is multithread safe.
- *
- * @author bratseth
- */
-public class InstanceSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- // Application fields
- private static final String idField = "id";
- private static final String createdAtField = "createdAt";
- private static final String deploymentSpecField = "deploymentSpecField";
- private static final String validationOverridesField = "validationOverrides";
- private static final String deploymentsField = "deployments";
- private static final String deploymentJobsField = "deploymentJobs";
- private static final String deployingField = "deployingField";
- private static final String pinnedField = "pinned";
- private static final String outstandingChangeField = "outstandingChangeField";
- private static final String ownershipIssueIdField = "ownershipIssueId";
- private static final String ownerField = "confirmedOwner";
- private static final String majorVersionField = "majorVersion";
- private static final String writeQualityField = "writeQuality";
- private static final String queryQualityField = "queryQuality";
- private static final String pemDeployKeyField = "pemDeployKey";
- private static final String assignedRotationsField = "assignedRotations";
- private static final String assignedRotationEndpointField = "endpointId";
- private static final String assignedRotationClusterField = "clusterId";
- private static final String assignedRotationRotationField = "rotationId";
- private static final String applicationCertificateField = "applicationCertificate";
-
- // Deployment fields
- private static final String zoneField = "zone";
- private static final String environmentField = "environment";
- private static final String regionField = "region";
- private static final String deployTimeField = "deployTime";
- private static final String applicationBuildNumberField = "applicationBuildNumber";
- private static final String applicationPackageRevisionField = "applicationPackageRevision";
- private static final String sourceRevisionField = "sourceRevision";
- private static final String repositoryField = "repositoryField";
- private static final String branchField = "branchField";
- private static final String commitField = "commitField";
- private static final String authorEmailField = "authorEmailField";
- private static final String compileVersionField = "compileVersion";
- private static final String buildTimeField = "buildTime";
- private static final String lastQueriedField = "lastQueried";
- private static final String lastWrittenField = "lastWritten";
- private static final String lastQueriesPerSecondField = "lastQueriesPerSecond";
- private static final String lastWritesPerSecondField = "lastWritesPerSecond";
-
- // DeploymentJobs fields
- private static final String projectIdField = "projectId";
- private static final String jobStatusField = "jobStatus";
- private static final String issueIdField = "jiraIssueId";
- private static final String builtInternallyField = "builtInternally";
-
- // JobStatus field
- private static final String jobTypeField = "jobType";
- private static final String errorField = "jobError";
- private static final String lastTriggeredField = "lastTriggered";
- private static final String lastCompletedField = "lastCompleted";
- private static final String firstFailingField = "firstFailing";
- private static final String lastSuccessField = "lastSuccess";
- private static final String pausedUntilField = "pausedUntil";
-
- // JobRun fields
- private static final String jobRunIdField = "id";
- private static final String versionField = "version";
- private static final String revisionField = "revision";
- private static final String sourceVersionField = "sourceVersion";
- private static final String sourceApplicationField = "sourceRevision";
- private static final String reasonField = "reason";
- private static final String atField = "at";
-
- // ClusterInfo fields
- private static final String clusterInfoField = "clusterInfo";
- private static final String clusterInfoFlavorField = "flavor";
- private static final String clusterInfoCostField = "cost";
- private static final String clusterInfoCpuField = "flavorCpu";
- private static final String clusterInfoMemField = "flavorMem";
- private static final String clusterInfoDiskField = "flavorDisk";
- private static final String clusterInfoTypeField = "clusterType";
- private static final String clusterInfoHostnamesField = "hostnames";
-
- // ClusterUtils fields
- private static final String clusterUtilsField = "clusterUtils";
- private static final String clusterUtilsCpuField = "cpu";
- private static final String clusterUtilsMemField = "mem";
- private static final String clusterUtilsDiskField = "disk";
- private static final String clusterUtilsDiskBusyField = "diskbusy";
-
- // Deployment metrics fields
- private static final String deploymentMetricsField = "metrics";
- private static final String deploymentMetricsQPSField = "queriesPerSecond";
- private static final String deploymentMetricsWPSField = "writesPerSecond";
- private static final String deploymentMetricsDocsField = "documentCount";
- private static final String deploymentMetricsQueryLatencyField = "queryLatencyMillis";
- private static final String deploymentMetricsWriteLatencyField = "writeLatencyMillis";
- private static final String deploymentMetricsUpdateTime = "lastUpdated";
- private static final String deploymentMetricsWarningsField = "warnings";
-
- // RotationStatus fields
- private static final String rotationStatusField = "rotationStatus2";
- private static final String rotationIdField = "rotationId";
- private static final String rotationStateField = "state";
- private static final String statusField = "status";
-
- // ------------------ Serialization
-
- public Slime toSlime(Instance instance) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString(idField, instance.id().serializedForm());
- root.setLong(createdAtField, instance.createdAt().toEpochMilli());
- root.setString(deploymentSpecField, instance.deploymentSpec().xmlForm());
- root.setString(validationOverridesField, instance.validationOverrides().xmlForm());
- deploymentsToSlime(instance.deployments().values(), root.setArray(deploymentsField));
- toSlime(instance.deploymentJobs(), root.setObject(deploymentJobsField));
- toSlime(instance.change(), root, deployingField);
- toSlime(instance.outstandingChange(), root, outstandingChangeField);
- instance.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value()));
- instance.owner().ifPresent(owner -> root.setString(ownerField, owner.username()));
- instance.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion));
- root.setDouble(queryQualityField, instance.metrics().queryServiceQuality());
- root.setDouble(writeQualityField, instance.metrics().writeServiceQuality());
- instance.pemDeployKey().ifPresent(pemDeployKey -> root.setString(pemDeployKeyField, pemDeployKey));
- assignedRotationsToSlime(instance.rotations(), root, assignedRotationsField);
- toSlime(instance.rotationStatus(), root.setArray(rotationStatusField));
- return slime;
- }
-
- private void deploymentsToSlime(Collection<Deployment> deployments, Cursor array) {
- for (Deployment deployment : deployments)
- deploymentToSlime(deployment, array.addObject());
- }
-
- private void deploymentToSlime(Deployment deployment, Cursor object) {
- zoneIdToSlime(deployment.zone(), object.setObject(zoneField));
- object.setString(versionField, deployment.version().toString());
- object.setLong(deployTimeField, deployment.at().toEpochMilli());
- toSlime(deployment.applicationVersion(), object.setObject(applicationPackageRevisionField));
- clusterInfoToSlime(deployment.clusterInfo(), object);
- clusterUtilsToSlime(deployment.clusterUtils(), object);
- deploymentMetricsToSlime(deployment.metrics(), object);
- deployment.activity().lastQueried().ifPresent(instant -> object.setLong(lastQueriedField, instant.toEpochMilli()));
- deployment.activity().lastWritten().ifPresent(instant -> object.setLong(lastWrittenField, instant.toEpochMilli()));
- deployment.activity().lastQueriesPerSecond().ifPresent(value -> object.setDouble(lastQueriesPerSecondField, value));
- deployment.activity().lastWritesPerSecond().ifPresent(value -> object.setDouble(lastWritesPerSecondField, value));
- }
-
- private void deploymentMetricsToSlime(DeploymentMetrics metrics, Cursor object) {
- Cursor root = object.setObject(deploymentMetricsField);
- root.setDouble(deploymentMetricsQPSField, metrics.queriesPerSecond());
- root.setDouble(deploymentMetricsWPSField, metrics.writesPerSecond());
- root.setDouble(deploymentMetricsDocsField, metrics.documentCount());
- root.setDouble(deploymentMetricsQueryLatencyField, metrics.queryLatencyMillis());
- root.setDouble(deploymentMetricsWriteLatencyField, metrics.writeLatencyMillis());
- metrics.instant().ifPresent(instant -> root.setLong(deploymentMetricsUpdateTime, instant.toEpochMilli()));
- if (!metrics.warnings().isEmpty()) {
- Cursor warningsObject = root.setObject(deploymentMetricsWarningsField);
- metrics.warnings().forEach((warning, count) -> warningsObject.setLong(warning.name(), count));
- }
- }
-
- private void clusterInfoToSlime(Map<ClusterSpec.Id, ClusterInfo> clusters, Cursor object) {
- Cursor root = object.setObject(clusterInfoField);
- for (Map.Entry<ClusterSpec.Id, ClusterInfo> entry : clusters.entrySet()) {
- toSlime(entry.getValue(), root.setObject(entry.getKey().value()));
- }
- }
-
- private void toSlime(ClusterInfo info, Cursor object) {
- object.setString(clusterInfoFlavorField, info.getFlavor());
- object.setLong(clusterInfoCostField, info.getFlavorCost());
- object.setDouble(clusterInfoCpuField, info.getFlavorCPU());
- object.setDouble(clusterInfoMemField, info.getFlavorMem());
- object.setDouble(clusterInfoDiskField, info.getFlavorDisk());
- object.setString(clusterInfoTypeField, info.getClusterType().name());
- Cursor array = object.setArray(clusterInfoHostnamesField);
- for (String host : info.getHostnames()) {
- array.addString(host);
- }
- }
-
- private void clusterUtilsToSlime(Map<ClusterSpec.Id, ClusterUtilization> clusters, Cursor object) {
- Cursor root = object.setObject(clusterUtilsField);
- for (Map.Entry<ClusterSpec.Id, ClusterUtilization> entry : clusters.entrySet()) {
- toSlime(entry.getValue(), root.setObject(entry.getKey().value()));
- }
- }
-
- private void toSlime(ClusterUtilization utils, Cursor object) {
- object.setDouble(clusterUtilsCpuField, utils.getCpu());
- object.setDouble(clusterUtilsMemField, utils.getMemory());
- object.setDouble(clusterUtilsDiskField, utils.getDisk());
- object.setDouble(clusterUtilsDiskBusyField, utils.getDiskBusy());
- }
-
- private void zoneIdToSlime(ZoneId zone, Cursor object) {
- object.setString(environmentField, zone.environment().value());
- object.setString(regionField, zone.region().value());
- }
-
- private void toSlime(ApplicationVersion applicationVersion, Cursor object) {
- if (applicationVersion.buildNumber().isPresent() && applicationVersion.source().isPresent()) {
- object.setLong(applicationBuildNumberField, applicationVersion.buildNumber().getAsLong());
- toSlime(applicationVersion.source().get(), object.setObject(sourceRevisionField));
- applicationVersion.authorEmail().ifPresent(email -> object.setString(authorEmailField, email));
- applicationVersion.compileVersion().ifPresent(version -> object.setString(compileVersionField, version.toString()));
- applicationVersion.buildTime().ifPresent(time -> object.setLong(buildTimeField, time.toEpochMilli()));
- }
- }
-
- private void toSlime(SourceRevision sourceRevision, Cursor object) {
- object.setString(repositoryField, sourceRevision.repository());
- object.setString(branchField, sourceRevision.branch());
- object.setString(commitField, sourceRevision.commit());
- }
-
- private void toSlime(DeploymentJobs deploymentJobs, Cursor cursor) {
- deploymentJobs.projectId().ifPresent(projectId -> cursor.setLong(projectIdField, projectId));
- jobStatusToSlime(deploymentJobs.jobStatus().values(), cursor.setArray(jobStatusField));
- deploymentJobs.issueId().ifPresent(jiraIssueId -> cursor.setString(issueIdField, jiraIssueId.value()));
- cursor.setBool(builtInternallyField, deploymentJobs.deployedInternally());
- }
-
- private void jobStatusToSlime(Collection<JobStatus> jobStatuses, Cursor jobStatusArray) {
- for (JobStatus jobStatus : jobStatuses)
- toSlime(jobStatus, jobStatusArray.addObject());
- }
-
- private void toSlime(JobStatus jobStatus, Cursor object) {
- object.setString(jobTypeField, jobStatus.type().jobName());
- if (jobStatus.jobError().isPresent())
- object.setString(errorField, jobStatus.jobError().get().name());
-
- jobStatus.lastTriggered().ifPresent(run -> jobRunToSlime(run, object, lastTriggeredField));
- jobStatus.lastCompleted().ifPresent(run -> jobRunToSlime(run, object, lastCompletedField));
- jobStatus.lastSuccess().ifPresent(run -> jobRunToSlime(run, object, lastSuccessField));
- jobStatus.firstFailing().ifPresent(run -> jobRunToSlime(run, object, firstFailingField));
- jobStatus.pausedUntil().ifPresent(until -> object.setLong(pausedUntilField, until));
- }
-
- private void jobRunToSlime(JobStatus.JobRun jobRun, Cursor parent, String jobRunObjectName) {
- Cursor object = parent.setObject(jobRunObjectName);
- object.setLong(jobRunIdField, jobRun.id());
- object.setString(versionField, jobRun.platform().toString());
- toSlime(jobRun.application(), object.setObject(revisionField));
- jobRun.sourcePlatform().ifPresent(version -> object.setString(sourceVersionField, version.toString()));
- jobRun.sourceApplication().ifPresent(version -> toSlime(version, object.setObject(sourceApplicationField)));
- object.setString(reasonField, jobRun.reason());
- object.setLong(atField, jobRun.at().toEpochMilli());
- }
-
- private void toSlime(Change deploying, Cursor parentObject, String fieldName) {
- if (deploying.isEmpty()) return;
-
- Cursor object = parentObject.setObject(fieldName);
- if (deploying.platform().isPresent())
- object.setString(versionField, deploying.platform().get().toString());
- if (deploying.application().isPresent())
- toSlime(deploying.application().get(), object);
- if (deploying.isPinned())
- object.setBool(pinnedField, true);
- }
-
- private void toSlime(RotationStatus status, Cursor array) {
- status.asMap().forEach((rotationId, zoneStatus) -> {
- Cursor rotationObject = array.addObject();
- rotationObject.setString(rotationIdField, rotationId.asString());
- Cursor statusArray = rotationObject.setArray(statusField);
- zoneStatus.forEach((zone, state) -> {
- Cursor statusObject = statusArray.addObject();
- zoneIdToSlime(zone, statusObject);
- statusObject.setString(rotationStateField, state.name());
- });
- });
- }
-
- private void assignedRotationsToSlime(List<AssignedRotation> rotations, Cursor parent, String fieldName) {
- var rotationsArray = parent.setArray(fieldName);
- for (var rotation : rotations) {
- var object = rotationsArray.addObject();
- object.setString(assignedRotationEndpointField, rotation.endpointId().id());
- object.setString(assignedRotationRotationField, rotation.rotationId().asString());
- object.setString(assignedRotationClusterField, rotation.clusterId().value());
- }
- }
-
- // ------------------ Deserialization
-
- public Instance fromSlime(Slime slime) {
- Inspector root = slime.get();
-
- ApplicationId id = ApplicationId.fromSerializedForm(root.field(idField).asString());
- Instant createdAt = Instant.ofEpochMilli(root.field(createdAtField).asLong());
- DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(root.field(deploymentSpecField).asString(), false);
- ValidationOverrides validationOverrides = ValidationOverrides.fromXml(root.field(validationOverridesField).asString());
- List<Deployment> deployments = deploymentsFromSlime(root.field(deploymentsField));
- DeploymentJobs deploymentJobs = deploymentJobsFromSlime(root.field(deploymentJobsField));
- Change deploying = changeFromSlime(root.field(deployingField));
- Change outstandingChange = changeFromSlime(root.field(outstandingChangeField));
- Optional<IssueId> ownershipIssueId = Serializers.optionalString(root.field(ownershipIssueIdField)).map(IssueId::from);
- Optional<User> owner = Serializers.optionalString(root.field(ownerField)).map(User::from);
- OptionalInt majorVersion = Serializers.optionalInteger(root.field(majorVersionField));
- ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
- root.field(writeQualityField).asDouble());
- Optional<String> pemDeployKey = Serializers.optionalString(root.field(pemDeployKeyField));
- List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(deploymentSpec, root);
- RotationStatus rotationStatus = rotationStatusFromSlime(root);
-
- return new Instance(id, createdAt, deploymentSpec, validationOverrides, deployments, deploymentJobs,
- deploying, outstandingChange, ownershipIssueId, owner, majorVersion, metrics,
- pemDeployKey, assignedRotations, rotationStatus);
- }
-
- private List<Deployment> deploymentsFromSlime(Inspector array) {
- List<Deployment> deployments = new ArrayList<>();
- array.traverse((ArrayTraverser) (int i, Inspector item) -> deployments.add(deploymentFromSlime(item)));
- return deployments;
- }
-
- private Deployment deploymentFromSlime(Inspector deploymentObject) {
- return new Deployment(zoneIdFromSlime(deploymentObject.field(zoneField)),
- applicationVersionFromSlime(deploymentObject.field(applicationPackageRevisionField)),
- Version.fromString(deploymentObject.field(versionField).asString()),
- Instant.ofEpochMilli(deploymentObject.field(deployTimeField).asLong()),
- clusterUtilsMapFromSlime(deploymentObject.field(clusterUtilsField)),
- clusterInfoMapFromSlime(deploymentObject.field(clusterInfoField)),
- deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)),
- DeploymentActivity.create(Serializers.optionalInstant(deploymentObject.field(lastQueriedField)),
- Serializers.optionalInstant(deploymentObject.field(lastWrittenField)),
- Serializers.optionalDouble(deploymentObject.field(lastQueriesPerSecondField)),
- Serializers.optionalDouble(deploymentObject.field(lastWritesPerSecondField))));
- }
-
- private DeploymentMetrics deploymentMetricsFromSlime(Inspector object) {
- Optional<Instant> instant = object.field(deploymentMetricsUpdateTime).valid() ?
- Optional.of(Instant.ofEpochMilli(object.field(deploymentMetricsUpdateTime).asLong())) :
- Optional.empty();
- return new DeploymentMetrics(object.field(deploymentMetricsQPSField).asDouble(),
- object.field(deploymentMetricsWPSField).asDouble(),
- object.field(deploymentMetricsDocsField).asDouble(),
- object.field(deploymentMetricsQueryLatencyField).asDouble(),
- object.field(deploymentMetricsWriteLatencyField).asDouble(),
- instant,
- deploymentWarningsFrom(object.field(deploymentMetricsWarningsField)));
- }
-
- private Map<DeploymentMetrics.Warning, Integer> deploymentWarningsFrom(Inspector object) {
- Map<DeploymentMetrics.Warning, Integer> warnings = new HashMap<>();
- object.traverse((ObjectTraverser) (name, value) -> warnings.put(DeploymentMetrics.Warning.valueOf(name),
- (int) value.asLong()));
- return Collections.unmodifiableMap(warnings);
- }
-
- private RotationStatus rotationStatusFromSlime(Inspector parentObject) {
- var object = parentObject.field(rotationStatusField);
- var statusMap = new LinkedHashMap<RotationId, Map<ZoneId, RotationState>>();
- object.traverse((ArrayTraverser) (idx, statusObject) -> statusMap.put(new RotationId(statusObject.field(rotationIdField).asString()),
- singleRotationStatusFromSlime(statusObject.field(statusField))));
- return RotationStatus.from(statusMap);
- }
-
- private Map<ZoneId, RotationState> singleRotationStatusFromSlime(Inspector object) {
- if (!object.valid()) {
- return Collections.emptyMap();
- }
- Map<ZoneId, RotationState> rotationStatus = new LinkedHashMap<>();
- object.traverse((ArrayTraverser) (idx, statusObject) -> {
- var zone = zoneIdFromSlime(statusObject);
- var status = RotationState.valueOf(statusObject.field(rotationStateField).asString());
- rotationStatus.put(zone, status);
- });
- return Collections.unmodifiableMap(rotationStatus);
- }
-
- private Map<ClusterSpec.Id, ClusterInfo> clusterInfoMapFromSlime (Inspector object) {
- Map<ClusterSpec.Id, ClusterInfo> map = new HashMap<>();
- object.traverse((String name, Inspector value) -> map.put(new ClusterSpec.Id(name), clusterInfoFromSlime(value)));
- return map;
- }
-
- private Map<ClusterSpec.Id, ClusterUtilization> clusterUtilsMapFromSlime(Inspector object) {
- Map<ClusterSpec.Id, ClusterUtilization> map = new HashMap<>();
- object.traverse((String name, Inspector value) -> map.put(new ClusterSpec.Id(name), clusterUtililzationFromSlime(value)));
- return map;
- }
-
- private ClusterUtilization clusterUtililzationFromSlime(Inspector object) {
- double cpu = object.field(clusterUtilsCpuField).asDouble();
- double mem = object.field(clusterUtilsMemField).asDouble();
- double disk = object.field(clusterUtilsDiskField).asDouble();
- double diskBusy = object.field(clusterUtilsDiskBusyField).asDouble();
-
- return new ClusterUtilization(mem, cpu, disk, diskBusy);
- }
-
- private ClusterInfo clusterInfoFromSlime(Inspector inspector) {
- String flavor = inspector.field(clusterInfoFlavorField).asString();
- int cost = (int)inspector.field(clusterInfoCostField).asLong();
- String type = inspector.field(clusterInfoTypeField).asString();
- double flavorCpu = inspector.field(clusterInfoCpuField).asDouble();
- double flavorMem = inspector.field(clusterInfoMemField).asDouble();
- double flavorDisk = inspector.field(clusterInfoDiskField).asDouble();
-
- List<String> hostnames = new ArrayList<>();
- inspector.field(clusterInfoHostnamesField).traverse((ArrayTraverser)(int index, Inspector value) -> hostnames.add(value.asString()));
- return new ClusterInfo(flavor, cost, flavorCpu, flavorMem, flavorDisk, ClusterSpec.Type.from(type), hostnames);
- }
-
- private ZoneId zoneIdFromSlime(Inspector object) {
- return ZoneId.from(object.field(environmentField).asString(), object.field(regionField).asString());
- }
-
- private ApplicationVersion applicationVersionFromSlime(Inspector object) {
- if ( ! object.valid()) return ApplicationVersion.unknown;
- OptionalLong applicationBuildNumber = Serializers.optionalLong(object.field(applicationBuildNumberField));
- Optional<SourceRevision> sourceRevision = sourceRevisionFromSlime(object.field(sourceRevisionField));
- if (sourceRevision.isEmpty() || applicationBuildNumber.isEmpty()) {
- return ApplicationVersion.unknown;
- }
- Optional<String> authorEmail = Serializers.optionalString(object.field(authorEmailField));
- Optional<Version> compileVersion = Serializers.optionalString(object.field(compileVersionField)).map(Version::fromString);
- Optional<Instant> buildTime = Serializers.optionalInstant(object.field(buildTimeField));
-
- if (authorEmail.isEmpty())
- return ApplicationVersion.from(sourceRevision.get(), applicationBuildNumber.getAsLong());
-
- if (compileVersion.isEmpty() || buildTime.isEmpty())
- return ApplicationVersion.from(sourceRevision.get(), applicationBuildNumber.getAsLong(), authorEmail.get());
-
- return ApplicationVersion.from(sourceRevision.get(), applicationBuildNumber.getAsLong(), authorEmail.get(),
- compileVersion.get(), buildTime.get());
- }
-
- private Optional<SourceRevision> sourceRevisionFromSlime(Inspector object) {
- if ( ! object.valid()) return Optional.empty();
- return Optional.of(new SourceRevision(object.field(repositoryField).asString(),
- object.field(branchField).asString(),
- object.field(commitField).asString()));
- }
-
- private DeploymentJobs deploymentJobsFromSlime(Inspector object) {
- OptionalLong projectId = Serializers.optionalLong(object.field(projectIdField));
- List<JobStatus> jobStatusList = jobStatusListFromSlime(object.field(jobStatusField));
- Optional<IssueId> issueId = Serializers.optionalString(object.field(issueIdField)).map(IssueId::from);
- boolean builtInternally = object.field(builtInternallyField).asBool();
-
- return new DeploymentJobs(projectId, jobStatusList, issueId, builtInternally);
- }
-
- private Change changeFromSlime(Inspector object) {
- if ( ! object.valid()) return Change.empty();
- Inspector versionFieldValue = object.field(versionField);
- Change change = Change.empty();
- if (versionFieldValue.valid())
- change = Change.of(Version.fromString(versionFieldValue.asString()));
- if (object.field(applicationBuildNumberField).valid())
- change = change.with(applicationVersionFromSlime(object));
- if (object.field(pinnedField).asBool())
- change = change.withPin();
- return change;
- }
-
- private List<JobStatus> jobStatusListFromSlime(Inspector array) {
- List<JobStatus> jobStatusList = new ArrayList<>();
- array.traverse((ArrayTraverser) (int i, Inspector item) -> jobStatusFromSlime(item).ifPresent(jobStatusList::add));
- return jobStatusList;
- }
-
- private Optional<JobStatus> jobStatusFromSlime(Inspector object) {
- // if the job type has since been removed, ignore it
- Optional<JobType> jobType =
- JobType.fromOptionalJobName(object.field(jobTypeField).asString());
- if (jobType.isEmpty()) return Optional.empty();
-
- Optional<JobError> jobError = Optional.empty();
- if (object.field(errorField).valid())
- jobError = Optional.of(JobError.valueOf(object.field(errorField).asString()));
-
- return Optional.of(new JobStatus(jobType.get(),
- jobError,
- jobRunFromSlime(object.field(lastTriggeredField)),
- jobRunFromSlime(object.field(lastCompletedField)),
- jobRunFromSlime(object.field(firstFailingField)),
- jobRunFromSlime(object.field(lastSuccessField)),
- Serializers.optionalLong(object.field(pausedUntilField))));
- }
-
- private Optional<JobStatus.JobRun> jobRunFromSlime(Inspector object) {
- if ( ! object.valid()) return Optional.empty();
- return Optional.of(new JobStatus.JobRun(object.field(jobRunIdField).asLong(),
- new Version(object.field(versionField).asString()),
- applicationVersionFromSlime(object.field(revisionField)),
- Serializers.optionalString(object.field(sourceVersionField)).map(Version::fromString),
- Optional.of(object.field(sourceApplicationField)).filter(Inspector::valid).map(this::applicationVersionFromSlime),
- object.field(reasonField).asString(),
- Instant.ofEpochMilli(object.field(atField).asLong())));
- }
-
- private List<AssignedRotation> assignedRotationsFromSlime(DeploymentSpec deploymentSpec, Inspector root) {
- var assignedRotations = new LinkedHashMap<EndpointId, AssignedRotation>();
-
- root.field(assignedRotationsField).traverse((ArrayTraverser) (idx, inspector) -> {
- var clusterId = new ClusterSpec.Id(inspector.field(assignedRotationClusterField).asString());
- var endpointId = EndpointId.of(inspector.field(assignedRotationEndpointField).asString());
- var rotationId = new RotationId(inspector.field(assignedRotationRotationField).asString());
- var regions = deploymentSpec.endpoints().stream()
- .filter(endpoint -> endpoint.endpointId().equals(endpointId.id()))
- .flatMap(endpoint -> endpoint.regions().stream())
- .collect(Collectors.toSet());
- assignedRotations.putIfAbsent(endpointId, new AssignedRotation(clusterId, endpointId, rotationId, regions));
- });
-
- return List.copyOf(assignedRotations.values());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
index 3a4e6c3954c..78d166607df 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
@@ -1,6 +1,8 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
@@ -10,6 +12,7 @@ import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
@@ -18,6 +21,7 @@ import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import java.net.URI;
+import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -52,6 +56,7 @@ public class TenantSerializer {
private static final String billingInfoField = "billingInfo";
private static final String customerIdField = "customerId";
private static final String productCodeField = "productCode";
+ private static final String pemDeveloperKeysField = "pemDeveloperKeys";
public Slime toSlime(Tenant tenant) {
Slime slime = new Slime();
@@ -86,9 +91,18 @@ public class TenantSerializer {
}
private void toSlime(CloudTenant tenant, Cursor root) {
+ pemDeveloperKeysToSlime(tenant.pemDeveloperKeys(), root.setArray(pemDeveloperKeysField));
toSlime(tenant.billingInfo(), root.setObject(billingInfoField));
}
+ private void pemDeveloperKeysToSlime(BiMap<String, Principal> keys, Cursor array) {
+ keys.forEach((key, user) -> {
+ Cursor object = array.addObject();
+ object.setString("key", key);
+ object.setString("user", user.getName());
+ });
+ }
+
private void toSlime(BillingInfo billingInfo, Cursor billingInfoObject) {
billingInfoObject.setString(customerIdField, billingInfo.customerId());
billingInfoObject.setString(productCodeField, billingInfo.productCode());
@@ -97,10 +111,7 @@ public class TenantSerializer {
public Tenant tenantFrom(Slime slime) {
Inspector tenantObject = slime.get();
Tenant.Type type;
- if (tenantObject.field(typeField).valid())
- type = typeOf(tenantObject.field(typeField).asString());
- else // TODO jvenstad: Remove once all tenants are stored on updated format.
- type = tenantObject.field(nameField).asString().startsWith(Tenant.userPrefix) ? Tenant.Type.user : Tenant.Type.athenz;
+ type = typeOf(tenantObject.field(typeField).asString());
switch (type) {
case athenz: return athenzTenantFrom(tenantObject);
@@ -128,7 +139,16 @@ public class TenantSerializer {
private CloudTenant cloudTenantFrom(Inspector tenantObject) {
TenantName name = TenantName.from(tenantObject.field(nameField).asString());
BillingInfo billingInfo = billingInfoFrom(tenantObject.field(billingInfoField));
- return new CloudTenant(name, billingInfo);
+ BiMap<String, Principal> pemDeveloperKeys = pemDeveloperKeysFromSlime(tenantObject.field(pemDeveloperKeysField));
+ return new CloudTenant(name, billingInfo, pemDeveloperKeys);
+ }
+
+ private BiMap<String, Principal> pemDeveloperKeysFromSlime(Inspector array) {
+ ImmutableBiMap.Builder<String, Principal> keys = ImmutableBiMap.builder();
+ array.traverse((ArrayTraverser) (__, keyObject) -> {
+ keys.put(keyObject.field("key").asString(), new SimplePrincipal(keyObject.field("user").asString()));
+ });
+ return keys.build();
}
private Optional<Contact> contactFrom(Inspector object) {
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 4b7414a42a6..4c4478c9af6 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
@@ -20,7 +20,6 @@ import com.yahoo.restapi.Path;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
-import com.yahoo.slime.Type;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzPrincipal;
import com.yahoo.vespa.athenz.api.AthenzUser;
@@ -30,6 +29,7 @@ import com.yahoo.vespa.hosted.controller.AlreadyExistsException;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.NotExistsException;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
@@ -79,6 +79,7 @@ import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
import com.yahoo.vespa.hosted.controller.security.AccessControlRequests;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
@@ -238,11 +239,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse handlePOST(Path path, HttpRequest request) {
if (path.matches("/application/v4/tenant/{tenant}")) return createTenant(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/key")) return addDeveloperKey(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), "default", request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), "default", false, request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), "default", true, request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), "default", request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/jobreport")) return notifyJobCompletion(path.get("tenant"), path.get("application"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return addDeployKey(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return submit(path.get("tenant"), path.get("application"), "default", request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return createApplication(path.get("tenant"), path.get("application"), path.get("instance"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{jobtype}")) return jobDeploy(appIdFromPath(path), jobTypeFromPath(path), request);
@@ -270,9 +273,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse handleDELETE(Path path, HttpRequest request) {
if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", "all");
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", path.get("choice"));
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return removeDeployKey(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return JobControllerApiHandlerHelper.unregisterResponse(controller.jobController(), path.get("tenant"), path.get("application"), "default");
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return deleteInstance(path.get("tenant"), path.get("application"), path.get("instance"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), "all");
@@ -366,6 +371,44 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
+ private HttpResponse addDeveloperKey(String tenantName, HttpRequest request) {
+ if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
+ throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
+
+ Principal user = request.getJDiscRequest().getUserPrincipal();
+ String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString();
+ controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant ->
+ controller.tenants().store(tenant.withPemDeveloperKey(pemDeveloperKey, user)));
+ return new MessageResponse("Set developer key " + pemDeveloperKey + " for " + user);
+ }
+
+ private HttpResponse removeDeveloperKey(String tenantName, HttpRequest request) {
+ if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
+ throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
+
+ String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString();
+ Principal user = ((CloudTenant) controller.tenants().require(TenantName.from(tenantName))).pemDeveloperKeys().get(pemDeveloperKey);
+ controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant ->
+ controller.tenants().store(tenant.withoutPemDeveloperKey(pemDeveloperKey)));
+ return new MessageResponse("Removed developer key " + pemDeveloperKey + " for " + user);
+ }
+
+ private HttpResponse addDeployKey(String tenantName, String applicationName, HttpRequest request) {
+ String pemDeployKey = toSlime(request.getData()).get().field("key").asString();
+ controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
+ controller.applications().store(application.withPemDeployKey(pemDeployKey));
+ });
+ return new MessageResponse("Added deploy key " + pemDeployKey);
+ }
+
+ private HttpResponse removeDeployKey(String tenantName, String applicationName, HttpRequest request) {
+ String pemDeployKey = toSlime(request.getData()).get().field("key").asString();
+ controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
+ controller.applications().store(application.withoutPemDeployKey(pemDeployKey));
+ });
+ return new MessageResponse("Removed deploy key " + pemDeployKey);
+ }
+
private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) {
Inspector requestObject = toSlime(request.getData()).get();
StringJoiner messageBuilder = new StringJoiner("\n").setEmptyValue("No applicable changes.");
@@ -377,12 +420,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
messageBuilder.add("Set major version to " + (majorVersion == null ? "empty" : majorVersion));
}
+ // TODO jonmv: Remove when clients are updated.
Inspector pemDeployKeyField = requestObject.field("pemDeployKey");
if (pemDeployKeyField.valid()) {
- String pemDeployKey = pemDeployKeyField.type() == Type.NIX ? null : pemDeployKeyField.asString();
+ String pemDeployKey = pemDeployKeyField.asString();
application = application.withPemDeployKey(pemDeployKey);
- messageBuilder.add("Set pem deploy key to " + (pemDeployKey == null ? "empty" : pemDeployKey));
+ messageBuilder.add("Added deploy key " + pemDeployKey);
}
+
controller.applications().store(application);
});
return new MessageResponse(messageBuilder.toString());
@@ -608,7 +653,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
}
- application.pemDeployKey().ifPresent(key -> object.setString("pemDeployKey", key));
+ // TODO jonmv: Remove when clients are updated
+ application.pemDeployKeys().stream().findFirst().ifPresent(key -> object.setString("pemDeployKey", key));
+
+ application.pemDeployKeys().forEach(object.setArray("pemDeployKeys")::addString);
// Metrics
Cursor metricsObject = object.setObject("metrics");
@@ -1296,6 +1344,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private void toSlime(Cursor object, Tenant tenant, HttpRequest request) {
object.setString("tenant", tenant.name().value());
object.setString("type", tenantType(tenant));
+ List<Application> applications = controller.applications().asList(tenant.name());
switch (tenant.type()) {
case athenz:
AthenzTenant athenzTenant = (AthenzTenant) tenant;
@@ -1314,11 +1363,30 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
});
break;
case user: break;
- case cloud: break;
+ case cloud: {
+ CloudTenant cloudTenant = (CloudTenant) tenant;
+
+ Cursor pemDeployKeysArray = object.setArray("pemDeployKeys");
+ for (Application application : applications)
+ for (String key : application.pemDeployKeys()) {
+ Cursor keyObject = pemDeployKeysArray.addObject();
+ keyObject.setString("key", key);
+ keyObject.setString("application", application.id().application().value());
+ }
+
+ Cursor pemDeveloperKeysArray = object.setArray("pemDeveloperKeys");
+ cloudTenant.pemDeveloperKeys().forEach((key, user) -> {
+ Cursor keyObject = pemDeveloperKeysArray.addObject();
+ keyObject.setString("key", key);
+ keyObject.setString("user", user.getName());
+ });
+
+ break;
+ }
default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
}
Cursor applicationArray = object.setArray("applications");
- for (Application application : controller.applications().asList(tenant.name()))
+ for (Application application : applications)
for (Instance instance : application.instances().values())
if (recurseOverApplications(request))
toSlime(applicationArray.addObject(), instance, application, request);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
index e86a5d16452..6755110bb49 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
@@ -11,10 +11,10 @@ import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.yolean.Exceptions;
@@ -47,18 +47,17 @@ public class SignatureFilter extends JsonSecurityRequestFilterBase {
&& request.getHeader("X-Authorization") != null)
try {
ApplicationId id = ApplicationId.fromSerializedForm(request.getHeader("X-Key-Id"));
- boolean verified = controller.applications().getApplication(TenantAndApplicationId.from(id))
- .flatMap(Application::pemDeployKey)
+ boolean verified = controller.applications().getApplication(TenantAndApplicationId.from(id)).stream()
+ .flatMap(application -> application.pemDeployKeys().stream())
.map(key -> new RequestVerifier(key, controller.clock()))
- .map(verifier -> verifier.verify(Method.valueOf(request.getMethod()),
- request.getUri(),
- request.getHeader("X-Timestamp"),
- request.getHeader("X-Content-Hash"),
- request.getHeader("X-Authorization")))
- .orElse(false);
+ .anyMatch(verifier -> verifier.verify(Method.valueOf(request.getMethod()),
+ request.getUri(),
+ request.getHeader("X-Timestamp"),
+ request.getHeader("X-Content-Hash"),
+ request.getHeader("X-Authorization")));
if (verified) {
- Principal principal = () -> "buildService@" + id.tenant() + "." + id.application();
+ Principal principal = new SimplePrincipal("buildService@" + id.tenant() + "." + id.application());
request.setUserPrincipal(principal);
request.setRemoteUser(principal.getName());
request.setAttribute(SecurityContext.ATTRIBUTE_NAME,
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index 410194a8ef7..807e74b7c75 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -13,6 +13,8 @@ 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.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
import com.yahoo.vespa.hosted.controller.api.integration.user.User;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
@@ -22,17 +24,22 @@ import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.SlimeJsonResponse;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.yolean.Exceptions;
+import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
/**
* API for user management related to access control.
@@ -46,11 +53,13 @@ public class UserApiHandler extends LoggingRequestHandler {
private static final String optionalPrefix = "/api";
private final UserManagement users;
+ private final Controller controller;
@Inject
- public UserApiHandler(Context parentCtx, UserManagement users) {
+ public UserApiHandler(Context parentCtx, UserManagement users, Controller controller) {
super(parentCtx);
this.users = users;
+ this.controller = controller;
}
@Override
@@ -183,11 +192,18 @@ public class UserApiHandler extends LoggingRequestHandler {
String roleName = require("roleName", Inspector::asString, requestObject);
UserId user = new UserId(require("user", Inspector::asString, requestObject));
Role role = Roles.toRole(TenantName.from(tenantName), roleName);
- List<User> currentUsers = users.listUsers(role);
- if (role.definition() == RoleDefinition.tenantOwner
- && currentUsers.size() == 1
- && currentUsers.get(0).email().equals(user.value()))
- throw new IllegalArgumentException("Can't remove the last owner of a tenant.");
+
+ if ( role.definition() == RoleDefinition.tenantOwner
+ && Set.of(user.value()).equals(users.listUsers(role).stream().map(User::email).collect(Collectors.toSet())))
+ throw new IllegalArgumentException("Can't remove the last owner of a tenant.");
+
+ // TODO jonmv: Change to developer role, when this exists.
+ if (role.definition().equals(RoleDefinition.tenantOperator))
+ controller.tenants().lockIfPresent(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
+ String key = tenant.get().pemDeveloperKeys().inverse().get(new SimplePrincipal(user.value()));
+ if (key != null)
+ controller.tenants().store(tenant.withoutPemDeveloperKey(key));
+ });
users.removeUsers(role, List.of(user));
return new MessageResponse(user+" is no longer a member of "+role);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
index 33529c342a3..7da3e43c9a5 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
@@ -35,7 +35,7 @@ public class CloudAccessControl implements AccessControl {
@Override
public CloudTenant createTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing) {
CloudTenantSpec spec = (CloudTenantSpec) tenantSpec;
- CloudTenant tenant = new CloudTenant(spec.tenant(), defaultBillingInfo);
+ CloudTenant tenant = CloudTenant.create(spec.tenant(), defaultBillingInfo);
for (Role role : Roles.tenantRoles(spec.tenant()))
userManagement.createRole(role);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
index 2d59e539bbd..6ef9b5e6a4f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -1,35 +1,44 @@
package com.yahoo.vespa.hosted.controller.tenant;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
+import java.security.Principal;
+import java.util.Objects;
import java.util.Optional;
/**
- * A tenant as vague as its name.
- *
- * Only a reference to a cloud identity provider, and some billing info, is known for this tenant type.
+ * A paying tenant in a Vespa cloud service.
*
* @author jonmv
*/
public class CloudTenant extends Tenant {
private final BillingInfo billingInfo;
+ private final BiMap<String, Principal> pemDeveloperKeys;
/** Public for the serialization layer — do not use! */
- public CloudTenant(TenantName name, BillingInfo info) {
+ public CloudTenant(TenantName name, BillingInfo info, BiMap<String, Principal> pemDeveloperKeys) {
super(name, Optional.empty());
billingInfo = info;
+ this.pemDeveloperKeys = pemDeveloperKeys;
}
/** Creates a tenant with the given name, provided it passes validation. */
public static CloudTenant create(TenantName tenantName, BillingInfo billingInfo) {
- return new CloudTenant(requireName(tenantName), billingInfo);
+ return new CloudTenant(requireName(tenantName),
+ Objects.requireNonNull(billingInfo),
+ ImmutableBiMap.of());
}
/** Returns the billing info for this tenant. */
public BillingInfo billingInfo() { return billingInfo; }
+ /** Returns the set of developer keys and their corresponding developers for this tenant. */
+ public BiMap<String, Principal> pemDeveloperKeys() { return pemDeveloperKeys; }
+
@Override
public Type type() {
return Type.cloud;
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 90d257d754d..a4f3a804c55 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
@@ -7,16 +7,12 @@ import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.JsonFormat;
import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
@@ -24,17 +20,12 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificate;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-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.RoutingEndpoint;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
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;
@@ -43,25 +34,15 @@ import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.persistence.InstanceSerializer;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.persistence.OldMockCuratorDb;
import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
-import com.yahoo.vespa.hosted.controller.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
import org.junit.Test;
-import java.io.IOException;
import java.time.Duration;
-import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -72,7 +53,6 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobTy
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.productionUsWest1;
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest;
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.temporal.ChronoUnit.MILLIS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -790,103 +770,6 @@ public class ControllerTest {
}
}
-
- @Test
- public void testInstanceDataMigration() throws IOException {
- /*
- Set up initial state, using old DB:
- Create two instances of an application; the default will be the base.
-
- Read, modify and write the application using the new DB.
- Verify results using both old and new DBs.
- */
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().allow(ValidationId.contentClusterRemoval)
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .endpoint("endpoint", "container", "us-east-1")
- .region("us-east-1")
- .build();
- ApplicationId defaultId = ApplicationId.from("t1", "a1", "default");
- Instance old1 = new Instance(defaultId,
- Instant.ofEpochMilli(123),
- applicationPackage.deploymentSpec(),
- applicationPackage.validationOverrides(),
- List.of(new Deployment(ZoneId.from("prod", "us-east-1"),
- ApplicationVersion.from(new SourceRevision("repo", "branch", "commit"), 3),
- Version.fromString("7.8.9"),
- Instant.ofEpochMilli(321))),
- new DeploymentJobs(OptionalLong.of(72),
- List.of(new JobStatus(JobType.productionAwsUsEast1a,
- Optional.empty(),
- Optional.of(new JobStatus.JobRun(32,
- Version.fromString("7.8.9"),
- ApplicationVersion.unknown,
- Optional.empty(),
- Optional.empty(),
- "make the job great again",
- Instant.ofEpochMilli(200))),
- Optional.empty(),
- Optional.empty(),
- Optional.empty(),
- OptionalLong.empty())),
- Optional.of(IssueId.from("issue")),
- true),
- Change.of(Version.fromString("9")),
- Change.empty(),
- Optional.of(IssueId.from("tissue")),
- Optional.of(User.from("user")),
- OptionalInt.of(3),
- new ApplicationMetrics(2, 3),
- Optional.of("key"),
- List.of(AssignedRotation.fromStrings("container", "endpoint", "rot13", List.of("us-east-1"))),
- RotationStatus.from(Map.of(new RotationId("rot13"), Map.of(ZoneId.from("prod", "us-east-1"), RotationState.in))));
-
- Instance old2 = new Instance(ApplicationId.from("t1", "a1", "i1"),
- Instant.ofEpochMilli(400));
-
-
- InstanceSerializer instanceSerializer = new InstanceSerializer();
- String old1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(old1)), UTF_8);
-
- MockCuratorDb newDb = new MockCuratorDb();
- OldMockCuratorDb oldDb = new OldMockCuratorDb(newDb.curator());
-
- oldDb.writeApplication(Application.aggregate(List.of(old1, old2)).orElseThrow());
-
- Application application = oldDb.readApplication(TenantAndApplicationId.from("t1", "a1")).orElseThrow();
- Instance new1 = application.legacy(InstanceName.defaultName());
- String new1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(new1)), UTF_8);
- assertEquals(old1Serialized, new1Serialized);
-
- LockedApplication locked = new LockedApplication(application, newDb.lock(application.id()));
- oldDb.writeApplication(locked.with(new ApplicationMetrics(8, 9)).get());
-
- Application newApp = newDb.readApplication(application.id()).orElseThrow();
- Instance mod1 = newApp.legacy(old1.name());
- Instance mod2 = newApp.legacy(old2.name());
-
- old1 = old1.with(new ApplicationMetrics(8, 9));
- old1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(old1)), UTF_8);
- String mod1Serialized = new String(JsonFormat.toJsonBytes(instanceSerializer.toSlime(mod1)), UTF_8);
- assertEquals(old1Serialized, mod1Serialized);
-
- assertEquals(old1.createdAt(), mod2.createdAt());
- assertEquals(old1.change(), mod2.change());
- assertEquals(old1.outstandingChange(), mod2.outstandingChange());
- assertEquals(old1.deploymentSpec(), mod2.deploymentSpec());
- assertEquals(old2.deployments(), mod2.deployments());
- assertEquals(old2.deploymentJobs().jobStatus(), mod2.deploymentJobs().jobStatus());
-
- application = new LockedApplication(application, newDb.lock(application.id())).without(old1.name()).get();
- newDb.storeWithoutInstance(application, old1.id());
- assertEquals(1, newDb.readApplication(application.id()).orElseThrow().instances().size());
- assertEquals(1, oldDb.readApplication(application.id()).orElseThrow().instances().size());
- application = new LockedApplication(application, newDb.lock(application.id())).without(old2.name()).get();
- newDb.storeWithoutInstance(application, old2.id());
- assertTrue(newDb.readApplication(application.id()).isEmpty());
- assertTrue(oldDb.readApplication(application.id()).isEmpty());
- }
-
private void runUpgrade(DeploymentTester tester, ApplicationId application, ApplicationVersion version) {
Version next = Version.fromString("6.2");
tester.upgradeSystem(next);
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 82d9690f7d7..cefdc3bed61 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
@@ -33,7 +33,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
import com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock;
import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import com.yahoo.vespa.hosted.controller.persistence.InstanceSerializer;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
@@ -182,15 +181,6 @@ public final class ControllerTester {
createAndDeploy(tenantName, domainName, applicationName, environment, projectId, null);
}
- /** Create application from slime */
- public void createApplication(Slime slime) {
- Instance instance = new InstanceSerializer().fromSlime(slime);
- Application application = Application.aggregate(List.of(instance)).get();
- try (Lock lock = controller().applications().lock(application.id())) {
- controller().applications().store(new LockedApplication(application, lock));
- }
- }
-
public ZoneId toZone(Environment environment) {
switch (environment) {
case dev: case test:
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
index 545729b5474..744ef0f3b08 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
@@ -36,6 +36,7 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
+import java.util.OptionalLong;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -109,6 +110,12 @@ public class DeploymentTriggerTest {
tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), true, JobType.systemTest);
tester.assertRunning(productionUsWest1, app.id().defaultInstance());
+
+ // system-test fails again, but the app loses its projectId, and the job isn't retried.
+ tester.applications().lockApplicationOrThrow(app.id(), locked ->
+ tester.applications().store(locked.withProjectId(OptionalLong.empty())));
+ tester.deployAndNotify(instance.id(), Optional.of(applicationPackage), false, productionUsWest1);
+ assertEquals("Job is not triggered when no projectId is present", 0, tester.buildService().jobs().size());
}
@Test
@@ -1061,23 +1068,6 @@ public class DeploymentTriggerTest {
}
@Test
- public void applicationWithoutProjectIdIsNotTriggered() throws Exception {
- // Current system version, matches version in test data
- Version version = Version.fromString("6.42.1");
- tester.upgradeSystem(version);
- assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber());
-
- // Load test data data
- byte[] json = Files.readAllBytes(Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/application-without-project-id.json"));
- Slime slime = SlimeUtils.jsonToSlime(json);
- tester.controllerTester().createApplication(slime);
-
- // Failure redeployer does not restart deployment
- tester.readyJobTrigger().maintain();
- assertTrue("No jobs scheduled", tester.buildService().jobs().isEmpty());
- }
-
- @Test
public void testPlatformVersionSelection() {
// Setup system
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
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 5889b71bdd9..3ba1181f762 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
@@ -1,6 +1,7 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
@@ -15,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
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.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -46,6 +48,7 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
+import java.util.stream.Collectors;
import static com.yahoo.config.provision.SystemName.main;
import static java.util.Optional.empty;
@@ -101,7 +104,7 @@ public class ApplicationSerializerTest {
.withTriggering(Version.fromString("5.6.6"), ApplicationVersion.unknown, deployments.stream().findFirst(), "Test 3", Instant.ofEpochMilli(6))
.withCompletion(11, empty(), Instant.ofEpochMilli(7)));
- DeploymentJobs deploymentJobs = new DeploymentJobs(OptionalLong.empty(), statusList, empty(), true);
+ DeploymentJobs deploymentJobs = new DeploymentJobs(statusList);
var rotationStatus = RotationStatus.from(Map.of(new RotationId("my-rotation"),
Map.of(ZoneId.from("prod", "us-west-1"), RotationState.in,
@@ -116,7 +119,7 @@ public class ApplicationSerializerTest {
rotationStatus),
new Instance(id3,
List.of(),
- new DeploymentJobs(OptionalLong.empty(), List.of(), empty(), true),
+ new DeploymentJobs(List.of()),
List.of(),
RotationStatus.EMPTY));
@@ -131,7 +134,7 @@ public class ApplicationSerializerTest {
Optional.of(User.from("by-username")),
OptionalInt.of(7),
new ApplicationMetrics(0.5, 0.9),
- Optional.of("-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"),
+ Set.of("-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n\n-----END PUBLIC KEY-----", "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"),
projectId,
true,
instances);
@@ -163,7 +166,6 @@ public class ApplicationSerializerTest {
assertEquals(original.require(id1.instance()).deployments().get(zone2).activity().lastQueried().get(), serialized.require(id1.instance()).deployments().get(zone2).activity().lastQueried().get());
assertEquals(original.require(id1.instance()).deployments().get(zone2).activity().lastWritten().get(), serialized.require(id1.instance()).deployments().get(zone2).activity().lastWritten().get());
- assertEquals(original.require(id1.instance()).deploymentJobs().projectId(), serialized.require(id1.instance()).deploymentJobs().projectId());
assertEquals(original.require(id1.instance()).deploymentJobs().jobStatus().size(), serialized.require(id1.instance()).deploymentJobs().jobStatus().size());
assertEquals( original.require(id1.instance()).deploymentJobs().jobStatus().get(JobType.systemTest),
serialized.require(id1.instance()).deploymentJobs().jobStatus().get(JobType.systemTest));
@@ -176,7 +178,7 @@ public class ApplicationSerializerTest {
assertEquals(original.owner(), serialized.owner());
assertEquals(original.majorVersion(), serialized.majorVersion());
assertEquals(original.change(), serialized.change());
- assertEquals(original.pemDeployKey(), serialized.pemDeployKey());
+ assertEquals(original.pemDeployKeys(), serialized.pemDeployKeys());
assertEquals(original.require(id1.instance()).rotations(), serialized.require(id1.instance()).rotations());
assertEquals(original.require(id1.instance()).rotationStatus(), serialized.require(id1.instance()).rotationStatus());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java
deleted file mode 100644
index 3bb5e3d0c2b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/InstanceSerializerTest.java
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-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.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
-import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
-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.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
-import org.junit.Test;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalDouble;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.Set;
-
-import static com.yahoo.config.provision.SystemName.main;
-import static java.util.Optional.empty;
-import static org.junit.Assert.assertEquals;
-
-/**
- * @author bratseth
- */
-public class InstanceSerializerTest {
-
- private static final InstanceSerializer INSTANCE_SERIALIZER = new InstanceSerializer();
- private static final Path testData = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/");
- 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() {
- DeploymentSpec deploymentSpec = DeploymentSpec.fromXml("<deployment version='1.0'>" +
- " <staging/>" +
- "</deployment>");
- ValidationOverrides validationOverrides = ValidationOverrides.fromXml("<validation-overrides version='1.0'>" +
- " <allow until='2017-06-15'>deployment-removal</allow>" +
- "</validation-overrides>");
-
- List<Deployment> deployments = new ArrayList<>();
- ApplicationVersion applicationVersion1 = ApplicationVersion.from(new SourceRevision("repo1", "branch1", "commit1"), 31);
- ApplicationVersion applicationVersion2 = ApplicationVersion
- .from(new SourceRevision("repo1", "branch1", "commit1"), 32, "a@b",
- Version.fromString("6.3.1"), Instant.ofEpochMilli(496));
- Instant activityAt = Instant.parse("2018-06-01T10:15:30.00Z");
- deployments.add(new Deployment(zone1, applicationVersion1, Version.fromString("1.2.3"), Instant.ofEpochMilli(3))); // One deployment without cluster info and utils
- deployments.add(new Deployment(zone2, applicationVersion2, Version.fromString("1.2.3"), Instant.ofEpochMilli(5),
- createClusterUtils(3, 0.2), createClusterInfo(3, 4),
- new DeploymentMetrics(2, 3, 4, 5, 6,
- Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS)),
- Map.of(DeploymentMetrics.Warning.all, 3)),
- DeploymentActivity.create(Optional.of(activityAt), Optional.of(activityAt),
- OptionalDouble.of(200), OptionalDouble.of(10))));
-
- OptionalLong projectId = OptionalLong.of(123L);
- List<JobStatus> statusList = new ArrayList<>();
-
- statusList.add(JobStatus.initial(JobType.systemTest)
- .withTriggering(Version.fromString("5.6.7"), ApplicationVersion.unknown, empty(), "Test", Instant.ofEpochMilli(7))
- .withCompletion(30, empty(), Instant.ofEpochMilli(8))
- .withPause(OptionalLong.of(1L << 32)));
- statusList.add(JobStatus.initial(JobType.stagingTest)
- .withTriggering(Version.fromString("5.6.6"), ApplicationVersion.unknown, empty(), "Test 2", Instant.ofEpochMilli(5))
- .withCompletion(11, Optional.of(JobError.unknown), Instant.ofEpochMilli(6)));
- statusList.add(JobStatus.initial(JobType.from(main, zone1).get())
- .withTriggering(Version.fromString("5.6.6"), ApplicationVersion.unknown, deployments.stream().findFirst(), "Test 3", Instant.ofEpochMilli(6))
- .withCompletion(11, empty(), Instant.ofEpochMilli(7)));
-
- DeploymentJobs deploymentJobs = new DeploymentJobs(projectId, statusList, empty(), true);
-
- var rotationStatus = RotationStatus.from(Map.of(new RotationId("my-rotation"),
- Map.of(ZoneId.from("prod", "us-west-1"), RotationState.in,
- ZoneId.from("prod", "us-east-3"), RotationState.out)));
-
- Instance original = new Instance(ApplicationId.from("t1", "a1", "i1"),
- Instant.now().truncatedTo(ChronoUnit.MILLIS),
- deploymentSpec,
- validationOverrides,
- deployments, deploymentJobs,
- Change.of(Version.fromString("6.7")).withPin(),
- Change.of(ApplicationVersion.from(new SourceRevision("repo", "master", "deadcafe"), 42)),
- Optional.of(IssueId.from("1234")),
- Optional.of(User.from("by-username")),
- OptionalInt.of(7),
- new ApplicationMetrics(0.5, 0.9),
- Optional.of("-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"),
- List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of())),
- rotationStatus);
-
- Instance serialized = INSTANCE_SERIALIZER.fromSlime(INSTANCE_SERIALIZER.toSlime(original));
-
- assertEquals(original.id(), serialized.id());
- assertEquals(original.createdAt(), serialized.createdAt());
-
- assertEquals(original.deploymentSpec().xmlForm(), serialized.deploymentSpec().xmlForm());
- assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm());
-
- assertEquals(2, serialized.deployments().size());
- assertEquals(original.deployments().get(zone1).applicationVersion(), serialized.deployments().get(zone1).applicationVersion());
- assertEquals(original.deployments().get(zone2).applicationVersion(), serialized.deployments().get(zone2).applicationVersion());
- assertEquals(original.deployments().get(zone1).version(), serialized.deployments().get(zone1).version());
- assertEquals(original.deployments().get(zone2).version(), serialized.deployments().get(zone2).version());
- assertEquals(original.deployments().get(zone1).at(), serialized.deployments().get(zone1).at());
- assertEquals(original.deployments().get(zone2).at(), serialized.deployments().get(zone2).at());
- assertEquals(original.deployments().get(zone2).activity().lastQueried().get(), serialized.deployments().get(zone2).activity().lastQueried().get());
- assertEquals(original.deployments().get(zone2).activity().lastWritten().get(), serialized.deployments().get(zone2).activity().lastWritten().get());
-
- assertEquals(original.deploymentJobs().projectId(), serialized.deploymentJobs().projectId());
- assertEquals(original.deploymentJobs().jobStatus().size(), serialized.deploymentJobs().jobStatus().size());
- assertEquals( original.deploymentJobs().jobStatus().get(JobType.systemTest),
- serialized.deploymentJobs().jobStatus().get(JobType.systemTest));
- assertEquals( original.deploymentJobs().jobStatus().get(JobType.stagingTest),
- serialized.deploymentJobs().jobStatus().get(JobType.stagingTest));
-
- assertEquals(original.outstandingChange(), serialized.outstandingChange());
-
- assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId());
- assertEquals(original.owner(), serialized.owner());
- assertEquals(original.majorVersion(), serialized.majorVersion());
- assertEquals(original.change(), serialized.change());
- assertEquals(original.pemDeployKey(), serialized.pemDeployKey());
-
- assertEquals(original.rotations(), serialized.rotations());
- assertEquals(original.rotationStatus(), serialized.rotationStatus());
-
- // Test cluster utilization
- assertEquals(0, serialized.deployments().get(zone1).clusterUtils().size());
- assertEquals(3, serialized.deployments().get(zone2).clusterUtils().size());
- assertEquals(0.4, serialized.deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id2")).getCpu(), 0.01);
- assertEquals(0.2, serialized.deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id1")).getCpu(), 0.01);
- assertEquals(0.2, serialized.deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id1")).getMemory(), 0.01);
-
- // Test cluster info
- assertEquals(3, serialized.deployments().get(zone2).clusterInfo().size());
- assertEquals(10, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorCost());
- assertEquals(ClusterSpec.Type.content, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getClusterType());
- assertEquals("flavor2", serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavor());
- assertEquals(4, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getHostnames().size());
- assertEquals(2, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorCPU(), Double.MIN_VALUE);
- assertEquals(4, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorMem(), Double.MIN_VALUE);
- assertEquals(50, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorDisk(), Double.MIN_VALUE);
-
- // Test metrics
- assertEquals(original.metrics().queryServiceQuality(), serialized.metrics().queryServiceQuality(), Double.MIN_VALUE);
- assertEquals(original.metrics().writeServiceQuality(), serialized.metrics().writeServiceQuality(), Double.MIN_VALUE);
- assertEquals(original.deployments().get(zone2).metrics().queriesPerSecond(), serialized.deployments().get(zone2).metrics().queriesPerSecond(), Double.MIN_VALUE);
- assertEquals(original.deployments().get(zone2).metrics().writesPerSecond(), serialized.deployments().get(zone2).metrics().writesPerSecond(), Double.MIN_VALUE);
- assertEquals(original.deployments().get(zone2).metrics().documentCount(), serialized.deployments().get(zone2).metrics().documentCount(), Double.MIN_VALUE);
- assertEquals(original.deployments().get(zone2).metrics().queryLatencyMillis(), serialized.deployments().get(zone2).metrics().queryLatencyMillis(), Double.MIN_VALUE);
- assertEquals(original.deployments().get(zone2).metrics().writeLatencyMillis(), serialized.deployments().get(zone2).metrics().writeLatencyMillis(), Double.MIN_VALUE);
- assertEquals(original.deployments().get(zone2).metrics().instant(), serialized.deployments().get(zone2).metrics().instant());
- assertEquals(original.deployments().get(zone2).metrics().warnings(), serialized.deployments().get(zone2).metrics().warnings());
- }
-
- private Map<ClusterSpec.Id, ClusterInfo> createClusterInfo(int clusters, int hosts) {
- Map<ClusterSpec.Id, ClusterInfo> result = new HashMap<>();
-
- for (int cluster = 0; cluster < clusters; cluster++) {
- List<String> hostnames = new ArrayList<>();
- for (int host = 0; host < hosts; host++) {
- hostnames.add("hostname" + cluster*host + host);
- }
-
- result.put(ClusterSpec.Id.from("id" + cluster), new ClusterInfo("flavor" + cluster, 10,
- 2, 4, 50, ClusterSpec.Type.content, hostnames));
- }
- return result;
- }
-
- private Map<ClusterSpec.Id, ClusterUtilization> createClusterUtils(int clusters, double inc) {
- Map<ClusterSpec.Id, ClusterUtilization> result = new HashMap<>();
-
- ClusterUtilization util = new ClusterUtilization(0,0,0,0);
- for (int cluster = 0; cluster < clusters; cluster++) {
- double agg = cluster*inc;
- result.put(ClusterSpec.Id.from("id" + cluster), new ClusterUtilization(
- util.getMemory()+ agg,
- util.getCpu()+ agg,
- util.getDisk() + agg,
- util.getDiskBusy() + agg));
- }
- return result;
- }
-
- @Test
- public void testCompleteApplicationDeserialization() throws Exception {
- byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-instance.json"));
- INSTANCE_SERIALIZER.fromSlime(SlimeUtils.jsonToSlime(applicationJson));
- // ok if no error
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldCuratorDb.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldCuratorDb.java
deleted file mode 100644
index f7fdd91d74e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldCuratorDb.java
+++ /dev/null
@@ -1,687 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.common.util.concurrent.UncheckedTimeoutException;
-import com.google.inject.Inject;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.path.Path;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.config.SlimeUtils;
-import com.yahoo.vespa.curator.Curator;
-import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.ByteBuffer;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
-import java.util.stream.Stream;
-
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.collectingAndThen;
-
-/**
- * Copy the current to replace this class before doing a data migration.
- * Use old serializers too, if needed.
- *
- * @author jonmv
- */
-public class OldCuratorDb {
-
- private static final Logger log = Logger.getLogger(CuratorDb.class.getName());
- private static final Duration deployLockTimeout = Duration.ofMinutes(30);
- private static final Duration defaultLockTimeout = Duration.ofMinutes(5);
- private static final Duration defaultTryLockTimeout = Duration.ofSeconds(1);
-
- private static final Path root = Path.fromString("/controller/v1");
- private static final Path lockRoot = root.append("locks");
- private static final Path tenantRoot = root.append("tenants");
- private static final Path applicationRoot = root.append("applications");
- private static final Path instanceRoot = root.append("instances");
- private static final Path jobRoot = root.append("jobs");
- private static final Path controllerRoot = root.append("controllers");
- private static final Path routingPoliciesRoot = root.append("routingPolicies");
- private static final Path applicationCertificateRoot = root.append("applicationCertificates");
-
- private final StringSetSerializer stringSetSerializer = new StringSetSerializer();
- private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer();
- private final ControllerVersionSerializer controllerVersionSerializer = new ControllerVersionSerializer();
- private final ConfidenceOverrideSerializer confidenceOverrideSerializer = new ConfidenceOverrideSerializer();
- private final TenantSerializer tenantSerializer = new TenantSerializer();
- private final ApplicationSerializer applicationSerializer = new ApplicationSerializer();
- private final InstanceSerializer instanceSerializer = new InstanceSerializer();
- private final RunSerializer runSerializer = new RunSerializer();
- private final OsVersionSerializer osVersionSerializer = new OsVersionSerializer();
- private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer);
- private final RoutingPolicySerializer routingPolicySerializer = new RoutingPolicySerializer();
- private final AuditLogSerializer auditLogSerializer = new AuditLogSerializer();
- private final NameServiceQueueSerializer nameServiceQueueSerializer = new NameServiceQueueSerializer();
-
- private final Curator curator;
- private final Duration tryLockTimeout;
-
- /**
- * All keys, to allow reentrancy.
- * This will grow forever, but this should be too slow to be a problem.
- */
- private final ConcurrentHashMap<Path, Lock> locks = new ConcurrentHashMap<>();
-
- @Inject
- public OldCuratorDb(Curator curator) {
- this(curator, defaultTryLockTimeout);
- }
-
- OldCuratorDb(Curator curator, Duration tryLockTimeout) {
- this.curator = curator;
- this.tryLockTimeout = tryLockTimeout;
- }
-
- /** Returns all hosts configured to be part of this ZooKeeper cluster */
- public List<HostName> cluster() {
- return Arrays.stream(curator.zooKeeperEnsembleConnectionSpec().split(","))
- .filter(hostAndPort -> !hostAndPort.isEmpty())
- .map(hostAndPort -> hostAndPort.split(":")[0])
- .map(HostName::from)
- .collect(Collectors.toList());
- }
-
- // -------------- Locks ---------------------------------------------------
-
- /** Creates a reentrant lock */
- private Lock lock(Path path, Duration timeout) {
- curator.create(path);
- Lock lock = locks.computeIfAbsent(path, (pathArg) -> new Lock(pathArg.getAbsolute(), curator));
- lock.acquire(timeout);
- return lock;
- }
-
- public Lock lock(TenantName name) {
- return lock(lockPath(name), defaultLockTimeout.multipliedBy(2));
- }
-
- public Lock lock(TenantAndApplicationId id) {
- return lock(lockPath(id), defaultLockTimeout.multipliedBy(2));
- }
-
- public Lock lock(ApplicationId id) {
- return lock(lockPath(id), defaultLockTimeout.multipliedBy(2));
- }
-
- public Lock lockForDeployment(ApplicationId id, ZoneId zone) {
- return lock(lockPath(id, zone), deployLockTimeout);
- }
-
- public Lock lock(ApplicationId id, JobType type) {
- return lock(lockPath(id, type), defaultLockTimeout);
- }
-
- public Lock lock(ApplicationId id, JobType type, Step step) throws TimeoutException {
- return tryLock(lockPath(id, type, step));
- }
-
- public Lock lockRotations() {
- return lock(lockRoot.append("rotations"), defaultLockTimeout);
- }
-
- public Lock lockConfidenceOverrides() {
- return lock(lockRoot.append("confidenceOverrides"), defaultLockTimeout);
- }
-
- public Lock lockInactiveJobs() {
- return lock(lockRoot.append("inactiveJobsLock"), defaultLockTimeout);
- }
-
- public Lock lockMaintenanceJob(String jobName) throws TimeoutException {
- return tryLock(lockRoot.append("maintenanceJobLocks").append(jobName));
- }
-
- @SuppressWarnings("unused") // Called by internal code
- public Lock lockProvisionState(String provisionStateId) {
- return lock(lockPath(provisionStateId), Duration.ofSeconds(1));
- }
-
- public Lock lockOsVersions() {
- return lock(lockRoot.append("osTargetVersion"), defaultLockTimeout);
- }
-
- public Lock lockOsVersionStatus() {
- return lock(lockRoot.append("osVersionStatus"), defaultLockTimeout);
- }
-
- public Lock lockRoutingPolicies() {
- return lock(lockRoot.append("routingPolicies"), defaultLockTimeout);
- }
-
- public Lock lockAuditLog() {
- return lock(lockRoot.append("auditLog"), defaultLockTimeout);
- }
-
- public Lock lockNameServiceQueue() {
- return lock(lockRoot.append("nameServiceQueue"), defaultLockTimeout);
- }
-
- // -------------- Helpers ------------------------------------------
-
- /** Try locking with a low timeout, meaning it is OK to fail lock acquisition.
- *
- * Useful for maintenance jobs, where there is no point in running the jobs back to back.
- */
- private Lock tryLock(Path path) throws TimeoutException {
- try {
- return lock(path, tryLockTimeout);
- }
- catch (UncheckedTimeoutException e) {
- throw new TimeoutException(e.getMessage());
- }
- }
-
- private <T> Optional<T> read(Path path, Function<byte[], T> mapper) {
- return curator.getData(path).filter(data -> data.length > 0).map(mapper);
- }
-
- private Optional<Slime> readSlime(Path path) {
- return read(path, SlimeUtils::jsonToSlime);
- }
-
- private static byte[] asJson(Slime slime) {
- try {
- return SlimeUtils.toJsonBytes(slime);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- // -------------- Deployment orchestration --------------------------------
-
- public Set<String> readInactiveJobs() {
- try {
- return readSlime(inactiveJobsPath()).map(stringSetSerializer::fromSlime).orElseGet(HashSet::new);
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Error reading inactive jobs, deleting inactive state");
- writeInactiveJobs(Collections.emptySet());
- return new HashSet<>();
- }
- }
-
- public void writeInactiveJobs(Set<String> inactiveJobs) {
- curator.set(inactiveJobsPath(), stringSetSerializer.toJson(inactiveJobs));
- }
-
- public double readUpgradesPerMinute() {
- return read(upgradesPerMinutePath(), ByteBuffer::wrap).map(ByteBuffer::getDouble).orElse(0.125);
- }
-
- public void writeUpgradesPerMinute(double n) {
- curator.set(upgradesPerMinutePath(), ByteBuffer.allocate(Double.BYTES).putDouble(n).array());
- }
-
- public Optional<Integer> readTargetMajorVersion() {
- return read(targetMajorVersionPath(), ByteBuffer::wrap).map(ByteBuffer::getInt);
- }
-
- public void writeTargetMajorVersion(Optional<Integer> targetMajorVersion) {
- if (targetMajorVersion.isPresent())
- curator.set(targetMajorVersionPath(), ByteBuffer.allocate(Integer.BYTES).putInt(targetMajorVersion.get()).array());
- else
- curator.delete(targetMajorVersionPath());
- }
-
- public void writeVersionStatus(VersionStatus status) {
- curator.set(versionStatusPath(), asJson(versionStatusSerializer.toSlime(status)));
- }
-
- public VersionStatus readVersionStatus() {
- return readSlime(versionStatusPath()).map(versionStatusSerializer::fromSlime).orElseGet(VersionStatus::empty);
- }
-
- public void writeConfidenceOverrides(Map<Version, VespaVersion.Confidence> overrides) {
- curator.set(confidenceOverridesPath(), asJson(confidenceOverrideSerializer.toSlime(overrides)));
- }
-
- public Map<Version, VespaVersion.Confidence> readConfidenceOverrides() {
- return readSlime(confidenceOverridesPath()).map(confidenceOverrideSerializer::fromSlime)
- .orElseGet(Collections::emptyMap);
- }
-
- public void writeControllerVersion(HostName hostname, ControllerVersion version) {
- curator.set(controllerPath(hostname.value()), asJson(controllerVersionSerializer.toSlime(version)));
- }
-
- public ControllerVersion readControllerVersion(HostName hostname) {
- return readSlime(controllerPath(hostname.value()))
- .map(controllerVersionSerializer::fromSlime)
- .orElse(ControllerVersion.CURRENT);
- }
-
- // Infrastructure upgrades
-
- public void writeOsVersions(Set<OsVersion> versions) {
- curator.set(osTargetVersionPath(), asJson(osVersionSerializer.toSlime(versions)));
- }
-
- public Set<OsVersion> readOsVersions() {
- return readSlime(osTargetVersionPath()).map(osVersionSerializer::fromSlime).orElseGet(Collections::emptySet);
- }
-
- public void writeOsVersionStatus(OsVersionStatus status) {
- curator.set(osVersionStatusPath(), asJson(osVersionStatusSerializer.toSlime(status)));
- }
-
- public OsVersionStatus readOsVersionStatus() {
- return readSlime(osVersionStatusPath()).map(osVersionStatusSerializer::fromSlime).orElse(OsVersionStatus.empty);
- }
-
- // -------------- Tenant --------------------------------------------------
-
- public void writeTenant(Tenant tenant) {
- curator.set(tenantPath(tenant.name()), asJson(tenantSerializer.toSlime(tenant)));
- }
-
- public Optional<Tenant> readTenant(TenantName name) {
- return readSlime(tenantPath(name)).map(tenantSerializer::tenantFrom);
- }
-
- public List<Tenant> readTenants() {
- return readTenantNames().stream()
- .map(this::readTenant)
- .flatMap(Optional::stream)
- .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
- }
-
- public List<TenantName> readTenantNames() {
- return curator.getChildren(tenantRoot).stream()
- .map(TenantName::from)
- .collect(Collectors.toList());
- }
-
- public void removeTenant(TenantName name) {
- curator.delete(tenantPath(name));
- }
-
- // -------------- Applications ---------------------------------------------
-
- public void writeApplication(Application application) {
- curator.set(applicationPath(application.id()), asJson(applicationSerializer.toSlime(application)));
- for (InstanceName name : application.instances().keySet()) {
- curator.set(oldApplicationPath(application.id().instance(name)),
- asJson(instanceSerializer.toSlime(application.legacy(name))));
- }
- }
-
- public Optional<Application> readApplication(TenantAndApplicationId application) {
- List<Instance> instances = readInstances(id -> TenantAndApplicationId.from(id).equals(application));
- return Application.aggregate(instances);
- }
-
- public List<Application> readApplications() {
- return readApplications(ignored -> true);
- }
-
- public List<Application> readApplications(TenantName name) {
- return readApplications(application -> application.tenant().equals(name));
- }
-
- private Stream<TenantAndApplicationId> readTenantAndApplicationIds() {
- return readInstanceIds().map(TenantAndApplicationId::from).distinct();
- }
-
- private List<Application> readApplications(Predicate<TenantAndApplicationId> applicationFilter) {
- return readTenantAndApplicationIds().filter(applicationFilter)
- .sorted()
- .map(this::readApplication)
- .flatMap(Optional::stream)
- .collect(Collectors.toUnmodifiableList());
- }
-
- private Optional<Instance> readInstance(ApplicationId application) {
- return readSlime(oldApplicationPath(application)).map(instanceSerializer::fromSlime);
- }
-
- private Stream<ApplicationId> readInstanceIds() {
- return curator.getChildren(applicationRoot).stream()
- .filter(id -> id.split(":").length == 3)
- .map(ApplicationId::fromSerializedForm);
- }
-
- private List<Instance> readInstances(Predicate<ApplicationId> applicationFilter) {
- return readInstanceIds().filter(applicationFilter)
- .sorted()
- .map(this::readInstance)
- .flatMap(Optional::stream)
- .collect(Collectors.toUnmodifiableList());
- }
-
- public void removeApplication(ApplicationId id) {
- // WARNING: This is part of a multi-step data move operation, so don't touch!!!
- curator.delete(oldApplicationPath(id));
- if (readApplication(TenantAndApplicationId.from(id)).isEmpty())
- curator.delete(applicationPath(TenantAndApplicationId.from(id)));
- }
-
- public void clearInstanceRoot() {
- curator.delete(instanceRoot);
- }
-
- /**
- * Migration plan:
- *
- * Add filter for reading only Instance from old application path RELEASED
- * Write Instance to instance and old application path RELEASED
- *
- * Lock on application level for instance mutations MERGED
- *
- * Write Instance to instance and application and old application paths DONE TO CHANGE DONE
- * Read Instance from instance path DONE TO REMOVE DONE
- * Duplicate Application from Instance, with helper classes DONE
- * Write Application to instance and application and old application paths DONE TO CHANGE DONE
- * Read Application from instance path DONE TO REMOVE DONE
- * Use Application where applicable DONE !!!
- * Lock instances and application on same level: tenant + application DONE TO CHANGE DONE
- * When reading an application, read all instances, and aggregate them DONE
- * Write application with instances to application path DONE
- * Write all instances of an application to old application path DONE
- * Remove everything under instance root DONE
- *
- * Read Application with instances from application path (with filter)
- * Stop locking applications on instance level
- *
- * Stop writing Instance to old application path
- * Remove unused parts of Instance (Used only for legacy serialization)
- */
-
- // -------------- Job Runs ------------------------------------------------
-
- public void writeLastRun(Run run) {
- curator.set(lastRunPath(run.id().application(), run.id().type()), asJson(runSerializer.toSlime(run)));
- }
-
- public void writeHistoricRuns(ApplicationId id, JobType type, Iterable<Run> runs) {
- curator.set(runsPath(id, type), asJson(runSerializer.toSlime(runs)));
- }
-
- public Optional<Run> readLastRun(ApplicationId id, JobType type) {
- return readSlime(lastRunPath(id, type)).map(runSerializer::runFromSlime);
- }
-
- public SortedMap<RunId, Run> readHistoricRuns(ApplicationId id, JobType type) {
- return readSlime(runsPath(id, type)).map(runSerializer::runsFromSlime).orElse(new TreeMap<>(comparing(RunId::number)));
- }
-
- public void deleteRunData(ApplicationId id, JobType type) {
- curator.delete(runsPath(id, type));
- curator.delete(lastRunPath(id, type));
- }
-
- public void deleteRunData(ApplicationId id) {
- curator.delete(jobRoot.append(id.serializedForm()));
- }
-
- public List<ApplicationId> applicationsWithJobs() {
- return curator.getChildren(jobRoot).stream()
- .map(ApplicationId::fromSerializedForm)
- .collect(Collectors.toList());
- }
-
-
- public Optional<byte[]> readLog(ApplicationId id, JobType type, long chunkId) {
- return curator.getData(logPath(id, type, chunkId));
- }
-
- public void writeLog(ApplicationId id, JobType type, long chunkId, byte[] log) {
- curator.set(logPath(id, type, chunkId), log);
- }
-
- public void deleteLog(ApplicationId id, JobType type) {
- curator.delete(runsPath(id, type).append("logs"));
- }
-
- public Optional<Long> readLastLogEntryId(ApplicationId id, JobType type) {
- return curator.getData(lastLogPath(id, type))
- .map(String::new).map(Long::parseLong);
- }
-
- public void writeLastLogEntryId(ApplicationId id, JobType type, long lastId) {
- curator.set(lastLogPath(id, type), Long.toString(lastId).getBytes());
- }
-
- public LongStream getLogChunkIds(ApplicationId id, JobType type) {
- return curator.getChildren(runsPath(id, type).append("logs")).stream()
- .mapToLong(Long::parseLong)
- .sorted();
- }
-
- // -------------- Audit log -----------------------------------------------
-
- public AuditLog readAuditLog() {
- return readSlime(auditLogPath()).map(auditLogSerializer::fromSlime)
- .orElse(AuditLog.empty);
- }
-
- public void writeAuditLog(AuditLog log) {
- curator.set(auditLogPath(), asJson(auditLogSerializer.toSlime(log)));
- }
-
-
- // -------------- Name service log ----------------------------------------
-
- public NameServiceQueue readNameServiceQueue() {
- return readSlime(nameServiceQueuePath()).map(nameServiceQueueSerializer::fromSlime)
- .orElse(NameServiceQueue.EMPTY);
- }
-
- public void writeNameServiceQueue(NameServiceQueue queue) {
- curator.set(nameServiceQueuePath(), asJson(nameServiceQueueSerializer.toSlime(queue)));
- }
-
- // -------------- Provisioning (called by internal code) ------------------
-
- @SuppressWarnings("unused")
- public Optional<byte[]> readProvisionState(String provisionId) {
- return curator.getData(provisionStatePath(provisionId));
- }
-
- @SuppressWarnings("unused")
- public void writeProvisionState(String provisionId, byte[] data) {
- curator.set(provisionStatePath(provisionId), data);
- }
-
- @SuppressWarnings("unused")
- public List<String> readProvisionStateIds() {
- return curator.getChildren(provisionStatePath());
- }
-
- // -------------- Routing policies ----------------------------------------
-
- public void writeRoutingPolicies(ApplicationId application, Set<RoutingPolicy> policies) {
- curator.set(routingPolicyPath(application), asJson(routingPolicySerializer.toSlime(policies)));
- }
-
- public Map<ApplicationId, Set<RoutingPolicy>> readRoutingPolicies() {
- return curator.getChildren(routingPoliciesRoot).stream()
- .map(ApplicationId::fromSerializedForm)
- .collect(Collectors.toUnmodifiableMap(Function.identity(), this::readRoutingPolicies));
- }
-
- public Set<RoutingPolicy> readRoutingPolicies(ApplicationId application) {
- return readSlime(routingPolicyPath(application)).map(slime -> routingPolicySerializer.fromSlime(application, slime))
- .orElseGet(Collections::emptySet);
- }
-
- // -------------- Application web certificates ----------------------------
-
- public void writeApplicationCertificate(ApplicationId applicationId, ApplicationCertificate applicationCertificate) {
- curator.set(applicationCertificatePath(applicationId), applicationCertificate.secretsKeyNamePrefix().getBytes());
- }
-
- public Optional<ApplicationCertificate> readApplicationCertificate(ApplicationId applicationId) {
- return curator.getData(applicationCertificatePath(applicationId)).map(String::new).map(ApplicationCertificate::new);
- }
-
- // -------------- Paths ---------------------------------------------------
-
- private Path lockPath(TenantName tenant) {
- return lockRoot
- .append(tenant.value());
- }
-
- private Path lockPath(TenantAndApplicationId application) {
- return lockPath(application.tenant())
- .append(application.application().value());
- }
-
- private Path lockPath(ApplicationId instance) {
- return lockPath(TenantAndApplicationId.from(instance))
- .append(instance.instance().value());
- }
-
- private Path lockPath(ApplicationId application, ZoneId zone) {
- return lockPath(application)
- .append(zone.environment().value())
- .append(zone.region().value());
- }
-
- private Path lockPath(ApplicationId application, JobType type) {
- return lockPath(application)
- .append(type.jobName());
- }
-
- private Path lockPath(ApplicationId application, JobType type, Step step) {
- return lockPath(application, type)
- .append(step.name());
- }
-
- private Path lockPath(String provisionId) {
- return lockRoot
- .append(provisionStatePath())
- .append(provisionId);
- }
-
- private static Path inactiveJobsPath() {
- return root.append("inactiveJobs");
- }
-
- private static Path upgradesPerMinutePath() {
- return root.append("upgrader").append("upgradesPerMinute");
- }
-
- private static Path targetMajorVersionPath() {
- return root.append("upgrader").append("targetMajorVersion");
- }
-
- private static Path confidenceOverridesPath() {
- return root.append("upgrader").append("confidenceOverrides");
- }
-
- private static Path osTargetVersionPath() {
- return root.append("osUpgrader").append("targetVersion");
- }
-
- private static Path osVersionStatusPath() {
- return root.append("osVersionStatus");
- }
-
- private static Path versionStatusPath() {
- return root.append("versionStatus");
- }
-
- private static Path routingPolicyPath(ApplicationId application) {
- return routingPoliciesRoot.append(application.serializedForm());
- }
-
- private static Path nameServiceQueuePath() {
- return root.append("nameServiceQueue");
- }
-
- private static Path auditLogPath() {
- return root.append("auditLog");
- }
-
- private static Path provisionStatePath() {
- return root.append("provisioning").append("states");
- }
-
- private static Path provisionStatePath(String provisionId) {
- return provisionStatePath().append(provisionId);
- }
-
- private static Path tenantPath(TenantName name) {
- return tenantRoot.append(name.value());
- }
-
- private static Path applicationPath(TenantAndApplicationId id) {
- return applicationRoot.append(id.serialized());
- }
-
- private static Path oldApplicationPath(ApplicationId application) {
- return applicationRoot.append(application.serializedForm());
- }
-
- private static Path instancePath(ApplicationId id) {
- return instanceRoot.append(id.serializedForm());
- }
-
- private static Path runsPath(ApplicationId id, JobType type) {
- return jobRoot.append(id.serializedForm()).append(type.jobName());
- }
-
- private static Path lastRunPath(ApplicationId id, JobType type) {
- return runsPath(id, type).append("last");
- }
-
- private static Path logPath(ApplicationId id, JobType type, long first) {
- return runsPath(id, type).append("logs").append(Long.toString(first));
- }
-
- private static Path lastLogPath(ApplicationId id, JobType type) {
- return runsPath(id, type).append("logs");
- }
-
- private static Path controllerPath(String hostname) {
- return controllerRoot.append(hostname);
- }
-
- private static Path applicationCertificatePath(ApplicationId id) {
- return applicationCertificateRoot.append(id.serializedForm());
- }
-
-}
-
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldMockCuratorDb.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldMockCuratorDb.java
deleted file mode 100644
index 341fe05f67d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OldMockCuratorDb.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.inject.Inject;
-import com.yahoo.vespa.curator.Curator;
-import com.yahoo.vespa.curator.mock.MockCurator;
-
-import java.time.Duration;
-
-/**
- * A curator db backed by a mock curator.
- *
- * @author bratseth
- */
-@SuppressWarnings("unused") // injected
-public class OldMockCuratorDb extends OldCuratorDb {
-
- public OldMockCuratorDb(MockCurator curator) {
- super(curator, Duration.ofMillis(100));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
index b262cd5cee7..51df0e4b08b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
@@ -1,14 +1,15 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import org.junit.Test;
@@ -75,17 +76,14 @@ public class TenantSerializerTest {
@Test
public void cloud_tenant() {
- CloudTenant tenant = CloudTenant.create(TenantName.from("elderly-lady"),
- new BillingInfo("old cat lady", "vespa"));
+ CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
+ new BillingInfo("old cat lady", "vespa"),
+ ImmutableBiMap.of("-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n\n-----END PUBLIC KEY-----", new SimplePrincipal("joe"),
+ "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----", new SimplePrincipal("jane")));
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(tenant.name(), serialized.name());
assertEquals(tenant.billingInfo(), serialized.billingInfo());
- }
-
- @Test
- public void legacy_deserialization() {
- UserTenant legayUserTenant = (UserTenant) serializer.tenantFrom(SlimeUtils.jsonToSlime("{\"name\":\"by-someone\"}"));
- assertTrue(legayUserTenant.is("someone"));
+ assertEquals(tenant.pemDeveloperKeys(), serialized.pemDeveloperKeys());
}
private Contact contact() {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
index ec976c1b922..8ab277a3795 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
@@ -19,6 +19,12 @@
"pemDeployKeys": [
"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"
],
+ "pemDeveloperKeys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
+ "user": "joe@dev"
+ }
+ ],
"instances": [
{
"instanceName": "default",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json
deleted file mode 100644
index 28f505e88ec..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-instance.json
+++ /dev/null
@@ -1,534 +0,0 @@
-{
- "id": "tenant1:app1:default",
- "deploymentSpecField": "<deployment version='1.0'>\n <test />\n <!--<staging />-->\n <prod global-service-id=\"foo\">\n <region active=\"true\">us-east-3</region>\n <region active=\"true\">us-west-1</region>\n </prod>\n</deployment>\n",
- "validationOverrides": "<validation-overrides>\n <allow until=\"2016-04-28\" comment=\"Renaming content cluster\">content-cluster-removal</allow>\n <allow until=\"2016-08-22\" comment=\"Migrating us-east-3 to C-2E\">cluster-size-reduction</allow>\n <allow until=\"2017-06-30\" comment=\"Test Vespa upgrade tests\">force-automatic-tenant-upgrade-test</allow>\n</validation-overrides>\n",
- "deployments": [
- {
- "zone": {
- "environment": "prod",
- "region": "us-west-1"
- },
- "version": "6.173.62",
- "deployTime": 1510837817704,
- "applicationPackageRevision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "clusterInfo": {
- "cluster1": {
- "flavor": "d-3-16-100",
- "cost": 9,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "container",
- "hostnames": [
- "node1",
- "node2"
- ]
- },
- "cluster2": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node3",
- "node4",
- "node5"
- ]
- },
- "cluster3": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node6",
- "node7",
- "node8",
- "node9"
- ]
- }
- },
- "clusterUtils": {
- "cluster1": {
- "cpu": 0.1720353499228221,
- "mem": 0.4986146831512451,
- "disk": 0.0617671330041831,
- "diskbusy": 0
- },
- "cluster2": {
- "cpu": 0.07505730001866318,
- "mem": 0.7936344432830811,
- "disk": 0.2260549694485994,
- "diskbusy": 0
- },
- "cluster3": {
- "cpu": 0.01712671480989384,
- "mem": 0.0225852754983035,
- "disk": 0.006084436856721915,
- "diskbusy": 0
- }
- },
- "metrics": {
- "queriesPerSecond": 1.25,
- "writesPerSecond": 43.83199977874756,
- "documentCount": 525880277.9999999,
- "queryLatencyMillis": 5.607503938674927,
- "writeLatencyMillis": 20.57866265104621
- }
- },
- {
- "zone": {
- "environment": "test",
- "region": "us-east-1"
- },
- "version": "6.173.62",
- "deployTime": 1511256872316,
- "applicationPackageRevision": {
- "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a"
- }
- },
- "clusterInfo": {},
- "clusterUtils": {},
- "metrics": {
- "queriesPerSecond": 0,
- "writesPerSecond": 0,
- "documentCount": 0,
- "queryLatencyMillis": 0,
- "writeLatencyMillis": 0
- }
- },
- {
- "zone": {
- "environment": "dev",
- "region": "us-east-1"
- },
- "version": "6.173.62",
- "deployTime": 1510597489464,
- "applicationPackageRevision": {
- "applicationPackageHash": "59b883f263c2a3c23dfab249730097d7e0e1ed32"
- },
- "clusterInfo": {
- "cluster1": {
- "flavor": "d-2-8-50",
- "cost": 5,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "container",
- "hostnames": [
- "node1"
- ]
- },
- "cluster2": {
- "flavor": "d-2-8-50",
- "cost": 5,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node2"
- ]
- },
- "cluster3": {
- "flavor": "d-2-8-50",
- "cost": 5,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node3"
- ]
- }
- },
- "clusterUtils": {
- "cluster1": {
- "cpu": 0.191833330678661,
- "mem": 0.4625738318415235,
- "disk": 0.05582004563850269,
- "diskbusy": 0
- },
- "cluster2": {
- "cpu": 0.2227037978608054,
- "mem": 0.2051752598416401,
- "disk": 0.05471533698695047,
- "diskbusy": 0
- },
- "cluster3": {
- "cpu": 0.1869410834020498,
- "mem": 0.1691722576000564,
- "disk": 0.04977374774258153,
- "diskbusy": 0
- }
- },
- "metrics": {
- "queriesPerSecond": 0,
- "writesPerSecond": 0,
- "documentCount": 30916,
- "queryLatencyMillis": 0,
- "writeLatencyMillis": 0
- }
- },
- {
- "zone": {
- "environment": "prod",
- "region": "us-east-3"
- },
- "version": "6.173.62",
- "deployTime": 1510817190016,
- "applicationPackageRevision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "clusterInfo": {
- "cluster1": {
- "flavor": "d-3-16-100",
- "cost": 9,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "container",
- "hostnames": [
- "node1",
- "node2"
- ]
- },
- "cluster2": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node1",
- "node2",
- "node3"
- ]
- },
- "cluster3": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node1",
- "node2",
- "node3",
- "node4"
- ]
- }
- },
- "clusterUtils": {
- "cluster1": {
- "cpu": 0.2295038983007097,
- "mem": 0.4627357390237263,
- "disk": 0.05559941525894966,
- "diskbusy": 0
- },
- "cluster2": {
- "cpu": 0.05340429087579549,
- "mem": 0.8107630891552372,
- "disk": 0.226444914138854,
- "diskbusy": 0
- },
- "cluster3": {
- "cpu": 0.02148227413975218,
- "mem": 0.02162174219104161,
- "disk": 0.006057760545243265,
- "diskbusy": 0
- }
- },
- "metrics": {
- "queriesPerSecond": 1.734000012278557,
- "writesPerSecond": 44.59999895095825,
- "documentCount": 525868193.9999999,
- "queryLatencyMillis": 5.65284947195106,
- "writeLatencyMillis": 17.34593812832452
- }
- }
- ],
- "deploymentJobs": {
- "projectId": 102889,
- "jobStatus": [
- {
- "jobType": "staging-test",
- "lastTriggered": {
- "id": -1,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "system-test completed",
- "at": 1510830134259
- },
- "lastCompleted": {
- "id": 1184,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "system-test completed",
- "at": 1510830684960
- },
- "lastSuccess": {
- "id": 1184,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "system-test completed",
- "at": 1510830684960
- }
- },
- {
- "jobType": "component",
- "lastCompleted": {
- "id": 849,
- "version": "6.174.156",
- "upgrade": false,
- "reason": "Application commit",
- "at": 1511217733555
- },
- "lastSuccess": {
- "id": 849,
- "version": "6.174.156",
- "upgrade": false,
- "reason": "Application commit",
- "at": 1511217733555
- }
- },
- {
- "jobType": "production-us-east-3",
- "lastTriggered": {
- "id": -1,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "staging-test completed",
- "at": 1510830685127
- },
- "lastCompleted": {
- "id": 923,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "staging-test completed",
- "at": 1510837650046
- },
- "lastSuccess": {
- "id": 923,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "staging-test completed",
- "at": 1510837650046
- }
- },
- {
- "jobType": "production-us-west-1",
- "lastTriggered": {
- "id": -1,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "production-us-east-3 completed",
- "at": 1510837650139
- },
- "lastCompleted": {
- "id": 646,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "production-us-east-3 completed",
- "at": 1510843559162
- },
- "lastSuccess": {
- "id": 646,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "production-us-east-3 completed",
- "at": 1510843559162
- }
- },
- {
- "jobType": "system-test",
- "jobError": "unknown",
- "lastTriggered": {
- "id": -1,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a"
- }
- },
- "upgrade": false,
- "reason": "Available change in component",
- "at": 1511256608649
- },
- "lastCompleted": {
- "id": 1686,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a"
- }
- },
- "upgrade": false,
- "reason": "Available change in component",
- "at": 1511256603353
- },
- "firstFailing": {
- "id": 1659,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a"
- }
- },
- "upgrade": false,
- "reason": "component completed",
- "at": 1511219070725
- },
- "lastSuccess": {
- "id": 1658,
- "version": "6.173.62",
- "revision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "upgrade": true,
- "reason": "Upgrading to 6.173.62",
- "at": 1511175754163
- }
- }
- ]
- },
- "deployingField": {
- "buildNumber": 42,
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a"
- }
- },
- "outstandingChangeField": false,
- "queryQuality": 100,
- "writeQuality": 99.99894341115082,
- "pemDeployKey": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
- "assignedRotations": [
- {
- "rotationId": "rotation-foo",
- "clusterId": "qrs",
- "endpointId": "default"
- }
- ],
- "rotationStatus2": [
- {
- "rotationId": "rotation-foo",
- "status": [
- {
- "environment": "prod",
- "region": "us-east-3",
- "state": "in"
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
index b6154876dba..0c0d1f433cd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
@@ -4,6 +4,7 @@ import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.SystemName;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
@@ -78,7 +79,7 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
public RequestBuilder data(byte[] data) { this.data = data; return this; }
public RequestBuilder data(String data) { this.data = data.getBytes(StandardCharsets.UTF_8); return this; }
- public RequestBuilder user(String user) { this.user = () -> user; return this; }
+ public RequestBuilder user(String user) { this.user = new SimplePrincipal(user); return this; }
public RequestBuilder roles(Set<Role> roles) { this.roles = roles; return this; }
@Override
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 836e3c07763..7e00a2f9f64 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
@@ -345,11 +345,17 @@ public class ApplicationApiTest extends ControllerContainerTest {
.data("{\"majorVersion\":7}"),
"{\"message\":\"Set major version to 7\"}");
- // PATCH in a pem deploy key
+ // POST a pem deploy key
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", POST)
+ .userIdentity(USER_ID)
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
+ "{\"message\":\"Added deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+
+ // PATCH in a pem deploy key at deprecated path
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", PATCH)
.userIdentity(USER_ID)
.data("{\"pemDeployKey\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Set pem deploy key to -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+ "{\"message\":\"Added deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
// GET an application with a major version override
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
@@ -362,17 +368,11 @@ public class ApplicationApiTest extends ControllerContainerTest {
.data("{\"majorVersion\":null}"),
"{\"message\":\"Set major version to empty\"}");
- // PATCH in removal of the pem deploy key
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", PATCH)
- .userIdentity(USER_ID)
- .data("{\"pemDeployKey\":null}"),
- "{\"message\":\"Set pem deploy key to empty\"}");
-
- // PATCH in removal of the pem deploy key on deprecated path
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", PATCH)
+ // DELETE the pem deploy key
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", DELETE)
.userIdentity(USER_ID)
- .data("{\"pemDeployKey\":null}"),
- "{\"message\":\"Set pem deploy key to empty\"}");
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}"),
+ "{\"message\":\"Removed deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
.userIdentity(USER_ID),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-with-routing-policy.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-with-routing-policy.json
index 627afbf2674..776bfbf3880 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-with-routing-policy.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-with-routing-policy.json
@@ -195,6 +195,7 @@
"url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default"
}
],
+ "pemDeployKeys": [],
"metrics": {
"queryServiceQuality": 0.0,
"writeServiceQuality": 0.0
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 1f79e960782..3579f64f6c9 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
@@ -266,6 +266,7 @@
"url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-east-3"
}
],
+ "pemDeployKeys": [],
"metrics": {
"queryServiceQuality": 0.5,
"writeServiceQuality": 0.7
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 65ea213ebbc..4469b7cb321 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
@@ -245,6 +245,7 @@
"url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1"
}
],
+ "pemDeployKeys": [],
"metrics": {
"queryServiceQuality": 0.5,
"writeServiceQuality": 0.7
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 c0e9d10a40c..603404bffae 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
@@ -226,6 +226,7 @@
@include(dev-us-east-1.json),
@include(prod-us-central-1.json)
],
+ "pemDeployKeys": [],
"metrics": {
"queryServiceQuality": 0.5,
"writeServiceQuality": 0.7
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
index 46f18f9d813..331aabd32d0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
@@ -81,6 +81,7 @@
"globalRotations": [],
"instances": [],
"pemDeployKey": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
+ "pemDeployKeys": ["-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"],
"metrics": {
"queryServiceQuality": 0.0,
"writeServiceQuality": 0.0
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json
index 3063bb62b7e..e53684d501a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json
@@ -79,6 +79,7 @@
"compileVersion": "6.1.0",
"globalRotations": [],
"instances": [],
+ "pemDeployKeys": [],
"metrics": {
"queryServiceQuality": 0.0,
"writeServiceQuality": 0.0
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index d0e9ae77965..b17dda7f810 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -12,6 +12,7 @@ import java.io.File;
import java.util.Set;
import static com.yahoo.application.container.handler.Request.Method.DELETE;
+import static com.yahoo.application.container.handler.Request.Method.PATCH;
import static com.yahoo.application.container.handler.Request.Method.POST;
import static com.yahoo.application.container.handler.Request.Method.PUT;
import static org.junit.Assert.assertEquals;
@@ -128,6 +129,45 @@ public class UserApiTest extends ControllerContainerCloudTest {
.roles(Set.of(Role.tenantOperator(id.tenant()))),
new File("application-roles.json"));
+ // POST a pem deploy key
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST)
+ .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
+ "{\"message\":\"Added deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+
+ // POST a pem developer key
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
+ .user("joe@dev")
+ .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
+ "{\"message\":\"Set developer key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY----- for joe@dev\"}");
+
+ // POST the same pem developer key for a different user is forbidden
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
+ .user("operator@tenant")
+ .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Multiple entries with same key: -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----=operator@tenant and -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----=joe@dev\"}",
+ 400);
+
+ // PATCH in a different pem developer key
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
+ .user("operator@tenant")
+ .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n-----END PUBLIC KEY-----\"}"),
+ "{\"message\":\"Set developer key -----BEGIN PUBLIC KEY-----\\nƪ(`▿▿▿▿´ƪ)\\n-----END PUBLIC KEY----- for operator@tenant\"}");
+
+ // GET tenant information with keys
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/")
+ .roles(Set.of(Role.applicationReader(id.tenant(), id.application()))),
+ new File("tenant-with-keys.json"));
+
+ // DELETE a pem developer key
+ tester.assertResponse(request("/application/v4/tenant/my-tenant/key", DELETE)
+ .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}"),
+ "{\"message\":\"Removed developer key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY----- for joe@dev\"}");
+
// DELETE an application role is allowed for an application admin.
tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", DELETE)
.roles(Set.of(Role.applicationAdmin(id.tenant(), id.application())))
@@ -140,6 +180,8 @@ public class UserApiTest extends ControllerContainerCloudTest {
"{\"message\":\"Deleted application my-tenant.my-app\"}");
// DELETE a tenant role is available to tenant admins.
+ // DELETE the tenantOperator role clears any developer key.
+ // TODO jonmv: Change to developer, when this role exists.
tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
.roles(Set.of(Role.tenantAdmin(id.tenant())))
.data("{\"user\":\"operator@tenant\",\"roleName\":\"tenantOperator\"}"),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
new file mode 100644
index 00000000000..5aaa900c3f0
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
@@ -0,0 +1,27 @@
+{
+ "tenant": "my-tenant",
+ "type": "CLOUD",
+ "pemDeployKeys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
+ "application": "my-app"
+ }
+ ],
+ "pemDeveloperKeys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
+ "user": "joe@dev"
+ },
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n-----END PUBLIC KEY-----",
+ "user": "operator@tenant"
+ }],
+ "applications": [
+ {
+ "tenant":"my-tenant",
+ "application":"my-app",
+ "instance":"default",
+ "url":"http://localhost:8080/application/v4/tenant/my-tenant/application/my-app/instance/default"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
index e4ca5a02446..a89a0f5360c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
@@ -1,5 +1,7 @@
{
"tenant": "my-tenant",
"type": "CLOUD",
+ "pemDeployKeys": [],
+ "pemDeveloperKeys": [],
"applications": []
}