diff options
16 files changed, 280 insertions, 10 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index 64a2906b1be..413a7dbc62e 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -121,6 +121,11 @@ public interface ModelContext { // TODO(bjorncs): Temporary feature flag default double feedCoreThreadPoolSizeFactor() { return 1.0; } + + default Quota quota() { + return Quota.empty(); + } + } } diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java b/config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java new file mode 100644 index 00000000000..cb600bc0a5e --- /dev/null +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/Quota.java @@ -0,0 +1,72 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.model.api; + +import com.yahoo.slime.Inspector; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; + +import java.util.Objects; +import java.util.Optional; + +/** + * Quota for the application deployed. If the application exceeds this quota, deployment will fail. + * + * @author ogronnesby + */ +public class Quota { + private final Optional<Integer> maxClusterSize; + private final Optional<Integer> budget; + + public Quota(Optional<Integer> maybeClusterSize, Optional<Integer> budget) { + this.maxClusterSize = maybeClusterSize; + this.budget = budget; + } + + public static Quota fromSlime(Inspector inspector) { + var clusterSize = SlimeUtils.optionalLong(inspector.field("clusterSize")); + var budget = SlimeUtils.optionalLong(inspector.field("budget")); + return new Quota(clusterSize.map(Long::intValue), budget.map(Long::intValue)); + } + + public Slime toSlime() { + var slime = new Slime(); + var root = slime.setObject(); + maxClusterSize.ifPresent(clusterSize -> root.setLong("clusterSize", clusterSize)); + budget.ifPresent(b -> root.setLong("budget", b)); + return slime; + } + + public static Quota empty() { + return new Quota(Optional.empty(), Optional.empty()); + } + + public Optional<Integer> maxClusterSize() { + return maxClusterSize; + } + + public Optional<Integer> budget() { + return budget; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Quota quota = (Quota) o; + return Objects.equals(maxClusterSize, quota.maxClusterSize) && + Objects.equals(budget, quota.budget); + } + + @Override + public int hashCode() { + return Objects.hash(maxClusterSize, budget); + } + + @Override + public String toString() { + return "Quota{" + + "maxClusterSize=" + maxClusterSize + + ", budget=" + budget + + '}'; + } +} diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java index abe14fbed32..fc799449379 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java @@ -7,6 +7,7 @@ import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.Quota; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.HostName; @@ -46,6 +47,7 @@ public class TestProperties implements ModelContext.Properties { private Optional<EndpointCertificateSecrets> endpointCertificateSecrets = Optional.empty(); private AthenzDomain athenzDomain; private ApplicationRoles applicationRoles; + private Quota quota = Quota.empty(); @Override public boolean multitenant() { return multitenant; } @Override public ApplicationId applicationId() { return applicationId; } @@ -78,6 +80,7 @@ public class TestProperties implements ModelContext.Properties { @Override public boolean skipCommunicationManagerThread() { return false; } @Override public boolean skipMbusRequestThread() { return false; } @Override public boolean skipMbusReplyThread() { return false; } + @Override public Quota quota() { return quota; } public TestProperties setJvmGCOptions(String gcOptions) { jvmGCOptions = gcOptions; @@ -164,6 +167,11 @@ public class TestProperties implements ModelContext.Properties { return this; } + public TestProperties setQuota(Quota quota) { + this.quota = quota; + return this; + } + public static class Spec implements ConfigServerSpec { private final String hostName; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java new file mode 100644 index 00000000000..6670b7ce94a --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java @@ -0,0 +1,38 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.application.validation; + +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.vespa.model.VespaModel; + +import java.util.stream.Collectors; + +/** + * Checks that the generated model does not have resources that exceeds the given quota. + * + * @author ogronnesby + */ +public class QuotaValidator extends Validator { + @Override + public void validate(VespaModel model, DeployState deployState) { + var quota = deployState.getProperties().quota(); + quota.maxClusterSize().ifPresent(maxClusterSize -> validateMaxClusterSize(maxClusterSize, model)); + } + + /** Check that all clusters in the application do not exceed the quota max cluster size. */ + private void validateMaxClusterSize(int maxClusterSize, VespaModel model) { + var invalidClusters = model.allClusters().stream() + .filter(clusterId -> { + var cluster = model.provisioned().all().get(clusterId); + var clusterSize = cluster.maxResources().nodes(); + return clusterSize > maxClusterSize; + }) + .map(ClusterSpec.Id::value) + .collect(Collectors.toList()); + + if (!invalidClusters.isEmpty()) { + var clusterNames = String.join(", ", invalidClusters); + throw new IllegalArgumentException("Clusters " + clusterNames + " exceeded max cluster size of " + maxClusterSize); + } + } +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java index 22dd0289390..f3ccc2d3447 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java @@ -61,6 +61,7 @@ public class Validation { new AccessControlFilterValidator().validate(model, deployState); new CloudWatchValidator().validate(model, deployState); new AwsAccessControlValidator().validate(model, deployState); + new QuotaValidator().validate(model, deployState); List<ConfigChangeAction> result = Collections.emptyList(); if (deployState.getProperties().isFirstTimeDeployment()) { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java new file mode 100644 index 00000000000..f8aa3fd5298 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java @@ -0,0 +1,52 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.application.validation; + +import com.yahoo.config.model.api.Quota; +import com.yahoo.config.model.deploy.TestProperties; +import com.yahoo.config.provision.Environment; +import org.junit.Test; + +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author ogronnesby + */ +public class QuotaValidatorTest { + + private final Quota quota = new Quota(Optional.of(5), Optional.empty()); + + @Test + public void test_deploy_under_quota() { + var tester = new ValidationTester(5, new TestProperties().setHostedVespa(true).setQuota(quota)); + tester.deploy(null, getServices("testCluster", 5), Environment.prod, null); + } + + @Test + public void test_deploy_above_quota() { + var tester = new ValidationTester(6, new TestProperties().setHostedVespa(true).setQuota(quota)); + try { + tester.deploy(null, getServices("testCluster", 6), Environment.prod, null); + fail(); + } catch (RuntimeException e) { + assertEquals("Clusters testCluster exceeded max cluster size of 5", e.getMessage()); + } + } + + private static String getServices(String contentClusterId, int nodeCount) { + return "<services version='1.0'>" + + " <content id='" + contentClusterId + "' version='1.0'>" + + " <redundancy>1</redundancy>" + + " <engine>" + + " <proton/>" + + " </engine>" + + " <documents>" + + " <document type='music' mode='index'/>" + + " </documents>" + + " <nodes count='" + nodeCount + "'/>" + + " </content>" + + "</services>"; + } +} diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java index 362a083993e..6961ffa682b 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java @@ -31,6 +31,7 @@ import static com.yahoo.config.model.test.MockApplicationPackage.MUSIC_SEARCHDEF */ public class ValidationTester { + private final TestProperties properties; private final InMemoryProvisioner hostProvisioner; /** Creates a validation tester with 1 node available */ @@ -38,14 +39,25 @@ public class ValidationTester { this(1); } + /** Creates a validation tester with number of nodes available and the given test properties */ + public ValidationTester(int nodeCount, TestProperties properties) { + this(new InMemoryProvisioner(nodeCount), properties); + } + + /** Creates a validation tester with a given host provisioner */ + public ValidationTester(InMemoryProvisioner hostProvisioner) { + this(hostProvisioner, new TestProperties().setHostedVespa(true)); + } + /** Creates a validation tester with a number of nodes available */ public ValidationTester(int nodeCount) { - this(new InMemoryProvisioner(nodeCount)); + this(new InMemoryProvisioner(nodeCount), new TestProperties().setHostedVespa(true)); } /** Creates a validation tester with a given host provisioner */ - public ValidationTester(InMemoryProvisioner hostProvisioner) { + public ValidationTester(InMemoryProvisioner hostProvisioner, TestProperties testProperties) { this.hostProvisioner = hostProvisioner; + this.properties = testProperties; } /** @@ -74,7 +86,7 @@ public class ValidationTester { environment, RegionName.defaultName())) .applicationPackage(newApp) - .properties(new TestProperties().setHostedVespa(true)) + .properties(properties) .modelHostProvisioner(hostProvisioner) .provisioned(provisioned) .now(now); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java index 815cc0a3a18..3d4198c65a9 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -14,6 +14,7 @@ import com.yahoo.config.model.api.HostProvisioner; import com.yahoo.config.model.api.Model; import com.yahoo.config.model.api.ModelContext; import com.yahoo.config.model.api.Provisioned; +import com.yahoo.config.model.api.Quota; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.DockerImage; @@ -162,6 +163,7 @@ public class ModelContextImpl implements ModelContext { private final Optional<AthenzDomain> athenzDomain; private final Optional<ApplicationRoles> applicationRoles; private final double feedCoreThreadPoolSizeFactor; + private final Quota quota; public Properties(ApplicationId applicationId, boolean multitenantFromConfig, @@ -177,7 +179,8 @@ public class ModelContextImpl implements ModelContext { FlagSource flagSource, Optional<EndpointCertificateSecrets> endpointCertificateSecrets, Optional<AthenzDomain> athenzDomain, - Optional<ApplicationRoles> applicationRoles) { + Optional<ApplicationRoles> applicationRoles, + Optional<Quota> maybeQuota) { this.applicationId = applicationId; this.multitenant = multitenantFromConfig || hostedVespa || Boolean.getBoolean("multitenant"); this.configServerSpecs = configServerSpecs; @@ -218,6 +221,7 @@ public class ModelContextImpl implements ModelContext { this.applicationRoles = applicationRoles; feedCoreThreadPoolSizeFactor = Flags.FEED_CORE_THREAD_POOL_SIZE_FACTOR.bindTo(flagSource) .with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()).value(); + this.quota = maybeQuota.orElseGet(Quota::empty); } @Override @@ -301,6 +305,8 @@ public class ModelContextImpl implements ModelContext { @Override public boolean skipMbusRequestThread() { return skipMbusRequestThread; } @Override public boolean skipMbusReplyThread() { return skipMbusReplyThread; } @Override public double feedCoreThreadPoolSizeFactor() { return feedCoreThreadPoolSizeFactor; } + + @Override public Quota quota() { return quota; } } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java index 6b42ca7fa95..7fc6b35722f 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java @@ -149,7 +149,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> { .flatMap(new EndpointCertificateRetriever(secretStore)::readEndpointCertificateSecrets), zkClient.readAthenzDomain(), new ApplicationRolesStore(curator, TenantRepository.getTenantPath(tenant)) - .readApplicationRoles(applicationId)); + .readApplicationRoles(applicationId), + zkClient.readQuota()); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java index 34974c00a84..1fea966503b 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.model.api.ApplicationRoles; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.EndpointCertificateMetadata; +import com.yahoo.config.model.api.Quota; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.DockerImage; @@ -42,6 +43,7 @@ public final class PrepareParams { static final String ATHENZ_DOMAIN = "athenzDomain"; static final String APPLICATION_HOST_ROLE = "applicationHostRole"; static final String APPLICATION_CONTAINER_ROLE = "applicationContainerRole"; + static final String QUOTA_PARAM_NAME = "quota"; private final ApplicationId applicationId; private final TimeoutBudget timeoutBudget; @@ -56,13 +58,14 @@ public final class PrepareParams { private final Optional<DockerImage> dockerImageRepository; private final Optional<AthenzDomain> athenzDomain; private final Optional<ApplicationRoles> applicationRoles; + private final Optional<Quota> quota; private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors, boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion, List<ContainerEndpoint> containerEndpoints, Optional<String> tlsSecretsKeyName, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, Optional<DockerImage> dockerImageRepository, Optional<AthenzDomain> athenzDomain, - Optional<ApplicationRoles> applicationRoles) { + Optional<ApplicationRoles> applicationRoles, Optional<Quota> quota) { this.timeoutBudget = timeoutBudget; this.applicationId = Objects.requireNonNull(applicationId); this.ignoreValidationErrors = ignoreValidationErrors; @@ -76,6 +79,7 @@ public final class PrepareParams { this.dockerImageRepository = dockerImageRepository; this.athenzDomain = athenzDomain; this.applicationRoles = applicationRoles; + this.quota = quota; } public static class Builder { @@ -93,6 +97,7 @@ public final class PrepareParams { private Optional<DockerImage> dockerImageRepository = Optional.empty(); private Optional<AthenzDomain> athenzDomain = Optional.empty(); private Optional<ApplicationRoles> applicationRoles = Optional.empty(); + private Optional<Quota> quota = Optional.empty(); public Builder() { } @@ -187,11 +192,18 @@ public final class PrepareParams { return this; } + public Builder quota(String serialized) { + this.quota = (serialized == null) + ? Optional.empty() + : Optional.of(Quota.fromSlime(SlimeUtils.jsonToSlime(serialized).get())); + return this; + } + public PrepareParams build() { return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun, verbose, isBootstrap, vespaVersion, containerEndpoints, tlsSecretsKeyName, endpointCertificateMetadata, dockerImageRepository, athenzDomain, - applicationRoles); + applicationRoles, quota); } } @@ -208,6 +220,7 @@ public final class PrepareParams { .dockerImageRepository(request.getProperty(DOCKER_IMAGE_REPOSITORY)) .athenzDomain(request.getProperty(ATHENZ_DOMAIN)) .applicationRoles(ApplicationRoles.fromString(request.getProperty(APPLICATION_HOST_ROLE), request.getProperty(APPLICATION_CONTAINER_ROLE))) + .quota(request.getProperty(QUOTA_PARAM_NAME)) .build(); } @@ -278,4 +291,8 @@ public final class PrepareParams { public Optional<ApplicationRoles> applicationRoles() { return applicationRoles; } + + public Optional<Quota> quota() { + return quota; + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java index 35bbc1a8233..7b40184e72a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -17,6 +17,7 @@ import com.yahoo.config.model.api.EndpointCertificateMetadata; import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.FileDistribution; import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.Quota; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; @@ -216,7 +217,8 @@ public class SessionPreparer { currentActiveApplicationSet.isEmpty(), flagSource, endpointCertificateSecrets, - athenzDomain, applicationRoles); + athenzDomain, applicationRoles, + params.quota()); this.fileDistributionProvider = fileDistributionFactory.createProvider(serverDbSessionDir); this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry, permanentApplicationPackage, @@ -286,7 +288,8 @@ public class SessionPreparer { logger, prepareResult.getFileRegistries(), prepareResult.allocatedHosts(), - athenzDomain); + athenzDomain, + params.quota()); checkTimeout("write state to zookeeper"); } @@ -335,7 +338,8 @@ public class SessionPreparer { DeployLogger deployLogger, Map<Version, FileRegistry> fileRegistryMap, AllocatedHosts allocatedHosts, - Optional<AthenzDomain> athenzDomain) { + Optional<AthenzDomain> athenzDomain, + Optional<Quota> quota) { ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger); try { zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts); @@ -345,6 +349,7 @@ public class SessionPreparer { zooKeeperClient.writeVespaVersion(vespaVersion); zooKeeperClient.writeDockerImageRepository(dockerImageRepository); zooKeeperClient.writeAthenzDomain(athenzDomain); + zooKeeperClient.writeQuota(quota); } catch (RuntimeException | IOException e) { zkDeployer.cleanup(); throw new RuntimeException("Error preparing session", e); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java index d28a322a05a..ff168e102b9 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java @@ -7,12 +7,15 @@ import com.yahoo.config.FileReference; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.model.api.Quota; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.SlimeUtils; import com.yahoo.text.Utf8; import com.yahoo.transaction.Transaction; import com.yahoo.vespa.config.server.UserConfigDefinitionRepo; @@ -25,10 +28,13 @@ import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.transaction.CuratorOperations; import com.yahoo.vespa.curator.transaction.CuratorTransaction; +import java.io.ByteArrayOutputStream; import java.time.Instant; import java.util.Optional; import java.util.logging.Level; +import static com.yahoo.yolean.Exceptions.uncheck; + /** * Zookeeper client for a specific session. Path for a session is /config/v2/tenants/<tenant>/sessions/<sessionid> * Can be used to read and write session status and create and get prepare and active barrier. @@ -47,6 +53,7 @@ public class SessionZooKeeperClient { private static final String CREATE_TIME_PATH = "createTime"; private static final String DOCKER_IMAGE_REPOSITORY_PATH = "dockerImageRepository"; private static final String ATHENZ_DOMAIN = "athenzDomain"; + private static final String QUOTA_PATH = "quota"; private final Curator curator; private final ConfigCurator configCurator; private final TenantName tenantName; @@ -179,6 +186,10 @@ public class SessionZooKeeperClient { return sessionPath.append(ATHENZ_DOMAIN).getAbsolute(); } + private String quotaPath() { + return sessionPath.append(QUOTA_PATH).getAbsolute(); + } + public void writeVespaVersion(Version version) { configCurator.putData(versionPath(), version.toString()); } @@ -240,6 +251,20 @@ public class SessionZooKeeperClient { .map(AthenzDomain::from); } + public void writeQuota(Optional<Quota> maybeQuota) { + maybeQuota.ifPresent(quota -> { + var bytes = uncheck(() -> SlimeUtils.toJsonBytes(quota.toSlime())); + configCurator.putData(quotaPath(), bytes); + }); + } + + public Optional<Quota> readQuota() { + if ( ! configCurator.exists(quotaPath())) return Optional.empty(); + return Optional.ofNullable(configCurator.getData(quotaPath())) + .map(SlimeUtils::jsonToSlime) + .map(slime -> Quota.fromSlime(slime.get())); + } + /** * Create necessary paths atomically for a new session. * diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java index 158b8ea55d2..9b827293516 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java @@ -63,6 +63,7 @@ public class ModelContextImplTest { flagSource, null, Optional.empty(), + Optional.empty(), Optional.empty()), Optional.empty(), Optional.empty(), diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java index bf21f800815..941f2726b0e 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java @@ -84,6 +84,15 @@ public class PrepareParamsTest { assertEquals("containerRole", applicationRoles.get().applicationContainerRole()); } + @Test + public void testQuotaParsing() { + var quotaParam = "{\"clusterSize\": 23, \"budget\": 23232323}"; + var quotaEncoded = URLEncoder.encode(quotaParam, StandardCharsets.UTF_8); + var prepareParams = createParams(request + "&" + PrepareParams.QUOTA_PARAM_NAME + "=" + quotaEncoded, TenantName.from("foo")); + assertEquals(23, (int) prepareParams.quota().get().maxClusterSize().get()); + assertEquals(23232323, (int) prepareParams.quota().get().budget().get()); + } + // Create PrepareParams from a request (based on uri and tenant name) private static PrepareParams createParams(String uri, TenantName tenantName) { return PrepareParams.fromHttpRequest( diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java index de55a6677ff..cc1137ad9d8 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.config.server.session; import com.yahoo.config.FileReference; +import com.yahoo.config.model.api.Quota; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; @@ -17,8 +18,10 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import java.time.Instant; +import java.util.Optional; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -137,6 +140,14 @@ public class SessionZooKeeperClientTest { assertThat(zkc.readApplicationPackageReference(), is(testRef)); } + @Test + public void require_quota_written_and_parsed() { + var quota = Optional.of(new Quota(Optional.of(23), Optional.of(32))); + var zkc = createSessionZKClient(4); + zkc.writeQuota(quota); + assertEquals(quota, zkc.readQuota()); + } + private void assertApplicationIdParse(long sessionId, String idString, String expectedIdString) { SessionZooKeeperClient zkc = createSessionZKClient(sessionId); String path = sessionPath(sessionId).append(SessionZooKeeperClient.APPLICATION_ID_PATH).getAbsolute(); diff --git a/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java b/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java index 5084e6554cb..51a4fc167c7 100644 --- a/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java +++ b/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java @@ -124,6 +124,13 @@ public class SlimeUtils { return Optional.of(inspector.asString()).filter(s -> !s.isEmpty()); } + public static Optional<Long> optionalLong(Inspector inspector) { + if (inspector.type() == Type.LONG) { + return Optional.of(inspector.asLong()); + } + return Optional.empty(); + } + public static Iterator<Inspector> entriesIterator(Inspector inspector) { return new Iterator<>() { private int current = 0; |