diff options
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": [] } |