summaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/pricing/PricingApiHandler.java
diff options
context:
space:
mode:
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.java151
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) {
+
+ }
+
}