From b0a3f19fa2d8e6ffafdee93ca4c56ea572ef466a Mon Sep 17 00:00:00 2001 From: Ola Aunrønning Date: Wed, 23 Oct 2019 13:56:56 +0200 Subject: Revert "Revert "Added TenantCost"" --- .../restapi/application/ApplicationApiHandler.java | 51 ++++++++++++++++------ .../integration/ServiceRegistryMock.java | 6 +++ .../restapi/application/ApplicationApiTest.java | 46 ++++++++++++++++++- .../restapi/application/responses/cost-report.json | 20 +++++++++ 4 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/cost-report.json (limited to 'controller-server') 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 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("^(?[0-9]{4})-(?[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 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 @@ -1071,6 +1077,44 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("instance1-metering.json")); } + @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(); 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 -- cgit v1.2.3