diff options
author | Ola Aunrønning <ola.aunroe@gmail.com> | 2023-10-20 18:13:27 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-20 18:13:27 +0200 |
commit | 16b0ac7203d6433e631de558e30622b297879acb (patch) | |
tree | b1ecbe268a94310302c88b9520d88e08d027622c | |
parent | 46bd10fe1fbbbbf388155bf72e73ab26fbd0bfab (diff) |
Revert "Move PricingApiHandler and related code to internal repo"
10 files changed, 584 insertions, 0 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java new file mode 100644 index 00000000000..b451df87727 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java @@ -0,0 +1,64 @@ +// 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.api.integration; + +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 java.math.BigDecimal; +import java.util.List; + +import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel.BASIC; +import static java.math.BigDecimal.ZERO; + +public class MockPricingController implements PricingController { + + private static final BigDecimal cpuCost = new BigDecimal("1.00"); + private static final BigDecimal memoryCost = new BigDecimal("0.10"); + private static final BigDecimal diskCost = new BigDecimal("0.005"); + + @Override + public Prices priceForApplications(List<ApplicationResources> applicationResources, PricingInfo pricingInfo, Plan plan) { + List<PriceInformation> appPrices = applicationResources.stream() + .map(resources -> { + BigDecimal listPrice = resources.vcpu().multiply(cpuCost) + .add(resources.memoryGb().multiply(memoryCost)) + .add(resources.diskGb().multiply(diskCost)) + .add(resources.enclaveVcpu().multiply(cpuCost)) + .add(resources.enclaveMemoryGb().multiply(memoryCost)) + .add(resources.enclaveDiskGb().multiply(diskCost)); + + BigDecimal supportLevelCost = pricingInfo.supportLevel() == BASIC ? new BigDecimal("-1.00") : new BigDecimal("8.00"); + BigDecimal listPriceWithSupport = listPrice.add(supportLevelCost); + BigDecimal enclaveDiscount = isEnclave(resources) ? new BigDecimal("-0.15") : BigDecimal.ZERO; + BigDecimal volumeDiscount = new BigDecimal("-0.1"); + BigDecimal appTotalAmount = listPrice.add(supportLevelCost).add(enclaveDiscount).add(volumeDiscount); + + return new PriceInformation(listPriceWithSupport, + volumeDiscount, + ZERO, + enclaveDiscount, + appTotalAmount); + }) + .toList(); + + PriceInformation sum = PriceInformation.sum(appPrices); + System.out.println(pricingInfo.committedHourlyAmount()); + var committedAmountDiscount = pricingInfo.committedHourlyAmount().compareTo(ZERO) > 0 ? new BigDecimal("-0.2") : ZERO; + var totalAmount = sum.totalAmount().add(committedAmountDiscount); + var enclave = ZERO; + if (applicationResources.stream().anyMatch(ApplicationResources::enclave) && totalAmount.compareTo(new BigDecimal("14.00")) < 0) + enclave = new BigDecimal("14.00").subtract(totalAmount); + var totalPrice = new PriceInformation(ZERO, ZERO, committedAmountDiscount, enclave, totalAmount); + + return new Prices(appPrices, totalPrice); + } + + private static boolean isEnclave(ApplicationResources resources) { + return resources.enclaveVcpu().compareTo(ZERO) > 0; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/ApplicationResources.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/ApplicationResources.java new file mode 100644 index 00000000000..106d9ab6bbe --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/ApplicationResources.java @@ -0,0 +1,33 @@ +package com.yahoo.vespa.hosted.controller.api.integration.pricing; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ZERO; + +/** + * @param vcpu vcpus summed over all clusters, instances, zones + * @param memoryGb memory in Gb summed over all clusters, instances, zones + * @param diskGb disk in Gb summed over all clusters, instances, zones + * @param gpuMemoryGb GPU memory in Gb summed over all clusters, instances, zones + * @param enclaveVcpu vcpus summed over all clusters, instances, zones + * @param enclaveMemoryGb memory in Gb summed over all clusters, instances, zones + * @param enclaveDiskGb disk in Gb summed over all clusters, instances, zones + * @param enclaveGpuMemoryGb GPU memory in Gb summed over all clusters, instances, zones + */ +public record ApplicationResources(BigDecimal vcpu, BigDecimal memoryGb, BigDecimal diskGb, + BigDecimal gpuMemoryGb, BigDecimal enclaveVcpu, BigDecimal enclaveMemoryGb, + BigDecimal enclaveDiskGb, BigDecimal enclaveGpuMemoryGb) { + + public static ApplicationResources create(BigDecimal vcpu, BigDecimal memoryGb, + BigDecimal diskGb, BigDecimal gpuMemoryGb) { + return new ApplicationResources(vcpu, memoryGb, diskGb, gpuMemoryGb, ZERO, ZERO, ZERO, ZERO); + } + + public static ApplicationResources createEnclave(BigDecimal vcpu, BigDecimal memoryGb, + BigDecimal diskGb, BigDecimal gpuMemoryGb) { + return new ApplicationResources(ZERO, ZERO, ZERO, ZERO, vcpu, memoryGb, diskGb, gpuMemoryGb); + } + + public boolean enclave() { return enclaveVcpu().compareTo(ZERO) > 0; } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PriceInformation.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PriceInformation.java new file mode 100644 index 00000000000..50463553f8e --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PriceInformation.java @@ -0,0 +1,29 @@ +// 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.api.integration.pricing; + +import java.math.BigDecimal; +import java.util.List; + +import static java.math.BigDecimal.ZERO; + +public record PriceInformation(BigDecimal listPriceWithSupport, BigDecimal volumeDiscount, + BigDecimal committedAmountDiscount, BigDecimal enclaveDiscount, BigDecimal totalAmount) { + + public static PriceInformation empty() { return new PriceInformation(ZERO, ZERO, ZERO, ZERO, ZERO); } + + public static PriceInformation sum(List<PriceInformation> priceInformationList) { + var result = PriceInformation.empty(); + for (var prices : priceInformationList) + result = result.add(prices); + return result; + } + + public PriceInformation add(PriceInformation priceInformation) { + return new PriceInformation(this.listPriceWithSupport().add(priceInformation.listPriceWithSupport()), + this.volumeDiscount().add(priceInformation.volumeDiscount()), + this.committedAmountDiscount().add(priceInformation.committedAmountDiscount()), + this.enclaveDiscount().add(priceInformation.enclaveDiscount()), + this.totalAmount().add(priceInformation.totalAmount())); + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/Prices.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/Prices.java new file mode 100644 index 00000000000..650a07c51e0 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/Prices.java @@ -0,0 +1,8 @@ +// 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.api.integration.pricing; + +import java.util.List; + +public record Prices(List<PriceInformation> priceInformationApplications, PriceInformation totalPriceInformation) { + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingController.java new file mode 100644 index 00000000000..7b082932588 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingController.java @@ -0,0 +1,24 @@ +// 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.api.integration.pricing; + +import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan; + +import java.util.List; + +/** + * A service that calculates price information based on cluster resources, plan, service level etc. + * + * @author hmusum + */ +public interface PricingController { + + /** + * + * @param applicationResources resources used by an application + * @param pricingInfo pricing info + * @param plan the plan to use for this calculation + * @return a PriceInformation instance + */ + Prices priceForApplications(List<ApplicationResources> applicationResources, PricingInfo pricingInfo, Plan plan); + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingInfo.java new file mode 100644 index 00000000000..ba6f1939fc5 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingInfo.java @@ -0,0 +1,14 @@ +// 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.api.integration.pricing; + +import java.math.BigDecimal; + +import static java.math.BigDecimal.ZERO; + +public record PricingInfo(SupportLevel supportLevel, BigDecimal committedHourlyAmount) { + + public enum SupportLevel { BASIC, COMMERCIAL, ENTERPRISE } + + public static PricingInfo empty() { return new PricingInfo(SupportLevel.COMMERCIAL, ZERO); } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/package-info.java new file mode 100644 index 00000000000..649ab2a80f4 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/package-info.java @@ -0,0 +1,5 @@ +// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.vespa.hosted.controller.api.integration.pricing; + +import com.yahoo.osgi.annotation.ExportPackage; 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 new file mode 100644 index 00000000000..8ca2936eee7 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java @@ -0,0 +1,218 @@ +// 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 3ada598f4f8..6103b715744 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,6 +77,7 @@ 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'/> @@ -117,6 +118,9 @@ 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 new file mode 100644 index 00000000000..f2ce0dfeef2 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandlerTest.java @@ -0,0 +1,185 @@ +// 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"; + } + +} |