aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java120
1 files changed, 81 insertions, 39 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 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();