diff options
5 files changed, 59 insertions, 0 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..1bd1faf5dd1 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); + Map<TenantName, PlanId> getPlans(List<TenantName> tenants); + 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..3f241510ed6 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,14 @@ public class MockBillingController implements BillingController { } @Override + public Map<TenantName, PlanId> getPlans(List<TenantName> tenants) { + return tenants.stream().collect(Collectors.toMap( + (TenantName t) -> t, + (TenantName t) -> plans.getOrDefault(t, PlanId.from("trial")) + )); + } + + @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/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java index 6d2b2d58e78..f7b7f379129 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 @@ -7,9 +7,11 @@ 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.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; @@ -34,18 +36,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 = Flags.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()); @@ -62,6 +67,18 @@ 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.getPlans(tenantNames).values().stream() + .filter(trialPlanId::equals) + .count(); + + 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 5b33f989163..1da7bdeca19 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 @@ -7,6 +7,7 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.flags.InMemoryFlagSource; 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; @@ -19,6 +20,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; @@ -26,6 +28,8 @@ import static com.yahoo.application.container.handler.Request.Method.GET; import static com.yahoo.application.container.handler.Request.Method.PUT; import static com.yahoo.application.container.handler.Request.Method.POST; 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 { @@ -94,6 +98,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") @@ -108,6 +130,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/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index f087714896b..ab9dcd6415d 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -254,6 +254,12 @@ public class Flags { CONSOLE_USER_EMAIL ); + public static final UnboundIntFlag MAX_TRIAL_TENANTS = defineIntFlag( + "max-trial-tenants", -1, + "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, "Provision load balancer for controller cluster", |