diff options
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", |