diff options
author | Harald Musum <musum@yahooinc.com> | 2023-10-20 14:36:41 +0200 |
---|---|---|
committer | Harald Musum <musum@yahooinc.com> | 2023-10-21 07:54:25 +0200 |
commit | 72ce49c81b93126a1136f2a4bcb8b7a76a5ca013 (patch) | |
tree | 37c8e940004f64ac2fa0fe538749f36ce354db8b /controller-server | |
parent | 80d99d306cdf46eed3ff41ab5783b9f3a5dcf5df (diff) |
Move PricingApiHandler and related code to internal repo
Diffstat (limited to 'controller-server')
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"; - } - -} |