diff options
10 files changed, 258 insertions, 18 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java index f545cb8ff47..dec5d9264a7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java @@ -17,6 +17,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer; 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.TenantCost; import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator; @@ -70,4 +71,6 @@ public interface ServiceRegistry { // TODO: No longer used. Remove this once untangled from test code BuildService buildService(); + TenantCost tenantCost(); + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java new file mode 100644 index 00000000000..50c257acd23 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java @@ -0,0 +1,69 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.resource; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.zone.ZoneId; + +import java.math.BigDecimal; + +/** + * @author olaa + */ +public class CostInfo { + + private final ApplicationId applicationId; + private final ZoneId zoneId; + private final BigDecimal cpuHours; + private final BigDecimal memoryHours; + private final BigDecimal diskHours; + private final int cpuCost; + private final int memoryCost; + private final int diskCost; + + + public CostInfo(ApplicationId applicationId, ZoneId zoneId, + BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, + int cpuCost, int memoryCost, int diskCost) { + this.applicationId = applicationId; + this.zoneId = zoneId; + this.cpuHours = cpuHours; + this.memoryHours = memoryHours; + this.diskHours = diskHours; + this.cpuCost = cpuCost; + this.memoryCost = memoryCost; + this.diskCost = diskCost; + } + + public ApplicationId getApplicationId() { + return applicationId; + } + + public ZoneId getZoneId() { + return zoneId; + } + + public BigDecimal getCpuHours() { + return cpuHours; + } + + public BigDecimal getMemoryHours() { + return memoryHours; + } + + public BigDecimal getDiskHours() { + return diskHours; + } + + public int getCpuCost() { + return cpuCost; + } + + public int getMemoryCost() { + return memoryCost; + } + + public int getDiskCost() { + return diskCost; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MeteringClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MeteringClient.java index 632dbaad419..3e06b24c6be 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MeteringClient.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MeteringClient.java @@ -1,8 +1,10 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.api.integration.resource; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; + import java.util.Collection; -import java.util.List; /** * Consumes and retrieves snapshots of resources allocated per application. @@ -13,6 +15,6 @@ public interface MeteringClient { void consume(Collection<ResourceSnapshot> resources); - MeteringInfo getResourceSnapshots(String tenantName, String applicationName); + MeteringInfo getResourceSnapshots(TenantName tenantName, ApplicationName applicationName); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MockTenantCost.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MockTenantCost.java new file mode 100644 index 00000000000..03ba44e04c7 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/MockTenantCost.java @@ -0,0 +1,37 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.resource; + +import com.yahoo.config.provision.TenantName; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * @author olaa + */ +public class MockTenantCost implements TenantCost { + + private Set<YearMonth> monthsOfMetering = Collections.emptySet(); + private List<CostInfo> costInfoList = Collections.emptyList(); + + @Override + public Set<YearMonth> monthsWithMetering(TenantName tenantName) { + return monthsOfMetering; + } + + @Override + public List<CostInfo> getTenantCostOfMonth(TenantName tenantName, YearMonth month) { + return costInfoList; + } + + public void setMonthsWithMetering(Set<YearMonth> monthsOfMetering) { + this.monthsOfMetering = monthsOfMetering; + } + + public void setCostInfoList(List<CostInfo> costInfoList) { + this.costInfoList = costInfoList; + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/TenantCost.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/TenantCost.java new file mode 100644 index 00000000000..b4ca0cd7076 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/TenantCost.java @@ -0,0 +1,34 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.resource; + +import com.yahoo.config.provision.TenantName; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * @author olaa + */ +public interface TenantCost { + + Set<YearMonth> monthsWithMetering(TenantName tenantName); + + List<CostInfo> getTenantCostOfMonth(TenantName tenantName, YearMonth month); + + static TenantCost empty() { + return new TenantCost() { + @Override + public Set<YearMonth> monthsWithMetering(TenantName tenantName) { + return Collections.emptySet(); + } + + @Override + public List<CostInfo> getTenantCostOfMonth(TenantName tenantName, YearMonth month) { + return Collections.emptyList(); + } + }; + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMeteringClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMeteringClient.java index 10e1eb39c8a..45ead36f622 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMeteringClient.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMeteringClient.java @@ -1,6 +1,8 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.api.integration.stubs; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; 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; @@ -26,7 +28,7 @@ public class MockMeteringClient implements MeteringClient { } @Override - public MeteringInfo getResourceSnapshots(String tenantName, String applicationName) { + public MeteringInfo getResourceSnapshots(TenantName tenantName, ApplicationName applicationName) { return meteringInfo.orElseGet(() -> { ResourceAllocation emptyAllocation = new ResourceAllocation(0, 0, 0); return new MeteringInfo(emptyAllocation, emptyAllocation, emptyAllocation, Collections.emptyMap()); 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 |