diff options
20 files changed, 423 insertions, 177 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index f51bccff5d1..d53d3137991 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 @@ -227,7 +227,7 @@ public class ApplicationController { if (asList(id.tenant()).stream().noneMatch(application -> application.id().application().equals(id.application()))) com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value()); - Optional<Tenant> tenant = controller.tenants().tenant(id.tenant()); + Optional<Tenant> tenant = controller.tenants().get(id.tenant()); if ( ! tenant.isPresent()) throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist"); if (get(id).isPresent()) @@ -555,7 +555,7 @@ public class ApplicationController { if ( ! application.get().deployments().isEmpty()) throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments"); - Tenant tenant = controller.tenants().tenant(id.tenant()).get(); + Tenant tenant = controller.tenants().get(id.tenant()).get(); if (tenant instanceof AthenzTenant && ! token.isPresent()) throw new IllegalArgumentException("Could not delete '" + application + "': No Okta Access Token provided"); @@ -728,7 +728,7 @@ public class ApplicationController { public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage, Optional<AthenzIdentity> deployingIdentity) { applicationPackage.deploymentSpec().athenzDomain() .ifPresent(identityDomain -> { - Optional<Tenant> tenant = controller.tenants().tenant(tenantName); + Optional<Tenant> tenant = controller.tenants().get(tenantName); if(!tenant.isPresent()) { throw new IllegalArgumentException("Tenant does not exist"); } else { 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 590b18f929d..e69e28b6432 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 @@ -6,88 +6,142 @@ import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; -import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; +import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.BillingInfo; +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.util.Objects; import java.util.Optional; -import java.util.function.Consumer; + +import static java.util.Objects.requireNonNull; /** * A tenant that has been locked for modification. Provides methods for modifying a tenant's fields. * * @author mpolden + * @author jonmv */ -public class LockedTenant { - - private final Lock lock; - private final TenantName name; - private AthenzDomain domain; - private Property property; - private Optional<PropertyId> propertyId; - private final Optional<Contact> contact; - private final boolean isAthenzTenant; - - /** - * Should never be constructed directly. - * - * Use {@link TenantController#lockIfPresent(TenantName, Consumer)} or - * {@link TenantController#lockOrThrow(TenantName, Consumer)} - */ - LockedTenant(AthenzTenant tenant, Lock lock) { - this(lock, tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId(), tenant.contact()); - } +public abstract class LockedTenant { - LockedTenant(UserTenant tenant, Lock lock) { - this(lock, tenant.name(), tenant.contact()); - } + final TenantName name; - private LockedTenant(Lock lock, TenantName name, AthenzDomain domain, Property property, - Optional<PropertyId> propertyId, Optional<Contact> contact) { - this.lock = Objects.requireNonNull(lock, "lock must be non-null"); - this.name = Objects.requireNonNull(name, "name must be non-null"); - this.domain = Objects.requireNonNull(domain, "domain must be non-null"); - this.property = Objects.requireNonNull(property, "property must be non-null"); - this.propertyId = Objects.requireNonNull(propertyId, "propertyId must be non-null"); - this.contact = Objects.requireNonNull(contact, "contact must be non-null"); - this.isAthenzTenant = true; + private LockedTenant(TenantName name) { + this.name = requireNonNull(name); } - private LockedTenant(Lock lock, TenantName name, Optional<Contact> contact) { - this.lock = Objects.requireNonNull(lock, "lock must be non-null"); - this.name = Objects.requireNonNull(name, "name must be non-null"); - this.contact = Objects.requireNonNull(contact, "contact must be non-null"); - this.isAthenzTenant = false; + static LockedTenant of(Tenant tenant, Lock lock) { + switch (tenant.type()) { + case athenz: return new Athenz((AthenzTenant) tenant); + case user: return new User((UserTenant) tenant); + case cloud: return new Cloud((CloudTenant) tenant); + default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.getClass().getName() + "'."); + } } /** Returns a read-only copy of this */ - public Tenant get() { - if (isAthenzTenant) return new AthenzTenant(name, domain, property, propertyId, contact); - else return new UserTenant(name, contact); - } + public abstract Tenant get(); - public LockedTenant with(AthenzDomain domain) { - return new LockedTenant(lock, name, domain, property, propertyId, contact); + @Override + public String toString() { + return "tenant '" + name + "'"; } - public LockedTenant with(Property property) { - return new LockedTenant(lock, name, domain, property, propertyId, contact); - } - public LockedTenant with(PropertyId propertyId) { - return new LockedTenant(lock, name, domain, property, Optional.of(propertyId), contact); + /** A locked AthenzTenant. */ + public static class Athenz extends LockedTenant { + + private final AthenzDomain domain; + private final Property property; + private final Optional<PropertyId> propertyId; + private final Optional<Contact> contact; + + private Athenz(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId, Optional<Contact> contact) { + super(name); + this.domain = domain; + this.property = property; + this.propertyId = propertyId; + this.contact = contact; + } + + private Athenz(AthenzTenant tenant) { + this(tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId(), tenant.contact()); + } + + @Override + public AthenzTenant get() { + return new AthenzTenant(name, domain, property, propertyId, contact); + } + + public Athenz with(AthenzDomain domain) { + return new Athenz(name, domain, property, propertyId, contact); + } + + public Athenz with(Property property) { + return new Athenz(name, domain, property, propertyId, contact); + } + + public Athenz with(PropertyId propertyId) { + return new Athenz(name, domain, property, Optional.of(propertyId), contact); + } + + public Athenz with(Contact contact) { + return new Athenz(name, domain, property, propertyId, Optional.of(contact)); + } + } - public LockedTenant with(Contact contact) { - if (isAthenzTenant) return new LockedTenant(lock, name, domain, property, propertyId, Optional.of(contact)); - return new LockedTenant(lock, name, Optional.of(contact)); + + /** A locked UserTenant. */ + public static class User extends LockedTenant { + + private final Optional<Contact> contact; + + private User(TenantName name, Optional<Contact> contact) { + super(name); + this.contact = contact; + } + + private User(UserTenant tenant) { + this(tenant.name(), tenant.contact()); + } + + @Override + public UserTenant get() { + return new UserTenant(name, contact); + } + + public User with(Contact contact) { + return new User(name, Optional.of(contact)); + } + } - @Override - public String toString() { - return "tenant '" + name + "'"; + + /** A locked CloudTenant. */ + public static class Cloud extends LockedTenant { + + private final BillingInfo billingInfo; + + private Cloud(TenantName name, BillingInfo billingInfo) { + super(name); + this.billingInfo = billingInfo; + } + + private Cloud(CloudTenant tenant) { + this(tenant.name(), tenant.billingInfo()); + } + + @Override + public CloudTenant get() { + return new CloudTenant(name, billingInfo); + } + + public Cloud with(BillingInfo billingInfo) { + return new Cloud(name, billingInfo); + } + } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java index 78099fac34e..e274fc2fe87 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java @@ -60,7 +60,7 @@ public class TenantController { for (TenantName name : curator.readTenantNames()) { try (Lock lock = lock(name)) { // Get while holding lock so that we know we're operating on a current version - Optional<Tenant> optionalTenant = tenant(name); + Optional<Tenant> optionalTenant = get(name); if (!optionalTenant.isPresent()) continue; // Deleted while updating, skip Tenant tenant = optionalTenant.get(); @@ -96,27 +96,27 @@ public class TenantController { .collect(Collectors.toList()); } - /** - * Lock a tenant for modification and apply action. Only valid for Athenz tenants as it's the only type that - * accepts modification. - */ - public void lockIfPresent(TenantName name, Consumer<LockedTenant> action) { + /** Locks a tenant for modification and applies the given action. */ + public <T extends LockedTenant> void lockIfPresent(TenantName name, Class<T> token, Consumer<T> action) { try (Lock lock = lock(name)) { - tenant(name).map(tenant -> { - tenant = tenant instanceof AthenzTenant ? (AthenzTenant) tenant : (UserTenant) tenant; - if (tenant instanceof AthenzTenant) return new LockedTenant((AthenzTenant) tenant, lock); - else return new LockedTenant((UserTenant) tenant, lock); - }).ifPresent(action); + get(name).map(tenant -> LockedTenant.of(tenant, lock)) + .map(token::cast) + .ifPresent(action); } } /** Lock a tenant for modification and apply action. Throws if the tenant does not exist */ - public void lockOrThrow(TenantName name, Consumer<LockedTenant> action) { + public <T extends LockedTenant> void lockOrThrow(TenantName name, Class<T> token, Consumer<T> action) { try (Lock lock = lock(name)) { - action.accept(new LockedTenant(requireAthenzTenant(name), lock)); + action.accept(token.cast(LockedTenant.of(require(name), lock))); } } + /** Returns the tenant with the given name, or throws. */ + public Tenant require(TenantName name) { + return get(name).orElseThrow(() -> new IllegalArgumentException("No such tenant '" + name + "'.")); + } + /** Replace and store any previous version of given tenant */ public void store(LockedTenant tenant) { curator.writeTenant(tenant.get()); @@ -156,18 +156,20 @@ public class TenantController { } /** Find tenant by name */ - public Optional<Tenant> tenant(TenantName name) { + public Optional<Tenant> get(TenantName name) { return curator.readTenant(name); } /** Find tenant by name */ - public Optional<Tenant> tenant(String name) { - return tenant(TenantName.from(name)); + public Optional<Tenant> get(String name) { + return get(TenantName.from(name)); } /** Find Athenz tenant by name */ public Optional<AthenzTenant> athenzTenant(TenantName name) { - return curator.readAthenzTenant(name); + return curator.readTenant(name) + .filter(AthenzTenant.class::isInstance) + .map(AthenzTenant.class::cast); } /** Returns Athenz tenant with name or throws if no such tenant exists */ @@ -176,8 +178,8 @@ public class TenantController { } /** Update Athenz domain for tenant. Returns the updated tenant which must be explicitly stored */ - public LockedTenant withDomain(LockedTenant tenant, AthenzDomain newDomain, OktaAccessToken token) { - AthenzTenant athenzTenant = (AthenzTenant) tenant.get(); + public LockedTenant.Athenz withDomain(LockedTenant.Athenz tenant, AthenzDomain newDomain, OktaAccessToken token) { + AthenzTenant athenzTenant = tenant.get(); AthenzDomain existingDomain = athenzTenant.domain(); if (existingDomain.equals(newDomain)) return tenant; Optional<Tenant> existingTenantWithNewDomain = tenantIn(newDomain); @@ -219,10 +221,10 @@ public class TenantController { } private void requireNonExistent(TenantName name) { - if (tenant(name).isPresent() || + if (get(name).isPresent() || // Underscores are allowed in existing Athenz tenant names, but tenants with - and _ cannot co-exist. E.g. // my-tenant cannot be created if my_tenant exists. - tenant(dashToUnderscore(name.value())).isPresent()) { + get(dashToUnderscore(name.value())).isPresent()) { throw new IllegalArgumentException("Tenant '" + name + "' already exists"); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java index d5c84c38e91..a3b1aab8f40 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java @@ -52,11 +52,13 @@ public class ApplicationOwnershipConfirmer extends Maintainer { .forEach(application -> { try { Tenant tenant = tenantOf(application.id()); - Optional<IssueId> ourIssueId = application.ownershipIssueId(); - Contact contact = tenant.contact().orElseThrow(RuntimeException::new); - User assignee = determineAssignee(tenant, application); - ourIssueId = ownershipIssues.confirmOwnership(ourIssueId, application.id(), assignee, contact); - ourIssueId.ifPresent(issueId -> store(issueId, application.id())); + tenant.contact().ifPresent(contact -> { // TODO jvenstad: Makes sense to require, and run this only in main? + ownershipIssues.confirmOwnership(application.ownershipIssueId(), + application.id(), + determineAssignee(tenant, application), + contact) + .ifPresent(newIssueId -> store(newIssueId, application.id())); + }); } catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout. log.log(Level.INFO, "Exception caught when attempting to file an issue for '" + application.id() + "': " + Exceptions.toMessageString(e)); @@ -70,11 +72,8 @@ public class ApplicationOwnershipConfirmer extends Maintainer { for (Application application : controller().applications().asList()) application.ownershipIssueId().ifPresent(issueId -> { try { - Optional<Contact> contact = Optional.of(application.id()) - .map(this::tenantOf) - .filter(t -> t instanceof AthenzTenant) - .flatMap(Tenant::contact); - ownershipIssues.ensureResponse(issueId, contact); + Tenant tenant = tenantOf(application.id()); + ownershipIssues.ensureResponse(issueId, tenant.type() == Tenant.Type.athenz ? tenant.contact() : Optional.empty()); } catch (RuntimeException e) { log.log(Level.INFO, "Exception caught when attempting to escalate issue with id '" + issueId + "': " + Exceptions.toMessageString(e)); @@ -105,7 +104,7 @@ public class ApplicationOwnershipConfirmer extends Maintainer { } private Tenant tenantOf(ApplicationId applicationId) { - return controller().tenants().tenant(applicationId.tenant()) + return controller().tenants().get(applicationId.tenant()) .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java index a02f28e371d..7dfbc135df9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java @@ -1,13 +1,12 @@ // 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.maintenance; -import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; -import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; -import com.yahoo.vespa.hosted.controller.api.integration.organization.ContactRetriever; import com.yahoo.config.provision.SystemName; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.LockedTenant; +import com.yahoo.vespa.hosted.controller.TenantController; +import com.yahoo.vespa.hosted.controller.api.integration.organization.ContactRetriever; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.yolean.Exceptions; @@ -35,14 +34,19 @@ public class ContactInformationMaintainer extends Maintainer { @Override protected void maintain() { - for (Tenant tenant : controller().tenants().asList()) { + TenantController tenants = controller().tenants(); + for (Tenant tenant : tenants.asList()) { try { - Optional<PropertyId> tenantPropertyId = Optional.empty(); - if (tenant instanceof AthenzTenant) { - tenantPropertyId = ((AthenzTenant) tenant).propertyId(); + switch (tenant.type()) { + case athenz: tenants.lockIfPresent(tenant.name(), LockedTenant.Athenz.class, lockedTenant -> + tenants.store(lockedTenant.with(contactRetriever.getContact(lockedTenant.get().propertyId())))); + return; + case user: tenants.lockIfPresent(tenant.name(), LockedTenant.User.class, lockedTenant -> + tenants.store(lockedTenant.with(contactRetriever.getContact(Optional.empty())))); + return; + case cloud: return; + default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'."); } - Contact contact = contactRetriever.getContact(tenantPropertyId); - controller().tenants().lockIfPresent(tenant.name(), lockedTenant -> controller().tenants().store(lockedTenant.with(contact))); } catch (Exception e) { log.log(LogLevel.WARNING, "Failed to update contact information for " + tenant + ": " + Exceptions.toMessageString(e) + ". Retrying in " + @@ -51,5 +55,4 @@ public class ContactInformationMaintainer extends Maintainer { } } - } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java index 3f5c2d1f317..48e2702b9e0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java @@ -6,8 +6,6 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; -import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; @@ -106,7 +104,7 @@ public class DeploymentIssueReporter extends Maintainer { } private Tenant ownerOf(ApplicationId applicationId) { - return controller().tenants().tenant(applicationId.tenant()) + return controller().tenants().get(applicationId.tenant()) .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId)); } @@ -118,10 +116,12 @@ public class DeploymentIssueReporter extends Maintainer { private void fileDeploymentIssueFor(ApplicationId applicationId) { try { Tenant tenant = ownerOf(applicationId); - User asignee = tenant instanceof UserTenant ? userFor(tenant) : null; - Optional<IssueId> ourIssueId = controller().applications().require(applicationId).deploymentJobs().issueId(); - IssueId issueId = deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, asignee, tenant.contact().get()); - store(applicationId, issueId); + tenant.contact().ifPresent(contact -> { + User assignee = tenant.type() == Tenant.Type.user ? userFor(tenant) : null; + Optional<IssueId> ourIssueId = controller().applications().require(applicationId).deploymentJobs().issueId(); + IssueId issueId = deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, assignee, contact); + store(applicationId, issueId); + }); } catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout. log.log(Level.INFO, "Exception caught when attempting to file an issue for '" + applicationId + "': " + Exceptions.toMessageString(e)); @@ -132,11 +132,10 @@ public class DeploymentIssueReporter extends Maintainer { private void escalateInactiveDeploymentIssues(Collection<Application> applications) { applications.forEach(application -> application.deploymentJobs().issueId().ifPresent(issueId -> { try { - AthenzTenant tenant = Optional.of(application.id()) - .map(this::ownerOf) - .filter(t -> t instanceof AthenzTenant) - .map(AthenzTenant.class::cast).orElseThrow(RuntimeException::new); - deploymentIssues.escalateIfInactive(issueId, maxInactivity, tenant.contact()); + Tenant tenant = ownerOf(application.id()); + deploymentIssues.escalateIfInactive(issueId, + maxInactivity, + tenant.type() == Tenant.Type.athenz ? tenant.contact() : Optional.empty()); } catch (RuntimeException e) { log.log(Level.INFO, "Exception caught when attempting to escalate issue with id '" + issueId + "': " + Exceptions.toMessageString(e)); @@ -148,4 +147,5 @@ public class DeploymentIssueReporter extends Maintainer { controller().applications().lockIfPresent(id, application -> controller().applications().store(application.withDeploymentIssueId(issueId))); } + } 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 1648040fc2b..ec34585a950 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 @@ -21,9 +21,7 @@ import com.yahoo.vespa.hosted.controller.application.RoutingPolicy; 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.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; -import com.yahoo.vespa.hosted.controller.tenant.UserTenant; import com.yahoo.vespa.hosted.controller.versions.OsVersion; import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus; import com.yahoo.vespa.hosted.controller.versions.VersionStatus; @@ -312,19 +310,8 @@ public class CuratorDb { curator.set(tenantPath(tenant.name()), asJson(tenantSerializer.toSlime(tenant))); } - public Optional<UserTenant> readUserTenant(TenantName name) { - return readSlime(tenantPath(name)).map(tenantSerializer::userTenantFrom); - } - - public Optional<AthenzTenant> readAthenzTenant(TenantName name) { - return readSlime(tenantPath(name)).map(tenantSerializer::athenzTenantFrom); - } - public Optional<Tenant> readTenant(TenantName name) { - if (name.value().startsWith(Tenant.userPrefix)) { - return readUserTenant(name).map(Tenant.class::cast); - } - return readAthenzTenant(name).map(Tenant.class::cast); + return readSlime(tenantPath(name)).map(tenantSerializer::tenantFrom); } public List<Tenant> readTenants() { 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 245cb0f4dae..91a01435b68 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,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.persistence; - import com.yahoo.config.provision.TenantName; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; @@ -13,6 +12,8 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; +import com.yahoo.vespa.hosted.controller.tenant.BillingInfo; +import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; @@ -29,6 +30,7 @@ import java.util.Optional; public class TenantSerializer { private static final String nameField = "name"; + private static final String typeField = "type"; private static final String athenzDomainField = "athenzDomain"; private static final String propertyField = "property"; private static final String propertyIdField = "propertyId"; @@ -40,58 +42,91 @@ public class TenantSerializer { private static final String personField = "person"; private static final String queueField = "queue"; private static final String componentField = "component"; + private static final String billingInfoField = "billingInfo"; + private static final String customerIdField = "customerId"; + private static final String productCodeField = "productCode"; public Slime toSlime(Tenant tenant) { - if (tenant instanceof AthenzTenant) return toSlime((AthenzTenant) tenant); - return toSlime((UserTenant) tenant); + Slime slime = new Slime(); + Cursor tenantObject = slime.setObject(); + tenantObject.setString(nameField, tenant.name().value()); + tenantObject.setString(typeField, valueOf(tenant.type())); + + switch (tenant.type()) { + case athenz: toSlime((AthenzTenant) tenant, tenantObject); break; + case user: toSlime((UserTenant) tenant, tenantObject); break; + case cloud: toSlime((CloudTenant) tenant, tenantObject); break; + default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'."); + } + return slime; } - private Slime toSlime(AthenzTenant tenant) { - Slime slime = new Slime(); - Cursor root = slime.setObject(); - root.setString(nameField, tenant.name().value()); - root.setString(athenzDomainField, tenant.domain().getName()); - root.setString(propertyField, tenant.property().id()); - tenant.propertyId().ifPresent(propertyId -> root.setString(propertyIdField, propertyId.id())); + private void toSlime(AthenzTenant tenant, Cursor tenantObject) { + tenantObject.setString(athenzDomainField, tenant.domain().getName()); + tenantObject.setString(propertyField, tenant.property().id()); + tenant.propertyId().ifPresent(propertyId -> tenantObject.setString(propertyIdField, propertyId.id())); tenant.contact().ifPresent(contact -> { - Cursor contactCursor = root.setObject(contactField); + Cursor contactCursor = tenantObject.setObject(contactField); writeContact(contact, contactCursor); }); - return slime; } - private Slime toSlime(UserTenant tenant) { - Slime slime = new Slime(); - Cursor root = slime.setObject(); - root.setString(nameField, tenant.name().value()); + private void toSlime(UserTenant tenant, Cursor tenantObject) { tenant.contact().ifPresent(contact -> { - Cursor contactCursor = root.setObject(contactField); + Cursor contactCursor = tenantObject.setObject(contactField); writeContact(contact, contactCursor); }); - return slime; } - public AthenzTenant athenzTenantFrom(Slime slime) { - Inspector root = slime.get(); - TenantName name = TenantName.from(root.field(nameField).asString()); - AthenzDomain domain = new AthenzDomain(root.field(athenzDomainField).asString()); - Property property = new Property(root.field(propertyField).asString()); - Optional<PropertyId> propertyId = SlimeUtils.optionalString(root.field(propertyIdField)).map(PropertyId::new); - Optional<Contact> contact = contactFrom(root.field(contactField)); + private void toSlime(CloudTenant tenant, Cursor root) { + toSlime(tenant.billingInfo(), root.setObject(billingInfoField)); + } + + private void toSlime(BillingInfo billingInfo, Cursor billingInfoObject) { + billingInfoObject.setString(customerIdField, billingInfo.customerId()); + billingInfoObject.setString(productCodeField, billingInfo.productCode()); + } + + 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; + + switch (type) { + case athenz: return athenzTenantFrom(tenantObject); + case user: return userTenantFrom(tenantObject); + case cloud: return cloudTenantFrom(tenantObject); + default: throw new IllegalArgumentException("Unexpected tenant type '" + type + "'."); + } + } + + private AthenzTenant athenzTenantFrom(Inspector tenantObject) { + TenantName name = TenantName.from(tenantObject.field(nameField).asString()); + AthenzDomain domain = new AthenzDomain(tenantObject.field(athenzDomainField).asString()); + Property property = new Property(tenantObject.field(propertyField).asString()); + Optional<PropertyId> propertyId = SlimeUtils.optionalString(tenantObject.field(propertyIdField)).map(PropertyId::new); + Optional<Contact> contact = contactFrom(tenantObject.field(contactField)); return new AthenzTenant(name, domain, property, propertyId, contact); } - public UserTenant userTenantFrom(Slime slime) { - Inspector root = slime.get(); - TenantName name = TenantName.from(root.field(nameField).asString()); - Optional<Contact> contact = contactFrom(root.field(contactField)); + private UserTenant userTenantFrom(Inspector tenantObject) { + TenantName name = TenantName.from(tenantObject.field(nameField).asString()); + Optional<Contact> contact = contactFrom(tenantObject.field(contactField)); return new UserTenant(name, contact); } + private CloudTenant cloudTenantFrom(Inspector tenantObject) { + TenantName name = TenantName.from(tenantObject.field(nameField).asString()); + BillingInfo billingInfo = billingInfoFrom(tenantObject.field(billingInfoField)); + return new CloudTenant(name, billingInfo); + } + private Optional<Contact> contactFrom(Inspector object) { - if (!object.valid()) { - return Optional.empty(); - } + if ( ! object.valid()) return Optional.empty(); + URI contactUrl = URI.create(object.field(contactUrlField).asString()); URI propertyUrl = URI.create(object.field(propertyUrlField).asString()); URI issueTrackerUrl = URI.create(object.field(issueTrackerUrlField).asString()); @@ -132,4 +167,27 @@ public class TenantSerializer { return personLists; } + private BillingInfo billingInfoFrom(Inspector billingInfoObject) { + return new BillingInfo(billingInfoObject.field(customerIdField).asString(), + billingInfoObject.field(productCodeField).asString()); + } + + private static Tenant.Type typeOf(String value) { + switch (value) { + case "athenz": return Tenant.Type.athenz; + case "user": return Tenant.Type.user; + case "cloud": return Tenant.Type.cloud; + default: throw new IllegalArgumentException("Unknown tenant type '" + value + "'."); + } + } + + private static String valueOf(Tenant.Type type) { + switch (type) { + case athenz: return "athenz"; + case user: return "user"; + case cloud: return "cloud"; + default: throw new IllegalArgumentException("Unexpected tenant type '" + type + "'."); + } + } + } 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 6fc5cc645e8..37c0847f167 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 @@ -30,6 +30,7 @@ import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.AlreadyExistsException; import com.yahoo.vespa.hosted.controller.Application; 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.ApplicationResource; @@ -333,7 +334,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse tenant(String tenantName, HttpRequest request) { - return controller.tenants().tenant(TenantName.from(tenantName)) + return controller.tenants().get(TenantName.from(tenantName)) .map(tenant -> tenant(tenant, request, true)) .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist")); } @@ -756,7 +757,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Inspector requestData = toSlime(request.getData()).get(); OktaAccessToken token = requireOktaAccessToken(request, "Could not update " + tenantName); - controller.tenants().lockOrThrow(tenant.get().name(), lockedTenant -> { + controller.tenants().lockOrThrow(tenant.get().name(), LockedTenant.Athenz.class, lockedTenant -> { lockedTenant = lockedTenant.with(new Property(mandatory("property", requestData).asString())); lockedTenant = controller.tenants().withDomain( lockedTenant, @@ -983,7 +984,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse deleteTenant(String tenantName, HttpRequest request) { - Optional<Tenant> tenant = controller.tenants().tenant(tenantName); + Optional<Tenant> tenant = controller.tenants().get(tenantName); if ( ! tenant.isPresent()) return ErrorResponse.notFoundError("Could not delete tenant '" + tenantName + "': Tenant not found"); // NOTE: The Jersey implementation would silently ignore this @@ -1099,7 +1100,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private Tenant getTenantOrThrow(String tenantName) { - return controller.tenants().tenant(tenantName) + return controller.tenants().get(tenantName) .orElseThrow(() -> new NotExistsException(new TenantId(tenantName))); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java index 77e626509b3..3d0e21617c5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java @@ -158,7 +158,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase { } private void verifyIsTenantAdmin(AthenzPrincipal principal, TenantName name) { - tenantController.tenant(name) + tenantController.get(name) .ifPresent(tenant -> { if (!isTenantAdmin(principal.getIdentity(), tenant)) { throw new ForbiddenException("Tenant admin or Vespa operator role required"); @@ -182,7 +182,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase { private void verifyIsTenantPipelineOperator(AthenzPrincipal principal, TenantName name, ApplicationName application) { - tenantController.tenant(name) + tenantController.get(name) .ifPresent(tenant -> verifyIsTenantPipelineOperator(principal.getIdentity(), tenant, application)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java index abe09090761..f8edeee5939 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java @@ -75,4 +75,10 @@ public class AthenzTenant extends Tenant { } return name; } + + @Override + public Type type() { + return Type.athenz; + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingInfo.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingInfo.java new file mode 100644 index 00000000000..0eeb331b59f --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingInfo.java @@ -0,0 +1,54 @@ +package com.yahoo.vespa.hosted.controller.tenant; + +import java.util.Objects; +import java.util.StringJoiner; + +import static java.util.Objects.requireNonNull; + +/** + * Information pertinent to billing a tenant for use of hosted Vespa services. + * + * @author jonmv + */ +public class BillingInfo { + + private final String customerId; + private final String productCode; + + /** Creates a new BillingInfo with the given data. Assumes data has already been validated. */ + public BillingInfo(String customerId, String productCode) { + this.customerId = requireNonNull(customerId); + this.productCode = requireNonNull(productCode); + } + + public String customerId() { + return customerId; + } + + public String productCode() { + return productCode; + } + + @Override + public String toString() { + return new StringJoiner(", ", BillingInfo.class.getSimpleName() + "[", "]") + .add("customerId='" + customerId + "'") + .add("productCode='" + productCode + "'") + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if ( ! (o instanceof BillingInfo)) return false; + BillingInfo that = (BillingInfo) o; + return Objects.equals(customerId, that.customerId) && + Objects.equals(productCode, that.productCode); + } + + @Override + public int hashCode() { + return Objects.hash(customerId, productCode); + } + +} 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 new file mode 100644 index 00000000000..cf68c8f3bf9 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java @@ -0,0 +1,37 @@ +package com.yahoo.vespa.hosted.controller.tenant; + +import com.yahoo.config.provision.TenantName; + +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. + * + * @author jonmv + */ +public class CloudTenant extends Tenant { + + private final BillingInfo billingInfo; + + /** Public for the serialization layer — do not use! */ + public CloudTenant(TenantName name, BillingInfo info) { + super(name, Optional.empty()); + billingInfo = info; + } + + /** 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); + } + + /** Returns the billing info for this tenant. */ + public BillingInfo billingInfo() { return billingInfo; } + + @Override + public Type type() { + return Type.cloud; + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java index c6ed9f7b559..19b7229515b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.tenant; - import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; @@ -35,6 +34,8 @@ public abstract class Tenant { return contact; } + public abstract Type type(); + @Override public boolean equals(Object o) { if (this == o) return true; @@ -49,11 +50,26 @@ public abstract class Tenant { } static TenantName requireName(TenantName name) { - if (!name.value().matches("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$")) { + if ( ! name.value().matches("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$")) { throw new IllegalArgumentException("New tenant or application names must start with a letter, may " + "contain no more than 20 characters, and may only contain lowercase " + "letters, digits or dashes, but no double-dashes."); } return name; } + + + public enum Type { + + /** Tenant authenticated through Athenz. */ + athenz, + + /** Tenant authenticated through Okta, as a user. */ + user, + + /** Tenant authenticated through some cloud identity provider. */ + cloud; + + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/UserTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/UserTenant.java index 47e5580fbe4..a46d847f6f3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/UserTenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/UserTenant.java @@ -21,6 +21,11 @@ public class UserTenant extends Tenant { super(name, contact); } + @Override + public Type type() { + return Type.user; + } + public UserTenant(TenantName name) { super(name, Optional.empty()); } @@ -64,4 +69,5 @@ public class UserTenant extends Tenant { } return name; } + } 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 6eb5061056f..cf5f8fac69d 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 @@ -114,7 +114,7 @@ public class ControllerTest { applications = tester.controller().applications(); - assertNotNull(tester.controller().tenants().tenant(TenantName.from("tenant1"))); + assertNotNull(tester.controller().tenants().get(TenantName.from("tenant1"))); assertNotNull(applications.get(ApplicationId.from(TenantName.from("tenant1"), ApplicationName.from("application1"), InstanceName.from("default")))); 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 615fb017363..e573c12af3b 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 @@ -249,14 +249,14 @@ public final class ControllerTester { public TenantName createTenant(String tenantName, String domainName, Long propertyId, Optional<Contact> contact) { TenantName name = TenantName.from(tenantName); - Optional<Tenant> existing = controller().tenants().tenant(name); + Optional<Tenant> existing = controller().tenants().get(name); if (existing.isPresent()) return name; AthenzTenant tenant = AthenzTenant.create(name, createDomain(domainName), new Property("Property"+propertyId), Optional.ofNullable(propertyId) .map(Object::toString) .map(PropertyId::new), contact); controller().tenants().create(tenant, new OktaAccessToken("okta-token")); - assertNotNull(controller().tenants().tenant(name)); + assertNotNull(controller().tenants().get(name)); return name; } 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 0b680149bff..dd4558dbe2c 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 @@ -112,7 +112,7 @@ public class ApplicationSerializerTest { Optional.of(User.from("by-username")), OptionalInt.of(7), new MetricsService.ApplicationMetrics(0.5, 0.9), - Optional.of("---begin---\nKEY\n---end---"), + Optional.of("-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"), Optional.of(new RotationId("my-rotation")), rotationStatus); 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 2e0d7715d7d..b78cff88ccf 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 @@ -3,10 +3,14 @@ package com.yahoo.vespa.hosted.controller.persistence;// Copyright 2018 Yahoo Ho 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.Contact; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.BillingInfo; +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 org.junit.Test; @@ -32,7 +36,7 @@ public class TenantSerializerTest { new AthenzDomain("domain1"), new Property("property1"), Optional.of(new PropertyId("1"))); - AthenzTenant serialized = serializer.athenzTenantFrom(serializer.toSlime(tenant)); + AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.name(), serialized.name()); assertEquals(tenant.domain(), serialized.domain()); assertEquals(tenant.property(), serialized.property()); @@ -46,7 +50,7 @@ public class TenantSerializerTest { new AthenzDomain("domain1"), new Property("property1"), Optional.empty()); - AthenzTenant serialized = serializer.athenzTenantFrom(serializer.toSlime(tenant)); + AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertFalse(serialized.propertyId().isPresent()); assertEquals(tenant.propertyId(), serialized.propertyId()); } @@ -58,18 +62,33 @@ public class TenantSerializerTest { new Property("property1"), Optional.of(new PropertyId("1")), Optional.of(contact())); - AthenzTenant serialized = serializer.athenzTenantFrom(serializer.toSlime(tenant)); + AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.contact(), serialized.contact()); } @Test public void user_tenant() { UserTenant tenant = UserTenant.create("by-foo", Optional.of(contact())); - UserTenant serialized = serializer.userTenantFrom(serializer.toSlime(tenant)); + UserTenant serialized = (UserTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.name(), serialized.name()); assertEquals(contact(), serialized.contact().get()); } + @Test + public void cloud_tenant() { + CloudTenant tenant = CloudTenant.create(TenantName.from("elderly-lady"), + new BillingInfo("old cat lady", "vespa")); + 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")); + } + private Contact contact() { return new Contact( URI.create("http://contact1.test"), @@ -83,4 +102,5 @@ public class TenantSerializerTest { Optional.empty() ); } + } 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 c0c2d4043d9..705fc8adbac 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 @@ -22,6 +22,7 @@ import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ApplicationController; +import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; @@ -1541,7 +1542,9 @@ public class ApplicationApiTest extends ControllerContainerTest { private void updateContactInformation() { Contact contact = new Contact(URI.create("www.contacts.tld/1234"), URI.create("www.properties.tld/1234"), URI.create("www.issues.tld/1234"), List.of(List.of("alice"), List.of("bob")), "queue", Optional.empty()); - tester.controller().tenants().lockIfPresent(TenantName.from("tenant2"), lockedTenant -> tester.controller().tenants().store(lockedTenant.with(contact))); + tester.controller().tenants().lockIfPresent(TenantName.from("tenant2"), + LockedTenant.Athenz.class, + lockedTenant -> tester.controller().tenants().store(lockedTenant.with(contact))); } private void registerContact(long propertyId) { |