diff options
Diffstat (limited to 'controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi')
50 files changed, 513 insertions, 638 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java index 4c4633df0ec..56844887caf 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.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; import com.yahoo.container.jdisc.HttpRequest; 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 16d862a66ef..5548928b9d0 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 @@ -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.application; import ai.vespa.hosted.api.Signatures; @@ -74,7 +74,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneToken; -import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; @@ -112,6 +111,7 @@ import com.yahoo.vespa.hosted.controller.notification.NotificationSource; import com.yahoo.vespa.hosted.controller.persistence.SupportAccessSerializer; import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses; import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService; +import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService.State; import com.yahoo.vespa.hosted.controller.routing.RoutingStatus; import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext; import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId; @@ -127,6 +127,8 @@ import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; import com.yahoo.vespa.hosted.controller.tenant.Email; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; +import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder; +import com.yahoo.vespa.hosted.controller.tenant.TaxId; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.TenantAddress; import com.yahoo.vespa.hosted.controller.tenant.TenantBilling; @@ -692,7 +694,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { var contact = root.setObject("contact"); contact.setString("name", billingContact.contact().name()); contact.setString("email", billingContact.contact().email().getEmailAddress()); + contact.setBool("emailVerified", billingContact.contact().email().isVerified()); contact.setString("phone", billingContact.contact().phone()); + root.setString("taxId", billingContact.getTaxId().value()); + root.setString("purchaseOrder", billingContact.getPurchaseOrder().value()); + root.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress()); toSlime(billingContact.address(), root); // will create "address" on the parent } @@ -702,15 +708,22 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private SlimeJsonResponse putTenantInfoBilling(CloudTenant cloudTenant, Inspector inspector) { var info = cloudTenant.info(); - var contact = info.billingContact().contact(); - var address = info.billingContact().address(); + var billing = info.billingContact(); + var contact = billing.contact(); + var address = billing.address(); - var mergedContact = updateTenantInfoContact(inspector.field("contact"), cloudTenant.name(), contact, false); - var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.billingContact().address()); + var mergedContact = updateBillingContact(inspector.field("contact"), cloudTenant.name(), contact); + var mergedAddress = updateTenantInfoAddress(inspector.field("address"), billing.address()); + var mergedTaxId = optional("taxId", inspector).map(TaxId::new).orElse(billing.getTaxId()); + var mergedPurchaseOrder = optional("purchaseOrder", inspector).map(PurchaseOrder::new).orElse(billing.getPurchaseOrder()); + var mergedInvoiceEmail = optional("invoiceEmail", inspector).map(mail -> new Email(mail, false)).orElse(billing.getInvoiceEmail()); var mergedBilling = info.billingContact() .withContact(mergedContact) - .withAddress(mergedAddress); + .withAddress(mergedAddress) + .withTaxId(mergedTaxId) + .withPurchaseOrder(mergedPurchaseOrder) + .withInvoiceEmail(mergedInvoiceEmail); var mergedInfo = info.withBilling(mergedBilling); @@ -763,6 +776,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { throw new IllegalArgumentException("'website' needs to be a valid address"); } } + if (! mergedInfo.billingContact().getInvoiceEmail().isBlank()) { + // TODO: Validate invoice email is set if collection method is INVOICE + if (! mergedInfo.billingContact().getInvoiceEmail().getEmailAddress().contains("@")) + throw new IllegalArgumentException("'Invoice email' needs to be an email address"); + } } private void toSlime(TenantAddress address, Cursor parentCursor) { @@ -779,11 +797,15 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private void toSlime(TenantBilling billingContact, Cursor parentCursor) { if (billingContact.isEmpty()) return; - Cursor addressCursor = parentCursor.setObject("billingContact"); - addressCursor.setString("name", billingContact.contact().name()); - addressCursor.setString("email", billingContact.contact().email().getEmailAddress()); - addressCursor.setString("phone", billingContact.contact().phone()); - toSlime(billingContact.address(), addressCursor); + Cursor billingCursor = parentCursor.setObject("billingContact"); + billingCursor.setString("name", billingContact.contact().name()); + billingCursor.setString("email", billingContact.contact().email().getEmailAddress()); + billingCursor.setBool("emailVerified", billingContact.contact().email().isVerified()); + billingCursor.setString("phone", billingContact.contact().phone()); + billingCursor.setString("taxId", billingContact.getTaxId().value()); + billingCursor.setString("purchaseOrder", billingContact.getPurchaseOrder().value()); + billingCursor.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress()); + toSlime(billingContact.address(), billingCursor); } private void toSlime(TenantContacts contacts, Cursor parentCursor) { @@ -892,15 +914,13 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { throw new IllegalArgumentException("All address fields must be set"); } - private TenantContact updateTenantInfoContact(Inspector insp, TenantName tenantName, TenantContact oldContact, boolean isBillingContact) { + private TenantContact updateBillingContact(Inspector insp, TenantName tenantName, TenantContact oldContact) { if (!insp.valid()) return oldContact; var mergedEmail = optional("email", insp) .filter(address -> !address.equals(oldContact.email().getEmailAddress())) .map(address -> { - if (isBillingContact) - return new Email(address, true); - controller.mailVerifier().sendMailVerification(tenantName, address, PendingMailVerification.MailType.TENANT_CONTACT); + controller.mailVerifier().sendMailVerification(tenantName, address, PendingMailVerification.MailType.BILLING); return new Email(address, false); }) .orElse(oldContact.email()); @@ -914,9 +934,15 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private TenantBilling updateTenantInfoBillingContact(Inspector insp, TenantName tenantName, TenantBilling oldContact) { if (!insp.valid()) return oldContact; + var taxId = optional("taxId", insp).map(TaxId::new).orElse(oldContact.getTaxId()); + var purchaseOrder = optional("purchaseOrder", insp).map(PurchaseOrder::new).orElse(oldContact.getPurchaseOrder()); + var invoiceEmail = optional("invoiceEmail", insp).map(mail -> new Email(mail, false)).orElse(oldContact.getInvoiceEmail()); return TenantBilling.empty() - .withContact(updateTenantInfoContact(insp, tenantName, oldContact.contact(), true)) - .withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address())); + .withContact(updateBillingContact(insp, tenantName, oldContact.contact())) + .withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address())) + .withTaxId(taxId) + .withPurchaseOrder(purchaseOrder) + .withInvoiceEmail(invoiceEmail); } private TenantContacts updateTenantInfoContacts(Inspector insp, TenantName tenantName, TenantContacts oldContacts) { @@ -964,27 +990,43 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { } private HttpResponse listTokens(String tenant, HttpRequest request) { - var tokens = controller.dataplaneTokenService().listTokens(TenantName.from(tenant)) - .stream().sorted(Comparator.comparing(DataplaneTokenVersions::tokenId)).toList(); Slime slime = new Slime(); Cursor tokensArray = slime.setObject().setArray("tokens"); - for (DataplaneTokenVersions token : tokens) { + controller.dataplaneTokenService().listTokensWithState(TenantName.from(tenant)).forEach((token, states) -> { Cursor tokenObject = tokensArray.addObject(); tokenObject.setString("id", token.tokenId().value()); + tokenObject.setLong("lastUpdatedMillis", token.lastUpdated().toEpochMilli()); Cursor fingerprintsArray = tokenObject.setArray("versions"); - var versions = token.tokenVersions().stream() - .sorted(Comparator.comparing(DataplaneTokenVersions.Version::creationTime)).toList(); - for (var tokenVersion : versions) { + for (var tokenVersion : token.tokenVersions()) { Cursor fingerprintObject = fingerprintsArray.addObject(); fingerprintObject.setString("fingerprint", tokenVersion.fingerPrint().value()); fingerprintObject.setString("created", tokenVersion.creationTime().toString()); fingerprintObject.setString("author", tokenVersion.author()); fingerprintObject.setString("expiration", tokenVersion.expiration().map(Instant::toString).orElse("none")); + String tokenState = tokenVersion.expiration().map(controller.clock().instant()::isAfter).orElse(false) + ? "expired" + : valueOf(states.get(tokenVersion.fingerPrint())); + fingerprintObject.setString("state", tokenState); } - } + states.forEach((print, state) -> { + if (state != State.REVOKING) return; + Cursor fingerprintObject = fingerprintsArray.addObject(); + fingerprintObject.setString("fingerprint", print.value()); + fingerprintObject.setString("state", valueOf(state)); + }); + }); return new SlimeJsonResponse(slime); } + private static String valueOf(DataplaneTokenService.State state) { + return switch (state) { + case UNUSED: yield "unused"; + case DEPLOYING: yield "deploying"; + case ACTIVE: yield "active"; + case REVOKING: yield "revoking"; + }; + } + private HttpResponse generateToken(String tenant, String tokenid, HttpRequest request) { var expiration = resolveExpiration(request).orElse(null); @@ -1032,6 +1074,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { cursor.setString("level", notificationLevelAsString(notification.level())); cursor.setString("type", notificationTypeAsString(notification.type())); if (!excludeMessages) { + cursor.setString("title", notification.title()); Cursor messagesArray = cursor.setArray("messages"); notification.messages().forEach(messagesArray::addString); } @@ -1055,6 +1098,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { case deployment: yield "deployment"; case feedBlock: yield "feedBlock"; case reindex: yield "reindex"; + case account: yield "account"; }; } @@ -1684,6 +1728,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { var mailType = switch (type) { case "contact" -> PendingMailVerification.MailType.TENANT_CONTACT; case "notifications" -> PendingMailVerification.MailType.NOTIFICATIONS; + case "billing" -> PendingMailVerification.MailType.BILLING; default -> throw new IllegalArgumentException("Unknown mail type " + type); }; @@ -1983,10 +2028,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { response.setString("region", deploymentId.zoneId().region().value()); addAvailabilityZone(response, deployment.zone()); var application = controller.applications().requireApplication(TenantAndApplicationId.from(deploymentId.applicationId())); - boolean includeAllEndpoints = request.getBooleanProperty("includeAllEndpoints") || - request.getBooleanProperty("includeLegacyEndpoints"); + boolean includeAllEndpoints = request.getBooleanProperty("includeAllEndpoints"); + boolean includeWeightedEndpoints = includeAllEndpoints || request.getBooleanProperty("includeWeightedEndpoints"); + boolean includeLegacyEndpoints = includeAllEndpoints || request.getBooleanProperty("includeLegacyEndpoints"); var endpointArray = response.setArray("endpoints"); - for (var endpoint : endpointsOf(deploymentId, application, includeAllEndpoints)) { + for (var endpoint : endpointsOf(deploymentId, application, includeLegacyEndpoints, includeWeightedEndpoints)) { toSlime(endpoint, endpointArray.addObject()); } response.setString("clusters", withPath(toPath(deploymentId) + "/clusters", request.getUri()).toString()); @@ -2061,19 +2107,15 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { metrics.instant().ifPresent(instant -> metricsObject.setLong("lastUpdated", instant.toEpochMilli())); } - private EndpointList endpointsOf(DeploymentId deploymentId, Application application, boolean includeHidden) { + private EndpointList endpointsOf(DeploymentId deploymentId, Application application, boolean includeLegacy, boolean includeWeighted) { EndpointList zoneEndpoints = controller.routing().readEndpointsOf(deploymentId).direct(); EndpointList declaredEndpoints = controller.routing().readDeclaredEndpointsOf(application).targets(deploymentId); EndpointList endpoints = zoneEndpoints.and(declaredEndpoints); - EndpointList generatedEndpoints = endpoints.generated(); - if (!includeHidden) { - // If we have generated endpoints, hide non-generated - if (!generatedEndpoints.isEmpty()) { - endpoints = endpoints.generated(); - } - // Hide legacy and weighted endpoints - endpoints = endpoints.not().legacy() - .not().scope(Endpoint.Scope.weighted); + if (!includeLegacy) { + endpoints = endpoints.not().legacy(); + } + if (!includeWeighted) { + endpoints = endpoints.not().scope(Endpoint.Scope.weighted); } return endpoints; } @@ -2223,7 +2265,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { Cursor array = slime.setObject().setArray("globalrotationoverride"); Optional<Endpoint> primaryEndpoint = controller.routing().readDeclaredEndpointsOf(deploymentId.applicationId()) .requiresRotation() - .primary(); + .first(); if (primaryEndpoint.isPresent()) { DeploymentRoutingContext context = controller.routing().of(deploymentId); RoutingStatus status = context.routingStatus(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java index 1cded3227a5..3bf2f070f97 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.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.application; import com.yahoo.container.jdisc.HttpResponse; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java index 0edfdb51055..18221d82e44 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.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.application; import com.yahoo.config.application.api.DeploymentSpec; @@ -519,6 +519,8 @@ class JobControllerApiHandlerHelper { run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli())); runObject.setString("status", nameOf(run.status())); toSlime(runObject, run.versions(), run.reason(), application); + run.cloudAccount().filter(account -> ! account.isUnspecified()) + .ifPresent(cloudAccount -> runObject.setObject("enclave").setString("cloudAccount", cloudAccount.value())); Cursor runStepsArray = runObject.setArray("steps"); run.steps().forEach((step, info) -> { Cursor runStepObject = runStepsArray.addObject(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java index a28f0e9733d..35eb495a564 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.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.application; import com.yahoo.container.jdisc.HttpRequest; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java index f45ef49402b..73f9db7165c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.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.application; import com.yahoo.container.jdisc.HttpResponse; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java index 56d82d286cd..2ff0c1ab05c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.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.athenz; import com.yahoo.component.annotation.Inject; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java deleted file mode 100644 index d29603c529c..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.restapi.billing; - -import com.yahoo.config.provision.TenantName; -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; -import com.yahoo.restapi.ErrorResponse; -import com.yahoo.restapi.JacksonJsonResponse; -import com.yahoo.restapi.MessageResponse; -import com.yahoo.restapi.Path; -import com.yahoo.restapi.SlimeJsonResponse; -import com.yahoo.restapi.StringResponse; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Inspector; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.vespa.hosted.controller.ApplicationController; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.TenantController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod; -import com.yahoo.vespa.hosted.controller.api.integration.billing.InstrumentOwner; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry; -import com.yahoo.vespa.hosted.controller.api.role.Role; -import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; -import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses; -import com.yahoo.vespa.hosted.controller.tenant.Tenant; -import com.yahoo.yolean.Exceptions; - -import java.io.IOException; -import java.math.BigDecimal; -import java.security.Principal; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.Executor; - -/** - * @author andreer - * @author olaa - */ -public class BillingApiHandler extends ThreadedHttpRequestHandler { - - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - private final BillingController billingController; - private final ApplicationController applicationController; - private final TenantController tenantController; - private final PlanRegistry planRegistry; - - public BillingApiHandler(Executor executor, - Controller controller) { - super(executor); - this.billingController = controller.serviceRegistry().billingController(); - this.planRegistry = controller.serviceRegistry().planRegistry(); - this.applicationController = controller.applications(); - this.tenantController = controller.tenants(); - } - - @Override - public HttpResponse handle(HttpRequest request) { - try { - Optional<String> userId = Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()).map(Principal::getName); - if (userId.isEmpty()) - return ErrorResponse.unauthorized("Must be authenticated to use this API"); - - Path path = new Path(request.getUri()); - return switch (request.getMethod()) { - case GET -> handleGET(request, path, userId.get()); - case PATCH -> handlePATCH(request, path, userId.get()); - case DELETE -> handleDELETE(path, userId.get()); - case POST -> handlePOST(path, request, userId.get()); - default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported"); - }; - } - catch (IllegalArgumentException e) { - return ErrorResponse.badRequest(Exceptions.toMessageString(e)); - } catch (Exception e) { - return ErrorResponses.logThrowing(request, log, e); - } - } - - private HttpResponse handleGET(HttpRequest request, Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/token")) return getToken(path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/instrument")) return getInstruments(path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/billing")) return getBilling(path.get("tenant"), request.getProperty("until")); - if (path.matches("/billing/v1/tenant/{tenant}/plan")) return getPlan(path.get("tenant")); - if (path.matches("/billing/v1/billing")) return getBillingAllTenants(request.getProperty("until")); - if (path.matches("/billing/v1/invoice/export")) return getAllBills(); - if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return getLineItems(path.get("tenant")); - if (path.matches("/billing/v1/plans")) return getPlans(); - return ErrorResponse.notFoundError("Nothing at " + path); - } - - private HttpResponse getAllBills() { - var bills = billingController.getBills(); - var headers = new String[]{ "ID", "Tenant", "From", "To", "CpuHours", "MemoryHours", "DiskHours", "Cpu", "Memory", "Disk", "Additional" }; - var rows = bills.stream() - .map(bill -> { - return new Object[] { - bill.id().value(), bill.tenant().value(), - bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE), - bill.getEndDate().format(DateTimeFormatter.ISO_LOCAL_DATE), - bill.sumCpuHours(), bill.sumMemoryHours(), bill.sumDiskHours(), - bill.sumCpuCost(), bill.sumMemoryCost(), bill.sumDiskCost(), - bill.sumAdditionalCost() - }; - }) - .toList(); - return new CsvResponse(headers, rows); - } - - private HttpResponse handlePATCH(HttpRequest request, Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/instrument")) return patchActiveInstrument(request, path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/plan")) return patchPlan(request, path.get("tenant")); - if (path.matches("/billing/v1/tenant/{tenant}/collection")) return patchCollectionMethod(request, path.get("tenant")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse handleDELETE(Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/instrument/{instrument}")) return deleteInstrument(path.get("tenant"), userId, path.get("instrument")); - if (path.matches("/billing/v1/invoice/line-item/{line-item-id}")) return deleteLineItem(path.get("line-item-id")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse handlePOST(Path path, HttpRequest request, String userId) { - if (path.matches("/billing/v1/invoice")) return createBill(request, userId); - if (path.matches("/billing/v1/invoice/{invoice-id}/status")) return setBillStatus(request, path.get("invoice-id"), userId); - if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return addLineItem(request, path.get("tenant"), userId); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse getPlan(String tenant) { - var plan = billingController.getPlan(TenantName.from(tenant)); - var slime = new Slime(); - var root = slime.setObject(); - root.setString("tenant", tenant); - root.setString("plan", plan.value()); - return new SlimeJsonResponse(slime); - } - - private HttpResponse patchPlan(HttpRequest request, String tenant) { - var tenantName = TenantName.from(tenant); - var slime = inspectorOrThrow(request); - var planId = PlanId.from(slime.field("plan").asString()); - var roles = requestRoles(request); - var isAccountant = roles.contains(Role.hostedAccountant()); - - var hasDeployments = hasDeployments(tenantName); - var result = billingController.setPlan(tenantName, planId, hasDeployments, isAccountant); - - if (result.isSuccess()) - return new StringResponse("Plan: " + planId.value()); - - return ErrorResponse.forbidden(result.getErrorMessage().orElse("Invalid plan change")); - } - - private HttpResponse patchCollectionMethod(HttpRequest request, String tenant) { - var tenantName = TenantName.from(tenant); - var slime = inspectorOrThrow(request); - var newMethod = slime.field("collection").valid() ? - slime.field("collection").asString().toUpperCase() : - slime.field("collectionMethod").asString().toUpperCase(); - if (newMethod.isEmpty()) return ErrorResponse.badRequest("No collection method specified"); - - try { - var result = billingController.setCollectionMethod(tenantName, CollectionMethod.valueOf(newMethod)); - if (result.isSuccess()) - return new StringResponse("Collection method updated to " + newMethod); - - return ErrorResponse.forbidden(result.getErrorMessage().orElse("Invalid collection method change")); - } catch (IllegalArgumentException iea){ - return ErrorResponse.badRequest("Invalid collection method: " + newMethod); - } - } - - private HttpResponse getBillingAllTenants(String until) { - try { - var untilDate = untilParameter(until); - var uncommittedBills = billingController.createUncommittedBills(untilDate); - - var slime = new Slime(); - var root = slime.setObject(); - root.setString("until", untilDate.format(DateTimeFormatter.ISO_DATE)); - var tenants = root.setArray("tenants"); - - tenantController.asList().stream().sorted(Comparator.comparing(Tenant::name)).forEach(tenant -> { - var bill = uncommittedBills.get(tenant.name()); - var tc = tenants.addObject(); - tc.setString("tenant", tenant.name().value()); - getPlanForTenant(tc, tenant.name()); - getCollectionForTenant(tc, tenant.name()); - renderCurrentUsage(tc.setObject("current"), bill); - renderAdditionalItems(tc.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenant.name())); - - billingController.getDefaultInstrument(tenant.name()).ifPresent(card -> - renderInstrument(tc.setObject("payment"), card) - ); - }); - - return new SlimeJsonResponse(slime); - } catch (DateTimeParseException e) { - return ErrorResponse.badRequest("Could not parse date: " + until); - } - } - - private void getCollectionForTenant(Cursor tc, TenantName tenant) { - var collection = billingController.getCollectionMethod(tenant); - tc.setString("collection", collection.name()); - } - - private HttpResponse addLineItem(HttpRequest request, String tenant, String userId) { - Inspector inspector = inspectorOrThrow(request); - - Optional<Bill.Id> billId = SlimeUtils.optionalString(inspector.field("billId")).map(Bill.Id::of); - - billingController.addLineItem( - TenantName.from(tenant), - getInspectorFieldOrThrow(inspector, "description"), - new BigDecimal(getInspectorFieldOrThrow(inspector, "amount")), - billId, - userId); - - return new MessageResponse("Added line item for tenant " + tenant); - } - - private HttpResponse setBillStatus(HttpRequest request, String billId, String userId) { - Inspector inspector = inspectorOrThrow(request); - String status = getInspectorFieldOrThrow(inspector, "status"); - billingController.updateBillStatus(Bill.Id.of(billId), userId, status); - return new MessageResponse("Updated status of invoice " + billId); - } - - private HttpResponse createBill(HttpRequest request, String userId) { - Inspector inspector = inspectorOrThrow(request); - TenantName tenantName = TenantName.from(getInspectorFieldOrThrow(inspector, "tenant")); - - LocalDate startDate = LocalDate.parse(getInspectorFieldOrThrow(inspector, "startTime")); - LocalDate endDate = LocalDate.parse(getInspectorFieldOrThrow(inspector, "endTime")); - - var billId = billingController.createBillForPeriod(tenantName, startDate, endDate, userId); - - Slime slime = new Slime(); - Cursor root = slime.setObject(); - root.setString("message", "Created invoice with ID " + billId.value()); - root.setString("id", billId.value()); - return new SlimeJsonResponse(slime); - } - - private HttpResponse getInstruments(String tenant, String userId) { - var instrumentListResponse = billingController.listInstruments(TenantName.from(tenant), userId); - return new JacksonJsonResponse<>(200, instrumentListResponse); - } - - private HttpResponse getToken(String tenant, String userId) { - return new StringResponse(billingController.createClientToken(tenant, userId)); - } - - private HttpResponse getBilling(String tenant, String until) { - try { - var untilDate = untilParameter(until); - var tenantId = TenantName.from(tenant); - var slimeResponse = new Slime(); - var root = slimeResponse.setObject(); - - root.setString("until", untilDate.format(DateTimeFormatter.ISO_DATE)); - - getPlanForTenant(root, tenantId); - renderCurrentUsage(root.setObject("current"), getCurrentUsageForTenant(tenantId, untilDate)); - renderAdditionalItems(root.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenantId)); - renderBills(root.setArray("bills"), getBillsForTenant(tenantId)); - - billingController.getDefaultInstrument(tenantId).ifPresent( card -> - renderInstrument(root.setObject("payment"), card) - ); - - root.setString("collection", billingController.getCollectionMethod(tenantId).name()); - return new SlimeJsonResponse(slimeResponse); - } catch (DateTimeParseException e) { - return ErrorResponse.badRequest("Could not parse date: " + until); - } - } - - private HttpResponse getPlans() { - var slime = new Slime(); - var root = slime.setObject(); - var plans = root.setArray("plans"); - for (var plan : planRegistry.all()) { - var p = plans.addObject(); - p.setString("id", plan.id().value()); - p.setString("name", plan.displayName()); - } - return new SlimeJsonResponse(slime); - } - - private HttpResponse getLineItems(String tenant) { - var slimeResponse = new Slime(); - var root = slimeResponse.setObject(); - var lineItems = root.setArray("lineItems"); - - billingController.getUnusedLineItems(TenantName.from(tenant)) - .forEach(lineItem -> { - var itemCursor = lineItems.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - - return new SlimeJsonResponse(slimeResponse); - } - - private void getPlanForTenant(Cursor cursor, TenantName tenant) { - PlanId plan = billingController.getPlan(tenant); - cursor.setString("plan", plan.value()); - cursor.setString("planName", billingController.getPlanDisplayName(plan)); - } - - private void renderInstrument(Cursor cursor, PaymentInstrument instrument) { - cursor.setString("pi-id", instrument.getId()); - cursor.setString("type", instrument.getType()); - cursor.setString("brand", instrument.getBrand()); - cursor.setString("endingWith", instrument.getEndingWith()); - cursor.setString("expiryDate", instrument.getExpiryDate()); - cursor.setString("displayText", instrument.getDisplayText()); - cursor.setString("nameOnCard", instrument.getNameOnCard()); - cursor.setString("addressLine1", instrument.getAddressLine1()); - cursor.setString("addressLine2", instrument.getAddressLine2()); - cursor.setString("zip", instrument.getZip()); - cursor.setString("city", instrument.getCity()); - cursor.setString("state", instrument.getState()); - cursor.setString("country", instrument.getCountry()); - - } - - private void renderCurrentUsage(Cursor cursor, Bill currentUsage) { - if (currentUsage == null) return; - cursor.setString("amount", currentUsage.sum().toPlainString()); - cursor.setString("status", "accrued"); - cursor.setString("from", currentUsage.getStartDate().format(DATE_TIME_FORMATTER)); - var itemsCursor = cursor.setArray("items"); - currentUsage.lineItems().forEach(lineItem -> { - var itemCursor = itemsCursor.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - } - - private void renderAdditionalItems(Cursor cursor, List<Bill.LineItem> items) { - items.forEach(item -> { - renderLineItemToCursor(cursor.addObject(), item); - }); - } - - private Bill getCurrentUsageForTenant(TenantName tenant, LocalDate until) { - return billingController.createUncommittedBill(tenant, until); - } - - private List<Bill> getBillsForTenant(TenantName tenant) { - return billingController.getBillsForTenant(tenant); - } - - private void renderBills(Cursor cursor, List<Bill> bills) { - bills.forEach(bill -> { - var billCursor = cursor.addObject(); - renderBillToCursor(billCursor, bill); - }); - } - - private void renderBillToCursor(Cursor billCursor, Bill bill) { - billCursor.setString("id", bill.id().value()); - billCursor.setString("from", bill.getStartDate().format(DATE_TIME_FORMATTER)); - billCursor.setString("to", bill.getEndDate().format(DATE_TIME_FORMATTER)); - - billCursor.setString("amount", bill.sum().toString()); - billCursor.setString("status", bill.status()); - var statusCursor = billCursor.setArray("statusHistory"); - renderStatusHistory(statusCursor, bill.statusHistory()); - - - var lineItemsCursor = billCursor.setArray("items"); - bill.lineItems().forEach(lineItem -> { - var itemCursor = lineItemsCursor.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - } - - private void renderStatusHistory(Cursor cursor, Bill.StatusHistory statusHistory) { - statusHistory.getHistory() - .entrySet() - .stream() - .forEach(entry -> { - var c = cursor.addObject(); - c.setString("at", entry.getKey().format(DATE_TIME_FORMATTER)); - c.setString("status", entry.getValue()); - }); - } - - private void renderLineItemToCursor(Cursor cursor, Bill.LineItem lineItem) { - cursor.setString("id", lineItem.id()); - cursor.setString("description", lineItem.description()); - cursor.setString("amount", lineItem.amount().toString()); - cursor.setString("plan", lineItem.plan()); - cursor.setString("planName", billingController.getPlanDisplayName(PlanId.from(lineItem.plan()))); - - lineItem.applicationId().ifPresent(appId -> { - cursor.setString("application", appId.application().value()); - cursor.setString("instance", appId.instance().value()); - }); - lineItem.zoneId().ifPresent(zoneId -> - cursor.setString("zone", zoneId.value()) - ); - - lineItem.getArchitecture().ifPresent(architecture -> { - cursor.setString("architecture", architecture.name()); - }); - - cursor.setLong("majorVersion", lineItem.getMajorVersion()); - - lineItem.getCpuHours().ifPresent(cpuHours -> - cursor.setString("cpuHours", cpuHours.toString()) - ); - lineItem.getMemoryHours().ifPresent(memoryHours -> - cursor.setString("memoryHours", memoryHours.toString()) - ); - lineItem.getDiskHours().ifPresent(diskHours -> - cursor.setString("diskHours", diskHours.toString()) - ); - lineItem.getGpuHours().ifPresent(gpuHours -> - cursor.setString("gpuHours", gpuHours.toString()) - ); - lineItem.getCpuCost().ifPresent(cpuCost -> - cursor.setString("cpuCost", cpuCost.toString()) - ); - lineItem.getMemoryCost().ifPresent(memoryCost -> - cursor.setString("memoryCost", memoryCost.toString()) - ); - lineItem.getDiskCost().ifPresent(diskCost -> - cursor.setString("diskCost", diskCost.toString()) - ); - lineItem.getGpuCost().ifPresent(gpuCost -> - cursor.setString("gpuCost", gpuCost.toString()) - ); - } - - private HttpResponse deleteInstrument(String tenant, String userId, String instrument) { - if (billingController.deleteInstrument(TenantName.from(tenant), userId, instrument)) { - return new StringResponse("OK"); - } else { - return ErrorResponse.forbidden("Cannot delete payment instrument you don't own"); - } - } - - private HttpResponse deleteLineItem(String lineItemId) { - billingController.deleteLineItem(lineItemId); - return new MessageResponse("Succesfully deleted line item " + lineItemId); - } - - private HttpResponse patchActiveInstrument(HttpRequest request, String tenant, String userId) { - var inspector = inspectorOrThrow(request); - String instrumentId = getInspectorFieldOrThrow(inspector, "active"); - InstrumentOwner paymentInstrument = new InstrumentOwner(TenantName.from(tenant), userId, instrumentId, true); - boolean success = billingController.setActivePaymentInstrument(paymentInstrument); - return success ? new StringResponse("OK") : ErrorResponse.internalServerError("Failed to patch active instrument"); - } - - private Inspector inspectorOrThrow(HttpRequest request) { - try { - return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get(); - } catch (IOException e) { - throw new IllegalArgumentException("Failed to parse request body"); - } - } - - private static String getInspectorFieldOrThrow(Inspector inspector, String field) { - if (!inspector.field(field).valid()) - throw new IllegalArgumentException("Field " + field + " cannot be null"); - return inspector.field(field).asString(); - } - - private LocalDate untilParameter(String until) { - if (until == null || until.isEmpty() || until.isBlank()) - return LocalDate.now(); - return LocalDate.parse(until); - } - - private boolean hasDeployments(TenantName tenantName) { - return applicationController.asList(tenantName) - .stream() - .flatMap(app -> app.instances().values() - .stream() - .flatMap(instance -> instance.deployments().values().stream()) - ) - .count() > 0; - } - - private static Set<Role> requestRoles(HttpRequest request) { - return Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME)) - .filter(SecurityContext.class::isInstance) - .map(SecurityContext.class::cast) - .map(SecurityContext::roles) - .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request")); - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java index c5fb1afbae8..85a77dcfa61 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.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.billing; import com.yahoo.config.provision.TenantName; @@ -12,26 +12,32 @@ import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; import com.yahoo.slime.Type; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.TenantController; import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; +import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporter; import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod; import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry; import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; +import com.yahoo.vespa.hosted.controller.api.integration.billing.StatusHistory; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses; +import com.yahoo.vespa.hosted.controller.tenant.BillingReference; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import java.math.BigDecimal; import java.time.Clock; +import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Comparator; @@ -51,6 +57,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler private final ApplicationController applications; private final TenantController tenants; private final BillingController billing; + private final BillingReporter billingReporter; private final PlanRegistry planRegistry; private final Clock clock; @@ -61,6 +68,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler this.billing = controller.serviceRegistry().billingController(); this.planRegistry = controller.serviceRegistry().planRegistry(); this.clock = controller.serviceRegistry().clock(); + this.billingReporter = controller.serviceRegistry().billingReporter(); } private static RestApi createRestApi(BillingApiHandlerV2 self) { @@ -82,9 +90,24 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler */ .addRoute(RestApi.route("/billing/v2/accountant") .get(self::accountant)) - .addRoute(RestApi.route("/billing/v2/accountant/preview/tenant/{tenant}") + .addRoute(RestApi.route("/billing/v2/accountant/preview") + .get(self::accountantPreview)) + .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}") + .get(self::accountantTenant)) + .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/preview") .get(self::previewBill) .post(Slime.class, self::createBill)) + .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/items") + .get(self::additionalItems) + .post(Slime.class, self::newAdditionalItem)) + .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/item/{item}") + .delete(self::deleteAdditionalItem)) + .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/plan") + .get(self::accountantTenantPlan) + .post(Slime.class, self::setAccountantTenantPlan)) + .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/collection") + .get(self::accountantTenantCollection) + .post(Slime.class, self::setAccountantTenantCollection)) .addRoute(RestApi.route("/billing/v2/accountant/bill/{invoice}/export") .put(Slime.class, self::putAccountantInvoiceExport)) .addRoute(RestApi.route("/billing/v2/accountant/plans") @@ -202,21 +225,39 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler // --------- ACCOUNTANT API ---------- private Slime accountant(RestApi.RequestContext requestContext) { - var untilAt = untilParameter(requestContext); - var usagePerTenant = billing.createUncommittedBills(untilAt); - var response = new Slime(); var tenantsResponse = response.setObject().setArray("tenants"); tenants.asList().stream().sorted(Comparator.comparing(Tenant::name)).forEach(tenant -> { - var usage = Optional.ofNullable(usagePerTenant.get(tenant.name())); var tenantResponse = tenantsResponse.addObject(); tenantResponse.setString("tenant", tenant.name().value()); toSlime(tenantResponse.setObject("plan"), planFor(tenant.name())); toSlime(tenantResponse.setObject("quota"), billing.getQuota(tenant.name())); tenantResponse.setString("collection", billing.getCollectionMethod(tenant.name()).name()); - tenantResponse.setString("lastBill", usage.map(Bill::getStartDate).map(DateTimeFormatter.ISO_DATE::format).orElse(null)); - tenantResponse.setString("unbilled", usage.map(Bill::sum).map(BigDecimal::toPlainString).orElse("0.00")); + tenantResponse.setString("lastBill", LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC).format(DateTimeFormatter.ISO_DATE)); + tenantResponse.setString("unbilled", "0.00"); + }); + + return response; + } + + private Slime accountantPreview(RestApi.RequestContext requestContext) { + var untilAt = untilParameter(requestContext); + var usagePerTenant = billing.createUncommittedBills(untilAt); + + var response = new Slime(); + var tenantsResponse = response.setObject().setArray("tenants"); + + usagePerTenant.entrySet().stream().sorted(Comparator.comparing(x -> x.getValue().sum())).forEachOrdered(x -> { + var tenant = x.getKey(); + var usage = x.getValue(); + var tenantResponse = tenantsResponse.addObject(); + tenantResponse.setString("tenant", tenant.value()); + toSlime(tenantResponse.setObject("plan"), planFor(tenant)); + toSlime(tenantResponse.setObject("quota"), billing.getQuota(tenant)); + tenantResponse.setString("collection", billing.getCollectionMethod(tenant).name()); + tenantResponse.setString("lastBill", usage.getStartDate().format(DateTimeFormatter.ISO_DATE)); + tenantResponse.setString("unbilled", usage.sum().toPlainString()); }); return response; @@ -265,17 +306,146 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler } private HttpResponse putAccountantInvoiceExport(RestApi.RequestContext ctx, Slime slime) { - var billId = ctx.attributes().get("invoice") - .map(id -> Bill.Id.of((String) id)) - .orElseThrow(() -> new RestApiException.BadRequest("Missing bill ID")); + var billId = Bill.Id.of(ctx.pathParameters().getStringOrThrow("invoice")); // TODO: try to find a way to retrieve the cloud tenant from BillingControllerImpl var bill = billing.getBill(billId); var cloudTenant = tenants.require(bill.tenant(), CloudTenant.class); var exportMethod = slime.get().field("method").asString(); - var result = billing.exportBill(bill, exportMethod, cloudTenant); - return new MessageResponse("Bill has been exported: " + result); + var result = billingReporter.exportBill(bill, exportMethod, cloudTenant); + + var responseSlime = new Slime(); + responseSlime.setObject().setString("invoiceId", result); + return new SlimeJsonResponse(responseSlime); + } + + private MessageResponse deleteAdditionalItem(RestApi.RequestContext requestContext) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.get(tenantName).orElseThrow(() -> new RestApiException.NotFound("No such tenant: " + tenantName)); + + var itemId = requestContext.pathParameters().getStringOrThrow("item"); + + var items = billing.getUnusedLineItems(tenant.name()); + var candidate = items.stream().filter(item -> item.id().equals(itemId)).findAny(); + + if (candidate.isEmpty()) { + throw new RestApiException.NotFound("Could not find item with ID " + itemId); + } + + billing.deleteLineItem(itemId);; + + return new MessageResponse("Successfully deleted line item " + itemId); + } + + private MessageResponse newAdditionalItem(RestApi.RequestContext requestContext, Slime body) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.get(tenantName).orElseThrow(() -> new RestApiException.NotFound("No such tenant: " + tenantName)); + + var inspector = body.get(); + + var billId = SlimeUtils.optionalString(inspector.field("billId")).map(Bill.Id::of); + + billing.addLineItem( + tenant.name(), + getInspectorFieldOrThrow(inspector, "description"), + new BigDecimal(getInspectorFieldOrThrow(inspector, "amount")), + billId, + requestContext.userPrincipalOrThrow().getName()); + + return new MessageResponse("Added line item for tenant " + tenantName); + } + + private Slime additionalItems(RestApi.RequestContext requestContext) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.get(tenantName).orElseThrow(() -> new RestApiException.NotFound("No such tenant: " + tenantName)); + + var slime = new Slime(); + var items = slime.setObject().setArray("items"); + + billing.getUnusedLineItems(tenant.name()).forEach(item -> { + var itemCursor = items.addObject(); + toSlime(itemCursor, item); + }); + + return slime; + } + + private MessageResponse setAccountantTenantPlan(RestApi.RequestContext requestContext, Slime body) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.require(tenantName, CloudTenant.class); + + var planId = PlanId.from(getInspectorFieldOrThrow(body.get(), "id")); + var response = billing.setPlan(tenant.name(), planId, false, true); + + if (response.isSuccess()) { + return new MessageResponse("Plan: " + planId.value()); + } else { + throw new RestApiException.BadRequest("Could not change plan: " + response.getErrorMessage()); + } + } + + private Slime accountantTenantPlan(RestApi.RequestContext requestContext) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.require(tenantName, CloudTenant.class); + + var planId = billing.getPlan(tenant.name()); + var plan = planRegistry.plan(planId); + + if (plan.isEmpty()) { + throw new RestApiException.BadRequest("Plan with ID '" + planId.value() + "' does not exist"); + } + + var slime = new Slime(); + var root = slime.setObject(); + root.setString("id", plan.get().id().value()); + root.setString("name", plan.get().displayName()); + + return slime; + } + + private MessageResponse setAccountantTenantCollection(RestApi.RequestContext requestContext, Slime body) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.require(tenantName, CloudTenant.class); + + var collection = CollectionMethod.valueOf(getInspectorFieldOrThrow(body.get(), "collection")); + var result = billing.setCollectionMethod(tenant.name(), collection); + + if (result.isSuccess()) { + return new MessageResponse("Collection: " + collection.name()); + } else { + throw new RestApiException.BadRequest("Could not change collection method: " + result.getErrorMessage()); + } + } + + private Slime accountantTenantCollection(RestApi.RequestContext requestContext) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.require(tenantName, CloudTenant.class); + + var collection = billing.getCollectionMethod(tenant.name()); + + var slime = new Slime(); + var root = slime.setObject(); + root.setString("collection", collection.name()); + + return slime; + } + + private Slime accountantTenant(RestApi.RequestContext requestContext) { + var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant")); + var tenant = tenants.require(tenantName, CloudTenant.class); + + var slime = new Slime(); + var root = slime.setObject(); + + var planId = billing.getPlan(tenant.name()); + var plan = planRegistry.plan(planId); + + var collection = billing.getCollectionMethod(tenant.name()); + + toSlime(root, tenant, planId, plan, collection); + + return slime; } // --------- INVOICE RENDERING ---------- @@ -289,7 +459,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler slime.setString("from", bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE)); slime.setString("to", bill.getEndDate().format(DateTimeFormatter.ISO_LOCAL_DATE)); slime.setString("total", bill.sum().toString()); - slime.setString("status", bill.status()); + slime.setString("status", bill.status().value()); } private void usageToSlime(Cursor slime, Bill bill) { @@ -304,16 +474,16 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler slime.setString("from", bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE)); slime.setString("to", bill.getEndDate().format(DateTimeFormatter.ISO_LOCAL_DATE)); slime.setString("total", bill.sum().toString()); - slime.setString("status", bill.status()); + slime.setString("status", bill.status().value()); toSlime(slime.setArray("statusHistory"), bill.statusHistory()); toSlime(slime.setArray("items"), bill.lineItems()); } - private void toSlime(Cursor slime, Bill.StatusHistory history) { + private void toSlime(Cursor slime, StatusHistory history) { history.getHistory().forEach((key, value) -> { var c = slime.addObject(); c.setString("at", key.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - c.setString("status", value); + c.setString("status", value.value()); }); } @@ -328,6 +498,8 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler toSlime(slime.setObject("plan"), planRegistry.plan(item.plan()).orElseThrow(() -> new RuntimeException("No such plan: '" + item.plan() + "'"))); item.getArchitecture().ifPresent(arch -> slime.setString("architecture", arch.name())); slime.setLong("majorVersion", item.getMajorVersion()); + if (! item.getCloudAccount().isUnspecified()) + slime.setString("cloudAccount", item.getCloudAccount().value()); item.applicationId().ifPresent(appId -> { slime.setString("application", appId.application().value()); @@ -339,6 +511,7 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler toSlime(slime.setObject("cpu"), item.getCpuHours(), item.getCpuCost()); toSlime(slime.setObject("memory"), item.getMemoryHours(), item.getMemoryCost()); toSlime(slime.setObject("disk"), item.getDiskHours(), item.getDiskCost()); + toSlime(slime.setObject("gpu"), item.getGpuHours(), item.getGpuCost()); } private void toSlime(Cursor slime, Optional<BigDecimal> hours, Optional<BigDecimal> cost) { @@ -346,6 +519,33 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler cost.ifPresent(c -> slime.setString("cost", c.toString())); } + private void toSlime(Cursor slime, CloudTenant tenant, PlanId planId, Optional<Plan> plan, CollectionMethod method) { + slime.setString("tenant", tenant.name().value()); + toSlime(slime.setObject("plan"), planId, plan); + toSlime(slime.setObject("billing"), tenant.billingReference()); + slime.setString("collection", method.name()); + } + + private void toSlime(Cursor slime, PlanId planId, Optional<Plan> plan) { + slime.setString("id", planId.value()); + if (plan.isPresent()) { + slime.setString("name", plan.get().displayName()); + slime.setBool("billed", plan.get().isBilled()); + slime.setBool("supported", plan.get().isSupported()); + } else { + slime.setString("name", "UNKNOWN"); + slime.setBool("billed", false); + slime.setBool("supported", false); + } + } + + private void toSlime(Cursor slime, Optional<BillingReference> billingReference) { + if (billingReference.isPresent()) { + slime.setString("id", billingReference.get().reference()); + slime.setLong("lastUpdated", billingReference.get().updated().toEpochMilli()); + } + } + private List<Object[]> toCsv(Bill bill) { return List.<Object[]>of(new Object[]{ bill.id().value(), bill.tenant().value(), diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java index e97a51e58a2..cf45bfb67f0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.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.billing; import com.yahoo.container.jdisc.HttpResponse; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java index 912bd051a31..b38bb73a98a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java @@ -1,3 +1,4 @@ +// 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.certificate; import com.yahoo.config.provision.ApplicationId; @@ -19,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.persistence.EndpointCertificateSerializer; +import com.yahoo.vespa.hosted.controller.routing.EndpointConfig; import java.util.List; import java.util.Optional; @@ -73,11 +75,11 @@ public class EndpointCertificatesHandler extends ThreadedHttpRequestHandler { public StringResponse reRequestEndpointCertificateFor(String instanceId, boolean ignoreExisting) { ApplicationId applicationId = ApplicationId.fromFullString(instanceId); - if (controller.routing().generatedEndpointsEnabled(applicationId)) { + if (controller.routing().endpointConfig(applicationId) == EndpointConfig.generated) { throw new IllegalArgumentException("Cannot re-request certificate. " + instanceId + " is assigned certificate from a pool"); } try (var lock = curator.lock(TenantAndApplicationId.from(applicationId))) { - AssignedCertificate assignedCertificate = curator.readAssignedCertificate(applicationId) + AssignedCertificate assignedCertificate = curator.readAssignedCertificate(TenantAndApplicationId.from(applicationId), Optional.of(applicationId.instance())) .orElseThrow(() -> new RestApiException.NotFound("No certificate found for application " + applicationId.serializedForm())); String algo = this.endpointCertificateAlgo.with(FetchVector.Dimension.INSTANCE_ID, applicationId.serializedForm()).value(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java index f0c851f50ef..f3b28691262 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.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.changemanagement; import com.yahoo.config.provision.Environment; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java index 14223b49abc..425c1fd894d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.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.configserver; import ai.vespa.http.HttpURL; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java index a44d138ff11..91dde82e233 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.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. /** * @author freva */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java index 30e103048cf..4863b91b3eb 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.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.controller; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java index f46806743e9..859281dbe18 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.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.controller; import com.yahoo.restapi.SlimeJsonResponse; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java index 6da4e788de1..b9ba4f691fc 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.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.controller; import com.yahoo.component.Version; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java index b3d966d20c9..f2e51b51752 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.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.controller; import com.yahoo.container.jdisc.HttpRequest; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java index 05768410891..0d15d9b2971 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.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.controller; import com.yahoo.concurrent.maintenance.JobControl; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java index ea7bce00794..5a8c4847ce6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.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.controller; import com.yahoo.config.provision.TenantName; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java index 884399f25d9..746f1d8ce2e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.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.controller; import com.yahoo.io.IOUtils; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java index 4714d0e5af1..2aab64a7c30 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.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.controller; import com.yahoo.restapi.SlimeJsonResponse; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java index 96a3c9f177d..ab12187c069 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.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.controller; import com.yahoo.config.provision.zone.ZoneId; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java index f9add356f19..e8ba1177c67 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.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.controller; import com.yahoo.restapi.SlimeJsonResponse; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java index c98a4cc72be..63f600aaa50 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.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.controller; import com.yahoo.container.jdisc.HttpRequest; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java index 385200a1624..834133e7eb5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java @@ -1,27 +1,53 @@ -// 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.dataplanetoken; +import com.yahoo.concurrent.DaemonThreadFactory; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.token.Token; import com.yahoo.security.token.TokenCheckHash; import com.yahoo.security.token.TokenDomain; import com.yahoo.security.token.TokenGenerator; import com.yahoo.transaction.Mutex; +import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.Instance; +import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneToken; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions; +import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions.Version; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint; import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.deployment.Run; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import java.security.Principal; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Phaser; +import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; + /** * Service to list, generate and delete data plane tokens * @@ -34,7 +60,7 @@ public class DataplaneTokenService { private static final int CHECK_HASH_BYTES = 32; public static final Duration DEFAULT_TTL = Duration.ofDays(30); - + private final ExecutorService executor = Executors.newCachedThreadPool(new DaemonThreadFactory("dataplane-token-service-")); private final Controller controller; public DataplaneTokenService(Controller controller) { @@ -48,6 +74,110 @@ public class DataplaneTokenService { return controller.curator().readDataplaneTokens(tenantName); } + public enum State { UNUSED, DEPLOYING, ACTIVE, REVOKING } + + /** List all known tokens for a tenant, with the state of each token version (both current and deactivating). */ + public Map<DataplaneTokenVersions, Map<FingerPrint, State>> listTokensWithState(TenantName tenantName) { + List<DataplaneTokenVersions> currentTokens = listTokens(tenantName); + Set<TokenId> usedTokens = new HashSet<>(); + Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokens = listActiveTokens(tenantName, usedTokens); + Map<TokenId, Map<FingerPrint, Boolean>> activeFingerprints = computeStates(activeTokens); + Map<DataplaneTokenVersions, Map<FingerPrint, State>> tokens = new TreeMap<>(comparing(DataplaneTokenVersions::tokenId)); + for (DataplaneTokenVersions token : currentTokens) { + Map<FingerPrint, State> states = new TreeMap<>(); + // Current tokens are active iff. they are active everywhere. + for (Version version : token.tokenVersions()) { + // If the token was not seen anywhere, it is deploying or unused. + // Otherwise, it is active iff. it is active everywhere. + Boolean isActive = activeFingerprints.getOrDefault(token.tokenId(), Map.of()).get(version.fingerPrint()); + states.put(version.fingerPrint(), + isActive == null ? usedTokens.contains(token.tokenId()) ? State.DEPLOYING : State.UNUSED + : isActive ? State.ACTIVE : State.DEPLOYING); + } + // Active, non-current token versions are deactivating. + for (FingerPrint print : activeFingerprints.getOrDefault(token.tokenId(), Map.of()).keySet()) { + states.putIfAbsent(print, State.REVOKING); + } + tokens.put(token, states); + } + // Active, non-current tokens are also deactivating. + activeFingerprints.forEach((id, prints) -> { + if (currentTokens.stream().noneMatch(token -> token.tokenId().equals(id))) { + Map<FingerPrint, State> states = new TreeMap<>(); + for (FingerPrint print : prints.keySet()) states.put(print, State.REVOKING); + tokens.put(new DataplaneTokenVersions(id, List.of(), Instant.EPOCH), states); + } + }); + return tokens; + } + + private Map<HostName, Map<TokenId, List<FingerPrint>>> listActiveTokens(TenantName tenantName, Set<TokenId> usedTokens) { + Map<HostName, Map<TokenId, List<FingerPrint>>> tokens = new ConcurrentHashMap<>(); + Phaser phaser = new Phaser(1); + for (Application application : controller.applications().asList(tenantName)) { + for (Instance instance : application.instances().values()) { + instance.deployments().forEach((zone, deployment) -> { + DeploymentId id = new DeploymentId(instance.id(), zone); + usedTokens.addAll(deployment.dataPlaneTokens().keySet()); + phaser.register(); + executor.execute(() -> { + try { tokens.putAll(controller.serviceRegistry().configServer().activeTokenFingerprints(id)); } + finally { phaser.arrive(); } + }); + }); + } + } + phaser.arriveAndAwaitAdvance(); + return tokens; + } + + /** Computes whether each print is active on all hosts where its token is present. */ + private Map<TokenId, Map<FingerPrint, Boolean>> computeStates(Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokens) { + Map<TokenId, Map<FingerPrint, Boolean>> states = new HashMap<>(); + for (Map<TokenId, List<FingerPrint>> token : activeTokens.values()) { + token.forEach((id, prints) -> { + states.merge(id, + prints.stream().collect(toMap(print -> print, __ -> true)), + (a, b) -> new HashMap<>() {{ // true iff. present in both, false iff. present in one. + a.forEach((p, s) -> put(p, s && b.getOrDefault(p, false))); + b.forEach((p, s) -> putIfAbsent(p, false)); + }}); + }); + } + return states; + } + + /** Triggers redeployment of all applications which reference a token which has changed. */ + public void triggerTokenChangeDeployments() { + controller.applications().asList().stream() + .collect(groupingBy(application -> application.id().tenant())) + .forEach((tenant, applications) -> { + List<DataplaneTokenVersions> currentTokens = listTokens(tenant); + for (Application application : applications) { + for (Instance instance : application.instances().values()) { + instance.deployments().forEach((zone, deployment) -> { + if (zone.environment().isTest()) return; + if (deployment.dataPlaneTokens().isEmpty()) return; + boolean needsRetrigger = false; + // If a token has a newer change than the deployed token data, we need to re-trigger. + for (DataplaneTokenVersions token : currentTokens) + needsRetrigger |= deployment.dataPlaneTokens().getOrDefault(token.tokenId(), Instant.MAX).isBefore(token.lastUpdated()); + + // If a token is no longer current, but was deployed with at least one version, we need to re-trigger. + for (var entry : deployment.dataPlaneTokens().entrySet()) + needsRetrigger |= ! Instant.EPOCH.equals(entry.getValue()) + && currentTokens.stream().noneMatch(token -> token.tokenId().equals(entry.getKey())); + + if (needsRetrigger && controller.jobController().last(instance.id(), JobType.deploymentTo(zone)).map(Run::hasEnded).orElse(true)) + controller.applications().deploymentTrigger().reTrigger(instance.id(), + JobType.deploymentTo(zone), + "Data plane tokens changed"); + }); + } + } + }); + } + /** * Generates a token using tenant name as the check access context. * Persists the token fingerprint and check access hash, but not the token value @@ -62,10 +192,11 @@ public class DataplaneTokenService { TokenDomain tokenDomain = TokenDomain.of("Vespa Cloud tenant data plane:%s".formatted(tenantName.value())); Token token = TokenGenerator.generateToken(tokenDomain, TOKEN_PREFIX, TOKEN_BYTES); TokenCheckHash checkHash = TokenCheckHash.of(token, CHECK_HASH_BYTES); + Instant now = controller.clock().instant(); DataplaneTokenVersions.Version newTokenVersion = new DataplaneTokenVersions.Version( FingerPrint.of(token.fingerprint().toDelimitedHexString()), checkHash.toHexString(), - controller.clock().instant(), + now, Optional.ofNullable(expiration), principal.getName()); @@ -81,18 +212,18 @@ public class DataplaneTokenService { .toList(); dataplaneTokenVersions = Stream.concat( dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)), - Stream.of(new DataplaneTokenVersions(tokenId, versions))) + Stream.of(new DataplaneTokenVersions(tokenId, versions, now))) .toList(); } else { - DataplaneTokenVersions newToken = new DataplaneTokenVersions(tokenId, List.of(newTokenVersion)); + DataplaneTokenVersions newToken = new DataplaneTokenVersions(tokenId, List.of(newTokenVersion), now); dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream(), Stream.of(newToken)).toList(); } curator.writeDataplaneTokens(tenantName, dataplaneTokenVersions); - - // Return the data plane token including the secret token. - return new DataplaneToken(tokenId, FingerPrint.of(token.fingerprint().toDelimitedHexString()), - token.secretTokenString(), Optional.ofNullable(expiration)); } + + // Return the data plane token including the secret token. + return new DataplaneToken(tokenId, FingerPrint.of(token.fingerprint().toDelimitedHexString()), + token.secretTokenString(), Optional.ofNullable(expiration)); } /** @@ -110,9 +241,13 @@ public class DataplaneTokenService { if (versions.isEmpty()) { dataplaneTokenVersions = dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)).toList(); } else { - boolean fingerPrintExists = existingToken.get().tokenVersions().stream().anyMatch(v -> v.fingerPrint().equals(tokenFingerprint)); - if (fingerPrintExists) { - dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)), Stream.of(new DataplaneTokenVersions(tokenId, versions))).toList(); + Optional<Version> existingVersion = existingToken.get().tokenVersions().stream().filter(v -> v.fingerPrint().equals(tokenFingerprint)).findAny(); + if (existingVersion.isPresent()) { + Instant now = controller.clock().instant(); + // If we removed an expired token, we keep the old lastUpdated timestamp. + Instant lastUpdated = existingVersion.get().expiration().map(now::isAfter).orElse(false) ? existingToken.get().lastUpdated() : now; + dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)), + Stream.of(new DataplaneTokenVersions(tokenId, versions, lastUpdated))).toList(); } else { throw new IllegalArgumentException("Fingerprint does not exist: " + tokenFingerprint); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java index c6eaf5abef7..839dbf76faa 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java @@ -1,7 +1,8 @@ -// 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.deployment; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.container.jdisc.EmptyResponse; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; @@ -53,7 +54,12 @@ public class BadgeApiHandler extends ThreadedHttpRequestHandler { Method method = request.getMethod(); try { return switch (method) { - case GET -> get(request); + case OPTIONS -> new SvgHttpResponse("") {{ + headers().add("Allow", "GET, HEAD, OPTIONS"); + headers().add("Access-Control-Allow-Origin", "*"); + headers().add("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS"); + }}; + case HEAD, GET -> get(request); default -> ErrorResponse.methodNotAllowed("Method '" + method + "' is unsupported"); }; } catch (IllegalArgumentException|IllegalStateException e) { @@ -98,20 +104,20 @@ public class BadgeApiHandler extends ThreadedHttpRequestHandler { } private HttpResponse cachedResponse(Key key, Instant now, Supplier<String> badge) { - return svgResponse(badgeCache.compute(key, (__, value) -> { + return new SvgHttpResponse(badgeCache.compute(key, (__, value) -> { return value != null && value.expiry.isAfter(now) ? value : new Value(badge.get(), now); }).badgeSvg); } - private static HttpResponse svgResponse(String svg) { - return new HttpResponse(200) { - @Override public void render(OutputStream outputStream) throws IOException { - outputStream.write(svg.getBytes(UTF_8)); - } - @Override public String getContentType() { - return "image/svg+xml; charset=UTF-8"; - } - }; + private static class SvgHttpResponse extends HttpResponse { + private final String svg; + SvgHttpResponse(String svg) { super(200); this.svg = svg; } + @Override public void render(OutputStream outputStream) throws IOException { + outputStream.write(svg.getBytes(UTF_8)); + } + @Override public String getContentType() { + return "image/svg+xml"; + } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java index ae1949e2214..41b5c833ec8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.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.deployment; import com.yahoo.config.provision.ApplicationId; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java index c67d0d04938..150acd297c2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.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.deployment; import com.yahoo.component.Version; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java index 4e3a8b7caf0..edfa4d01d78 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.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.deployment; import com.yahoo.component.Version; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java index c25502ab9bf..0a466b7ffe8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.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.filter; import com.auth0.jwt.JWT; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java index cef6840dfe1..115467ac805 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.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.filter; import com.yahoo.component.annotation.Inject; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java index e840b70a95a..114dfc8420c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.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.filter; import com.yahoo.component.annotation.Inject; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java index 5eaa6d7af1d..7173b086b79 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.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.filter; import ai.vespa.hosted.api.Method; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java index 7284bc70bfa..400576abfea 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.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.flags; import com.yahoo.container.jdisc.HttpRequest; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java index 3c0ec666415..4f12f00eace 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.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.horizon; import com.yahoo.component.annotation.Inject; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java index 5953c51782a..2f3957af70d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.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.horizon; import com.fasterxml.jackson.databind.JsonNode; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java index 1efccb8afe4..701761895c3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.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.os; import com.yahoo.component.Version; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java index bc83eeb73c1..2a6778870b1 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.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.routing; import com.yahoo.config.provision.ApplicationId; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java index 6327a6262ba..2b53b1a32f5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.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.systemflags; import ai.vespa.util.http.hc4.SslConnectionSocketFactory; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java index 1fe97fed2c7..e1b3da65e6e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.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.systemflags; import java.util.OptionalInt; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java index 872202dc222..c006fa13223 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.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.systemflags; import com.fasterxml.jackson.databind.JsonNode; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java index 2c38066eddd..0fa800e7367 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.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.systemflags; import com.yahoo.concurrent.DaemonThreadFactory; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java index bb285b8b742..6318dc8c6fa 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.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.systemflags; import com.yahoo.component.annotation.Inject; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java index 3811ec22555..11a5e178703 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.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.user; import com.yahoo.component.annotation.Inject; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java index c3acf01a53e..46de4b7a348 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.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.user; import com.yahoo.config.provision.TenantName; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java index 7978e64482b..90792e9febe 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.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.zone.v1; import com.yahoo.config.provision.Environment; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java index 6c27f12954a..c5b29dad8b9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.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. /** * @author mpolden */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java index 89a2067837b..722bdac2101 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.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.zone.v2; import ai.vespa.http.HttpURL; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java index 9cb62748b63..7902c38982c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.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. /** * @author mpolden */ |