diff options
Diffstat (limited to 'controller-api/src/main/java/com/yahoo')
35 files changed, 565 insertions, 249 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java index f406095d579..fd4a34118c5 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java @@ -14,9 +14,12 @@ import com.yahoo.yolean.concurrent.Memoized; import java.io.InputStream; import java.security.cert.X509Certificate; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; import static java.util.Objects.requireNonNull; @@ -28,6 +31,8 @@ import static java.util.Objects.requireNonNull; */ public class DeploymentData { + private static final Logger log = Logger.getLogger(DeploymentData.class.getName()); + private final ApplicationId instance; private final ZoneId zone; private final Supplier<InputStream> applicationPackage; @@ -56,14 +61,14 @@ public class DeploymentData { this.zone = requireNonNull(zone); this.applicationPackage = requireNonNull(applicationPackage); this.platform = requireNonNull(platform); - this.endpoints = new Memoized<>(requireNonNull(endpoints)); + this.endpoints = wrap(requireNonNull(endpoints), Duration.ofSeconds(30), "deployment endpoints for " + instance + " in " + zone); this.dockerImageRepo = requireNonNull(dockerImageRepo); this.athenzDomain = athenzDomain; - this.quota = new Memoized<>(requireNonNull(quota)); + this.quota = wrap(requireNonNull(quota), Duration.ofSeconds(10), "quota for " + instance); this.tenantSecretStores = List.copyOf(requireNonNull(tenantSecretStores)); this.operatorCertificates = List.copyOf(requireNonNull(operatorCertificates)); - this.cloudAccount = new Memoized<>(requireNonNull(cloudAccount)); - this.dataPlaneTokens = new Memoized<>(dataPlaneTokens); + this.cloudAccount = wrap(requireNonNull(cloudAccount), Duration.ofSeconds(5), "cloud account for " + instance + " in " + zone); + this.dataPlaneTokens = wrap(dataPlaneTokens, Duration.ofSeconds(5), "data plane tokens for " + instance + " in " + zone); this.dryRun = dryRun; } @@ -83,8 +88,8 @@ public class DeploymentData { return platform; } - public Supplier<DeploymentEndpoints> endpoints() { - return endpoints; + public DeploymentEndpoints endpoints() { + return endpoints.get(); } public Optional<DockerImage> dockerImageRepo() { @@ -119,4 +124,41 @@ public class DeploymentData { return dryRun; } + private static <T> Supplier<T> wrap(Supplier<T> delegate, Duration timeout, String description) { + return new TimingSupplier<>(new Memoized<>(delegate), timeout, description); + } + + public static class TimingSupplier<T> implements Supplier<T> { + + private final Supplier<T> delegate; + private final Duration timeout; + private final String description; + + public TimingSupplier(Supplier<T> delegate, Duration timeout, String description) { + this.delegate = delegate; + this.timeout = timeout; + this.description = description; + } + + @Override + public T get() { + long startNanos = System.nanoTime(); + Throwable thrown = null; + try { + return delegate.get(); + } + catch (Throwable t) { + thrown = t; + throw t; + } + finally { + long durationNanos = System.nanoTime() - startNanos; + Level level = durationNanos > timeout.toNanos() ? Level.WARNING : Level.FINE; + String thrownMessage = thrown == null ? "" : " with exception " + thrown; + log.log(level, () -> String.format("Getting %s took %.6f seconds%s", description, durationNanos / 1e9, thrownMessage)); + } + } + + } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java new file mode 100644 index 00000000000..82cddb46d9a --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java @@ -0,0 +1,89 @@ +// 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.api.integration; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * Generates URLs to various views in the Console. Prefer to create new methods and return + * String instead of URI to make it easier to track which views are linked from where. + * + * @author freva + */ +public class ConsoleUrls { + private final String root; + public ConsoleUrls(URI root) { + this.root = root.toString().replaceFirst("/$", ""); // Remove trailing slash + } + + public String root() { + return root; + } + + public String tenantOverview(TenantName tenantName) { + return "%s/tenant/%s".formatted(root, tenantName.value()); + } + + /** Returns URL to notification settings view for the given tenant */ + public String tenantNotifications(TenantName tenantName) { + return "%s/tenant/%s/account/notifications".formatted(root, tenantName.value()); + } + + public String tenantBilling(TenantName t) { return "%s/tenant/%s/account/billing".formatted(root, t.value()); } + + public String prodApplicationOverview(TenantName tenantName, ApplicationName applicationName) { + return "%s/tenant/%s/application/%s/prod/instance".formatted(root, tenantName.value(), applicationName.value()); + } + + public String instanceOverview(ApplicationId application, Environment environment) { + return "%s/tenant/%s/application/%s/%s/instance/%s".formatted(root, + application.tenant().value(), + application.application().value(), + environment.isManuallyDeployed() ? environment.value() : "prod", + application.instance().value()); + } + + public String clusterOverview(ApplicationId application, ZoneId zone, ClusterSpec.Id clusterId) { + return cluster(application, zone, clusterId, null); + } + + public String clusterReindexing(ApplicationId application, ZoneId zone, ClusterSpec.Id clusterId) { + return cluster(application, zone, clusterId, "reindexing"); + } + + public String deploymentRun(RunId id) { + return "%s/job/%s/run/%s".formatted( + instanceOverview(id.application(), id.type().environment()), id.type().jobName(), id.number()); + } + + /** Returns URL used to request support from the Vespa team. */ + public String support() { + return root + "/support"; + } + + /** Returns URL to verify an email address with the given verification code */ + public String verifyEmail(String verifyCode) { + return "%s/verify?%s".formatted(root, queryParam("code", verifyCode)); + } + + public String termsOfService() { return root + "/terms-of-service-trial.html"; } + + private String cluster(ApplicationId application, ZoneId zone, ClusterSpec.Id clusterId, String viewOrNull) { + return instanceOverview(application, zone.environment()) + '?' + + queryParam("%s.%s.%s".formatted(application.instance().value(), zone.environment().value(), zone.region().value()), + "clusters," + clusterId.value() + (viewOrNull == null ? "" : '=' + viewOrNull)); + } + + private static String queryParam(String key, String value) { + return URLEncoder.encode(key, StandardCharsets.UTF_8) + '=' + URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java deleted file mode 100644 index f72f80155ed..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/MockPricingController.java +++ /dev/null @@ -1,35 +0,0 @@ -// 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.api.integration; - -import com.yahoo.config.provision.ClusterResources; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan; -import com.yahoo.vespa.hosted.controller.api.integration.pricing.PriceInformation; -import com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingController; -import com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo; - -import java.math.BigDecimal; -import java.util.List; - -import static com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingInfo.SupportLevel.BASIC; - -public class MockPricingController implements PricingController { - - @Override - public PriceInformation price(List<ClusterResources> clusterResources, PricingInfo pricingInfo, Plan plan) { - BigDecimal listPrice = BigDecimal.valueOf(clusterResources.stream() - .mapToDouble(resources -> resources.nodes() * - (resources.nodeResources().vcpu() * 1000 + - resources.nodeResources().memoryGb() * 100 + - resources.nodeResources().diskGb() * 10)) - .sum()); - - BigDecimal supportLevelCost = pricingInfo.supportLevel() == BASIC ? new BigDecimal("-160.00") : new BigDecimal("800.00"); - BigDecimal listPriceWithSupport = listPrice.add(supportLevelCost); - BigDecimal enclaveDiscount = pricingInfo.enclave() ? new BigDecimal("-15.1234") : BigDecimal.ZERO; - BigDecimal volumeDiscount = new BigDecimal("-5.64315634"); - BigDecimal committedAmountDiscount = new BigDecimal("-1.23"); - BigDecimal totalAmount = listPrice.add(supportLevelCost).add(enclaveDiscount).add(volumeDiscount).add(committedAmountDiscount); - return new PriceInformation(listPriceWithSupport, volumeDiscount, committedAmountDiscount, enclaveDiscount, totalAmount); - } - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java index 1c5f5f972cd..e39a8cf38b7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java @@ -30,11 +30,10 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueHandl import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer; import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor; -import com.yahoo.vespa.hosted.controller.api.integration.pricing.PricingController; import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient; -import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore; import com.yahoo.vespa.hosted.controller.api.integration.secrets.EndpointSecretManager; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretService; import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainer; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient; @@ -91,6 +90,8 @@ public interface ServiceRegistry { ZoneRegistry zoneRegistry(); + ConsoleUrls consoleUrls(); + ResourceTagger resourceTagger(); EnclaveAccessService enclaveAccessService(); @@ -127,6 +128,4 @@ public interface ServiceRegistry { BillingReporter billingReporter(); - PricingController pricingController(); - } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java new file mode 100644 index 00000000000..c665b4fb7c2 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +import java.util.List; + +/** + * @author bjorncs + */ +public record AcceptedCountries(List<Country> countries) { + + public AcceptedCountries { + countries = List.copyOf(countries); + } + + public record Country(String code, String displayName, List<TaxType> taxTypes) { + public Country { + taxTypes = List.copyOf(taxTypes); + } + } + + public record TaxType(String id, String description, String pattern, String example) {} +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java index 1acb4964ea6..e7959d2057a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java @@ -8,16 +8,11 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import java.math.BigDecimal; -import java.time.Clock; import java.time.LocalDate; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.SortedMap; -import java.util.TreeMap; import java.util.UUID; import java.util.function.Function; @@ -69,7 +64,7 @@ public class Bill { return tenant; } - public String status() { + public BillStatus status() { return statusHistory.current(); } @@ -389,28 +384,4 @@ public class Bill { } } - public static class StatusHistory { - SortedMap<ZonedDateTime, String> history; - - public StatusHistory(SortedMap<ZonedDateTime, String> history) { - this.history = history; - } - - public static StatusHistory open(Clock clock) { - var now = clock.instant().atZone(ZoneOffset.UTC); - return new StatusHistory( - new TreeMap<>(Map.of(now, "OPEN")) - ); - } - - public String current() { - return history.get(history.lastKey()); - } - - public SortedMap<ZonedDateTime, String> getHistory() { - return history; - } - - } - } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java new file mode 100644 index 00000000000..4f35b47219a --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java @@ -0,0 +1,33 @@ +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +/** + * @author gjoranv + */ +public enum BillStatus { + OPEN, // All bills start in this state. The bill can be modified and exported/synced to external systems. + FROZEN, // Syncing to external systems is switched off. No changes can be made. + CLOSED, // End state for a valid bill. + VOID; // End state, indicating that the bill is not valid. + + // Legacy states, used by historical bills + private static final String LEGACY_ISSUED = "ISSUED"; + private static final String LEGACY_EXPORTED = "EXPORTED"; + private static final String LEGACY_CANCELED = "CANCELED"; + + private final String value; + + BillStatus() { + this.value = name(); + } + + public String value() { + return value; + } + + public static BillStatus from(String status) { + if (LEGACY_ISSUED.equals(status) || LEGACY_EXPORTED.equals(status)) return OPEN; + if (LEGACY_CANCELED.equals(status)) return VOID; + return Enum.valueOf(BillStatus.class, status.toUpperCase()); + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java index 95b2ba9f8f8..8b48c72f88e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java @@ -2,7 +2,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.billing; import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; +import com.yahoo.vespa.hosted.controller.tenant.TaxId; import java.math.BigDecimal; import java.time.LocalDate; @@ -90,7 +90,7 @@ public interface BillingController { boolean deleteInstrument(TenantName tenant, String userId, String instrumentId); /** Change the status of the given bill */ - void updateBillStatus(Bill.Id billId, String agent, String status); + void updateBillStatus(Bill.Id billId, String agent, BillStatus status); /** Add a line item to the given bill */ void addLineItem(TenantName tenant, String description, BigDecimal amount, Optional<Bill.Id> billId, String agent); @@ -130,7 +130,10 @@ public interface BillingController { default void updateCache(List<TenantName> tenants) {} - default String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { - return "NOT_IMPLEMENTED"; - } + /** Get the list of countries that are accepted */ + AcceptedCountries getAcceptedCountries(); + + /** Validation of tax id */ + void validateTaxId(TaxId id) throws IllegalArgumentException; + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java index 3e24314ba5c..c5859cd7d2f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java @@ -69,7 +69,7 @@ public interface BillingDatabaseClient { * @param agent The agent that added the status * @param status The new status of the bill */ - void setStatus(Bill.Id billId, String agent, String status); + void setStatus(Bill.Id billId, String agent, BillStatus status); List<Bill.LineItem> getUnusedLineItems(TenantName tenantName); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java index 300c1658c29..a6bcc9bf0ed 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java @@ -26,9 +26,10 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { private final Map<Bill.Id, List<Bill.LineItem>> lineItems = new HashMap<>(); private final Map<TenantName, List<Bill.LineItem>> uncommittedLineItems = new HashMap<>(); - private final Map<Bill.Id, Bill.StatusHistory> statuses = new HashMap<>(); + private final Map<Bill.Id, StatusHistory> statuses = new HashMap<>(); private final Map<Bill.Id, ZonedDateTime> startTimes = new HashMap<>(); private final Map<Bill.Id, ZonedDateTime> endTimes = new HashMap<>(); + private final Map<Bill.Id, String> exportedInvoiceIds = new HashMap<>(); private final ZonedDateTime startTime = LocalDate.of(2020, 4, 1).atStartOfDay(ZoneId.of("UTC")); private final ZonedDateTime endTime = LocalDate.of(2020, 5, 1).atStartOfDay(ZoneId.of("UTC")); @@ -53,7 +54,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { .findFirst(); } - public String getStatus(Bill.Id invoiceId) { + public BillStatus getStatus(Bill.Id invoiceId) { return statuses.get(invoiceId).current(); } @@ -61,7 +62,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { public Bill.Id createBill(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) { var invoiceId = Bill.Id.generate(); invoices.put(invoiceId, tenant); - statuses.computeIfAbsent(invoiceId, l -> Bill.StatusHistory.open(clock)); + statuses.computeIfAbsent(invoiceId, l -> StatusHistory.open(clock)); startTimes.put(invoiceId, startTime); endTimes.put(invoiceId, endTime); return invoiceId; @@ -71,10 +72,11 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { public Optional<Bill> readBill(Bill.Id billId) { var invoice = Optional.ofNullable(invoices.get(billId)); var lines = lineItems.getOrDefault(billId, List.of()); - var status = statuses.getOrDefault(billId, Bill.StatusHistory.open(clock)); + var status = statuses.getOrDefault(billId, StatusHistory.open(clock)); var start = startTimes.getOrDefault(billId, startTime); var end = endTimes.getOrDefault(billId, endTime); - return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end)); + var exportedId = exportedInvoiceId(billId); + return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end, exportedId)); } @Override @@ -88,8 +90,8 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { } @Override - public void setStatus(Bill.Id invoiceId, String agent, String status) { - statuses.computeIfAbsent(invoiceId, k -> Bill.StatusHistory.open(clock)) + public void setStatus(Bill.Id invoiceId, String agent, BillStatus status) { + statuses.computeIfAbsent(invoiceId, k -> StatusHistory.open(clock)) .getHistory() .put(ZonedDateTime.now(), status); } @@ -157,7 +159,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.get(invoiceId); var start = startTimes.get(invoiceId); var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); + return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId)); }) .toList(); } @@ -171,7 +173,7 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { var status = statuses.get(invoiceId); var start = startTimes.get(invoiceId); var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); + return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId)); }) .toList(); } @@ -180,9 +182,14 @@ public class BillingDatabaseClientMock implements BillingDatabaseClient { public void maintain() {} @Override - public void setExportedInvoiceId(Bill.Id billId, String invoiceId) { } + public void setExportedInvoiceId(Bill.Id billId, String invoiceId) { + exportedInvoiceIds.put(billId, invoiceId); + } @Override public void setExportedInvoiceItemId(String lineItemId, String invoiceItemId) { } + private String exportedInvoiceId(Bill.Id billId) { + return exportedInvoiceIds.getOrDefault(billId, null); + } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java index 7339555e578..676c29cec5d 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java @@ -6,4 +6,12 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; public interface BillingReporter { BillingReference maintainTenant(CloudTenant tenant); + + InvoiceUpdate maintainInvoice(Bill bill); + + /** Export a bill to a payment service. Returns the invoice ID in the external system. */ + default String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { + return "NOT_IMPLEMENTED"; + } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java index 29c7fbbf410..689ecc356dc 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java @@ -4,18 +4,42 @@ package com.yahoo.vespa.hosted.controller.api.integration.billing; import com.yahoo.vespa.hosted.controller.tenant.BillingReference; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; +import java.math.BigDecimal; import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.Optional; import java.util.UUID; public class BillingReporterMock implements BillingReporter { private final Clock clock; + private final BillingDatabaseClient dbClient; - public BillingReporterMock(Clock clock) { + public BillingReporterMock(Clock clock, BillingDatabaseClient dbClient) { this.clock = clock; + this.dbClient = dbClient; } @Override public BillingReference maintainTenant(CloudTenant tenant) { return new BillingReference(UUID.randomUUID().toString(), clock.instant()); } + + @Override + public InvoiceUpdate maintainInvoice(Bill bill) { + dbClient.addLineItem(bill.tenant(), maintainedMarkerItem(), Optional.of(bill.id())); + return new InvoiceUpdate(1,0,0); + } + + @Override + public String exportBill(Bill bill, String exportMethod, CloudTenant tenant) { + // Replace bill with a copy with exportedId set + var exportedId = "EXT-ID-123"; + dbClient.setExportedInvoiceId(bill.id(), exportedId); + return exportedId; + } + + private static Bill.LineItem maintainedMarkerItem() { + return new Bill.LineItem("maintained", "", BigDecimal.valueOf(0.0), "", "", ZonedDateTime.now()); + } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java index e7f87d3a628..ddcd5308986 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java @@ -5,6 +5,8 @@ import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceUsage; +import java.math.BigDecimal; + /** * @author ogronnesby */ @@ -16,4 +18,15 @@ public interface CostCalculator { /** Estimate the cost for the given resources */ double calculate(NodeResources resources); + /** CPU unit price */ + BigDecimal getCpuPrice(); + + /** Memory unit price */ + BigDecimal getMemoryPrice(); + + /** Disk unit price */ + BigDecimal getDiskPrice(); + + /** GPU unit price */ + BigDecimal getGpuPrice(); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java new file mode 100644 index 00000000000..6ca3cf6ebb1 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java @@ -0,0 +1,45 @@ +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +/** + * Helper to track changes to an invoice. + * + * @author gjoranv + */ +public record InvoiceUpdate(int itemsAdded, int itemsRemoved, int itemsModified) { + public boolean isEmpty() { + return itemsAdded == 0 && itemsRemoved == 0 && itemsModified == 0; + } + + public static InvoiceUpdate empty() { + return new InvoiceUpdate(0, 0, 0); + } + + public static class Counter { + private int itemsAdded = 0; + private int itemsRemoved = 0; + private int itemsModified = 0; + + public void addedItem() { + itemsAdded++; + } + + public void removedItem() { + itemsRemoved++; + } + + public void modifiedItem() { + itemsModified++; + } + + public void add(InvoiceUpdate other) { + itemsAdded += other.itemsAdded; + itemsRemoved += other.itemsRemoved; + itemsModified += other.itemsModified; + } + + public InvoiceUpdate finish() { + return new InvoiceUpdate(itemsAdded, itemsRemoved, itemsModified); + } + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java index 8ef14dd60ba..18dd339b4a1 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.billing; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.tenant.TaxId; import java.math.BigDecimal; import java.time.Clock; @@ -22,6 +23,7 @@ import java.util.stream.Stream; public class MockBillingController implements BillingController { private final Clock clock; + private final BillingDatabaseClient dbClient; PlanId defaultPlan = PlanId.from("trial"); List<TenantName> tenants = new ArrayList<>(); @@ -32,8 +34,9 @@ public class MockBillingController implements BillingController { Map<TenantName, List<Bill.LineItem>> unusedLineItems = new HashMap<>(); Map<TenantName, CollectionMethod> collectionMethod = new HashMap<>(); - public MockBillingController(Clock clock) { + public MockBillingController(Clock clock, BillingDatabaseClient dbClient) { this.clock = clock; + this.dbClient = dbClient; } @Override @@ -71,7 +74,7 @@ public class MockBillingController implements BillingController { .add(new Bill( billId, tenant, - Bill.StatusHistory.open(clock), + StatusHistory.open(clock), List.of(), startTime, endTime @@ -116,7 +119,7 @@ public class MockBillingController implements BillingController { } @Override - public void updateBillStatus(Bill.Id billId, String agent, String status) { + public void updateBillStatus(Bill.Id billId, String agent, BillStatus status) { var now = clock.instant().atZone(ZoneOffset.UTC); committedBills.values().stream() .flatMap(List::stream) @@ -134,7 +137,7 @@ public class MockBillingController implements BillingController { "line-item-id", description, amount, - "some-plan", + "paid", agent, ZonedDateTime.now())); } @@ -203,6 +206,29 @@ public class MockBillingController implements BillingController { return count < limit; } + @Override + public AcceptedCountries getAcceptedCountries() { + return new AcceptedCountries(List.of( + new AcceptedCountries.Country( + "NO", "Norway", + List.of(new AcceptedCountries.TaxType("no_vat", "Norwegian VAT number", "[0-9]{9}MVA", "123456789MVA"))), + new AcceptedCountries.Country( + "CA", "Canada", + List.of(new AcceptedCountries.TaxType("ca_gst_hst", "Canadian GST/HST number", "([0-9]{9}) ?RT ?([0-9]{4})", "123456789RT0002"), + new AcceptedCountries.TaxType("ca_pst_bc", "Canadian PST number (British Columbia)", "PST-?([0-9]{4})-?([0-9]{4})", "PST-1234-5678"))) + )); + } + + @Override + public void validateTaxId(TaxId id) throws IllegalArgumentException { + if (id.isEmpty() || id.isLegacy()) return; + if (!List.of("eu_vat", "no_vat").contains(id.type().value())) + throw new IllegalArgumentException("Unknown tax id type '%s'".formatted(id.type().value())); + if (!id.code().value().matches("\\w+")) + throw new IllegalArgumentException("Invalid tax id code '%s'".formatted(id.code().value())); + } + + public void setTenants(List<TenantName> tenants) { this.tenants = tenants; } @@ -234,6 +260,6 @@ public class MockBillingController implements BillingController { private Bill emptyBill() { var start = clock.instant().atZone(ZoneOffset.UTC); var end = clock.instant().atZone(ZoneOffset.UTC); - return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), Bill.StatusHistory.open(clock), List.of(), start, end); + return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), StatusHistory.open(clock), List.of(), start, end); } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java index 686a239a138..c0bd0dd29cd 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java @@ -20,6 +20,10 @@ public interface PlanRegistry { /** Get a set of all plans */ List<Plan> all(); + default Plan require(String planId) { + return plan(planId).orElseThrow(); + } + /** Get a plan give a plan ID */ default Optional<Plan> plan(String planId) { if (planId == null || planId.isBlank()) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java index 3ae2b0aa495..5af4d0cff29 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java @@ -144,5 +144,25 @@ public class PlanRegistryMock implements PlanRegistry { public double calculate(NodeResources resources) { return resources.cost(); } + + @Override + public BigDecimal getCpuPrice() { + return cpuHourCost; + } + + @Override + public BigDecimal getMemoryPrice() { + return memHourCost; + } + + @Override + public BigDecimal getDiskPrice() { + return dgbHourCost; + } + + @Override + public BigDecimal getGpuPrice() { + return gpuHourCost; + } } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java new file mode 100644 index 00000000000..f0c7f806c8c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java @@ -0,0 +1,61 @@ +package com.yahoo.vespa.hosted.controller.api.integration.billing; + +import java.time.Clock; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * @author ogronnesby + */ +public class StatusHistory { + SortedMap<ZonedDateTime, BillStatus> history; + + public StatusHistory(SortedMap<ZonedDateTime, BillStatus> history) { + // Validate the given history + var iter = history.values().iterator(); + BillStatus next = iter.hasNext() ? iter.next() : null; + while (iter.hasNext()) { + var current = next; + next = iter.next(); + if (! validateStatus(current, next)) { + throw new IllegalArgumentException("Invalid transition from " + current + " to " + next); + } + } + + this.history = history; + } + + public static StatusHistory open(Clock clock) { + var now = clock.instant().atZone(ZoneOffset.UTC); + return new StatusHistory( + new TreeMap<>(Map.of(now, BillStatus.OPEN)) + ); + } + + public BillStatus current() { + return history.get(history.lastKey()); + } + + public SortedMap<ZonedDateTime, BillStatus> getHistory() { + return history; + } + + public void checkValidTransition(BillStatus newStatus) { + if (! validateStatus(current(), newStatus)) { + throw new IllegalArgumentException("Invalid transition from " + current() + " to " + newStatus); + } + } + + private static boolean validateStatus(BillStatus current, BillStatus newStatus) { + return switch(current) { + case OPEN -> true; + case FROZEN -> newStatus != BillStatus.OPEN; // This could be subject to change. + case CLOSED -> newStatus == BillStatus.CLOSED; + case VOID -> newStatus == BillStatus.VOID; + }; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java index 421ec99d6f2..13fa6c862a7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java @@ -67,7 +67,7 @@ public class EndpointCertificateValidatorImpl implements EndpointCertificateVali } catch (SecretNotFoundException s) { // Normally because the cert is in the process of being provisioned - this will cause a retry in InternalStepRunner - throw new EndpointCertificateException(EndpointCertificateException.Type.CERT_NOT_AVAILABLE, "Certificate not found in secret store"); + throw new EndpointCertificateException(EndpointCertificateException.Type.CERT_NOT_AVAILABLE, "Certificate not found in secret store", s); } catch (EndpointCertificateException e) { if (!e.type().equals(EndpointCertificateException.Type.CERT_NOT_AVAILABLE)) { // such failures are normal and will be retried, it takes some time to show up in the secret store log.log(Level.WARNING, "Certificate validation failure for " + serializedInstanceId, e); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java index ef08c3a9adc..72728966dbc 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java @@ -1,9 +1,9 @@ // 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.api.integration.organization; +import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import java.util.Collection; @@ -14,10 +14,10 @@ import java.util.Collection; */ public class DeploymentFailureMails { - private final ZoneRegistry registry; + private final ConsoleUrls consoleUrls; - public DeploymentFailureMails(ZoneRegistry registry) { - this.registry = registry; + public DeploymentFailureMails(ConsoleUrls consoleUrls) { + this.consoleUrls = consoleUrls; } public Mail nodeAllocationFailure(RunId id, Collection<String> recipients) { @@ -66,8 +66,8 @@ public class DeploymentFailureMails { jobToString(id.type()), id.application(), messageDetail, - registry.dashboardUrl(id), - registry.supportUrl())); + consoleUrls.deploymentRun(id), + consoleUrls.support())); } private String jobToString(JobType type) { diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PriceInformation.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PriceInformation.java deleted file mode 100644 index 887741f9196..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PriceInformation.java +++ /dev/null @@ -1,9 +0,0 @@ -// 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.api.integration.pricing; - -import java.math.BigDecimal; - -public record PriceInformation(BigDecimal listPriceWithSupport, BigDecimal volumeDiscount, BigDecimal committedAmountDiscount, - BigDecimal enclaveDiscount, BigDecimal totalAmount) { - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingController.java deleted file mode 100644 index d8186f17796..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingController.java +++ /dev/null @@ -1,18 +0,0 @@ -// 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.api.integration.pricing; - -import com.yahoo.config.provision.ClusterResources; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan; - -import java.util.List; - -/** - * A service that calculates price information based on cluster resources, plan, service level etc. - * - * @author hmusum - */ -public interface PricingController { - - PriceInformation price(List<ClusterResources> clusterResources, PricingInfo pricingInfo, Plan plan); - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingInfo.java deleted file mode 100644 index 938991e2ed7..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/PricingInfo.java +++ /dev/null @@ -1,10 +0,0 @@ -// 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.api.integration.pricing; - -public record PricingInfo(boolean enclave, SupportLevel supportLevel, double committedHourlyAmount) { - - public enum SupportLevel { BASIC, COMMERCIAL, ENTERPRISE } - - public static PricingInfo empty() { return new PricingInfo(false, SupportLevel.COMMERCIAL, 0); } - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/package-info.java deleted file mode 100644 index 649ab2a80f4..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/pricing/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.vespa.hosted.controller.api.integration.pricing; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java index 747c6b72172..92c0a6b1fbb 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java @@ -1,8 +1,6 @@ // 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.api.integration.zone; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.CloudName; @@ -18,7 +16,6 @@ import com.yahoo.config.provision.zone.ZoneFilter; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import java.net.URI; import java.time.Duration; @@ -97,24 +94,6 @@ public interface ZoneRegistry { /** Returns the routing method used by given zone */ RoutingMethod routingMethod(ZoneId zone); - /** Returns a URL where an informative dashboard can be found. */ - URI dashboardUrl(); - - /** Returns a URL which displays information about the given tenant. */ - URI dashboardUrl(TenantName id); - - /** Returns a URL which displays information about the given application. */ - URI dashboardUrl(TenantName tenantName, ApplicationName applicationName); - - /** Returns a URL which displays information about the given application instance. */ - URI dashboardUrl(ApplicationId id); - - /** Returns a URL which displays information about the given job run. */ - URI dashboardUrl(RunId id); - - /** Returns a URL used to request support from the Vespa team. */ - URI supportUrl(); - /** Returns a URL to the controller's api endpoint */ URI apiUrl(); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 52900f83203..54f53d64f76 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -72,25 +72,11 @@ enum PathGroup { "/application/v4/tenant/{tenant}/archive-access/aws", "/application/v4/tenant/{tenant}/archive-access/gcp"), - - billingToken(Matcher.tenant, - "/billing/v1/tenant/{tenant}/token"), - - billingInstrument(Matcher.tenant, - "/billing/v1/tenant/{tenant}/instrument/{*}"), - - billingPlan(Matcher.tenant, - "/billing/v1/tenant/{tenant}/plan/{*}"), - - billingCollection(Matcher.tenant, - "/billing/v1/tenant/{tenant}/collection/{*}"), - - billingList(Matcher.tenant, - "/billing/v1/tenant/{tenant}/billing/{*}"), - billing(Matcher.tenant, "/billing/v2/tenant/{tenant}/{*}"), + billingAux("/billing/v2/countries"), + accountant("/billing/v2/accountant/{*}"), userSearch("/user/v1/find"), @@ -247,11 +233,6 @@ enum PathGroup { /** Paths used for receiving payment callbacks */ paymentProcessor("/payment/notification"), - /** Paths used for invoice management */ - hostedAccountant("/billing/v1/invoice/{*}", - "/billing/v1/billing", - "/billing/v1/plans"), - /** Path used for listing endpoint certificate request and re-requesting endpoint certificates */ endpointCertificates("/endpointcertificates/"), @@ -322,20 +303,12 @@ enum PathGroup { static Set<PathGroup> operatorRestrictedPaths() { var paths = billingPathsNoToken(); - paths.add(PathGroup.billingToken); paths.add(accessRequestApproval); return paths; } static Set<PathGroup> billingPathsNoToken() { - return EnumSet.of( - PathGroup.billingCollection, - PathGroup.billingInstrument, - PathGroup.billingList, - PathGroup.billingPlan, - PathGroup.billing, - PathGroup.hostedAccountant - ); + return EnumSet.of(PathGroup.billing, PathGroup.billingAux); } /** Returns whether this group matches path in given context */ diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java index 6b5130cf2e5..d1a8b2ef0c3 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java @@ -26,10 +26,7 @@ enum Policy { .in(SystemName.all()), Privilege.grant(Action.read) .on(PathGroup.billingPathsNoToken()) - .in(SystemName.all()), - Privilege.grant(Action.read) - .on(PathGroup.billingToken) - .in(SystemName.PublicCd)), + .in(SystemName.all())), /** Full access to everything. */ supporter(Privilege.grant(Action.read) @@ -155,40 +152,14 @@ enum Policy { .on(PathGroup.paymentProcessor) .in(SystemName.PublicCd)), - /** Read your own instrument information */ - paymentInstrumentRead(Privilege.grant(Action.read) - .on(PathGroup.billingInstrument) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Ability to update tenant payment instrument */ - paymentInstrumentUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingInstrument) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Ability to remove your own payment instrument */ - paymentInstrumentDelete(Privilege.grant(Action.delete) - .on(PathGroup.billingInstrument) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Get the token to view instrument form */ - paymentInstrumentCreate(Privilege.grant(Action.read) - .on(PathGroup.billingToken) - .in(SystemName.PublicCd, SystemName.Public)), - /** Ability to update tenant payment instrument */ planUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingPlan, PathGroup.billing) - .in(SystemName.PublicCd, SystemName.Public)), - - /** Ability to update tenant collection method */ - collectionMethodUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingCollection) + .on(PathGroup.billing) .in(SystemName.PublicCd, SystemName.Public)), - /** Read the generated bills */ billingInformationRead(Privilege.grant(Action.read) - .on(PathGroup.billingList, PathGroup.billing) + .on(PathGroup.billing, PathGroup.billingAux) .in(SystemName.PublicCd, SystemName.Public)), accessRequests(Privilege.grant(Action.all()) @@ -197,7 +168,7 @@ enum Policy { /** Invoice management */ hostedAccountant(Privilege.grant(Action.all()) - .on(PathGroup.hostedAccountant, PathGroup.accountant, PathGroup.userSearch) + .on(PathGroup.accountant, PathGroup.userSearch) .in(SystemName.PublicCd, SystemName.Public)), /** Listing endpoint certificates and re-requesting certificates */ diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index d57e38df239..31c8560c908 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -43,8 +43,6 @@ public enum RoleDefinition { Policy.applicationRead, Policy.deploymentRead, Policy.publicRead, - Policy.paymentInstrumentRead, - Policy.paymentInstrumentDelete, Policy.billingInformationRead, Policy.horizonProxyOperations), @@ -56,8 +54,6 @@ public enum RoleDefinition { Policy.developmentDeployment, Policy.keyManagement, Policy.submission, - Policy.paymentInstrumentRead, - Policy.paymentInstrumentDelete, Policy.billingInformationRead, Policy.secretStoreOperations, Policy.dataplaneToken), @@ -72,7 +68,6 @@ public enum RoleDefinition { Policy.tenantArchiveAccessManagement, Policy.applicationManager, Policy.keyRevokal, - Policy.paymentInstrumentRead, Policy.billingInformationRead, Policy.accessRequests ), @@ -99,7 +94,6 @@ public enum RoleDefinition { paymentProcessor(Policy.paymentProcessor), hostedAccountant(Policy.hostedAccountant, - Policy.collectionMethodUpdate, Policy.planUpdate, Policy.tenantUpdate); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java index b55157b90be..02bb669417c 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; @@ -192,7 +191,7 @@ public class SystemFlagsDataArchive { flagData.rules().forEach(rule -> rule.conditions().forEach(condition -> { int force_switch_expression_dummy = switch (condition.type()) { case RELATIONAL -> switch (condition.dimension()) { - case APPLICATION_ID, CLOUD, CLOUD_ACCOUNT, CLUSTER_ID, CLUSTER_TYPE, CONSOLE_USER_EMAIL, + case APPLICATION, CLOUD, CLOUD_ACCOUNT, CLUSTER_ID, CLUSTER_TYPE, CONSOLE_USER_EMAIL, ENVIRONMENT, HOSTNAME, INSTANCE_ID, NODE_TYPE, SYSTEM, TENANT_ID, ZONE_ID -> throw new FlagValidationException(condition.type().toWire() + " " + DimensionHelper.toWire(condition.dimension()) + @@ -207,7 +206,7 @@ public class SystemFlagsDataArchive { }; case WHITELIST, BLACKLIST -> switch (condition.dimension()) { - case APPLICATION_ID -> validateConditionValues(condition, SystemFlagsDataArchive::validateTenantApplication); + case APPLICATION -> validateConditionValues(condition, SystemFlagsDataArchive::validateTenantApplication); case CONSOLE_USER_EMAIL -> validateConditionValues(condition, email -> { if (!email.contains("@")) throw new FlagValidationException("Invalid email address: " + email); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java index 4a61ff30c25..9ceeba32061 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java @@ -51,11 +51,16 @@ public class CloudTenant extends Tenant { /** Creates a tenant with the given name, provided it passes validation. */ public static CloudTenant create(TenantName tenantName, Instant createdAt, Principal creator) { + // Initialize with creator as verified contact + var info = TenantInfo.empty().withContacts(new TenantContacts(List.of( + new TenantContacts.EmailContact( + List.of(TenantContacts.Audience.TENANT, TenantContacts.Audience.NOTIFICATIONS), + new Email(creator.getName(), true))))); return new CloudTenant(requireName(tenantName), createdAt, LastLoginInfo.EMPTY, Optional.ofNullable(creator).map(SimplePrincipal::of), - ImmutableBiMap.of(), TenantInfo.empty(), List.of(), new ArchiveAccess(), Optional.empty(), + ImmutableBiMap.of(), info, List.of(), new ArchiveAccess(), Optional.empty(), Instant.EPOCH, List.of(), Optional.empty(), PlanId.from("none")); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java index 995f1b1864f..702a183e7af 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java @@ -25,7 +25,7 @@ public class Email { } public static Email empty() { - return new Email("", true); + return new Email("", false); } public Email withEmailAddress(String emailAddress) { @@ -36,6 +36,10 @@ public class Email { return new Email(emailAddress, isVerified); } + public boolean isBlank() { + return emailAddress.isBlank(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java index 057c8bad89b..9c4bbc88f1f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java @@ -76,6 +76,7 @@ public class PendingMailVerification { public enum MailType { TENANT_CONTACT, - NOTIFICATIONS + NOTIFICATIONS, + BILLING } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java new file mode 100644 index 00000000000..d222864a388 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java @@ -0,0 +1,21 @@ +// 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.tenant; + +import ai.vespa.validation.StringWrapper; + +import static ai.vespa.validation.Validation.requireLength; + +/** + * @author olaa + */ +public class PurchaseOrder extends StringWrapper<PurchaseOrder> { + + public PurchaseOrder(String value) { + super(value); + requireLength(value, "purchase order length", 0, 64); + } + + public static PurchaseOrder empty() { + return new PurchaseOrder(""); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java new file mode 100644 index 00000000000..99c2400c58c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java @@ -0,0 +1,41 @@ +// 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.tenant; + +import ai.vespa.validation.StringWrapper; + +import static ai.vespa.validation.Validation.requireLength; + +/** + * @author olaa + */ +public record TaxId(Type type, Code code) { + + public TaxId(String type, String code) { this(new Type(type), new Code(code)); } + + public static TaxId empty() { return new TaxId(Type.empty(), Code.empty()); } + public boolean isEmpty() { return type.isEmpty() && code.isEmpty(); } + + // TODO(bjorncs) Remove legacy once no longer present in ZK + public static TaxId legacy(String code) { return new TaxId(Type.empty(), new Code(code)); } + public boolean isLegacy() { return type.isEmpty() && !code.isEmpty(); } + + public static class Type extends StringWrapper<Type> { + public Type(String value) { + super(value); + requireLength(value, "tax code type length", 0, 16); + } + + public static Type empty() { return new Type(""); } + public boolean isEmpty() { return value().isEmpty(); } + } + + public static class Code extends StringWrapper<Code> { + public Code(String value) { + super(value); + requireLength(value, "tax code value length", 0, 64); + } + + public static Code empty() { return new Code(""); } + public boolean isEmpty() { return value().isEmpty(); } + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java index 5377b820e18..6e3b26661e5 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java @@ -10,14 +10,20 @@ public class TenantBilling { private final TenantContact contact; private final TenantAddress address; + private final TaxId taxId; + private final PurchaseOrder purchaseOrder; + private final Email invoiceEmail; - public TenantBilling(TenantContact contact, TenantAddress address) { + public TenantBilling(TenantContact contact, TenantAddress address, TaxId taxId, PurchaseOrder purchaseOrder, Email invoiceEmail) { this.contact = Objects.requireNonNull(contact); this.address = Objects.requireNonNull(address); + this.taxId = Objects.requireNonNull(taxId); + this.purchaseOrder = Objects.requireNonNull(purchaseOrder); + this.invoiceEmail = Objects.requireNonNull(invoiceEmail); } public static TenantBilling empty() { - return new TenantBilling(TenantContact.empty(), TenantAddress.empty()); + return new TenantBilling(TenantContact.empty(), TenantAddress.empty(), TaxId.empty(), PurchaseOrder.empty(), Email.empty()); } public TenantContact contact() { @@ -28,12 +34,36 @@ public class TenantBilling { return address; } + public TaxId getTaxId() { + return taxId; + } + + public PurchaseOrder getPurchaseOrder() { + return purchaseOrder; + } + + public Email getInvoiceEmail() { + return invoiceEmail; + } + public TenantBilling withContact(TenantContact updatedContact) { - return new TenantBilling(updatedContact, this.address); + return new TenantBilling(updatedContact, this.address, this.taxId, this.purchaseOrder, this.invoiceEmail); } public TenantBilling withAddress(TenantAddress updatedAddress) { - return new TenantBilling(this.contact, updatedAddress); + return new TenantBilling(this.contact, updatedAddress, this.taxId, this.purchaseOrder, this.invoiceEmail); + } + + public TenantBilling withTaxId(TaxId updatedTaxId) { + return new TenantBilling(this.contact, this.address, updatedTaxId, this.purchaseOrder, this.invoiceEmail); + } + + public TenantBilling withPurchaseOrder(PurchaseOrder updatedPurchaseOrder) { + return new TenantBilling(this.contact, this.address, this.taxId, updatedPurchaseOrder, this.invoiceEmail); + } + + public TenantBilling withInvoiceEmail(Email updatedInvoiceEmail) { + return new TenantBilling(this.contact, this.address, this.taxId, this.purchaseOrder, updatedInvoiceEmail); } public boolean isEmpty() { @@ -45,19 +75,26 @@ public class TenantBilling { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TenantBilling that = (TenantBilling) o; - return Objects.equals(contact, that.contact) && Objects.equals(address, that.address); + return Objects.equals(contact, that.contact) && + Objects.equals(address, that.address) && + Objects.equals(taxId, that.taxId) && + Objects.equals(purchaseOrder, that.purchaseOrder) && + Objects.equals(invoiceEmail, that.invoiceEmail); } @Override public int hashCode() { - return Objects.hash(contact, address); + return Objects.hash(contact, address, taxId, purchaseOrder, invoiceEmail); } @Override public String toString() { - return "TenantInfoBillingContact{" + + return "TenantBilling{" + "contact=" + contact + ", address=" + address + + ", taxId='" + taxId + '\'' + + ", purchaseOrder='" + purchaseOrder + '\'' + + ", invoiceEmail=" + invoiceEmail + '}'; } } |