summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorHarald Musum <musum@yahooinc.com>2023-10-20 14:36:41 +0200
committerHarald Musum <musum@yahooinc.com>2023-10-21 07:54:25 +0200
commit72ce49c81b93126a1136f2a4bcb8b7a76a5ca013 (patch)
tree37c8e940004f64ac2fa0fe538749f36ce354db8b /controller-server
parent80d99d306cdf46eed3ff41ab5783b9f3a5dcf5df (diff)
Move PricingApiHandler and related code to internal repo
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java218
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java185
3 files changed, 0 insertions, 407 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java
deleted file mode 100644
index 8ca2936eee7..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.pricing;
-
-import com.yahoo.collections.Pair;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.pricing.ApplicationResources;
-import com.yahoo.vespa.hosted.controller.api.integration.pricing.PriceInformation;
-import com.yahoo.vespa.hosted.controller.api.integration.pricing.Prices;
-import com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingController;
-import com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.logging.Logger;
-
-import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
-import static com.yahoo.restapi.ErrorResponse.methodNotAllowed;
-import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel;
-import static java.math.BigDecimal.ZERO;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * API for calculating price information
- *
- * @author hmusum
- */
-@SuppressWarnings("unused") // Handler
-public class PricingApiHandler extends ThreadedHttpRequestHandler {
-
- private static final Logger log = Logger.getLogger(PricingApiHandler.class.getName());
-
- private final Controller controller;
- private final PricingController pricingController;
-
- @Inject
- public PricingApiHandler(Context parentCtx, Controller controller, PricingController pricingController) {
- super(parentCtx);
- this.controller = controller;
- this.pricingController = pricingController;
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- if (request.getMethod() != GET)
- return methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
-
- try {
- return handleGET(request);
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse handleGET(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/pricing/v1/pricing")) return pricing(request);
-
- return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
- request.getUri().getPath()));
- }
-
- private HttpResponse pricing(HttpRequest request) {
- String rawQuery = request.getUri().getRawQuery();
- var priceParameters = parseQuery(rawQuery);
- Prices price = calculatePrice(priceParameters);
- return response(price, priceParameters);
- }
-
- private Prices calculatePrice(PriceParameters priceParameters) {
- return pricingController.priceForApplications(priceParameters.appResources, priceParameters.pricingInfo, priceParameters.plan);
- }
-
- private PriceParameters parseQuery(String rawQuery) {
- if (rawQuery == null) throw new IllegalArgumentException("No price information found in query");
- List<String> elements = Arrays.stream(URLDecoder.decode(rawQuery, UTF_8).split("&")).toList();
- return parseQuery(elements);
- }
-
- private PriceParameters parseQuery(List<String> elements) {
- var supportLevel = SupportLevel.BASIC;
- var enclave = false;
- var committedSpend = ZERO;
- var applicationName = "default";
- var plan = controller.serviceRegistry().planRegistry().defaultPlan(); // fallback to default plan if not supplied
- List<ApplicationResources> appResources = new ArrayList<>();
-
- for (Pair<String, String> entry : keysAndValues(elements)) {
- var value = entry.getSecond();
- switch (entry.getFirst().toLowerCase()) {
- case "committedspend" -> committedSpend = new BigDecimal(value);
- case "planid" -> plan = plan(value).orElseThrow(() -> new IllegalArgumentException("Unknown plan id " + value));
- case "supportlevel" -> supportLevel = SupportLevel.valueOf(value.toUpperCase());
- case "application" -> appResources.add(applicationResources(value));
- default -> throw new IllegalArgumentException("Unknown query parameter '" + entry.getFirst() + '\'');
- }
- }
-
- PricingInfo pricingInfo = new PricingInfo(supportLevel, committedSpend);
- return new PriceParameters(List.of(), pricingInfo, plan, appResources);
- }
-
- private ApplicationResources applicationResources(String appResourcesString) {
- List<String> elements = List.of(appResourcesString.split(","));
-
- var vcpu = ZERO;
- var memoryGb = ZERO;
- var diskGb = ZERO;
- var gpuMemoryGb = ZERO;
- var enclaveVcpu = ZERO;
- var enclaveMemoryGb = ZERO;
- var enclaveDiskGb = ZERO;
- var enclaveGpuMemoryGb = ZERO;
-
- for (var element : keysAndValues(elements)) {
- var value = element.getSecond();
- switch (element.getFirst().toLowerCase()) {
- case "vcpu" -> vcpu = new BigDecimal(value);
- case "memorygb" -> memoryGb = new BigDecimal(value);
- case "diskgb" -> diskGb = new BigDecimal(value);
- case "gpumemorygb" -> gpuMemoryGb = new BigDecimal(value);
-
- case "enclavevcpu" -> enclaveVcpu = new BigDecimal(value);
- case "enclavememorygb" -> enclaveMemoryGb = new BigDecimal(value);
- case "enclavediskgb" -> enclaveDiskGb = new BigDecimal(value);
- case "enclavegpumemorygb" -> enclaveGpuMemoryGb = new BigDecimal(value);
-
- default -> throw new IllegalArgumentException("Unknown key '" + element.getFirst() + '\'');
- }
- }
-
- return new ApplicationResources(vcpu, memoryGb, diskGb, gpuMemoryGb,
- enclaveVcpu, enclaveMemoryGb, enclaveDiskGb, enclaveGpuMemoryGb);
- }
-
- private List<Pair<String, String>> keysAndValues(List<String> elements) {
- return elements.stream().map(element -> {
- var index = element.indexOf("=");
- if (index <= 0 || index == element.length() - 1)
- throw new IllegalArgumentException("Error in query parameter, expected '=' between key and value: '" + element + '\'');
- return new Pair<>(element.substring(0, index), element.substring(index + 1));
- })
- .toList();
- }
-
- private Optional<Plan> plan(String element) {
- return controller.serviceRegistry().planRegistry().plan(element);
- }
-
- private static SlimeJsonResponse response(Prices prices, PriceParameters priceParameters) {
- var slime = new Slime();
- Cursor cursor = slime.setObject();
-
- var applicationsArray = cursor.setArray("applications");
- applicationPrices(applicationsArray, prices.priceInformationApplications(), priceParameters);
-
- var priceInfoArray = cursor.setArray("priceInfo");
- addItem(priceInfoArray, "Enclave (minimum $10k per month)", prices.totalPriceInformation().enclaveDiscount());
- addItem(priceInfoArray, "Committed spend", prices.totalPriceInformation().committedAmountDiscount());
-
- setBigDecimal(cursor, "totalAmount", prices.totalPriceInformation().totalAmount());
-
- return new SlimeJsonResponse(slime);
- }
-
- private static void applicationPrices(Cursor applicationPricesArray, List<PriceInformation> applicationPrices, PriceParameters priceParameters) {
- applicationPrices.forEach(priceInformation -> {
- var element = applicationPricesArray.addObject();
- var array = element.setArray("priceInfo");
- addItem(array, supportLevelDescription(priceParameters), priceInformation.listPriceWithSupport());
- addItem(array, "Enclave", priceInformation.enclaveDiscount());
- addItem(array, "Volume discount", priceInformation.volumeDiscount());
- });
- }
-
- private static String supportLevelDescription(PriceParameters priceParameters) {
- String supportLevel = priceParameters.pricingInfo.supportLevel().name();
- return supportLevel.substring(0,1).toUpperCase() + supportLevel.substring(1).toLowerCase() + " support unit price";
- }
-
- private static void addItem(Cursor array, String name, BigDecimal amount) {
- if (amount.compareTo(BigDecimal.ZERO) != 0) {
- var o = array.addObject();
- o.setString("description", name);
- setBigDecimal(o, "amount", amount);
- }
- }
-
- private static void setBigDecimal(Cursor cursor, String name, BigDecimal value) {
- cursor.setString(name, value.setScale(2, RoundingMode.HALF_UP).toPlainString());
- }
-
- private record PriceParameters(List<ClusterResources> clusterResources, PricingInfo pricingInfo, Plan plan,
- List<ApplicationResources> appResources) {
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index 6103b715744..3ada598f4f8 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -77,7 +77,6 @@ public class ControllerContainerTest {
<component id='com.yahoo.vespa.hosted.controller.Controller'/>
<component id='com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock'/>
<component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>
- <component id='com.yahoo.vespa.hosted.controller.api.integration.MockPricingController'/>
<component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository'/>
<component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement'/>
<component id='com.yahoo.vespa.hosted.controller.integration.SecretStoreMock'/>
@@ -118,9 +117,6 @@ public class ControllerContainerTest {
<handler id='com.yahoo.vespa.hosted.controller.restapi.changemanagement.ChangeManagementApiHandler'>
<binding>http://localhost/changemanagement/v1/*</binding>
</handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.pricing.PricingApiHandler'>
- <binding>http://localhost/pricing/v1/*</binding>
- </handler>
%s
</container>
""".formatted(system().value(), variablePartXml());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java
deleted file mode 100644
index f2ce0dfeef2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.pricing;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
-import org.junit.jupiter.api.Test;
-
-import java.net.URLEncoder;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel.BASIC;
-import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel.COMMERCIAL;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author hmusum
- */
-public class PricingApiHandlerTest extends ControllerContainerCloudTest {
-
- @Test
- void testPricingInfoBasic() {
- tester().assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0"),
- """
- { "applications": [ ], "priceInfo": [ ], "totalAmount": "0.00" }
- """,
- 200);
-
- var request = request("/pricing/v1/pricing?" + urlEncodedPriceInformation1App(BASIC));
- tester().assertJsonResponse(request, """
- {
- "applications": [
- {
- "priceInfo": [
- {"description": "Basic support unit price", "amount": "4.30"},
- {"description": "Volume discount", "amount": "-0.10"}
- ]
- }
- ],
- "priceInfo": [
- {"description": "Committed spend", "amount": "-0.20"}
- ],
- "totalAmount": "4.00"
- }
- """,
- 200);
- }
-
- @Test
- void testPricingInfoBasicEnclave() {
- var request = request("/pricing/v1/pricing?" + urlEncodedPriceInformation1AppEnclave(BASIC));
- tester().assertJsonResponse(request, """
- {
- "applications": [
- {
- "priceInfo": [
- {"description": "Basic support unit price", "amount": "4.30"},
- {"description": "Enclave", "amount": "-0.15"},
- {"description": "Volume discount", "amount": "-0.10"}
- ]
- }
- ],
- "priceInfo": [
- {"description": "Enclave (minimum $10k per month)", "amount": "10.15"},
- {"description": "Committed spend", "amount": "-0.20"}
- ],
- "totalAmount": "3.85"
- }
- """,
- 200);
- }
-
- @Test
- void testPricingInfoCommercialEnclave() {
- var request = request("/pricing/v1/pricing?" + urlEncodedPriceInformation1AppEnclave(COMMERCIAL));
- tester().assertJsonResponse(request, """
- {
- "applications": [
- {
- "priceInfo": [
- {"description": "Commercial support unit price", "amount": "13.30"},
- {"description": "Enclave", "amount": "-0.15"},
- {"description": "Volume discount", "amount": "-0.10"}
- ]
- }
- ],
- "priceInfo": [
- {"description": "Enclave (minimum $10k per month)", "amount": "1.15"},
- {"description": "Committed spend", "amount": "-0.20"}
- ],
- "totalAmount": "12.85"
- }
- """,
- 200);
- }
-
- @Test
- void testPricingInfoCommercialEnclave2Apps() {
- var request = request("/pricing/v1/pricing?" + urlEncodedPriceInformation2AppsEnclave(COMMERCIAL));
- tester().assertJsonResponse(request, """
- {
- "applications": [
- {
- "priceInfo": [
- {"description": "Commercial support unit price", "amount": "13.30"},
- {"description": "Enclave", "amount": "-0.15"},
- {"description": "Volume discount", "amount": "-0.10"}
- ]
- },
- {
- "priceInfo": [
- {"description": "Commercial support unit price", "amount": "13.30"},
- {"description": "Enclave", "amount": "-0.15"},
- {"description": "Volume discount", "amount": "-0.10"}
- ]
- }
- ],
- "priceInfo": [ ],
- "totalAmount": "26.10"
- }
- """,
- 200);
- }
-
- @Test
- void testInvalidRequests() {
- ContainerTester tester = tester();
- tester.assertJsonResponse(request("/pricing/v1/pricing"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No price information found in query\"}",
- 400);
- tester.assertJsonResponse(request("/pricing/v1/pricing?"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error in query parameter, expected '=' between key and value: ''\"}",
- 400);
- tester.assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0&resources"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error in query parameter, expected '=' between key and value: 'resources'\"}",
- 400);
- tester.assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0&resources="),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error in query parameter, expected '=' between key and value: 'resources='\"}",
- 400);
- tester.assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0&key=value"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unknown query parameter 'key'\"}",
- 400);
- tester.assertJsonResponse(request("/pricing/v1/pricing?supportLevel=basic&committedSpend=0&application=key%3Dvalue"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unknown key 'key'\"}",
- 400);
- }
-
- private ContainerTester tester() {
- ContainerTester tester = new ContainerTester(container, null);
- assertEquals(SystemName.Public, tester.controller().system());
- return tester;
- }
-
- /**
- * 1 app, with 2 clusters (with total resources for all clusters with each having
- * 1 node, with 4 vcpu, 8 Gb memory, 100 Gb disk and no GPU,
- * price will be 20000 + 2000 + 200
- */
- String urlEncodedPriceInformation1App(PricingInfo.SupportLevel supportLevel) {
- return "application=" + URLEncoder.encode("vcpu=4,memoryGb=8,diskGb=100,gpuMemoryGb=0", UTF_8) +
- "&supportLevel=" + supportLevel.name().toLowerCase() + "&committedSpend=20";
- }
-
- /**
- * 1 app, with 2 clusters (with total resources for all clusters with each having
- * 1 node, with 4 vcpu, 8 Gb memory, 100 Gb disk and no GPU,
- * price will be 20000 + 2000 + 200
- */
- String urlEncodedPriceInformation1AppEnclave(PricingInfo.SupportLevel supportLevel) {
- return "application=" + URLEncoder.encode("enclaveVcpu=4,enclaveMemoryGb=8,enclaveDiskGb=100,enclaveGpuMemoryGb=0", UTF_8) +
- "&supportLevel=" + supportLevel.name().toLowerCase() + "&committedSpend=20";
- }
-
- /**
- * 2 apps, with 1 cluster (with total resources for all clusters with each having
- * 1 node, with 4 vcpu, 8 Gb memory, 100 Gb disk and no GPU,
- */
- String urlEncodedPriceInformation2AppsEnclave(PricingInfo.SupportLevel supportLevel) {
- return "application=" + URLEncoder.encode("enclaveVcpu=4,enclaveMemoryGb=8,enclaveDiskGb=100,enclaveGpuMemoryGb=0", UTF_8) +
- "&application=" + URLEncoder.encode("enclaveVcpu=4,enclaveMemoryGb=8,enclaveDiskGb=100,enclaveGpuMemoryGb=0", UTF_8) +
- "&supportLevel=" + supportLevel.name().toLowerCase() + "&committedSpend=0";
- }
-
-}