summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java17
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json12
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java7
11 files changed, 107 insertions, 3 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
index 0fc20095b41..3a864fc04c6 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
@@ -16,6 +16,8 @@ public interface BillingController {
PlanId getPlan(TenantName tenant);
+ List<TenantName> tenantsWithPlan(List<TenantName> existing, PlanId planId);
+
String getPlanDisplayName(PlanId planId);
Quota getQuota(TenantName tenant);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
index 21eada37ab1..b24d532d4a3 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
@@ -34,6 +34,13 @@ public class MockBillingController implements BillingController {
}
@Override
+ public List<TenantName> tenantsWithPlan(List<TenantName> tenants, PlanId planId) {
+ return tenants.stream()
+ .filter(t -> plans.getOrDefault(t, PlanId.from("trial")).equals(planId))
+ .collect(Collectors.toList());
+ }
+
+ @Override
public String getPlanDisplayName(PlanId planId) {
return "Plan with id: " + planId.value();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index f17be5896ba..e097b82b7d0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -21,9 +21,12 @@ import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.flags.BooleanFlag;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.IntFlag;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.LockedTenant;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
import com.yahoo.vespa.hosted.controller.api.integration.user.User;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
@@ -34,6 +37,7 @@ import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.yolean.Exceptions;
import java.security.PublicKey;
@@ -65,6 +69,7 @@ public class UserApiHandler extends LoggingRequestHandler {
private final UserManagement users;
private final Controller controller;
private final BooleanFlag enable_public_signup_flow;
+ private final IntFlag maxTrialTenants;
@Inject
public UserApiHandler(Context parentCtx, UserManagement users, Controller controller, FlagSource flagSource) {
@@ -72,6 +77,7 @@ public class UserApiHandler extends LoggingRequestHandler {
this.users = users;
this.controller = controller;
this.enable_public_signup_flow = PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource);
+ this.maxTrialTenants = Flags.MAX_TRIAL_TENANTS.bindTo(flagSource);
}
@Override
@@ -159,6 +165,7 @@ public class UserApiHandler extends LoggingRequestHandler {
root.setBool("isCd", controller.system().isCd());
root.setBool(enable_public_signup_flow.id().toString(),
enable_public_signup_flow.with(FetchVector.Dimension.CONSOLE_USER_EMAIL, user.email()).value());
+ root.setBool("hasTrialCapacity", hasTrialCapacity());
toSlime(root.setObject("user"), user);
@@ -342,6 +349,13 @@ public class UserApiHandler extends LoggingRequestHandler {
return new MessageResponse(user + " is no longer a member of " + role);
}
+ private boolean hasTrialCapacity() {
+ if (! controller.system().isPublic()) return true;
+ var existing = controller.tenants().asList().stream().map(Tenant::name).collect(Collectors.toList());
+ var trialTenants = controller.serviceRegistry().billingController().tenantsWithPlan(existing, PlanId.from("trial"));
+ return maxTrialTenants.value() < 0 || trialTenants.size() < maxTrialTenants.value();
+ }
+
private static Inspector bodyInspector(HttpRequest request) {
return Exceptions.uncheck(() -> SlimeUtils.jsonToSlime(IOUtils.readBytes(request.getData(), 1 << 10)).get());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
index ef13348fb74..d37e1e05030 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
@@ -6,10 +6,13 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.flags.BooleanFlag;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.IntFlag;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
@@ -36,18 +39,21 @@ public class CloudAccessControl implements AccessControl {
private final UserManagement userManagement;
private final BooleanFlag enablePublicSignup;
+ private final IntFlag maxTrialTenants;
private final BillingController billingController;
@Inject
public CloudAccessControl(UserManagement userManagement, FlagSource flagSource, ServiceRegistry serviceRegistry) {
this.userManagement = userManagement;
this.enablePublicSignup = PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource);
+ this.maxTrialTenants = Flags.MAX_TRIAL_TENANTS.bindTo(flagSource);
billingController = serviceRegistry.billingController();
}
@Override
public CloudTenant createTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing) {
requireTenantCreationAllowed((Auth0Credentials) credentials);
+ requireTenantTrialLimitNotReached(existing);
CloudTenantSpec spec = (CloudTenantSpec) tenantSpec;
CloudTenant tenant = CloudTenant.create(spec.tenant(), credentials.user());
@@ -64,6 +70,16 @@ public class CloudAccessControl implements AccessControl {
return tenant;
}
+ private void requireTenantTrialLimitNotReached(List<Tenant> existing) {
+ var trialPlanId = PlanId.from("trial");
+ var tenantNames = existing.stream().map(Tenant::name).collect(Collectors.toList());
+ var trialTenants = billingController.tenantsWithPlan(tenantNames, trialPlanId).size();
+
+ if (maxTrialTenants.value() >= 0 && maxTrialTenants.value() <= trialTenants) {
+ throw new ForbiddenException("Too many tenants with trial plans, please contact the Vespa support team");
+ }
+ }
+
private void requireTenantCreationAllowed(Auth0Credentials auth0Credentials) {
if (allowedByPrivilegedRole(auth0Credentials)) return;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
index 92f6b6cef27..f327851f902 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
@@ -6,6 +6,7 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
@@ -18,6 +19,7 @@ import com.yahoo.vespa.hosted.controller.security.Credentials;
import org.junit.Before;
import org.junit.Test;
+import javax.ws.rs.ForbiddenException;
import java.util.Collections;
import java.util.Set;
@@ -25,6 +27,8 @@ import static com.yahoo.application.container.handler.Request.Method.GET;
import static com.yahoo.application.container.handler.Request.Method.POST;
import static com.yahoo.application.container.handler.Request.Method.PUT;
import static com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiTest.createApplicationSubmissionData;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
@@ -93,6 +97,24 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
tester.assertResponse(infoRequest, fullInfo, 200);
}
+ @Test
+ public void trial_tenant_limit_reached() {
+ ((InMemoryFlagSource) tester.controller().flagSource()).withIntFlag(Flags.MAX_TRIAL_TENANTS.id(), 1);
+ tester.controller().serviceRegistry().billingController().setPlan(tenantName, PlanId.from("pay-as-you-go"), false);
+
+ // tests that we can create the one trial tenant the flag says we can have -- and that the tenant created
+ // in @Before does not count towards that limit.
+ tester.controller().tenants().create(tenantSpec("tenant1"), credentials("administrator"));
+
+ // tests that exceeding the limit throws a ForbiddenException
+ try {
+ tester.controller().tenants().create(tenantSpec("tenant2"), credentials("administrator"));
+ fail("Should not be allowed to create tenant that exceed trial limit");
+ } catch (ForbiddenException e) {
+ assertEquals("Too many tenants with trial plans, please contact the Vespa support team", e.getMessage());
+ }
+ }
+
private ApplicationPackageBuilder prodBuilder() {
return new ApplicationPackageBuilder()
.instances("default")
@@ -107,6 +129,10 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
tester.controller().applications().createApplication(appId, credentials("developer@scoober"));
}
+ private static CloudTenantSpec tenantSpec(String name) {
+ return new CloudTenantSpec(TenantName.from(name), "");
+ }
+
private static Credentials credentials(String name) {
return new Auth0Credentials(() -> name, Collections.emptySet());
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index 0ea3b061893..547ce769e87 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -244,4 +244,21 @@ public class UserApiTest extends ControllerContainerCloudTest {
.user(user),
new File("user-with-applications-cloud.json"));
}
+
+ @Test
+ public void maxTrialTenants() {
+ ContainerTester tester = new ContainerTester(container, responseFiles);
+ ((InMemoryFlagSource) tester.controller().flagSource())
+ .withIntFlag(Flags.MAX_TRIAL_TENANTS.id(), 1)
+ .withBooleanFlag(Flags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
+ ControllerTester controller = new ControllerTester(tester);
+ Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant());
+ User user = new User("dev@domail", "Joe Developer", "dev", null);
+
+ controller.createTenant("tenant1", Tenant.Type.cloud);
+
+ tester.assertResponse(
+ request("/api/user/v1/user").user(user),
+ new File("user-without-trial-capacity-cloud.json"));
+ }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
index 36918c743fa..2ae3514bec3 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
@@ -2,6 +2,7 @@
"isPublic": false,
"isCd": false,
"enable-public-signup-flow": (ignore),
+ "hasTrialCapacity": (ignore),
"user": {
"name": "Joe Developer",
"email": "dev@domail",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
index 27398352e53..2d2a137c2ca 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
@@ -1,7 +1,8 @@
{
-"isPublic": true,
-"isCd": false,
-"enable-public-signup-flow": (ignore),
+ "isPublic": true,
+ "isCd": false,
+ "enable-public-signup-flow": (ignore),
+ "hasTrialCapacity": true,
"user": {
"name": "Joe Developer",
"email": "dev@domail",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
index b62a70d1871..e03a18a1949 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
@@ -2,6 +2,7 @@
"isPublic": (ignore),
"isCd": (ignore),
"enable-public-signup-flow": (ignore),
+ "hasTrialCapacity": (ignore),
"user": {
"name": "Joe Developer",
"email": "dev@domail",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json
new file mode 100644
index 00000000000..a7410b14850
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json
@@ -0,0 +1,12 @@
+{
+ "isPublic": true,
+ "isCd": false,
+ "enable-public-signup-flow": true,
+ "hasTrialCapacity": false,
+ "user": {
+ "name": "Joe Developer",
+ "email": "dev@domail",
+ "nickname": "dev"
+ },
+ "tenants": {}
+} \ No newline at end of file
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
index 4f794302ca1..1279e11393d 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -199,6 +199,13 @@ public class Flags {
"Takes effect immediately on new hosts, on next redeploy for applications",
APPLICATION_ID);
+ public static final UnboundIntFlag MAX_TRIAL_TENANTS = defineIntFlag(
+ "max-trial-tenants", -1,
+ List.of("ogronnesby"), "2020-12-03", "2021-04-01",
+ "The maximum nr. of tenants with trial plan, -1 is unlimited",
+ "Takes effect immediately"
+ );
+
public static final UnboundBooleanFlag CONTROLLER_PROVISION_LB = defineFeatureFlag(
"controller-provision-lb", false,
List.of("mpolden"), "2020-12-02", "2021-02-01",