aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2018-04-06 09:14:47 +0200
committerGitHub <noreply@github.com>2018-04-06 09:14:47 +0200
commitaf0f1a92e8f74a6de63dd9c95f23147e7a476df1 (patch)
tree863b890689d228282ee8cde7d96fc368a8c41611
parent19ed18bae9e056084cec4a91e887344b7baec218 (diff)
parentd6cabc38ba3255663184b15eacb5bf9903c42c98 (diff)
Merge pull request #5464 from vespa-engine/mpolden/refactor-tenant
Refactor tenant
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java4
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java196
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/Tenant.java119
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java31
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java65
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java146
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java48
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java82
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java50
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/UserTenant.java55
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java32
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java38
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-underscore.json9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java4
29 files changed, 617 insertions, 428 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
index 2fd75e25498..2067a88e5fb 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
@@ -99,9 +99,5 @@ public abstract class Identifier {
throw new IllegalArgumentException(String.format("Invalid id: %s. %s", id, explanation));
}
- public static void throwInvalidId(String id, String explanation, String idName) {
- throw new IllegalArgumentException(String.format("Invalid %s id: %s. %s", idName, id, explanation));
- }
-
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java
index 2c619fb58ef..4974192e213 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java
@@ -26,9 +26,4 @@ public class TenantId extends NonDefaultIdentifier {
}
}
- /** Return true if this is the user tenant of the given user */
- public boolean isTenantFor(UserId userId) {
- return id().equals("by-" + userId.id().replace('_', '-'));
- }
-
}
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 f63966703d9..83ae5f5332f 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
@@ -12,14 +12,12 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.athenz.api.NToken;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ConfigChangeActions;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.Hostname;
import com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerClient;
@@ -37,9 +35,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerato
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.maintenance.DeploymentExpirer;
import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
@@ -140,7 +140,7 @@ public class ApplicationController {
/** Returns all applications of a tenant */
public List<Application> asList(TenantName tenant) {
- return db.listApplications(new TenantId(tenant.value()));
+ return db.listApplications(tenant);
}
/**
@@ -239,19 +239,19 @@ 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(new TenantId(id.tenant().value()));
+ Optional<Tenant> tenant = controller.tenants().tenant(id.tenant());
if ( ! tenant.isPresent())
throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist");
if (get(id).isPresent())
throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
if (get(dashToUnderscore(id)).isPresent()) // VESPA-1945
throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists");
- if (id.instance().isDefault() && tenant.get().isAthensTenant()) { // Only create the athens application for "default" instances.
+ if (id.instance().isDefault() && tenant.get() instanceof AthenzTenant) { // Only create the athenz application for "default" instances.
if ( ! token.isPresent())
throw new IllegalArgumentException("Could not create '" + id + "': No NToken provided");
ZmsClient zmsClient = zmsClientFactory.createZmsClientWithAuthorizedServiceToken(token.get());
- zmsClient.addApplication(tenant.get().getAthensDomain().get(),
+ zmsClient.addApplication(((AthenzTenant) tenant.get()).domain(),
new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value()));
}
LockedApplication application = new LockedApplication(new Application(id), lock);
@@ -501,14 +501,14 @@ public class ApplicationController {
if ( ! application.deployments().isEmpty())
throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments");
- Tenant tenant = controller.tenants().tenant(new TenantId(id.tenant().value())).get();
- if (tenant.isAthensTenant() && ! token.isPresent())
+ Tenant tenant = controller.tenants().tenant(id.tenant()).get();
+ if (tenant instanceof AthenzTenant && ! token.isPresent())
throw new IllegalArgumentException("Could not delete '" + application + "': No NToken provided");
// Only delete in Athenz once
- if (id.instance().isDefault() && tenant.isAthensTenant()) {
+ if (id.instance().isDefault() && tenant instanceof AthenzTenant) {
zmsClientFactory.createZmsClientWithAuthorizedServiceToken(token.get())
- .deleteApplication(tenant.getAthensDomain().get(),
+ .deleteApplication(((AthenzTenant) tenant).domain(),
new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value()));
}
db.deleteApplication(id);
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 482b6a6f3a9..ac48d9617fa 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
@@ -6,11 +6,12 @@ import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.athenz.api.NToken;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClient;
+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.persistence.ControllerDb;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.persistence.PersistenceException;
@@ -20,14 +21,14 @@ import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
- * A singleton owned by the Controller which contains the methods and state for controlling applications.
+ * A singleton owned by the Controller which contains the methods and state for controlling tenants.
*
* @author bratseth
+ * @author mpolden
*/
public class TenantController {
@@ -52,81 +53,84 @@ public class TenantController {
this.athenzClientFactory = athenzClientFactory;
}
+ /** Returns a list of all known tenants */
public List<Tenant> asList() {
return db.listTenants();
}
+ /** Returns a list of all tenants accessible by the given user */
public List<Tenant> asList(UserId user) {
+ AthenzUser athenzUser = AthenzUser.fromUserId(user.id());
Set<AthenzDomain> userDomains = new HashSet<>(athenzClientFactory.createZtsClientWithServicePrincipal()
- .getTenantDomainsForUser(AthenzUser.fromUserId(user.id())));
- Predicate<Tenant> hasUsersDomain = (tenant) -> tenant.getAthensDomain().isPresent() && userDomains.contains(tenant.getAthensDomain().get());
- Predicate<Tenant> isUserTenant = (tenant) -> tenant.getId().equals(user.toTenantId());
-
+ .getTenantDomainsForUser(athenzUser));
return asList().stream()
- .filter(t -> hasUsersDomain.test(t) || isUserTenant.test(t))
- .collect(Collectors.toList());
+ .filter(tenant -> tenant instanceof UserTenant||
+ userDomains.stream().anyMatch(domain -> inDomain(tenant, domain)))
+ .collect(Collectors.toList());
}
- public Tenant createUserTenant(String userName) {
- TenantId userTenantId = new UserId(userName).toTenantId();
- try (Lock lock = lock(userTenantId)) {
- Tenant tenant = Tenant.createUserTenant(userTenantId);
- internalCreateTenant(tenant, Optional.empty());
- return tenant;
+ /** Create an user tenant with given username */
+ public void create(UserTenant tenant) {
+ try (Lock lock = lock(tenant.name())) {
+ requireNonExistent(tenant.name());
+ db.createTenant(tenant);
+ log.info("Created " + tenant);
}
}
- /** Creates an Athens tenant. */
- public void createAthenzTenant(Tenant tenant, NToken token) {
- try (Lock lock = lock(tenant.getId())) {
- internalCreateTenant(tenant, Optional.of(token));
+ /** Create an Athenz tenant */
+ public void create(AthenzTenant tenant, NToken token) {
+ try (Lock lock = lock(tenant.name())) {
+ requireNonExistent(tenant.name());
+ AthenzDomain domain = tenant.domain();
+ Optional<Tenant> existingTenantWithDomain = tenantIn(domain);
+ if (existingTenantWithDomain.isPresent()) {
+ throw new IllegalArgumentException("Could not create tenant '" + tenant.name().value() +
+ "': The Athens domain '" +
+ domain.getName() + "' is already connected to tenant '" +
+ existingTenantWithDomain.get().name().value() +
+ "'");
+ }
+ athenzClientFactory.createZmsClientWithAuthorizedServiceToken(token).createTenant(domain);
+ db.createTenant(tenant);
+ log.info("Created " + tenant);
}
}
- private void internalCreateTenant(Tenant tenant, Optional<NToken> token) {
- TenantId.validate(tenant.getId().id());
- if (tenant(tenant.getId()).isPresent())
- throw new IllegalArgumentException("Tenant '" + tenant.getId() + "' already exists");
- if (tenant(dashToUnderscore(tenant.getId())).isPresent())
- throw new IllegalArgumentException("Could not create " + tenant + ": Tenant " + dashToUnderscore(tenant.getId()) + " already exists");
- if (tenant.isAthensTenant() && ! token.isPresent())
- throw new IllegalArgumentException("Could not create " + tenant + ": No NToken provided");
-
- if (tenant.isAthensTenant()) {
- AthenzDomain domain = tenant.getAthensDomain().get();
- Optional<Tenant> existingTenantWithDomain = tenantHaving(domain);
- if (existingTenantWithDomain.isPresent())
- throw new IllegalArgumentException("Could not create " + tenant + ": The Athens domain '" + domain.getName() +
- "' is already connected to " + existingTenantWithDomain.get());
- athenzClientFactory.createZmsClientWithAuthorizedServiceToken(token.get())
- .createTenant(domain);
+ /** Returns the tenant in the given Athenz domain, or empty if none */
+ private Optional<Tenant> tenantIn(AthenzDomain domain) {
+ return asList().stream()
+ .filter(tenant -> inDomain(tenant, domain))
+ .findFirst();
+ }
+
+ /** Find tenant by name */
+ public Optional<Tenant> tenant(TenantName name) {
+ try {
+ return db.getTenant(name);
+ } catch (PersistenceException e) {
+ throw new RuntimeException(e);
}
- db.createTenant(tenant);
- log.info("Created " + tenant);
}
- /** Returns the tenant having the given Athens domain, or empty if none */
- private Optional<Tenant> tenantHaving(AthenzDomain domain) {
- return asList().stream().filter(Tenant::isAthensTenant)
- .filter(t -> t.getAthensDomain().get().equals(domain))
- .findAny();
+ /** Find tenant by name */
+ public Optional<Tenant> tenant(String name) {
+ return tenant(TenantName.from(name));
}
- public Optional<Tenant> tenant(TenantId id) {
+ /** Find Athenz tenant by name */
+ public Optional<AthenzTenant> athenzTenant(TenantName name) {
try {
- return db.getTenant(id);
+ return db.getAthenzTenant(name);
} catch (PersistenceException e) {
throw new RuntimeException(e);
}
}
- public void updateTenant(Tenant updatedTenant, Optional<NToken> token) {
- try (Lock lock = lock(updatedTenant.getId())) {
- if ( ! tenant(updatedTenant.getId()).isPresent())
- throw new IllegalArgumentException("Could not update " + updatedTenant + ": Tenant does not exist");
- if (updatedTenant.isAthensTenant() && ! token.isPresent())
- throw new IllegalArgumentException("Could not update " + updatedTenant + ": No NToken provided");
-
+ /** Update Athenz tenant */
+ public void updateTenant(AthenzTenant updatedTenant, NToken token) {
+ try (Lock lock = lock(updatedTenant.name())) {
+ requireExists(updatedTenant.name());
updateAthenzDomain(updatedTenant, token);
db.updateTenant(updatedTenant);
log.info("Updated " + updatedTenant);
@@ -135,52 +139,68 @@ public class TenantController {
}
}
- private void updateAthenzDomain(Tenant updatedTenant, Optional<NToken> token) {
- Tenant existingTenant = tenant(updatedTenant.getId()).get();
- if ( ! existingTenant.isAthensTenant()) return;
+ /** Delete an user tenant */
+ public void deleteTenant(UserTenant tenant) {
+ try (Lock lock = lock(tenant.name())) {
+ deleteTenant(tenant.name());
+ }
+ }
+
+ /** Delete an Athenz tenant */
+ public void deleteTenant(AthenzTenant tenant, NToken nToken) {
+ try (Lock lock = lock(tenant.name())) {
+ deleteTenant(tenant.name());
+ athenzClientFactory.createZmsClientWithAuthorizedServiceToken(nToken).deleteTenant(tenant.domain());
+ }
+ }
+
+ private void deleteTenant(TenantName name) {
+ try {
+ if ( ! controller.applications().asList(name).isEmpty()) {
+ throw new IllegalArgumentException("Could not delete tenant '" + name.value()
+ + "': This tenant has active applications");
+ }
+ db.deleteTenant(name);
+ log.info("Deleted " + name);
+ } catch (PersistenceException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void updateAthenzDomain(AthenzTenant updatedTenant, NToken token) {
+ Optional<AthenzTenant> existingTenant = athenzTenant(updatedTenant.name());
+ if ( ! existingTenant.isPresent()) return;
- AthenzDomain existingDomain = existingTenant.getAthensDomain().get();
- AthenzDomain newDomain = updatedTenant.getAthensDomain().get();
+ AthenzDomain existingDomain = existingTenant.get().domain();
+ AthenzDomain newDomain = updatedTenant.domain();
if (existingDomain.equals(newDomain)) return;
- Optional<Tenant> existingTenantWithNewDomain = tenantHaving(newDomain);
+ Optional<Tenant> existingTenantWithNewDomain = tenantIn(newDomain);
if (existingTenantWithNewDomain.isPresent())
throw new IllegalArgumentException("Could not set domain of " + updatedTenant + " to '" + newDomain +
"':" + existingTenantWithNewDomain.get() + " already has this domain");
- ZmsClient zmsClient = athenzClientFactory.createZmsClientWithAuthorizedServiceToken(token.get());
+ ZmsClient zmsClient = athenzClientFactory.createZmsClientWithAuthorizedServiceToken(token);
zmsClient.createTenant(newDomain);
- List<Application> applications = controller.applications().asList(TenantName.from(existingTenant.getId().id()));
+ List<Application> applications = controller.applications().asList(existingTenant.get().name());
applications.forEach(a -> zmsClient.addApplication(newDomain, new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(a.id().application().value())));
applications.forEach(a -> zmsClient.deleteApplication(existingDomain, new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(a.id().application().value())));
zmsClient.deleteTenant(existingDomain);
log.info("Updated Athens domain for " + updatedTenant + " from " + existingDomain + " to " + newDomain);
}
- public void deleteTenant(TenantId id, Optional<NToken> token) {
- try (Lock lock = lock(id)) {
- if ( ! tenant(id).isPresent())
- throw new NotExistsException(id); // TODO: Change exception and message
- if ( ! controller.applications().asList(TenantName.from(id.id())).isEmpty())
- throw new IllegalArgumentException("Could not delete tenant '" + id + "': This tenant has active applications");
-
- Tenant tenant = tenant(id).get();
- if (tenant.isAthensTenant() && ! token.isPresent())
- throw new IllegalArgumentException("Could not delete tenant '" + id + "': No NToken provided");
-
- try {
- db.deleteTenant(id);
- } catch (PersistenceException e) { // TODO: Don't allow these to leak out
- throw new RuntimeException(e);
- }
- if (tenant.isAthensTenant())
- athenzClientFactory.createZmsClientWithAuthorizedServiceToken(token.get())
- .deleteTenant(tenant.getAthensDomain().get());
- log.info("Deleted " + tenant);
+ private void requireNonExistent(TenantName name) {
+ if (tenant(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()) {
+ throw new IllegalArgumentException("Tenant '" + name + "' already exists");
}
}
- private TenantId dashToUnderscore(TenantId id) {
- return new TenantId(id.id().replaceAll("-", "_"));
+ private void requireExists(TenantName name) {
+ if (!tenant(name).isPresent()) {
+ throw new IllegalArgumentException("Tenant '" + name + "' does not exist");
+ }
}
/**
@@ -188,8 +208,16 @@ public class TenantController {
* Any operation which stores a tenant need to first acquire this lock, then read, modify
* and store the tenant, and finally release (close) the lock.
*/
- private Lock lock(TenantId tenant) {
+ private Lock lock(TenantName tenant) {
return curator.lock(tenant, Duration.ofMinutes(10));
}
+ private static boolean inDomain(Tenant tenant, AthenzDomain domain) {
+ return tenant instanceof AthenzTenant && ((AthenzTenant) tenant).in(domain);
+ }
+
+ private static String dashToUnderscore(String s) {
+ return s.replace('-', '_');
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/Tenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/Tenant.java
deleted file mode 100644
index 0edc63c69f5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/api/Tenant.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api;
-
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantType;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-
-import java.util.Optional;
-
-/**
- * @author smorgrav
- */
-// TODO: Move this and everything it owns to com.yahoo.hosted.controller.Tenant and com.yahoo.hosted.controller.tenant.*
-// TODO: Use polymorphism to represent the two tenant types
-public class Tenant {
-
- private final TenantId id;
- private final Optional<Property> property;
- private final Optional<AthenzDomain> athenzDomain;
- private final Optional<PropertyId> propertyId;
-
- // TODO: Use factory methods. They're down at the bottom!
- public Tenant(TenantId id, Optional<Property> property, Optional<AthenzDomain> athenzDomain) {
- this(id, property, athenzDomain, Optional.empty());
- }
-
- public Tenant(TenantId id, Optional<Property> property, Optional<AthenzDomain> athenzDomain, Optional<PropertyId> propertyId) {
- if (id.isUser()) {
- require( ! property.isPresent(), "User tenant '%s' cannot have a property.", id);
- require( ! propertyId.isPresent(), "User tenant '%s' cannot have a property ID.", id);
- require( ! athenzDomain.isPresent(), "User tenant '%s' cannot have an athens domain.", id);
- } else {
- require( property.isPresent(), "Athens tenant '%s' must have a property.", id);
- require( athenzDomain.isPresent(), "Athens tenant '%s' must have an athens domain.", id);
- }
- this.id = id;
- this.property = property;
- this.athenzDomain = athenzDomain;
- this.propertyId = propertyId; // TODO: Check validity after TODO@14. OpsDb tenants have this set in Sherpa, while athens tenants do not.
- // TODO: Require PropertyId for non-users, and fetch Property from EntityService (which will be moved to Organization) in the controller.
- }
-
- public boolean isAthensTenant() { return athenzDomain.isPresent(); }
-
- public TenantType tenantType() {
- if (athenzDomain.isPresent()) {
- return TenantType.ATHENS;
- } else {
- return TenantType.USER;
- }
- }
-
- public TenantId getId() {
- return id;
- }
-
- /** OpsDB property name of the tenant, or Optional.empty() if none is stored. */
- public Optional<Property> getProperty() {
- return property;
- }
-
- /** OpsDB property ID of the tenant. Not (yet) required, so returns Optional.empty() if none is stored. */
- public Optional<PropertyId> getPropertyId() {
- return propertyId;
- }
-
- public Optional<AthenzDomain> getAthensDomain() {
- return athenzDomain;
- }
-
- private void require(boolean statement, String message, TenantId id) {
- if (!statement) throw new IllegalArgumentException(String.format(message, id));
- }
-
- public static Tenant createAthensTenant(TenantId id, AthenzDomain athensDomain, Property property, Optional<PropertyId> propertyId) {
- if (id.isUser()) {
- throw new IllegalArgumentException("Invalid id for non-user tenant: " + id);
- }
- return new Tenant(id, Optional.ofNullable(property), Optional.ofNullable(athensDomain), propertyId);
- }
-
- public static Tenant createUserTenant(TenantId id) {
- if (!id.isUser()) {
- throw new IllegalArgumentException("Invalid id for user tenant: " + id);
- }
- return new Tenant(id, Optional.empty(), Optional.empty(), Optional.empty());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Tenant tenant = (Tenant) o;
-
- if (!id.equals(tenant.id)) return false;
- if (!property.equals(tenant.property)) return false;
- if (!athenzDomain.equals(tenant.athenzDomain)) return false;
- if (!propertyId.equals(tenant.propertyId)) return false;
- return true;
- }
-
- @Override
- public int hashCode() {
- int result = id.hashCode();
- result = 31 * result + property.hashCode();
- result = 31 * result + athenzDomain.hashCode();
- result = 31 * result + propertyId.hashCode();
- return result;
- }
-
- @Override
- public String toString() {
- return "tenant '" + id + "'";
- }
-
-}
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 cc977295acf..1a6bce2dba9 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
@@ -4,14 +4,13 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantType;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.time.Duration;
import java.util.NoSuchElementException;
@@ -50,9 +49,9 @@ public class ApplicationOwnershipConfirmer extends Maintainer {
try {
Tenant tenant = ownerOf(application.id());
Optional<IssueId> ourIssueId = application.ownershipIssueId();
- ourIssueId = tenant.tenantType() == TenantType.USER
- ? ownershipIssues.confirmOwnership(ourIssueId, application.id(), userFor(tenant))
- : ownershipIssues.confirmOwnership(ourIssueId, application.id(), propertyIdFor(tenant));
+ ourIssueId = tenant instanceof AthenzTenant
+ ? ownershipIssues.confirmOwnership(ourIssueId, application.id(), propertyIdFor((AthenzTenant) tenant))
+ : ownershipIssues.confirmOwnership(ourIssueId, application.id(), userFor(tenant));
ourIssueId.ifPresent(issueId -> store(issueId, application.id()));
}
catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout.
@@ -67,7 +66,12 @@ public class ApplicationOwnershipConfirmer extends Maintainer {
for (Application application : controller().applications().asList())
application.ownershipIssueId().ifPresent(issueId -> {
try {
- ownershipIssues.ensureResponse(issueId, ownerOf(application.id()).getPropertyId());
+ Optional<PropertyId> propertyId = Optional.of(application.id())
+ .map(this::ownerOf)
+ .filter(t -> t instanceof AthenzTenant)
+ .map(AthenzTenant.class::cast)
+ .flatMap(AthenzTenant::propertyId);
+ ownershipIssues.ensureResponse(issueId, propertyId);
}
catch (RuntimeException e) {
log.log(Level.WARNING, "Exception caught when attempting to escalate issue with id " + issueId, e);
@@ -76,17 +80,18 @@ public class ApplicationOwnershipConfirmer extends Maintainer {
}
private Tenant ownerOf(ApplicationId applicationId) {
- return controller().tenants().tenant(new TenantId(applicationId.tenant().value()))
+ return controller().tenants().tenant(applicationId.tenant())
.orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId));
}
protected User userFor(Tenant tenant) {
- return User.from(tenant.getId().id().replaceFirst("by-", ""));
+ return User.from(tenant.name().value().replaceFirst(Tenant.userPrefix, ""));
}
- protected PropertyId propertyIdFor(Tenant tenant) {
- return tenant.getPropertyId()
- .orElseThrow(() -> new NoSuchElementException("No PropertyId is listed for non-user tenant " + tenant));
+ protected PropertyId propertyIdFor(AthenzTenant tenant) {
+ return tenant.propertyId()
+ .orElseThrow(() -> new NoSuchElementException("No PropertyId is listed for non-user tenant " +
+ tenant));
}
protected void store(IssueId issueId, ApplicationId applicationId) {
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 e30ccbe7950..d471e553bb9 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
@@ -5,14 +5,13 @@ import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantType;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
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;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.time.Duration;
import java.util.Collection;
@@ -94,17 +93,18 @@ public class DeploymentIssueReporter extends Maintainer {
}
private Tenant ownerOf(ApplicationId applicationId) {
- return controller().tenants().tenant(new TenantId(applicationId.tenant().value()))
+ return controller().tenants().tenant(applicationId.tenant())
.orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId));
}
private User userFor(Tenant tenant) {
- return User.from(tenant.getId().id().replaceFirst("by-", ""));
+ return User.from(tenant.name().value().replaceFirst(Tenant.userPrefix, ""));
}
- private PropertyId propertyIdFor(Tenant tenant) {
- return tenant.getPropertyId()
- .orElseThrow(() -> new NoSuchElementException("No PropertyId is listed for non-user tenant " + tenant));
+ private PropertyId propertyIdFor(AthenzTenant tenant) {
+ return tenant.propertyId()
+ .orElseThrow(() -> new NoSuchElementException("No PropertyId is listed for non-user tenant " +
+ tenant));
}
/** File an issue for applicationId, if it doesn't already have an open issue associated with it. */
@@ -112,9 +112,9 @@ public class DeploymentIssueReporter extends Maintainer {
try {
Tenant tenant = ownerOf(applicationId);
Optional<IssueId> ourIssueId = controller().applications().require(applicationId).deploymentJobs().issueId();
- IssueId issueId = tenant.tenantType() == TenantType.USER
- ? deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, userFor(tenant))
- : deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, propertyIdFor(tenant));
+ IssueId issueId = tenant instanceof AthenzTenant
+ ? deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, propertyIdFor((AthenzTenant) tenant))
+ : deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, userFor(tenant));
store(applicationId, issueId);
}
catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout.
@@ -126,7 +126,12 @@ public class DeploymentIssueReporter extends Maintainer {
private void escalateInactiveDeploymentIssues(Collection<Application> applications) {
applications.forEach(application -> application.deploymentJobs().issueId().ifPresent(issueId -> {
try {
- deploymentIssues.escalateIfInactive(issueId, ownerOf(application.id()).getPropertyId(), maxInactivity);
+ Optional<PropertyId> propertyId = Optional.of(application.id())
+ .map(this::ownerOf)
+ .filter(t -> t instanceof AthenzTenant)
+ .map(AthenzTenant.class::cast)
+ .flatMap(AthenzTenant::propertyId);
+ deploymentIssues.escalateIfInactive(issueId, propertyId, maxInactivity);
}
catch (RuntimeException e) {
log.log(Level.WARNING, "Exception caught when attempting to escalate issue with id " + issueId, e);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
index fb6608ea643..c2078aa48b6 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerDb.java
@@ -3,10 +3,11 @@ package com.yahoo.vespa.hosted.controller.persistence;
import com.google.common.base.Joiner;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Identifier;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
+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 java.util.List;
import java.util.Optional;
@@ -21,14 +22,18 @@ public interface ControllerDb {
// --------- Tenants
- void createTenant(Tenant tenant);
+ void createTenant(UserTenant tenant);
+
+ void createTenant(AthenzTenant tenant);
// TODO: Remove exception from all signatures
- void updateTenant(Tenant tenant) throws PersistenceException;
+ void updateTenant(AthenzTenant tenant) throws PersistenceException;
+
+ void deleteTenant(TenantName name) throws PersistenceException;
- void deleteTenant(TenantId tenantId) throws PersistenceException;
+ Optional<Tenant> getTenant(TenantName name) throws PersistenceException;
- Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException;
+ Optional<AthenzTenant> getAthenzTenant(TenantName name) throws PersistenceException;
List<Tenant> listTenants();
@@ -45,10 +50,10 @@ public interface ControllerDb {
List<Application> listApplications();
/** Returns all applications of a tenant */
- List<Application> listApplications(TenantId tenantId);
+ List<Application> listApplications(TenantName name);
/** Returns the given elements joined by dot "." */
- default String path(Identifier... elements) {
+ default String path(TenantName... elements) {
return Joiner.on(".").join(elements);
}
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 bef33e739be..3991d4322a1 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
@@ -4,12 +4,11 @@ package com.yahoo.vespa.hosted.controller.persistence;
import com.google.inject.Inject;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.path.Path;
-import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.curator.Curator;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
@@ -64,8 +63,8 @@ public class CuratorDb {
// -------------- Locks --------------------------------------------------
- public Lock lock(TenantId id, Duration timeout) {
- return lock(lockPath(id), timeout);
+ public Lock lock(TenantName name, Duration timeout) {
+ return lock(lockPath(name), timeout);
}
public Lock lock(ApplicationId id, Duration timeout) {
@@ -236,9 +235,9 @@ public class CuratorDb {
// -------------- Paths --------------------------------------------------
- private Path lockPath(TenantId tenant) {
+ private Path lockPath(TenantName tenant) {
Path lockPath = lockRoot
- .append(tenant.id());
+ .append(tenant.value());
curator.create(lockPath);
return lockPath;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
index 2c5d77c7773..509896dd63c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
@@ -2,11 +2,12 @@
package com.yahoo.vespa.hosted.controller.persistence;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.AlreadyExistsException;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.NotExistsException;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
+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 java.util.ArrayList;
import java.util.HashMap;
@@ -22,35 +23,57 @@ import java.util.stream.Collectors;
*/
public class MemoryControllerDb implements ControllerDb {
- private final Map<TenantId, Tenant> tenants = new HashMap<>();
+ private final Map<TenantName, Tenant> tenants = new HashMap<>();
private final Map<String, Application> applications = new HashMap<>();
- @Override
- public void createTenant(Tenant tenant) {
- if (tenants.containsKey(tenant.getId())) {
- throw new AlreadyExistsException(tenant.getId());
+ private void createTenant(Tenant tenant) {
+ if (tenants.containsKey(tenant.name())) {
+ throw new IllegalArgumentException("Tenant '" + tenant + "' already exists");
}
- tenants.put(tenant.getId(), tenant);
+ tenants.put(tenant.name(), tenant);
}
- @Override
- public void updateTenant(Tenant tenant) {
- if (!tenants.containsKey(tenant.getId())) {
- throw new NotExistsException(tenant.getId());
+ private void updateTenant(Tenant tenant) {
+ if (!tenants.containsKey(tenant.name())) {
+ throw new NotExistsException(tenant.name().value());
}
- tenants.put(tenant.getId(), tenant);
+ tenants.put(tenant.name(), tenant);
+ }
+
+ @Override
+ public void createTenant(UserTenant tenant) {
+ createTenant((Tenant) tenant);
+ }
+
+ @Override
+ public void createTenant(AthenzTenant tenant) {
+ createTenant((Tenant) tenant);
}
@Override
- public void deleteTenant(TenantId tenantId) {
- if (tenants.remove(tenantId) == null) {
- throw new NotExistsException(tenantId);
+ public void updateTenant(AthenzTenant tenant) {
+ updateTenant((Tenant) tenant);
+ }
+
+ @Override
+ public void deleteTenant(TenantName name) {
+ if (tenants.remove(name) == null) {
+ throw new NotExistsException(name.value());
}
}
@Override
- public Optional<Tenant> getTenant(TenantId tenantId) {
- return Optional.ofNullable(tenants.get(tenantId));
+ public Optional<Tenant> getTenant(TenantName name) {
+ return getTenant(name, Tenant.class);
+ }
+
+ @Override
+ public Optional<AthenzTenant> getAthenzTenant(TenantName name) {
+ return getTenant(name, AthenzTenant.class);
+ }
+
+ private <T extends Tenant> Optional<T> getTenant(TenantName name, Class<T> type) {
+ return Optional.ofNullable(tenants.get(name)).map(type::cast);
}
@Override
@@ -79,9 +102,9 @@ public class MemoryControllerDb implements ControllerDb {
}
@Override
- public List<Application> listApplications(TenantId tenantId) {
+ public List<Application> listApplications(TenantName name) {
return applications.values().stream()
- .filter(a -> a.id().tenant().value().equals(tenantId.id()))
+ .filter(a -> a.id().tenant().equals(name))
.collect(Collectors.toList());
}
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 e85157a57c2..58bd95a22d3 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,7 +30,6 @@ import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.NotExistsException;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.ApplicationResource;
import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
import com.yahoo.vespa.hosted.controller.api.application.v4.TenantResource;
@@ -76,10 +75,12 @@ import com.yahoo.vespa.hosted.controller.restapi.ResourceResponse;
import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse;
import com.yahoo.vespa.hosted.controller.restapi.StringResponse;
import com.yahoo.vespa.hosted.controller.restapi.filter.SetBouncerPassthruHeaderFilter;
+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.serviceview.bindings.ApplicationView;
import com.yahoo.yolean.Exceptions;
-import javax.ws.rs.BadRequestException;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotAuthorizedException;
@@ -253,7 +254,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Cursor tenantsArray = response.setArray("tenants");
for (Tenant tenant : tenants)
tenantInTenantsListToSlime(tenant, request.getUri(), tenantsArray.addObject());
- response.setBool("tenantExists", tenants.stream().map(Tenant::getId).anyMatch(id -> id.isTenantFor(userId)));
+ response.setBool("tenantExists", tenants.stream().anyMatch(tenant -> tenant instanceof UserTenant &&
+ ((UserTenant) tenant).is(userId.id())));
return new SlimeJsonResponse(slime);
}
@@ -314,9 +316,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse tenant(String tenantName, HttpRequest request) {
- return controller.tenants().tenant(new TenantId((tenantName)))
- .map(tenant -> tenant(tenant, request, true))
- .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist"));
+ return controller.tenants().tenant(TenantName.from(tenantName))
+ .map(tenant -> tenant(tenant, request, true))
+ .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist"));
}
private HttpResponse tenant(Tenant tenant, HttpRequest request, boolean listApplications) {
@@ -510,7 +512,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse setGlobalRotationOverride(String tenantName, String applicationName, String instanceName, String environment, String region, boolean inService, HttpRequest request) {
// Check if request is authorized
- Optional<Tenant> existingTenant = controller.tenants().tenant(new TenantId(tenantName));
+ Optional<Tenant> existingTenant = controller.tenants().tenant(tenantName);
if (!existingTenant.isPresent())
return ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist");
@@ -611,57 +613,41 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Optional<UserId> user = getUserId(request);
if ( ! user.isPresent() ) throw new ForbiddenException("Not authenticated or not an user.");
+ String username = UserTenant.normalizeUser(user.get().id());
try {
- controller.tenants().createUserTenant(user.get().id());
- return new MessageResponse("Created user '" + user.get() + "'");
+ controller.tenants().create(UserTenant.create(username));
+ return new MessageResponse("Created user '" + username + "'");
} catch (AlreadyExistsException e) {
// Ok
- return new MessageResponse("User '" + user + "' already exists");
+ return new MessageResponse("User '" + username + "' already exists");
}
}
private HttpResponse updateTenant(String tenantName, HttpRequest request) {
- Optional<Tenant> existingTenant = controller.tenants().tenant(new TenantId(tenantName));
+ Optional<AthenzTenant> existingTenant = controller.tenants().athenzTenant(TenantName.from(tenantName));
if ( ! existingTenant.isPresent()) return ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist");;
Inspector requestData = toSlime(request.getData()).get();
-
- Tenant updatedTenant;
- switch (existingTenant.get().tenantType()) {
- case USER: {
- throw new BadRequestException("Cannot set property or OpsDB user group for user tenant");
- }
- case ATHENS: {
- updatedTenant = Tenant.createAthensTenant(new TenantId(tenantName),
- new AthenzDomain(mandatory("athensDomain", requestData).asString()),
- new Property(mandatory("property", requestData).asString()),
- optional("propertyId", requestData).map(PropertyId::new));
- controller.tenants().updateTenant(updatedTenant, getUserPrincipal(request).getNToken());
- break;
- }
- default: {
- throw new BadRequestException("Unknown tenant type: " + existingTenant.get().tenantType());
- }
+ AthenzTenant updatedTenant = existingTenant.get()
+ .with(new AthenzDomain(mandatory("athensDomain", requestData).asString()))
+ .with(new Property(mandatory("property", requestData).asString()));
+ Optional<PropertyId> propertyId = optional("propertyId", requestData).map(PropertyId::new);
+ if (propertyId.isPresent()) {
+ updatedTenant = updatedTenant.with(propertyId.get());
}
+ controller.tenants().updateTenant(updatedTenant, requireNToken(request, "Could not update " + tenantName));
return tenant(updatedTenant, request, true);
}
private HttpResponse createTenant(String tenantName, HttpRequest request) {
- if (new TenantId(tenantName).isUser())
- return ErrorResponse.badRequest("Use User API to create user tenants.");
-
Inspector requestData = toSlime(request.getData()).get();
- Tenant tenant = new Tenant(new TenantId(tenantName),
- optional("property", requestData).map(Property::new),
- optional("athensDomain", requestData).map(AthenzDomain::new),
- optional("propertyId", requestData).map(PropertyId::new));
- if (tenant.isAthensTenant())
- throwIfNotAthenzDomainAdmin(new AthenzDomain(mandatory("athensDomain", requestData).asString()), request);
-
- NToken token = getUserPrincipal(request).getNToken()
- .orElseThrow(() -> new IllegalArgumentException("Could not create " + tenant + ": No NToken provided"));
- controller.tenants().createAthenzTenant(tenant, token);
+ AthenzTenant tenant = AthenzTenant.create(TenantName.from(tenantName),
+ new AthenzDomain(mandatory("athensDomain", requestData).asString()),
+ new Property(mandatory("property", requestData).asString()),
+ optional("propertyId", requestData).map(PropertyId::new));
+ throwIfNotAthenzDomainAdmin(tenant.domain(), request);
+ controller.tenants().create(tenant, requireNToken(request, "Could not create " + tenantName));
return tenant(tenant, request, true);
}
@@ -784,8 +770,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
// Validate that domain in identity configuration (deployment.xml) is same as tenant domain
applicationPackage.map(ApplicationPackage::deploymentSpec).flatMap(DeploymentSpec::athenzDomain)
.ifPresent(identityDomain -> {
- Tenant tenant = controller.tenants().tenant(new TenantId(tenantName)).orElseThrow(() -> new IllegalArgumentException("Tenant does not exist"));
- AthenzDomain tenantDomain = tenant.getAthensDomain().orElseThrow(() -> new IllegalArgumentException("Identity provider only available to Athenz onboarded tenants"));
+ AthenzTenant tenant = controller.tenants().athenzTenant(TenantName.from(tenantName))
+ .orElseThrow(() -> new IllegalArgumentException("Tenant does not exist"));
+ AthenzDomain tenantDomain = tenant.domain();
if (! Objects.equals(tenantDomain.getName(), identityDomain.value())) {
throw new ForbiddenException(
String.format(
@@ -798,10 +785,19 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse deleteTenant(String tenantName, HttpRequest request) {
- Optional<Tenant> tenant = controller.tenants().tenant(new TenantId(tenantName));
+ Optional<Tenant> tenant = controller.tenants().tenant(tenantName);
if ( ! tenant.isPresent()) return ErrorResponse.notFoundError("Could not delete tenant '" + tenantName + "': Tenant not found"); // NOTE: The Jersey implementation would silently ignore this
- controller.tenants().deleteTenant(new TenantId(tenantName), getUserPrincipal(request).getNToken());
+
+ if (tenant.get() instanceof AthenzTenant) {
+ controller.tenants().deleteTenant((AthenzTenant) tenant.get(),
+ requireNToken(request, "Could not delete " + tenantName));
+ } else if (tenant.get() instanceof UserTenant) {
+ controller.tenants().deleteTenant((UserTenant) tenant.get());
+ } else {
+ throw new IllegalArgumentException("Don't know how to delete " + tenant.get() + " of type " +
+ tenant.get().getClass().getSimpleName());
+ }
// TODO: Change to a message response saying the tenant was deleted
return tenant(tenant.get(), request, false);
@@ -901,19 +897,24 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private Tenant getTenantOrThrow(String tenantName) {
- return controller.tenants().tenant(new TenantId(tenantName))
- .orElseThrow(() -> new NotExistsException(new TenantId(tenantName)));
+ return controller.tenants().tenant(tenantName)
+ .orElseThrow(() -> new NotExistsException(new TenantId(tenantName)));
}
private void toSlime(Cursor object, Tenant tenant, HttpRequest request, boolean listApplications) {
- object.setString("tenant", tenant.getId().id());
- object.setString("type", tenant.tenantType().name());
- tenant.getAthensDomain().ifPresent(a -> object.setString("athensDomain", a.getName()));
- tenant.getProperty().ifPresent(p -> object.setString("property", p.id()));
- tenant.getPropertyId().ifPresent(p -> object.setString("propertyId", p.toString()));
+ object.setString("tenant", tenant.name().value());
+ object.setString("type", tentantType(tenant));
+ Optional<PropertyId> propertyId = Optional.empty();
+ if (tenant instanceof AthenzTenant) {
+ AthenzTenant athenzTenant = (AthenzTenant) tenant;
+ object.setString("athensDomain", athenzTenant.domain().getName());
+ object.setString("property", athenzTenant.property().id());
+ propertyId = athenzTenant.propertyId();
+ propertyId.ifPresent(id -> object.setString("propertyId", id.toString()));
+ }
Cursor applicationArray = object.setArray("applications");
if (listApplications) { // This cludge is needed because we call this after deleting the tenant. As this call makes another tenant lookup it will fail. TODO is to support lookup on tenant
- for (Application application : controller.applications().asList(TenantName.from(tenant.getId().id()))) {
+ for (Application application : controller.applications().asList(tenant.name())) {
if (application.id().instance().isDefault()) {// TODO: Skip non-default applications until supported properly
if (recurseOverApplications(request))
toSlime(applicationArray.addObject(), application, request);
@@ -922,20 +923,20 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
}
}
- tenant.getPropertyId().ifPresent(propertyId -> {
+ propertyId.ifPresent(id -> {
try {
- object.setString("propertyUrl", controller.organization().propertyUri(propertyId).toString());
- object.setString("contactsUrl", controller.organization().contactsUri(propertyId).toString());
- object.setString("issueCreationUrl", controller.organization().issueCreationUri(propertyId).toString());
+ object.setString("propertyUrl", controller.organization().propertyUri(id).toString());
+ object.setString("contactsUrl", controller.organization().contactsUri(id).toString());
+ object.setString("issueCreationUrl", controller.organization().issueCreationUri(id).toString());
Cursor lists = object.setArray("contacts");
- for (List<? extends User> contactList : controller.organization().contactsFor(propertyId)) {
+ for (List<? extends User> contactList : controller.organization().contactsFor(id)) {
Cursor list = lists.addArray();
for (User contact : contactList)
list.addString(contact.displayName());
}
}
catch (RuntimeException e) {
- log.log(Level.WARNING, "Error fetching property info for " + tenant + " with propertyId " + propertyId + ": " +
+ log.log(Level.WARNING, "Error fetching property info for " + tenant + " with propertyId " + id + ": " +
Exceptions.toMessageString(e));
}
});
@@ -943,12 +944,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
// A tenant has different content when in a list ... antipattern, but not solvable before application/v5
private void tenantInTenantsListToSlime(Tenant tenant, URI requestURI, Cursor object) {
- object.setString("tenant", tenant.getId().id());
+ object.setString("tenant", tenant.name().value());
Cursor metaData = object.setObject("metaData");
- metaData.setString("type", tenant.tenantType().name());
- tenant.getAthensDomain().ifPresent(a -> metaData.setString("athensDomain", a.getName()));
- tenant.getProperty().ifPresent(p -> metaData.setString("property", p.id()));
- object.setString("url", withPath("/application/v4/tenant/" + tenant.getId().id(), requestURI).toString());
+ metaData.setString("type", tentantType(tenant));
+ if (tenant instanceof AthenzTenant) {
+ AthenzTenant athenzTenant = (AthenzTenant) tenant;
+ metaData.setString("athensDomain", athenzTenant.domain().getName());
+ metaData.setString("property", athenzTenant.property().id());
+ }
+ object.setString("url", withPath("/application/v4/tenant/" + tenant.name().value(), requestURI).toString());
}
/** Returns a copy of the given URI with the host and port from the given URI and the path set to the given path */
@@ -1212,4 +1216,18 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return ImmutableSet.of("all", "true", "deployment").contains(request.getProperty("recursive"));
}
+ private static String tentantType(Tenant tenant) {
+ if (tenant instanceof AthenzTenant) {
+ return "ATHENS";
+ } else if (tenant instanceof UserTenant) {
+ return "USER";
+ }
+ throw new IllegalArgumentException("Unrecognized tenant type: " + tenant.getClass().getSimpleName());
+ }
+
+ private static NToken requireNToken(HttpRequest request, String message) {
+ return getUserPrincipal(request).getNToken().orElseThrow(() -> new IllegalArgumentException(
+ message + ": No NToken provided"));
+ }
+
}
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 e6623fd6508..dc9ead96e0a 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
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.restapi.filter;
import com.google.inject.Inject;
import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.jdisc.Response;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.HttpRequest.Method;
@@ -15,13 +16,13 @@ import com.yahoo.vespa.athenz.api.AthenzPrincipal;
import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException;
import com.yahoo.vespa.hosted.controller.restapi.Path;
+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.yolean.chain.After;
import com.yahoo.yolean.chain.Provides;
@@ -91,9 +92,9 @@ public class ControllerAuthorizationFilter implements SecurityRequestFilter {
} else if (isHostedOperatorOperation(path, method)) {
verifyIsHostedOperator(principal);
} else if (isTenantAdminOperation(path, method)) {
- verifyIsTenantAdmin(principal, getTenantId(path));
+ verifyIsTenantAdmin(principal, getTenantName(path));
} else if (isTenantPipelineOperation(path, method)) {
- verifyIsTenantPipelineOperator(principal, getTenantId(path), getApplicationName(path));
+ verifyIsTenantPipelineOperator(principal, getTenantName(path), getApplicationName(path));
} else {
throw new ForbiddenException("No access control is explicitly declared for this api.");
}
@@ -149,8 +150,8 @@ public class ControllerAuthorizationFilter implements SecurityRequestFilter {
.hasHostedOperatorAccess(identity);
}
- private void verifyIsTenantAdmin(AthenzPrincipal principal, TenantId tenantId) {
- tenantController.tenant(tenantId)
+ private void verifyIsTenantAdmin(AthenzPrincipal principal, TenantName name) {
+ tenantController.tenant(name)
.ifPresent(tenant -> {
if (!isTenantAdmin(principal.getIdentity(), tenant)) {
throw new ForbiddenException("Tenant admin or Vespa operator role required");
@@ -159,26 +160,23 @@ public class ControllerAuthorizationFilter implements SecurityRequestFilter {
}
private boolean isTenantAdmin(AthenzIdentity identity, Tenant tenant) {
- switch (tenant.tenantType()) {
- case ATHENS:
- return clientFactory.createZmsClientWithServicePrincipal()
- .hasTenantAdminAccess(identity, tenant.getAthensDomain().get());
- case USER: {
- if (!(identity instanceof AthenzUser)) {
- return false;
- }
- AthenzUser user = (AthenzUser) identity;
- return tenant.getId().equals(new UserId(user.getName()).toTenantId());
+ if (tenant instanceof AthenzTenant) {
+ return clientFactory.createZmsClientWithServicePrincipal()
+ .hasTenantAdminAccess(identity, ((AthenzTenant) tenant).domain());
+ } else if (tenant instanceof UserTenant) {
+ if (!(identity instanceof AthenzUser)) {
+ return false;
}
- default:
- throw new InternalServerErrorException("Unknown tenant type: " + tenant.tenantType());
+ AthenzUser user = (AthenzUser) identity;
+ return ((UserTenant) tenant).is(user.getName());
}
+ throw new InternalServerErrorException("Unknown tenant type: " + tenant.getClass().getSimpleName());
}
private void verifyIsTenantPipelineOperator(AthenzPrincipal principal,
- TenantId tenantId,
+ TenantName name,
ApplicationName application) {
- tenantController.tenant(tenantId)
+ tenantController.tenant(name)
.ifPresent(tenant -> verifyIsTenantPipelineOperator(principal.getIdentity(), tenant, application));
}
@@ -193,8 +191,8 @@ public class ControllerAuthorizationFilter implements SecurityRequestFilter {
}
// NOTE: no fine-grained deploy authorization for non-Athenz tenants
- if (tenant.isAthensTenant()) {
- AthenzDomain tenantDomain = tenant.getAthensDomain().get();
+ if (tenant instanceof AthenzTenant) {
+ AthenzDomain tenantDomain = ((AthenzTenant) tenant).domain();
if (!hasDeployerAccess(identity, tenantDomain, application)) {
throw new ForbiddenException(String.format(
"'%1$s' does not have access to '%2$s'. " +
@@ -218,10 +216,10 @@ public class ControllerAuthorizationFilter implements SecurityRequestFilter {
}
}
- private static TenantId getTenantId(Path path) {
+ private static TenantName getTenantName(Path path) {
if (!path.matches("/application/v4/tenant/{tenant}/{*}"))
throw new InternalServerErrorException("Unable to handle path: " + path.asString());
- return new TenantId(path.get("tenant"));
+ return TenantName.from(path.get("tenant"));
}
private static ApplicationName getApplicationName(Path path) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
index bcabcf48e91..4374fd0246e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
@@ -27,8 +27,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
/**
- * This implements a callback API from Screwdriver which lets deployment jobs notify the controller
- * on completion.
+ * This API lists deployment jobs that are queued for execution on Screwdriver.
*
* @author bratseth
* @author mpolden
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
index f38ea14bbd8..c20a8baf06d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
@@ -21,7 +21,7 @@ import java.util.logging.Level;
import java.util.stream.Collectors;
/**
- * REST API that provides information about Hosted Vespa zones (version 1)
+ * REST API that provides information about zones in hosted Vespa (version 1)
*
* @author mpolden
*/
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
new file mode 100644
index 00000000000..0ba0eea2dab
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
@@ -0,0 +1,82 @@
+// 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.athenz.api.AthenzDomain;
+import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+
+import java.util.Optional;
+
+/**
+ * Represents an Athenz tenant in hosted Vespa.
+ *
+ * @author mpolden
+ */
+public class AthenzTenant extends Tenant {
+
+ private final AthenzDomain domain;
+ private final Property property;
+ private final Optional<PropertyId> propertyId;
+
+ /**
+ * This should only be used by serialization.
+ * Use {@link #create(TenantName, AthenzDomain, Property, Optional)}.
+ * */
+ public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId) {
+ super(name);
+ this.domain = domain;
+ this.property = property;
+ this.propertyId = propertyId;
+ }
+
+ /** Property name of this tenant */
+ public Property property() {
+ return property;
+ }
+
+ /** Property ID of the tenant, if present */
+ public Optional<PropertyId> propertyId() {
+ return propertyId;
+ }
+
+ /** Athenz domain of this tenant */
+ public AthenzDomain domain() {
+ return domain;
+ }
+
+ /** Returns true if tenant is in given domain */
+ public boolean in(AthenzDomain domain) {
+ return this.domain.equals(domain);
+ }
+
+ @Override
+ public String toString() {
+ return "athenz tenant '" + name() + "'";
+ }
+
+ public AthenzTenant with(AthenzDomain domain) {
+ return new AthenzTenant(name(), domain, property(), propertyId());
+ }
+
+ public AthenzTenant with(Property property) {
+ return new AthenzTenant(name(), domain, property, propertyId());
+ }
+
+ public AthenzTenant with(PropertyId propertyId) {
+ return new AthenzTenant(name(), domain, property, Optional.of(propertyId));
+ }
+
+ /** Create a new Athenz tenant */
+ public static AthenzTenant create(TenantName name, AthenzDomain domain, Property property,
+ Optional<PropertyId> propertyId) {
+ return new AthenzTenant(requireName(requireNoPrefix(name)), domain, property, propertyId);
+ }
+
+ private static TenantName requireNoPrefix(TenantName name) {
+ if (name.value().startsWith(Tenant.userPrefix)) {
+ throw new IllegalArgumentException("Athenz tenant name cannot have prefix '" + Tenant.userPrefix + "'");
+ }
+ return name;
+ }
+}
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
new file mode 100644
index 00000000000..aac3fa20d11
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java
@@ -0,0 +1,50 @@
+// 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 java.util.Objects;
+
+/**
+ * A tenant in hosted Vespa.
+ *
+ * @author mpolden
+ */
+public abstract class Tenant {
+
+ public static final String userPrefix = "by-";
+
+ private final TenantName name;
+
+ Tenant(TenantName name) {
+ this.name = name;
+ }
+
+ /** Name of this tenant */
+ public TenantName name() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Tenant tenant = (Tenant) o;
+ return Objects.equals(name, tenant.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ static TenantName requireName(TenantName name) {
+ 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;
+ }
+}
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
new file mode 100644
index 00000000000..e110600639b
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/UserTenant.java
@@ -0,0 +1,55 @@
+// 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;
+
+/**
+ * Represents an user tenant in hosted Vespa.
+ *
+ * @author mpolden
+ */
+public class UserTenant extends Tenant {
+
+ /**
+ * This should only be used by serialization.
+ * Use {@link #create(String)}.
+ * */
+ public UserTenant(TenantName name) {
+ super(name);
+ }
+
+ /** Returns true if this is the tenant for the given user name */
+ public boolean is(String username) {
+ return name().value().equals(normalizeUser(username));
+ }
+
+ @Override
+ public String toString() {
+ return "user tenant '" + name() + "'";
+ }
+
+ /** Create a new user tenant */
+ public static UserTenant create(String username) {
+ TenantName name = TenantName.from(username);
+ return new UserTenant(requireName(requireUser(name)));
+ }
+
+ /** Normalize given username. E.g. foo_bar becomes by-foo-bar */
+ public static String normalizeUser(String username) {
+ int offset = 0;
+ if (username.startsWith(Tenant.userPrefix)) {
+ offset = Tenant.userPrefix.length();
+ }
+ return Tenant.userPrefix + username.substring(offset).replace('_', '-');
+ }
+
+ private static TenantName requireUser(TenantName name) {
+ if (!name.value().startsWith(Tenant.userPrefix)) {
+ throw new IllegalArgumentException("User tenant must have prefix '" + Tenant.userPrefix + "'");
+ }
+ if (name.value().substring(Tenant.userPrefix.length()).contains("_")) {
+ throw new IllegalArgumentException("User tenant cannot contain '_'");
+ }
+ return name;
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java
new file mode 100644
index 00000000000..9218bfcd850
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+@ExportPackage
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import com.yahoo.osgi.annotation.ExportPackage;
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 4d6de0bf4ef..fd0362e4552 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
@@ -17,7 +17,6 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ScrewdriverBuildJob;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
@@ -133,7 +132,7 @@ public class ControllerTest {
tester.restartController();
applications = tester.controller().applications();
- assertNotNull(tester.controller().tenants().tenant(new TenantId("tenant1")));
+ assertNotNull(tester.controller().tenants().tenant(TenantName.from("tenant1")));
assertNotNull(applications.get(ApplicationId.from(TenantName.from("tenant1"),
ApplicationName.from("application1"),
InstanceName.from("default"))));
@@ -500,7 +499,7 @@ public class ControllerTest {
public void testDeployUntestedChangeFails() {
DeploymentTester tester = new DeploymentTester();
ApplicationController applications = tester.controller().applications();
- TenantId tenant = tester.controllerTester().createTenant("tenant1", "domain1", 11L);
+ TenantName tenant = tester.controllerTester().createTenant("tenant1", "domain1", 11L);
Application app = tester.controllerTester().createApplication(tenant, "app1", "default", 1);
tester.deployCompletely(app, applicationPackage);
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 b4bbc0f68e7..3898b18cd7c 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
@@ -4,12 +4,12 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.slime.Slime;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.curator.mock.MockCurator;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.GitRevision;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ScrewdriverBuildJob;
@@ -19,7 +19,6 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.GitRepository;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.chef.ChefMock;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService;
@@ -30,6 +29,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrgani
import com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.integration.MockMetricsService;
@@ -140,7 +141,7 @@ public final class ControllerTester {
/** Creates the given tenant and application and deploys it */
public Application createAndDeploy(String tenantName, String domainName, String applicationName,
String instanceName, ZoneId zone, long projectId, Long propertyId) {
- TenantId tenant = createTenant(tenantName, domainName, propertyId);
+ TenantName tenant = createTenant(tenantName, domainName, propertyId);
Application application = createApplication(tenant, applicationName, instanceName, projectId);
deploy(application, zone);
return application;
@@ -189,20 +190,21 @@ public final class ControllerTester {
return domain;
}
- public TenantId createTenant(String tenantName, String domainName, Long propertyId) {
- TenantId id = new TenantId(tenantName);
- Optional<Tenant> existing = controller().tenants().tenant(id);
- if (existing.isPresent()) return id;
-
- Tenant tenant = Tenant.createAthensTenant(id, createDomain(domainName), new Property("app1Property"),
- propertyId == null ? Optional.empty() : Optional.of(new PropertyId(propertyId.toString())));
- controller().tenants().createAthenzTenant(tenant, TestIdentities.userNToken);
- assertNotNull(controller().tenants().tenant(id));
- return id;
+ public TenantName createTenant(String tenantName, String domainName, Long propertyId) {
+ TenantName name = TenantName.from(tenantName);
+ Optional<Tenant> existing = controller().tenants().tenant(name);
+ if (existing.isPresent()) return name;
+ AthenzTenant tenant = AthenzTenant.create(name, createDomain(domainName), new Property("app1Property"),
+ Optional.ofNullable(propertyId)
+ .map(Object::toString)
+ .map(PropertyId::new));
+ controller().tenants().create(tenant, TestIdentities.userNToken);
+ assertNotNull(controller().tenants().tenant(name));
+ return name;
}
- public Application createApplication(TenantId tenant, String applicationName, String instanceName, long projectId) {
- ApplicationId applicationId = ApplicationId.from(tenant.id(), applicationName, instanceName);
+ public Application createApplication(TenantName tenant, String applicationName, String instanceName, long projectId) {
+ ApplicationId applicationId = ApplicationId.from(tenant.value(), applicationName, instanceName);
controller().applications().createApplication(applicationId, Optional.of(TestIdentities.userNToken));
controller().applications().lockOrThrow(applicationId, lockedApplication ->
controller().applications().store(lockedApplication.withProjectId(projectId)));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
index c4e9240dfcb..27a1da941a0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
@@ -11,7 +12,6 @@ import com.yahoo.vespa.hosted.controller.ArtifactRepositoryMock;
import com.yahoo.vespa.hosted.controller.ConfigServerClientMock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Change;
@@ -109,7 +109,7 @@ public class DeploymentTester {
}
public Application createApplication(String applicationName, String tenantName, long projectId, Long propertyId) {
- TenantId tenant = tester.createTenant(tenantName, UUID.randomUUID().toString(), propertyId);
+ TenantName tenant = tester.createTenant(tenantName, UUID.randomUUID().toString(), propertyId);
return tester.createApplication(tenant, applicationName, "default", projectId);
}
@@ -122,13 +122,13 @@ public class DeploymentTester {
/** Simulate the full lifecycle of an application deployment as declared in given application package */
public Application createAndDeploy(String applicationName, int projectId, ApplicationPackage applicationPackage) {
- TenantId tenantId = tester.createTenant("tenant1", "domain1", 1L);
- return createAndDeploy(tenantId, applicationName, projectId, applicationPackage);
+ TenantName tenant = tester.createTenant("tenant1", "domain1", 1L);
+ return createAndDeploy(tenant, applicationName, projectId, applicationPackage);
}
/** Simulate the full lifecycle of an application deployment as declared in given application package */
- public Application createAndDeploy(TenantId tenantId, String applicationName, int projectId, ApplicationPackage applicationPackage) {
- Application application = tester.createApplication(tenantId, applicationName, "default", projectId);
+ public Application createAndDeploy(TenantName tenant, String applicationName, int projectId, ApplicationPackage applicationPackage) {
+ Application application = tester.createApplication(tenant, applicationName, "default", projectId);
deployCompletely(application, applicationPackage);
return applications().require(application.id());
}
@@ -139,8 +139,8 @@ public class DeploymentTester {
}
/** Simulate the full lifecycle of an application deployment to prod.us-west-1 with the given upgrade policy */
- public Application createAndDeploy(TenantId tenantId, String applicationName, int projectId, String upgradePolicy) {
- return createAndDeploy(tenantId, applicationName, projectId, applicationPackage(upgradePolicy));
+ public Application createAndDeploy(TenantName tenant, String applicationName, int projectId, String upgradePolicy) {
+ return createAndDeploy(tenant, applicationName, projectId, applicationPackage(upgradePolicy));
}
/** Complete an ongoing deployment */
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 dbe6c13bc68..e17fbc912ca 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
@@ -3,12 +3,11 @@ package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.component.Version;
import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.LockedApplication;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
@@ -86,7 +85,7 @@ public class DeploymentTriggerTest {
public void deploymentSpecDecidesTriggerOrder() {
DeploymentTester tester = new DeploymentTester();
DeploymentQueue deploymentQueue = tester.deploymentQueue();
- TenantId tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L);
+ TenantName tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L);
Application application = tester.controllerTester().createApplication(tenant, "app1", "default", 1L);
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
@@ -245,7 +244,7 @@ public class DeploymentTriggerTest {
public void testSuccessfulDeploymentApplicationPackageChanged() {
DeploymentTester tester = new DeploymentTester();
DeploymentQueue deploymentQueue = tester.deploymentQueue();
- TenantId tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L);
+ TenantName tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L);
Application application = tester.controllerTester().createApplication(tenant, "app1", "default", 1L);
ApplicationPackage previousApplicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
@@ -350,7 +349,7 @@ public class DeploymentTriggerTest {
public void testHandleMultipleNotificationsFromLastJob() {
DeploymentTester tester = new DeploymentTester();
DeploymentQueue deploymentQueue = tester.deploymentQueue();
- TenantId tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L);
+ TenantName tenant = tester.controllerTester().createTenant("tenant1", "domain1", 1L);
Application application = tester.controllerTester().createApplication(tenant, "app1", "default", 1L);
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.environment(Environment.prod)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
index 0309eaf7d25..b5941c441e2 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
@@ -2,12 +2,13 @@
package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
+import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import org.junit.Before;
@@ -39,13 +40,13 @@ public class ApplicationOwnershipConfirmerTest {
@Test
public void testConfirmation() {
- TenantId property = tester.controllerTester().createTenant("property", "domain", 1L);
+ TenantName property = tester.controllerTester().createTenant("property", "domain", 1L);
tester.createAndDeploy(property, "application", 1, "default");
Supplier<Application> propertyApp = () -> tester.controller().applications().require(ApplicationId.from("property", "application", "default"));
- TenantId user = new TenantId("by-user");
- tester.controller().tenants().createUserTenant("user");
- tester.createAndDeploy(user, "application", 2, "default");
+ UserTenant user = UserTenant.create("by-user");
+ tester.controller().tenants().create(user);
+ tester.createAndDeploy(user.name(), "application", 2, "default");
Supplier<Application> userApp = () -> tester.controller().applications().require(ApplicationId.from("by-user", "application", "default"));
assertFalse("No issue is initially stored for a new application.", propertyApp.get().ownershipIssueId().isPresent());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
index 8e60e63e873..f56642ad538 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
@@ -4,13 +4,13 @@ package com.yahoo.vespa.hosted.controller.restapi;
import com.yahoo.application.container.JDisc;
import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.utils.AthenzIdentities;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ArtifactRepositoryMock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.TestIdentities;
-import com.yahoo.vespa.hosted.controller.api.Tenant;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.GitRevision;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ScrewdriverBuildJob;
@@ -20,11 +20,11 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.GitRepository;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.HostedAthenzIdentities;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock;
@@ -75,10 +75,10 @@ public class ContainerControllerTester {
public Application createApplication(String athensDomain, String tenant, String application) {
AthenzDomain domain1 = addTenantAthenzDomain(athensDomain, "mytenant");
- controller().tenants().createAthenzTenant(Tenant.createAthensTenant(new TenantId(tenant), domain1,
- new Property("property1"),
- Optional.of(new PropertyId("1234"))),
- TestIdentities.userNToken);
+ controller().tenants().create(AthenzTenant.create(TenantName.from(tenant), domain1,
+ new Property("property1"),
+ Optional.of(new PropertyId("1234"))),
+ TestIdentities.userNToken);
ApplicationId app = ApplicationId.from(tenant, application, "default");
return controller().applications().createApplication(app, Optional.of(TestIdentities.userNToken));
}
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 b9eef2069d9..2dacadaaa3e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -9,6 +9,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.athenz.api.AthenzDomain;
@@ -18,6 +19,7 @@ import com.yahoo.vespa.athenz.api.NToken;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ConfigServerClientMock;
+import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
@@ -43,6 +45,7 @@ import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
@@ -331,9 +334,9 @@ public class ApplicationApiTest extends ControllerContainerTest {
// PUT (create) the authenticated user
byte[] data = new byte[0];
- tester.assertResponse(request("/application/v4/user?user=newuser&domain=by", PUT)
+ tester.assertResponse(request("/application/v4/user?user=new_user&domain=by", PUT)
.data(data)
- .userIdentity(new UserId("newuser")),
+ .userIdentity(new UserId("new_user")), // Normalized to by-new-user by API
new File("create-user-response.json"));
// OPTIONS return 200 OK
tester.assertResponse(request("/application/v4/", Request.Method.OPTIONS),
@@ -551,6 +554,22 @@ public class ApplicationApiTest extends ControllerContainerTest {
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'tenant1' already exists\"}",
400);
+ // POST (add) a Athenz tenant with underscore in name
+ tester.assertResponse(request("/application/v4/tenant/my_tenant_2", POST)
+ .userIdentity(USER_ID)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .nToken(N_TOKEN),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"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.\"}",
+ 400);
+
+ // POST (add) a Athenz tenant with by- prefix
+ tester.assertResponse(request("/application/v4/tenant/by-tenant2", POST)
+ .userIdentity(USER_ID)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .nToken(N_TOKEN),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz tenant name cannot have prefix 'by-'\"}",
+ 400);
+
// POST (create) an (empty) application
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
.userIdentity(USER_ID)
@@ -596,7 +615,8 @@ public class ApplicationApiTest extends ControllerContainerTest {
// DELETE tenant which has an application
tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
- .userIdentity(USER_ID),
+ .userIdentity(USER_ID)
+ .nToken(N_TOKEN),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not delete tenant 'tenant1': This tenant has active applications\"}",
400);
@@ -626,6 +646,18 @@ public class ApplicationApiTest extends ControllerContainerTest {
.userIdentity(USER_ID),
"{\"error-code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"Unable to promote Chef environments for application\"}",
500);
+
+ // Create legancy tenant name containing underscores
+ tester.controller().tenants().create(new AthenzTenant(TenantName.from("my_tenant"), ATHENZ_TENANT_DOMAIN,
+ new Property("property1"), Optional.empty()),
+ N_TOKEN);
+ // POST (add) a Athenz tenant with dashes duplicates existing one with underscores
+ tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
+ .userIdentity(USER_ID)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .nToken(N_TOKEN),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'my-tenant' already exists\"}",
+ 400);
}
@Test
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json
index 709548e87a7..a6130122650 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/create-user-response.json
@@ -1,3 +1,3 @@
{
- "message":"Created user 'newuser'"
-} \ No newline at end of file
+ "message":"Created user 'by-new-user'"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-underscore.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-underscore.json
new file mode 100644
index 00000000000..243d5fb20c6
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-underscore.json
@@ -0,0 +1,9 @@
+{
+ "tenant": "my_tenant_2",
+ "type": "ATHENS",
+ "athensDomain": "domain1",
+ "property": "property1",
+ "applications": [
+
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java
index 626d480257e..8d511b204e4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.controller.restapi.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.jdisc.http.HttpRequest.Method;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.vespa.athenz.api.AthenzDomain;
@@ -12,7 +13,6 @@ import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.HostedAthenzIdentities;
import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock;
@@ -48,7 +48,7 @@ public class ControllerAuthorizationFilterTest {
private static final AthenzDomain TENANT_DOMAIN = new AthenzDomain("tenantdomain");
private static final AthenzService TENANT_ADMIN = new AthenzService(TENANT_DOMAIN, "adminservice");
private static final AthenzService TENANT_PIPELINE = HostedAthenzIdentities.from(new ScrewdriverId("12345"));
- private static final TenantId TENANT = new TenantId("mytenant");
+ private static final TenantName TENANT = TenantName.from("mytenant");
private static final ApplicationId APPLICATION = new ApplicationId("myapp");
@Test
@@ -80,7 +80,7 @@ public class ControllerAuthorizationFilterTest {
public void only_hosted_operator_or_tenant_admin_can_access_tenant_admin_apis() {
ControllerTester controllerTester = new ControllerTester();
controllerTester.athenzDb().hostedOperators.add(HOSTED_OPERATOR);
- controllerTester.createTenant(TENANT.id(), TENANT_DOMAIN.getName(), null);
+ controllerTester.createTenant(TENANT.value(), TENANT_DOMAIN.getName(), null);
controllerTester.athenzDb().domains.get(TENANT_DOMAIN).admins.add(TENANT_ADMIN);
ControllerAuthorizationFilter filter = createFilter(controllerTester);
@@ -100,7 +100,7 @@ public class ControllerAuthorizationFilterTest {
public void only_hosted_operator_and_screwdriver_project_with_deploy_role_can_access_tenant_pipeline_apis() {
ControllerTester controllerTester = new ControllerTester();
controllerTester.athenzDb().hostedOperators.add(HOSTED_OPERATOR);
- controllerTester.createTenant(TENANT.id(), TENANT_DOMAIN.getName(), null);
+ controllerTester.createTenant(TENANT.value(), TENANT_DOMAIN.getName(), null);
controllerTester.createApplication(TENANT, APPLICATION.id(), "default", 12345);
AthenzDbMock.Domain domainMock = controllerTester.athenzDb().domains.get(TENANT_DOMAIN);
domainMock.admins.add(TENANT_ADMIN);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
index 14f5d00ec88..8eee7c2d8c2 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
@@ -5,10 +5,10 @@ import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.component.Vtag;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
@@ -138,7 +138,7 @@ public class VersionStatusTest {
Application ignored0 = tester.createApplication("ignored0", "tenant1", 1000, 1000L);
// Pull request builds
- tester.controllerTester().createApplication(new TenantId("tenant1"),
+ tester.controllerTester().createApplication(TenantName.from("tenant1"),
"ignored1",
"43", 1000);