summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorOla Aunrønning <olaa@verizonmedia.com>2019-10-23 10:02:31 +0200
committerGitHub <noreply@github.com>2019-10-23 10:02:31 +0200
commit4296052d01bec2d87021cb352cdd1c64377b3ee6 (patch)
tree72fb38984070b1bc9de6be06183bd211740d73a1 /controller-server
parent3b70a26194dd1856c4d9ee4b87c979d5f22198c4 (diff)
parenta2ac8ec8bae6fb2889e36fcbedbdf712bf68df67 (diff)
Merge pull request #11002 from vespa-engine/olaa/tenant-cost
Added TenantCost
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java51
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java46
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json20
4 files changed, 108 insertions, 15 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 5d83f4848e5..29806f1355c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -8,6 +8,7 @@ import com.google.inject.Inject;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
@@ -52,6 +53,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationV
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo;
import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringInfo;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
@@ -92,6 +94,7 @@ import javax.ws.rs.NotAuthorizedException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.math.RoundingMode;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.DigestInputStream;
@@ -100,7 +103,8 @@ import java.security.PublicKey;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
-import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Comparator;
@@ -112,7 +116,6 @@ import java.util.Scanner;
import java.util.Set;
import java.util.StringJoiner;
import java.util.logging.Level;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.yahoo.jdisc.Response.Status.BAD_REQUEST;
@@ -367,10 +370,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse tenantCost(Tenant tenant, HttpRequest request) {
+ Set<YearMonth> months = controller.serviceRegistry().tenantCost().monthsWithMetering(tenant.name());
+
var slime = new Slime();
var objectCursor = slime.setObject();
var monthsCursor = objectCursor.setArray("months");
+ months.forEach(month -> monthsCursor.addString(month.toString()));
return new SlimeJsonResponse(slime);
}
@@ -380,22 +386,37 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
.orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist"));
}
- private LocalDate tenantCostParseDate(String dateString) {
- var monthPattern = Pattern.compile("^(?<year>[0-9]{4})-(?<month>[0-9]{2})$");
- var matcher = monthPattern.matcher(dateString);
-
- if (matcher.matches()) {
- var year = Integer.parseInt(matcher.group("year"));
- var month = Integer.parseInt(matcher.group("month"));
- return LocalDate.of(year, month, 1);
- } else {
+ private YearMonth tenantCostParseDate(String dateString) {
+ try {
+ return YearMonth.parse(dateString);
+ } catch (DateTimeParseException e){
throw new IllegalArgumentException("Could not parse year-month '" + dateString + "'");
}
}
- private HttpResponse tenantCost(Tenant tenant, LocalDate month, HttpRequest request) {
+ private HttpResponse tenantCost(Tenant tenant, YearMonth month, HttpRequest request) {
var slime = new Slime();
- slime.setObject();
+ Cursor cursor = slime.setObject();
+ cursor.setString("month", month.toString());
+ List<CostInfo> costInfos = controller.serviceRegistry().tenantCost()
+ .getTenantCostOfMonth(tenant.name(), month);
+ Cursor array = cursor.setArray("items");
+
+ costInfos.forEach(costInfo -> {
+ Cursor costObject = array.addObject();
+ costObject.setString("applicationId", costInfo.getApplicationId().serializedForm());
+ costObject.setString("zoneId", costInfo.getZoneId().value());
+ Cursor cpu = costObject.setObject("cpu");
+ cpu.setDouble("usage", costInfo.getCpuHours().setScale(1, RoundingMode.HALF_UP).doubleValue());
+ cpu.setLong("charge", costInfo.getCpuCost());
+ Cursor memory = costObject.setObject("memory");
+ memory.setDouble("usage", costInfo.getMemoryHours().setScale(1, RoundingMode.HALF_UP).doubleValue());
+ memory.setLong("charge", costInfo.getMemoryCost());
+ Cursor disk = costObject.setObject("disk");
+ disk.setDouble("usage", costInfo.getDiskHours().setScale(1, RoundingMode.HALF_UP).doubleValue());
+ disk.setLong("charge", costInfo.getDiskCost());
+ });
+
return new SlimeJsonResponse(slime);
}
@@ -1128,7 +1149,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Slime slime = new Slime();
Cursor root = slime.setObject();
- MeteringInfo meteringInfo = controller.serviceRegistry().meteringService().getResourceSnapshots(tenant, application);
+ MeteringInfo meteringInfo = controller.serviceRegistry()
+ .meteringService()
+ .getResourceSnapshots(TenantName.from(tenant), ApplicationName.from(application));
ResourceAllocation currentSnapshot = meteringInfo.getCurrentSnapshot();
Cursor currentRate = root.setObject("currentrate");
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
index 965e1db0e2e..416f2ee89ad 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
@@ -28,6 +28,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueH
import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer;
import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringClient;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.MockTenantCost;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.TenantCost;
import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator;
@@ -69,6 +71,7 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
private final ApplicationStoreMock applicationStoreMock = new ApplicationStoreMock();
private final MockRunDataStore mockRunDataStore = new MockRunDataStore();
private final MockBuildService mockBuildService = new MockBuildService();
+ private final MockTenantCost mockTenantCost = new MockTenantCost();
@Override
public ConfigServer configServer() {
@@ -170,6 +173,9 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg
return memoryNameService;
}
+ @Override
+ public TenantCost tenantCost() { return mockTenantCost;}
+
public ZoneRegistryMock zoneRegistryMock() {
return zoneRegistryMock;
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index 09fd804ea0c..36366a34fa5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -40,7 +40,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo;
import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringInfo;
+import com.yahoo.vespa.hosted.controller.api.integration.resource.MockTenantCost;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMeteringClient;
@@ -75,10 +77,13 @@ import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.math.BigDecimal;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
+import java.time.LocalDate;
+import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
@@ -87,6 +92,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeSet;
import java.util.function.Supplier;
import static com.yahoo.application.container.handler.Request.Method.DELETE;
@@ -200,7 +206,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// GET cost for a month for a tenant
tester.assertResponse(request("/application/v4/tenant/tenant1/cost/2018-01", GET).userIdentity(USER_ID).oktaAccessToken(OKTA_AT),
- "{}");
+ "{\"month\":\"2018-01\",\"items\":[]}");
// Add another Athens domain, so we can try to create more tenants
createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN_2, USER_ID); // New domain to test tenant w/property ID
@@ -1072,6 +1078,44 @@ public class ApplicationApiTest extends ControllerContainerTest {
}
@Test
+ public void testTenantCostResponse() {
+ ApplicationId applicationId = createTenantAndApplication();
+ MockTenantCost mockTenantCost = (MockTenantCost) controllerTester.containerTester().serviceRegistry().tenantCost();
+
+ mockTenantCost.setMonthsWithMetering(
+ new TreeSet<>(Set.of(
+ YearMonth.of(2019, 10),
+ YearMonth.of(2019, 9)
+ ))
+ );
+
+ tester.assertResponse(request("/application/v4/tenant/" + applicationId.tenant().value() + "/cost", GET)
+ .userIdentity(USER_ID)
+ .oktaAccessToken(OKTA_AT),
+ "{\"months\":[\"2019-09\",\"2019-10\"]}");
+
+ CostInfo costInfo1 = new CostInfo(applicationId, ZoneId.from("prod", "us-south-1"),
+ new BigDecimal("7.0"),
+ new BigDecimal("600.0"),
+ new BigDecimal("1000.0"),
+ 35, 23, 10);
+ CostInfo costInfo2 = new CostInfo(applicationId, ZoneId.from("prod", "us-north-1"),
+ new BigDecimal("2.0"),
+ new BigDecimal("3.0"),
+ new BigDecimal("4.0"),
+ 10, 20, 30);
+
+ mockTenantCost.setCostInfoList(
+ List.of(costInfo1, costInfo2)
+ );
+
+ tester.assertResponse(request("/application/v4/tenant/" + applicationId.tenant().value() + "/cost/2019-09", GET)
+ .userIdentity(USER_ID)
+ .oktaAccessToken(OKTA_AT),
+ new File("cost-report.json"));
+ }
+
+ @Test
public void testErrorResponses() throws Exception {
tester.computeVersionStatus();
createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json
new file mode 100644
index 00000000000..8f6dbf17d51
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json
@@ -0,0 +1,20 @@
+{
+ "month": "2019-09",
+ "items": [
+ {
+ "applicationId":"tenant1:application1:instance1",
+ "zoneId":"prod.us-south-1",
+ "cpu": {"usage":7.0,"charge":35},
+ "memory": {"usage":600.0,"charge":23},
+ "disk": {"usage":1000.0,"charge":10}
+ },
+ {
+ "applicationId":"tenant1:application1:instance1",
+ "zoneId":"prod.us-north-1",
+ "cpu": {"usage":2.0,"charge":10},
+ "memory": {"usage":3.0,"charge":20},
+ "disk": {"usage":4.0,"charge":30}
+ }
+ ]
+
+} \ No newline at end of file