diff options
3 files changed, 64 insertions, 1 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 958ded06c78..b0ec1a38824 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -44,7 +44,9 @@ enum PathGroup { /** Paths used by tenant administrators. */ tenantInfo(Matcher.tenant, Optional.of("/api"), - "/application/v4/tenant/{tenant}/application/"), + "/application/v4/tenant/{tenant}/application/", + "/application/v4/tenant/{tenant}/cost", + "/application/v4/tenant/{tenant}/cost/{date}"), tenantKeys(Matcher.tenant, Optional.of("/api"), 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 c37309b87ad..c36ffd384a8 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 @@ -100,6 +100,7 @@ import java.security.PublicKey; import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.util.Arrays; import java.util.Base64; import java.util.Comparator; @@ -110,6 +111,7 @@ 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; @@ -198,6 +200,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/user")) return authenticatedUser(request); if (path.matches("/application/v4/tenant")) return tenants(request); if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/cost")) return tenantCost(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/cost/{month}")) return tenantCost(path.get("tenant"), path.get("month"), request); if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), "default", request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploying(path.get("tenant"), path.get("application"), "default", request); @@ -354,6 +358,45 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(slime); } + private HttpResponse tenantCost(String tenantName, HttpRequest request) { + return controller.tenants().get(TenantName.from(tenantName)) + .map(tenant -> tenantCost(tenant, request)) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist")); + } + + private HttpResponse tenantCost(Tenant tenant, HttpRequest request) { + var slime = new Slime(); + var objectCursor = slime.setObject(); + var monthsCursor = objectCursor.setArray("months"); + + return new SlimeJsonResponse(slime); + } + + private HttpResponse tenantCost(String tenantName, String dateString, HttpRequest request) { + return controller.tenants().get(TenantName.from(tenantName)) + .map(tenant -> tenantCost(tenant, tenantCostParseDate(dateString), request)) + .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 { + throw new IllegalArgumentException("Could not parse year-month '" + dateString + "'"); + } + } + + private HttpResponse tenantCost(Tenant tenant, LocalDate month, HttpRequest request) { + var slime = new Slime(); + slime.setObject(); + return new SlimeJsonResponse(slime); + } + private HttpResponse applications(String tenantName, Optional<String> applicationName, HttpRequest request) { TenantName tenant = TenantName.from(tenantName); Slime slime = new Slime(); 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 9c957785606..a13bdb3e547 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 @@ -190,6 +190,13 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/", GET).userIdentity(USER_ID), new File("tenant-list.json")); + // GET list of months for a tenant + tester.assertResponse(request("/application/v4/tenant/tenant1/cost", GET).userIdentity(USER_ID).oktaAccessToken(OKTA_AT), + "{\"months\":[]}"); + + // 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), + "{}"); // 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 @@ -1120,6 +1127,17 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete instance 'tenant1.application1.instance1': Instance not found\"}", 404); + // GET cost of unknown tenant + tester.assertResponse(request("/application/v4/tenant/no-such-tenant/cost", GET).userIdentity(USER_ID).oktaAccessToken(OKTA_AT), + "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'no-such-tenant' does not exist\"}", 404); + + tester.assertResponse(request("/application/v4/tenant/no-such-tenant/cost/2018-01-01", GET).userIdentity(USER_ID).oktaAccessToken(OKTA_AT), + "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'no-such-tenant' does not exist\"}", 404); + + // GET cost with invalid date string + tester.assertResponse(request("/application/v4/tenant/tenant1/cost/not-a-valid-date", GET).userIdentity(USER_ID).oktaAccessToken(OKTA_AT), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not parse year-month 'not-a-valid-date'\"}", 400); + // DELETE tenant tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE) .userIdentity(USER_ID) |