diff options
Diffstat (limited to 'controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java')
-rw-r--r-- | controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java | 151 |
1 files changed, 122 insertions, 29 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 index b6b3c8584fd..2d22ef86dce 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// 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; @@ -16,25 +16,27 @@ 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.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 java.util.stream.Collectors; 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.lang.Double.parseDouble; import static java.lang.Integer.parseInt; +import static java.math.BigDecimal.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; /** @@ -46,7 +48,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class PricingApiHandler extends ThreadedHttpRequestHandler { private static final Logger log = Logger.getLogger(PricingApiHandler.class.getName()); - private static final BigDecimal SCALED_ZERO = BigDecimal.ZERO.setScale(2); private final Controller controller; @@ -80,14 +81,30 @@ public class PricingApiHandler extends ThreadedHttpRequestHandler { private HttpResponse pricing(HttpRequest request) { String rawQuery = request.getUri().getRawQuery(); - PriceInformation price = parseQuery(rawQuery); - return response(price); + var priceParameters = parseQuery(rawQuery); + PriceInformation price = calculatePrice(priceParameters); + return response(price, priceParameters); } - private PriceInformation parseQuery(String rawQuery) { - String[] elements = URLDecoder.decode(rawQuery, UTF_8).split("&"); - if (elements.length == 0) throw new IllegalArgumentException("no price information found in query"); + private PriceInformation calculatePrice(PriceParameters priceParameters) { + var priceCalculator = controller.serviceRegistry().pricingController(); + if (priceParameters.appResources == null) + return priceCalculator.price(priceParameters.clusterResources, priceParameters.pricingInfo, priceParameters.plan); + else + return priceCalculator.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(); + if (keysAndValues(elements).stream().map(Pair::getFirst).toList().contains("resources")) + return parseQueryLegacy(elements); + else + return parseQuery(elements); + } + + private PriceParameters parseQueryLegacy(List<String> elements) { var supportLevel = SupportLevel.BASIC; var enclave = false; var committedSpend = 0d; @@ -95,25 +112,49 @@ public class PricingApiHandler extends ThreadedHttpRequestHandler { List<ClusterResources> clusterResources = new ArrayList<>(); for (Pair<String, String> entry : keysAndValues(elements)) { - switch (entry.getFirst()) { - case "committedSpend" -> committedSpend = parseDouble(entry.getSecond()); + switch (entry.getFirst().toLowerCase()) { + case "committedspend" -> committedSpend = parseDouble(entry.getSecond()); case "enclave" -> enclave = Boolean.parseBoolean(entry.getSecond()); - case "planId" -> plan = plan(entry.getSecond()) + case "planid" -> plan = plan(entry.getSecond()) .orElseThrow(() -> new IllegalArgumentException("Unknown plan id " + entry.getSecond())); - case "supportLevel" -> supportLevel = SupportLevel.valueOf(entry.getSecond().toUpperCase()); + case "supportlevel" -> supportLevel = SupportLevel.valueOf(entry.getSecond().toUpperCase()); case "resources" -> clusterResources.add(clusterResources(entry.getSecond())); + default -> throw new IllegalArgumentException("Unknown query parameter '" + entry.getFirst() + '\''); } } - if (clusterResources.size() < 1) throw new IllegalArgumentException("No cluster resources found in query"); + if (clusterResources.isEmpty()) throw new IllegalArgumentException("No cluster resources found in query"); PricingInfo pricingInfo = new PricingInfo(enclave, supportLevel, committedSpend); - return controller.serviceRegistry().pricingController().price(clusterResources, pricingInfo, plan); + return new PriceParameters(clusterResources, pricingInfo, plan, null); + } + + private PriceParameters parseQuery(List<String> elements) { + var supportLevel = SupportLevel.BASIC; + var enclave = false; + var committedSpend = 0d; + 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)) { + switch (entry.getFirst().toLowerCase()) { + case "committedspend" -> committedSpend = parseDouble(entry.getSecond()); + case "planid" -> plan = plan(entry.getSecond()) + .orElseThrow(() -> new IllegalArgumentException("Unknown plan id " + entry.getSecond())); + case "supportlevel" -> supportLevel = SupportLevel.valueOf(entry.getSecond().toUpperCase()); + case "application" -> appResources.add(applicationResources(entry.getSecond())); + default -> throw new IllegalArgumentException("Unknown query parameter '" + entry.getFirst() + '\''); + } + } + if (appResources.isEmpty()) throw new IllegalArgumentException("No application resources found in query"); + + // TODO: enclave does not make sense in PricingInfo anymore, remove when legacy method is removed + PricingInfo pricingInfo = new PricingInfo(false, supportLevel, committedSpend); + return new PriceParameters(List.of(), pricingInfo, plan, appResources); } private ClusterResources clusterResources(String resourcesString) { - String[] elements = resourcesString.split(","); - if (elements.length == 0) - throw new IllegalArgumentException("nothing found in cluster resources: " + resourcesString); + List<String> elements = Arrays.stream(resourcesString.split(",")).toList(); var nodes = 0; var vcpu = 0d; @@ -122,12 +163,13 @@ public class PricingApiHandler extends ThreadedHttpRequestHandler { var gpuMemoryGb = 0d; for (var element : keysAndValues(elements)) { - switch (element.getFirst()) { + switch (element.getFirst().toLowerCase()) { case "nodes" -> nodes = parseInt(element.getSecond()); case "vcpu" -> vcpu = parseDouble(element.getSecond()); - case "memoryGb" -> memoryGb = parseDouble(element.getSecond()); - case "diskGb" -> diskGb = parseDouble(element.getSecond()); - case "gpuMemoryGb" -> gpuMemoryGb = parseDouble(element.getSecond()); + case "memorygb" -> memoryGb = parseDouble(element.getSecond()); + case "diskgb" -> diskGb = parseDouble(element.getSecond()); + case "gpumemorygb" -> gpuMemoryGb = parseDouble(element.getSecond()); + default -> throw new IllegalArgumentException("Unknown resource type '" + element.getFirst() + '\''); } } @@ -137,40 +179,91 @@ public class PricingApiHandler extends ThreadedHttpRequestHandler { return new ClusterResources(nodes, 1, nodeResources); } - private List<Pair<String, String>> keysAndValues(String[] elements) { - return Arrays.stream(elements).map(element -> { + private ApplicationResources applicationResources(String appResourcesString) { + List<String> elements = Arrays.stream(appResourcesString.split(",")).toList(); + + var applicationName = "default"; + var vcpu = 0d; + var memoryGb = 0d; + var diskGb = 0d; + var gpuMemoryGb = 0d; + var enclaveVcpu = 0d; + var enclaveMemoryGb = 0d; + var enclaveDiskGb = 0d; + var enclaveGpuMemoryGb = 0d; + + for (var element : keysAndValues(elements)) { + switch (element.getFirst().toLowerCase()) { + case "name" -> applicationName = element.getSecond(); + + case "vcpu" -> vcpu = parseDouble(element.getSecond()); + case "memorygb" -> memoryGb = parseDouble(element.getSecond()); + case "diskgb" -> diskGb = parseDouble(element.getSecond()); + case "gpumemorygb" -> gpuMemoryGb = parseDouble(element.getSecond()); + + case "enclavevcpu" -> enclaveVcpu = parseDouble(element.getSecond()); + case "enclavememorygb" -> enclaveMemoryGb = parseDouble(element.getSecond()); + case "enclavediskgb" -> enclaveDiskGb = parseDouble(element.getSecond()); + case "enclavegpumemorygb" -> enclaveGpuMemoryGb = parseDouble(element.getSecond()); + + default -> throw new IllegalArgumentException("Unknown key '" + element.getFirst() + '\''); + } + } + + return new ApplicationResources(applicationName, + valueOf(vcpu), valueOf(memoryGb), valueOf(diskGb), valueOf(gpuMemoryGb), + valueOf(enclaveVcpu), valueOf(enclaveMemoryGb), valueOf(enclaveDiskGb), valueOf(enclaveGpuMemoryGb)); + } + + private List<Pair<String, String>> keysAndValues(List<String> elements) { + return elements.stream().map(element -> { var index = element.indexOf("="); - if (index <= 0 ) throw new IllegalArgumentException("Error in query parameter, expected '=' between key and value: " + element); + 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)); }) - .collect(Collectors.toList()); + .toList(); } private Optional<Plan> plan(String element) { return controller.serviceRegistry().planRegistry().plan(element); } - private static SlimeJsonResponse response(PriceInformation priceInfo) { + private static SlimeJsonResponse response(PriceInformation priceInfo, PriceParameters priceParameters) { var slime = new Slime(); Cursor cursor = slime.setObject(); var array = cursor.setArray("priceInfo"); - addItem(array, "List price", priceInfo.listPrice()); + addItem(array, supportLevelDescription(priceParameters), priceInfo.listPriceWithSupport()); addItem(array, "Enclave discount", priceInfo.enclaveDiscount()); addItem(array, "Volume discount", priceInfo.volumeDiscount()); addItem(array, "Committed spend", priceInfo.committedAmountDiscount()); - cursor.setString("totalAmount", priceInfo.totalAmount().toPlainString()); + setBigDecimal(cursor, "totalAmount", priceInfo.totalAmount()); return new SlimeJsonResponse(slime); } + 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); - o.setString("amount", SCALED_ZERO.add(amount).toPlainString()); + 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) { + + } + } |