From 292ba88e1a7254982deb5894248109b539e6ee1f Mon Sep 17 00:00:00 2001 From: Øyvind Grønnesby Date: Mon, 11 Oct 2021 12:02:31 +0200 Subject: Revert "Revert "Revert "Billing refactoring and cleanup""" --- .../api/integration/ServiceRegistry.java | 9 - .../controller/api/integration/billing/Bill.java | 347 --------------------- .../api/integration/billing/BillingController.java | 71 +---- .../integration/billing/BillingDatabaseClient.java | 135 -------- .../billing/BillingDatabaseClientMock.java | 178 ----------- .../api/integration/billing/Invoice.java | 347 +++++++++++++++++++++ .../integration/billing/MockBillingController.java | 60 ++-- .../api/integration/billing/PlanRegistry.java | 23 -- .../api/integration/billing/PlanRegistryMock.java | 122 -------- .../resource/ResourceDatabaseClient.java | 51 --- .../resource/ResourceDatabaseClientMock.java | 146 --------- .../api/integration/resource/ResourceSnapshot.java | 4 - 12 files changed, 384 insertions(+), 1109 deletions(-) delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java (limited to 'controller-api') 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 93e3f5585c8..4714b74ba94 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 @@ -7,8 +7,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService; import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEventFetcher; import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer; @@ -27,7 +25,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipI import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor; import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer; import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringClient; -import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient; import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretService; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient; @@ -91,10 +88,6 @@ public interface ServiceRegistry { BillingController billingController(); - ResourceDatabaseClient resourceDatabase(); - - BillingDatabaseClient billingDatabase(); - ContainerRegistry containerRegistry(); TenantSecretService tenantSecretService(); @@ -106,6 +99,4 @@ public interface ServiceRegistry { AccessControlService accessControlService(); HorizonClient horizonClient(); - - PlanRegistry planRegistry(); } 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 deleted file mode 100644 index d1af5b428de..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java +++ /dev/null @@ -1,347 +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.api.integration.billing; - -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.TenantName; -import com.yahoo.config.provision.zone.ZoneId; - -import java.math.BigDecimal; -import java.time.Clock; -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; - - -/** - * An Bill is an identifier with a status (with history) and line items. A line item is the meat and - * potatoes of the content of the bill, and are a history of items. Most line items are connected to - * a given deployment in Vespa Cloud, but they can also be manually added to e.g. give a discount or represent - * support. - *

- * All line items have a Plan associated with them - which was used to map from utilization to an actual price. - *

- * The bill has a status history, but only the latest status is exposed through this API. - * - * @author ogronnesby - */ -public class Bill { - private static final BigDecimal SCALED_ZERO = new BigDecimal("0.00"); - - private final Id id; - private final TenantName tenant; - private final List lineItems; - private final StatusHistory statusHistory; - private final ZonedDateTime startTime; - private final ZonedDateTime endTime; - - public Bill(Id id, TenantName tenant, StatusHistory statusHistory, List lineItems, ZonedDateTime startTime, ZonedDateTime endTime) { - this.id = id; - this.tenant = tenant; - this.lineItems = List.copyOf(lineItems); - this.statusHistory = statusHistory; - this.startTime = startTime; - this.endTime = endTime; - } - - public Id id() { - return id; - } - - public TenantName tenant() { - return tenant; - } - - public String status() { - return statusHistory.current(); - } - - public StatusHistory statusHistory() { - return statusHistory; - } - - public List lineItems() { - return lineItems; - } - - public ZonedDateTime getStartTime() { - return startTime; - } - - public ZonedDateTime getEndTime() { - return endTime; - } - - public BigDecimal sum() { - return lineItems.stream().map(LineItem::amount).reduce(SCALED_ZERO, BigDecimal::add); - } - - public BigDecimal sumCpuHours() { - return sumResourceValues(LineItem::getCpuHours); - } - - public BigDecimal sumMemoryHours() { - return sumResourceValues(LineItem::getMemoryHours); - } - - public BigDecimal sumDiskHours() { - return sumResourceValues(LineItem::getDiskHours); - } - - public BigDecimal sumCpuCost() { - return sumResourceValues(LineItem::getCpuCost); - } - - public BigDecimal sumMemoryCost() { - return sumResourceValues(LineItem::getMemoryCost); - } - - public BigDecimal sumDiskCost() { - return sumResourceValues(LineItem::getDiskCost); - } - - public BigDecimal sumAdditionalCost() { - // anything that is not covered by the cost for resources is "additional" costs - var resourceCosts = sumCpuCost().add(sumMemoryCost()).add(sumDiskCost()); - return sum().subtract(resourceCosts); - } - - private BigDecimal sumResourceValues(Function> f) { - return lineItems.stream().flatMap(li -> f.apply(li).stream()).reduce(SCALED_ZERO, BigDecimal::add); - } - - public static final class Id { - private final String value; - - public static Id of(String value) { - Objects.requireNonNull(value); - return new Id(value); - } - - public static Id generate() { - var id = UUID.randomUUID().toString(); - return new Id(id); - } - - private Id(String value) { - this.value = value; - } - - public String value() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Id billId = (Id) o; - return value.equals(billId.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } - - @Override - public String toString() { - return "BillId{" + - "value='" + value + '\'' + - '}'; - } - } - - /** - * Represents a chargeable line on a bill. - */ - public static class LineItem { - private final String id; - private final String description; - private final BigDecimal amount; - private final String plan; - private final String agent; - private final ZonedDateTime addedAt; - private ZonedDateTime startedAt; - private ZonedDateTime endedAt; - private ApplicationId applicationId; - private ZoneId zoneId; - private BigDecimal cpuHours; - private BigDecimal memoryHours; - private BigDecimal diskHours; - private BigDecimal cpuCost; - private BigDecimal memoryCost; - private BigDecimal diskCost; - - public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt) { - this.id = id; - this.description = description; - this.amount = amount; - this.plan = plan; - this.agent = agent; - this.addedAt = addedAt; - } - - public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt, ZonedDateTime startedAt, ZonedDateTime endedAt, ApplicationId applicationId, ZoneId zoneId, - BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, BigDecimal cpuCost, BigDecimal memoryCost, BigDecimal diskCost) { - this(id, description, amount, plan, agent, addedAt); - this.startedAt = startedAt; - this.endedAt = endedAt; - - if (applicationId == null && zoneId != null) - throw new IllegalArgumentException("Must supply applicationId if zoneId is supplied"); - - this.applicationId = applicationId; - this.zoneId = zoneId; - this.cpuHours = cpuHours; - this.memoryHours = memoryHours; - this.diskHours = diskHours; - this.cpuCost = cpuCost; - this.memoryCost = memoryCost; - this.diskCost = diskCost; - } - - /** The opaque ID of this */ - public String id() { - return id; - } - - /** The string description of this - used for display purposes */ - public String description() { - return description; - } - - /** The dollar amount of this */ - public BigDecimal amount() { - return SCALED_ZERO.add(amount); - } - - /** The plan used to calculate amount of this */ - public String plan() { - return plan; - } - - /** Who created this line item */ - public String agent() { - return agent; - } - - /** When was this line item added */ - public ZonedDateTime addedAt() { - return addedAt; - } - - /** What time period is this line item for - time start */ - public Optional startedAt() { - return Optional.ofNullable(startedAt); - } - - /** What time period is this line item for - time end */ - public Optional endedAt() { - return Optional.ofNullable(endedAt); - } - - /** Optionally - what application is this line item about */ - public Optional applicationId() { - return Optional.ofNullable(applicationId); - } - - /** Optionally - what zone deployment is this line item about */ - public Optional zoneId() { - return Optional.ofNullable(zoneId); - } - - public Optional getCpuHours() { - return Optional.ofNullable(cpuHours); - } - - public Optional getMemoryHours() { - return Optional.ofNullable(memoryHours); - } - - public Optional getDiskHours() { - return Optional.ofNullable(diskHours); - } - - public Optional getCpuCost() { - return Optional.ofNullable(cpuCost); - } - - public Optional getMemoryCost() { - return Optional.ofNullable(memoryCost); - } - - public Optional getDiskCost() { - return Optional.ofNullable(diskCost); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - LineItem lineItem = (LineItem) o; - return id.equals(lineItem.id) && - description.equals(lineItem.description) && - amount.equals(lineItem.amount) && - plan.equals(lineItem.plan) && - agent.equals(lineItem.agent) && - addedAt.equals(lineItem.addedAt) && - startedAt.equals(lineItem.startedAt) && - endedAt.equals(lineItem.endedAt) && - applicationId.equals(lineItem.applicationId) && - zoneId.equals(lineItem.zoneId); - } - - @Override - public int hashCode() { - return Objects.hash(id, description, amount, plan, agent, addedAt, startedAt, endedAt, applicationId, zoneId); - } - - @Override - public String toString() { - return "LineItem{" + - "id='" + id + '\'' + - ", description='" + description + '\'' + - ", amount=" + amount + - ", plan='" + plan + '\'' + - ", agent='" + agent + '\'' + - ", addedAt=" + addedAt + - ", startedAt=" + startedAt + - ", endedAt=" + endedAt + - ", applicationId=" + applicationId + - ", zoneId=" + zoneId + - '}'; - } - } - - public static class StatusHistory { - SortedMap history; - - public StatusHistory(SortedMap 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 getHistory() { - return history; - } - - } - -} 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 61f8844482c..91916975146 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 @@ -12,112 +12,55 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -/** - * A service that controls creation of bills based on the resource usage of a tenant, controls the quota for a - * tenant, and controls the plan the tenant is on. - * - * @author ogronnesby - * @author olaa - */ public interface BillingController { - /** - * Get the plan ID for the given tenant. - * This method will not fail if the tenant does not exist, it will return the default plan for that tenant instead. - */ PlanId getPlan(TenantName tenant); - /** - * Return the list of tenants with the given plan. - * @param existing All existing tenants in the system - * @param planId The ID of the plan to filter existing tenants on. - * @return The tenants that have the given plan. - */ List tenantsWithPlan(List existing, PlanId planId); - /** The display name of the given plan */ String getPlanDisplayName(PlanId planId); - /** - * The quota for the given tenant. - * This method will return default quota for tenants that do not exist. - */ Quota getQuota(TenantName tenant); /** - * Set the plan for the current tenant. Checks some pre-conditions to see if the tenant is eligible for the - * given plan. - * @param tenant The name of the tenant. - * @param planId The ID of the plan to change to. - * @param hasDeployments Does the tenant have active deployments. * @return String containing error message if something went wrong. Empty otherwise */ PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasDeployments); - /** - * Create a bill of unbilled use for the given tenant in the given time period. - * @param tenant The name of the tenant. - * @param startTime The start of the billing period - * @param endTime The end of the billing period - * @param agent The agent that creates the bill - * @return The ID of the new bill. - */ - Bill.Id createBillForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent); + Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent); - /** - * Create an unpersisted bill of unbilled use for the given tenant from the end of last bill until the given date. - * This is used to show "unbilled use" in the Console. - * @param tenant The name of the tenant. - * @param until The end date of the unbilled use period. - * @return A bill with the resource use and cost. - */ - Bill createUncommittedBill(TenantName tenant, LocalDate until); + Invoice createUncommittedInvoice(TenantName tenant, LocalDate until); - /** Run {createUncommittedBill} for all tenants with unbilled use */ - Map createUncommittedBills(LocalDate until); + Map createUncommittedInvoices(LocalDate until); - /** Get line items that have been manually added to a tenant, but is not yet part of a bill */ - List getUnusedLineItems(TenantName tenant); + List getUnusedLineItems(TenantName tenant); - /** Get the payment instrument for the given tenant */ Optional getDefaultInstrument(TenantName tenant); - /** Get the auth token needed to talk to payment services */ String createClientToken(String tenant, String userId); - /** Delete a payment instrument from the list of the tenant's instruments */ 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 updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status); - /** Add a line item to the given bill */ void addLineItem(TenantName tenant, String description, BigDecimal amount, String agent); - /** Delete a line item - only available for unused line items */ void deleteLineItem(String lineItemId); - /** Set the given payment instrument as the active instrument for the tenant */ boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument); - /** List the payment instruments from the tenant */ InstrumentList listInstruments(TenantName tenant, String userId); - /** Get all bills for the given tenant */ - List getBillsForTenant(TenantName tenant); + List getInvoicesForTenant(TenantName tenant); - /** Get all bills from the system */ - List getBills(); + List getInvoices(); - /** Delete billing contact information from the tenant */ void deleteBillingInfo(TenantName tenant, Set users, boolean isPrivileged); - /** Get the bill collection method for the given tenant */ default CollectionMethod getCollectionMethod(TenantName tenant) { return CollectionMethod.NONE; } - /** Set the bill collection method for the given tenant */ default CollectionResult setCollectionMethod(TenantName tenant, CollectionMethod method) { return CollectionResult.error("Method not implemented"); } 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 deleted file mode 100644 index 4891fe0ffa7..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright Verizon Media. 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 com.yahoo.config.provision.TenantName; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * Interface that talks about bills in the billing API. It is a layer on top of the SQL - * database where we store data about bills. - * - * @author olaa - * @author ogronnesby - */ -public interface BillingDatabaseClient { - - boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument); - - Optional getDefaultPaymentInstrumentForTenant(TenantName from); - - /** - * Create a completely new Bill in the open state with no LineItems. - * - * @param tenant The name of the tenant the bill is for - * @param agent The agent that created the bill - * @return The Id of the new bill - */ - Bill.Id createBill(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent); - - /** - * Read the given bill from the data source - * - * @param billId The Id of the bill to retrieve - * @return The Bill if it exists, Optional.empty() if not. - */ - Optional readBill(Bill.Id billId); - - /** - * Get all bills for a given tenant, ordered by date - * - * @param tenant The name of the tenant - * @return List of all bills ordered by date - */ - List readBillsForTenant(TenantName tenant); - - /** - * Read all bills, ordered by date - * @return List of all bills ordered by date - */ - List readBills(); - - /** - * Add a line item to an open bill - * - * @param lineItem - * @param billId The optional ID of the bill this line item is for - * @return The Id of the new line item - * @throws RuntimeException if the bill is not in OPEN state - */ - String addLineItem(TenantName tenantName, Bill.LineItem lineItem, Optional billId); - - /** - * Set status for the given bill - * - * @param billId The ID of the bill this status is for - * @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); - - List getUnusedLineItems(TenantName tenantName); - - /** - * Delete a line item - * This is only allowed if the line item has not yet been associated with an bill - * - * @param lineItemId The ID of the line item - * @throws RuntimeException if the line item is associated with an bill - */ - void deleteLineItem(String lineItemId); - - /** - * Associate all uncommitted line items to a given bill - * This is only allowed if the line item has not already been associated with an bill - * - * @param tenantName The tenant we want to commit line items for - * @param billId The ID of the line item - * @throws RuntimeException if the line item is already associated with an bill - */ - void commitLineItems(TenantName tenantName, Bill.Id billId); - - /** - * Return the plan for the given tenant - * - * @param tenantName The tenant to retrieve the plan for - * @return Optional.of the plan if present in DN, else Optional.empty - */ - Optional getPlan(TenantName tenantName); - - /** - * Return the plan for the given tenants if present. - * If the database does not know of the tenant, the tenant is not included in the result. - */ - Map> getPlans(List tenants); - - /** - * Set the current plan for the given tenant - * - * @param tenantName The tenant to set the plan for - * @param plan The plan to use - */ - void setPlan(TenantName tenantName, Plan plan); - - /** - * Deactivates the default payment instrument for a tenant, if it exists. - * Used during tenant deletion - */ - void deactivateDefaultPaymentInstrument(TenantName tenant); - - /** - * Get the current collection method for the tenant - if one has persisted - * @return Optional.empty if no collection method has been persisted for the tenant - */ - Optional getCollectionMethod(TenantName tenantName); - - /** - * Set the collection method for the tenant - * @param tenantName The name of the tenant to set collection method for - * @param collectionMethod The collection method for the tenant - */ - void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod); -} 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 deleted file mode 100644 index f53025a2e6d..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright Verizon Media. 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 com.yahoo.config.provision.TenantName; - -import java.time.Clock; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -/** - * @author olaa - */ -public class BillingDatabaseClientMock implements BillingDatabaseClient { - private final Clock clock; - private final PlanRegistry planRegistry; - private final Map tenantPlans = new HashMap<>(); - private final Map invoices = new HashMap<>(); - private final Map> lineItems = new HashMap<>(); - private final Map> uncommittedLineItems = new HashMap<>(); - - private final Map statuses = new HashMap<>(); - private final Map startTimes = new HashMap<>(); - private final Map endTimes = 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")); - - private final List paymentInstruments = new ArrayList<>(); - private final Map collectionMethods = new HashMap<>(); - - public BillingDatabaseClientMock(Clock clock, PlanRegistry planRegistry) { - this.clock = clock; - this.planRegistry = planRegistry; - } - - @Override - public boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument) { - return paymentInstruments.add(paymentInstrument); - } - - @Override - public Optional getDefaultPaymentInstrumentForTenant(TenantName tenantName) { - return paymentInstruments.stream() - .filter(paymentInstrument -> paymentInstrument.getTenantName().equals(tenantName)) - .findFirst(); - } - - public String getStatus(Bill.Id invoiceId) { - return statuses.get(invoiceId).current(); - } - - @Override - 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)); - startTimes.put(invoiceId, startTime); - endTimes.put(invoiceId, endTime); - return invoiceId; - } - - @Override - public Optional 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 start = startTimes.getOrDefault(billId, startTime); - var end = endTimes.getOrDefault(billId, endTime); - return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end)); - } - - @Override - public String addLineItem(TenantName tenantName, Bill.LineItem lineItem, Optional invoiceId) { - var lineItemId = UUID.randomUUID().toString(); - invoiceId.ifPresentOrElse( - invoice -> lineItems.computeIfAbsent(invoice, l -> new ArrayList<>()).add(lineItem), - () -> uncommittedLineItems.computeIfAbsent(tenantName, l -> new ArrayList<>()).add(lineItem) - ); - return lineItemId; - } - - @Override - public void setStatus(Bill.Id invoiceId, String agent, String status) { - statuses.computeIfAbsent(invoiceId, k -> Bill.StatusHistory.open(clock)) - .getHistory() - .put(ZonedDateTime.now(), status); - } - - @Override - public List getUnusedLineItems(TenantName tenantName) { - return uncommittedLineItems.getOrDefault(tenantName, new ArrayList<>()); - } - - @Override - public void deleteLineItem(String lineItemId) { - uncommittedLineItems.values() - .forEach(list -> - list.removeIf(lineItem -> lineItem.id().equals(lineItemId)) - ); - } - - @Override - public void commitLineItems(TenantName tenantName, Bill.Id invoiceId) { - - } - - @Override - public Optional getPlan(TenantName tenantName) { - return Optional.ofNullable(tenantPlans.get(tenantName)); - } - - @Override - public Map> getPlans(List tenants) { - return tenantPlans.entrySet().stream() - .filter(entry -> tenants.contains(entry.getKey())) - .collect(Collectors.toMap( - entry -> entry.getKey(), - entry -> planRegistry.plan(entry.getValue().id()) - )); - } - - @Override - public void setPlan(TenantName tenantName, Plan plan) { - tenantPlans.put(tenantName, plan); - } - - @Override - public void deactivateDefaultPaymentInstrument(TenantName tenant) { - paymentInstruments.removeIf(instrumentOwner -> instrumentOwner.getTenantName().equals(tenant)); - } - - @Override - public Optional getCollectionMethod(TenantName tenantName) { - return Optional.ofNullable(collectionMethods.get(tenantName)); - } - - @Override - public void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod) { - collectionMethods.put(tenantName, collectionMethod); - } - - @Override - public List readBillsForTenant(TenantName tenant) { - return invoices.entrySet().stream() - .filter(entry -> entry.getValue().equals(tenant)) - .map(Map.Entry::getKey) - .map(invoiceId -> { - var items = lineItems.getOrDefault(invoiceId, List.of()); - var status = statuses.get(invoiceId); - var start = startTimes.get(invoiceId); - var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); - }) - .collect(Collectors.toList()); - } - - @Override - public List readBills() { - return invoices.keySet().stream() - .map(invoiceId -> { - var tenant = invoices.get(invoiceId); - var items = lineItems.getOrDefault(invoiceId, List.of()); - var status = statuses.get(invoiceId); - var start = startTimes.get(invoiceId); - var end = endTimes.get(invoiceId); - return new Bill(invoiceId, tenant, status, items, start, end); - }) - .collect(Collectors.toList()); - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java new file mode 100644 index 00000000000..3789021ae8e --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java @@ -0,0 +1,347 @@ +// 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 com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.zone.ZoneId; + +import java.math.BigDecimal; +import java.time.Clock; +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; + + +/** + * An Invoice is an identifier with a status (with history) and line items. A line item is the meat and + * potatoes of the content of the invoice, and are a history of items. Most line items are connected to + * a given deployment in Vespa Cloud, but they can also be manually added to e.g. give a discount or represent + * support. + *

+ * All line items have a Plan associated with them - which was used to map from utilization to an actual price. + *

+ * The invoice has a status history, but only the latest status is exposed through this API. + * + * @author ogronnesby + */ +public class Invoice { + private static final BigDecimal SCALED_ZERO = new BigDecimal("0.00"); + + private final Id id; + private final TenantName tenant; + private final List lineItems; + private final StatusHistory statusHistory; + private final ZonedDateTime startTime; + private final ZonedDateTime endTime; + + public Invoice(Id id, TenantName tenant, StatusHistory statusHistory, List lineItems, ZonedDateTime startTime, ZonedDateTime endTime) { + this.id = id; + this.tenant = tenant; + this.lineItems = List.copyOf(lineItems); + this.statusHistory = statusHistory; + this.startTime = startTime; + this.endTime = endTime; + } + + public Id id() { + return id; + } + + public TenantName tenant() { + return tenant; + } + + public String status() { + return statusHistory.current(); + } + + public StatusHistory statusHistory() { + return statusHistory; + } + + public List lineItems() { + return lineItems; + } + + public ZonedDateTime getStartTime() { + return startTime; + } + + public ZonedDateTime getEndTime() { + return endTime; + } + + public BigDecimal sum() { + return lineItems.stream().map(LineItem::amount).reduce(SCALED_ZERO, BigDecimal::add); + } + + public BigDecimal sumCpuHours() { + return sumResourceValues(LineItem::getCpuHours); + } + + public BigDecimal sumMemoryHours() { + return sumResourceValues(LineItem::getMemoryHours); + } + + public BigDecimal sumDiskHours() { + return sumResourceValues(LineItem::getDiskHours); + } + + public BigDecimal sumCpuCost() { + return sumResourceValues(LineItem::getCpuCost); + } + + public BigDecimal sumMemoryCost() { + return sumResourceValues(LineItem::getMemoryCost); + } + + public BigDecimal sumDiskCost() { + return sumResourceValues(LineItem::getDiskCost); + } + + public BigDecimal sumAdditionalCost() { + // anything that is not covered by the cost for resources is "additional" costs + var resourceCosts = sumCpuCost().add(sumMemoryCost()).add(sumDiskCost()); + return sum().subtract(resourceCosts); + } + + private BigDecimal sumResourceValues(Function> f) { + return lineItems.stream().flatMap(li -> f.apply(li).stream()).reduce(SCALED_ZERO, BigDecimal::add); + } + + public static final class Id { + private final String value; + + public static Id of(String value) { + Objects.requireNonNull(value); + return new Id(value); + } + + public static Id generate() { + var id = UUID.randomUUID().toString(); + return new Id(id); + } + + private Id(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id invoiceId = (Id) o; + return value.equals(invoiceId.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return "InvoiceId{" + + "value='" + value + '\'' + + '}'; + } + } + + /** + * Represents a chargeable line on an invoice. + */ + public static class LineItem { + private final String id; + private final String description; + private final BigDecimal amount; + private final String plan; + private final String agent; + private final ZonedDateTime addedAt; + private ZonedDateTime startedAt; + private ZonedDateTime endedAt; + private ApplicationId applicationId; + private ZoneId zoneId; + private BigDecimal cpuHours; + private BigDecimal memoryHours; + private BigDecimal diskHours; + private BigDecimal cpuCost; + private BigDecimal memoryCost; + private BigDecimal diskCost; + + public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt) { + this.id = id; + this.description = description; + this.amount = amount; + this.plan = plan; + this.agent = agent; + this.addedAt = addedAt; + } + + public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt, ZonedDateTime startedAt, ZonedDateTime endedAt, ApplicationId applicationId, ZoneId zoneId, + BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, BigDecimal cpuCost, BigDecimal memoryCost, BigDecimal diskCost) { + this(id, description, amount, plan, agent, addedAt); + this.startedAt = startedAt; + this.endedAt = endedAt; + + if (applicationId == null && zoneId != null) + throw new IllegalArgumentException("Must supply applicationId if zoneId is supplied"); + + this.applicationId = applicationId; + this.zoneId = zoneId; + this.cpuHours = cpuHours; + this.memoryHours = memoryHours; + this.diskHours = diskHours; + this.cpuCost = cpuCost; + this.memoryCost = memoryCost; + this.diskCost = diskCost; + } + + /** The opaque ID of this */ + public String id() { + return id; + } + + /** The string description of this - used for display purposes */ + public String description() { + return description; + } + + /** The dollar amount of this */ + public BigDecimal amount() { + return SCALED_ZERO.add(amount); + } + + /** The plan used to calculate amount of this */ + public String plan() { + return plan; + } + + /** Who created this line item */ + public String agent() { + return agent; + } + + /** When was this line item added */ + public ZonedDateTime addedAt() { + return addedAt; + } + + /** What time period is this line item for - time start */ + public Optional startedAt() { + return Optional.ofNullable(startedAt); + } + + /** What time period is this line item for - time end */ + public Optional endedAt() { + return Optional.ofNullable(endedAt); + } + + /** Optionally - what application is this line item about */ + public Optional applicationId() { + return Optional.ofNullable(applicationId); + } + + /** Optionally - what zone deployment is this line item about */ + public Optional zoneId() { + return Optional.ofNullable(zoneId); + } + + public Optional getCpuHours() { + return Optional.ofNullable(cpuHours); + } + + public Optional getMemoryHours() { + return Optional.ofNullable(memoryHours); + } + + public Optional getDiskHours() { + return Optional.ofNullable(diskHours); + } + + public Optional getCpuCost() { + return Optional.ofNullable(cpuCost); + } + + public Optional getMemoryCost() { + return Optional.ofNullable(memoryCost); + } + + public Optional getDiskCost() { + return Optional.ofNullable(diskCost); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LineItem lineItem = (LineItem) o; + return id.equals(lineItem.id) && + description.equals(lineItem.description) && + amount.equals(lineItem.amount) && + plan.equals(lineItem.plan) && + agent.equals(lineItem.agent) && + addedAt.equals(lineItem.addedAt) && + startedAt.equals(lineItem.startedAt) && + endedAt.equals(lineItem.endedAt) && + applicationId.equals(lineItem.applicationId) && + zoneId.equals(lineItem.zoneId); + } + + @Override + public int hashCode() { + return Objects.hash(id, description, amount, plan, agent, addedAt, startedAt, endedAt, applicationId, zoneId); + } + + @Override + public String toString() { + return "LineItem{" + + "id='" + id + '\'' + + ", description='" + description + '\'' + + ", amount=" + amount + + ", plan='" + plan + '\'' + + ", agent='" + agent + '\'' + + ", addedAt=" + addedAt + + ", startedAt=" + startedAt + + ", endedAt=" + endedAt + + ", applicationId=" + applicationId + + ", zoneId=" + zoneId + + '}'; + } + } + + public static class StatusHistory { + SortedMap history; + + public StatusHistory(SortedMap 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 getHistory() { + return history; + } + + } + +} 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 f4d3577aeec..8816c4eb57b 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 @@ -26,9 +26,9 @@ public class MockBillingController implements BillingController { private final Clock clock; Map plans = new HashMap<>(); Map activeInstruments = new HashMap<>(); - Map> committedBills = new HashMap<>(); - Map uncommittedBills = new HashMap<>(); - Map> unusedLineItems = new HashMap<>(); + Map> committedInvoices = new HashMap<>(); + Map uncommittedInvoices = new HashMap<>(); + Map> unusedLineItems = new HashMap<>(); Map collectionMethod = new HashMap<>(); public MockBillingController(Clock clock) { @@ -64,32 +64,32 @@ public class MockBillingController implements BillingController { } @Override - public Bill.Id createBillForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) { - var billId = Bill.Id.of("id-123"); - committedBills.computeIfAbsent(tenant, l -> new ArrayList<>()) - .add(new Bill( - billId, + public Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) { + var invoiceId = Invoice.Id.of("id-123"); + committedInvoices.computeIfAbsent(tenant, l -> new ArrayList<>()) + .add(new Invoice( + invoiceId, tenant, - Bill.StatusHistory.open(clock), + Invoice.StatusHistory.open(clock), List.of(), startTime, endTime )); - return billId; + return invoiceId; } @Override - public Bill createUncommittedBill(TenantName tenant, LocalDate until) { - return uncommittedBills.getOrDefault(tenant, emptyBill()); + public Invoice createUncommittedInvoice(TenantName tenant, LocalDate until) { + return uncommittedInvoices.getOrDefault(tenant, emptyInvoice()); } @Override - public Map createUncommittedBills(LocalDate until) { - return uncommittedBills; + public Map createUncommittedInvoices(LocalDate until) { + return uncommittedInvoices; } @Override - public List getUnusedLineItems(TenantName tenant) { + public List getUnusedLineItems(TenantName tenant) { return unusedLineItems.getOrDefault(tenant, List.of()); } @@ -110,18 +110,18 @@ public class MockBillingController implements BillingController { } @Override - public void updateBillStatus(Bill.Id billId, String agent, String status) { + public void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status) { var now = clock.instant().atZone(ZoneOffset.UTC); - committedBills.values().stream() + committedInvoices.values().stream() .flatMap(List::stream) - .filter(bill -> billId.equals(bill.id())) - .forEach(bill -> bill.statusHistory().history.put(now, status)); + .filter(invoice -> invoiceId.equals(invoice.id())) + .forEach(invoice -> invoice.statusHistory().history.put(now, status)); } @Override public void addLineItem(TenantName tenant, String description, BigDecimal amount, String agent) { unusedLineItems.computeIfAbsent(tenant, l -> new ArrayList<>()) - .add(new Bill.LineItem( + .add(new Invoice.LineItem( "line-item-id", description, amount, @@ -152,13 +152,13 @@ public class MockBillingController implements BillingController { } @Override - public List getBillsForTenant(TenantName tenant) { - return committedBills.getOrDefault(tenant, List.of()); + public List getInvoicesForTenant(TenantName tenant) { + return committedInvoices.getOrDefault(tenant, List.of()); } @Override - public List getBills() { - return committedBills.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); + public List getInvoices() { + return committedInvoices.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); } @Override @@ -191,17 +191,17 @@ public class MockBillingController implements BillingController { "country"); } - public void addBill(TenantName tenantName, Bill bill, boolean committed) { + public void addInvoice(TenantName tenantName, Invoice invoice, boolean committed) { if (committed) - committedBills.computeIfAbsent(tenantName, i -> new ArrayList<>()) - .add(bill); + committedInvoices.computeIfAbsent(tenantName, i -> new ArrayList<>()) + .add(invoice); else - uncommittedBills.put(tenantName, bill); + uncommittedInvoices.put(tenantName, invoice); } - private Bill emptyBill() { + private Invoice emptyInvoice() { 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 Invoice(Invoice.Id.of("empty"), TenantName.defaultName(), Invoice.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 deleted file mode 100644 index d64d6e3ea04..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.yahoo.vespa.hosted.controller.api.integration.billing; - -import java.util.Optional; - -/** - * Registry of all current plans we have support for - * - * @author ogronnesby - */ -public interface PlanRegistry { - - /** Get the default plan */ - Plan defaultPlan(); - - /** Get a plan given a plan ID */ - Optional plan(PlanId planId); - - /** Get a plan give a plan ID */ - default Optional plan(String planId) { - return plan(PlanId.from(planId)); - } - -} 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 deleted file mode 100644 index 60eddbd24ff..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.yahoo.vespa.hosted.controller.api.integration.billing; - -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; -import java.math.RoundingMode; -import java.util.Optional; -import java.util.stream.Stream; - -public class PlanRegistryMock implements PlanRegistry { - - public static final Plan freeTrial = new MockPlan("trial", false, 0, 0, 0, 200, "Free Trial - for testing purposes"); - public static final Plan paidPlan = new MockPlan("paid", true, "0.09", "0.009", "0.0003", 500, "Paid Plan - for testing purposes"); - public static final Plan nonePlan = new MockPlan("none", false, 0, 0, 0, 0, "None Plan - for testing purposes"); - - @Override - public Plan defaultPlan() { - return freeTrial; - } - - @Override - public Optional plan(PlanId planId) { - return Stream.of(freeTrial, paidPlan, nonePlan) - .filter(p -> p.id().equals(planId)) - .findAny(); - } - - private static class MockPlan implements Plan { - private final PlanId planId; - private final String description; - private final CostCalculator costCalculator; - private final QuotaCalculator quotaCalculator; - private final boolean billed; - - public MockPlan(String planId, boolean billed, double cpuPrice, double memPrice, double dgbPrice, int quota, String description) { - this(PlanId.from(planId), billed, new MockCostCalculator(cpuPrice, memPrice, dgbPrice), () -> Quota.unlimited().withBudget(quota), description); - } - - public MockPlan(String planId, boolean billed, String cpuPrice, String memPrice, String dgbPrice, int quota, String description) { - this(PlanId.from(planId), billed, new MockCostCalculator(cpuPrice, memPrice, dgbPrice), () -> Quota.unlimited().withBudget(quota), description); - } - - public MockPlan(PlanId planId, boolean billed, MockCostCalculator calculator, QuotaCalculator quota, String description) { - this.planId = planId; - this.billed = billed; - this.costCalculator = calculator; - this.quotaCalculator = quota; - this.description = description; - } - - @Override - public PlanId id() { - return planId; - } - - @Override - public String displayName() { - return description; - } - - @Override - public CostCalculator calculator() { - return costCalculator; - } - - @Override - public QuotaCalculator quota() { - return quotaCalculator; - } - - @Override - public boolean isBilled() { - return billed; - } - } - - private static class MockCostCalculator implements CostCalculator { - private static final BigDecimal millisPerHour = BigDecimal.valueOf(60 * 60 * 1000); - private final BigDecimal cpuHourCost; - private final BigDecimal memHourCost; - private final BigDecimal dgbHourCost; - - public MockCostCalculator(String cpuPrice, String memPrice, String dgbPrice) { - this(new BigDecimal(cpuPrice), new BigDecimal(memPrice), new BigDecimal(dgbPrice)); - } - - public MockCostCalculator(double cpuPrice, double memPrice, double dgbPrice) { - this(BigDecimal.valueOf(cpuPrice), BigDecimal.valueOf(memPrice), BigDecimal.valueOf(dgbPrice)); - } - - public MockCostCalculator(BigDecimal cpuPrice, BigDecimal memPrice, BigDecimal dgbPrice) { - this.cpuHourCost = cpuPrice; - this.memHourCost = memPrice; - this.dgbHourCost = dgbPrice; - } - - @Override - public CostInfo calculate(ResourceUsage usage) { - var cpuCost = usage.getCpuMillis().multiply(cpuHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP); - var memCost = usage.getMemoryMillis().multiply(memHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP); - var dgbCost = usage.getDiskMillis().multiply(dgbHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP); - - return new CostInfo( - usage.getApplicationId(), - usage.getZoneId(), - usage.getCpuMillis().divide(millisPerHour, RoundingMode.HALF_UP), - usage.getMemoryMillis().divide(millisPerHour, RoundingMode.HALF_UP), - usage.getDiskMillis().divide(millisPerHour, RoundingMode.HALF_UP), - cpuCost, - memCost, - dgbCost - ); - } - - @Override - public double calculate(NodeResources resources) { - return resources.cost(); - } - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java deleted file mode 100644 index 2f277193231..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.api.integration.resource; - -import com.yahoo.config.provision.ApplicationName; -import com.yahoo.config.provision.TenantName; - -import java.time.LocalDate; -import java.time.YearMonth; -import java.time.temporal.ChronoUnit; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -/** - * @author olaa - */ -public interface ResourceDatabaseClient { - - void writeResourceSnapshots(Collection snapshots); - - List getResourceSnapshotsForMonth(TenantName tenantName, ApplicationName applicationName, YearMonth month); - - List getResourceSnapshotsForPeriod(TenantName tenantName, long startTimestamp, long endTimestamp); - - void refreshMaterializedView(); - - Set getMonthsWithSnapshotsForTenant(TenantName tenantName); - - List getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth); - - Set getTenants(); - - default List getResourceSnapshotsForMonth(TenantName tenantName, YearMonth month) { - return getResourceSnapshotsForPeriod(tenantName, getMonthStartTimeStamp(month), getMonthEndTimeStamp(month)); - } - - private long getMonthStartTimeStamp(YearMonth month) { - LocalDate startOfMonth = LocalDate.of(month.getYear(), month.getMonth(), 1); - return startOfMonth.atStartOfDay(java.time.ZoneId.of("UTC")) - .toInstant() - .toEpochMilli(); - } - private long getMonthEndTimeStamp(YearMonth month) { - LocalDate startOfMonth = LocalDate.of(month.getYear(), month.getMonth(), 1); - return startOfMonth.plus(1, ChronoUnit.MONTHS) - .atStartOfDay(java.time.ZoneId.of("UTC")) - .toInstant() - .toEpochMilli(); - } - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java deleted file mode 100644 index 5a4d250ea9d..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.api.integration.resource; - -import com.yahoo.config.provision.ApplicationName; -import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry; - -import java.math.BigDecimal; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.YearMonth; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -/** - * @author olaa - */ -public class ResourceDatabaseClientMock implements ResourceDatabaseClient { - - PlanRegistry planRegistry; - Map planMap = new HashMap<>(); - List resourceSnapshots = new ArrayList<>(); - private boolean hasRefreshedMaterializedView = false; - - public ResourceDatabaseClientMock(PlanRegistry planRegistry) { - this.planRegistry = planRegistry; - } - - @Override - public void writeResourceSnapshots(Collection items) { - this.resourceSnapshots.addAll(items); - } - - @Override - public List getResourceSnapshotsForMonth(TenantName tenantName, ApplicationName applicationName, YearMonth month) { - return resourceSnapshots.stream() - .filter(resourceSnapshot -> { - LocalDate snapshotDate = LocalDate.ofInstant(resourceSnapshot.getTimestamp(), ZoneId.of("UTC")); - return YearMonth.from(snapshotDate).equals(month) && - snapshotDate.getYear() == month.getYear() && - resourceSnapshot.getApplicationId().tenant().equals(tenantName) && - resourceSnapshot.getApplicationId().application().equals(applicationName); - }) - .collect(Collectors.toList()); - } - - @Override - public Set getMonthsWithSnapshotsForTenant(TenantName tenantName) { - return Collections.emptySet(); - } - - @Override - public List getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth) { - return resourceSnapshots; - } - - @Override - public Set getTenants() { - return resourceSnapshots.stream() - .map(snapshot -> snapshot.getApplicationId().tenant()) - .collect(Collectors.toSet()); - } - - private List resourceUsageFromSnapshots(Plan plan, List snapshots) { - snapshots.sort(Comparator.comparing(ResourceSnapshot::getTimestamp)); - - return IntStream.range(0, snapshots.size()) - .mapToObj(idx -> { - var a = snapshots.get(idx); - var b = (idx + 1) < snapshots.size() ? snapshots.get(idx + 1) : null; - var start = a.getTimestamp(); - var end = Optional.ofNullable(b).map(ResourceSnapshot::getTimestamp).orElse(start.plusSeconds(120)); - var d = BigDecimal.valueOf(Duration.between(start, end).toMillis()); - return new ResourceUsage( - a.getApplicationId(), - a.getZoneId(), - plan, - BigDecimal.valueOf(a.getCpuCores()).multiply(d), - BigDecimal.valueOf(a.getMemoryGb()).multiply(d), - BigDecimal.valueOf(a.getDiskGb()).multiply(d) - ); - }) - .collect(Collectors.toList()); - } - - private ResourceUsage resourceUsageAdd(ResourceUsage a, ResourceUsage b) { - assert a.getApplicationId().equals(b.getApplicationId()); - assert a.getZoneId().equals(b.getZoneId()); - assert a.getPlan().equals(b.getPlan()); - return new ResourceUsage( - a.getApplicationId(), - a.getZoneId(), - a.getPlan(), - a.getCpuMillis().add(b.getCpuMillis()), - a.getMemoryMillis().add(b.getMemoryMillis()), - a.getDiskMillis().add(b.getDiskMillis()) - ); - } - - @Override - public List getResourceSnapshotsForPeriod(TenantName tenantName, long start, long end) { - var tenantPlan = planMap.getOrDefault(tenantName, planRegistry.defaultPlan()); - - var snapshotsPerDeployment = resourceSnapshots.stream() - .filter(snapshot -> snapshot.getTimestamp().isAfter(Instant.ofEpochMilli(start))) - .filter(snapshot -> snapshot.getTimestamp().isBefore(Instant.ofEpochMilli(end))) - .filter(snapshot -> snapshot.getApplicationId().tenant().equals(tenantName)) - .collect(Collectors.groupingBy( - usage -> Objects.hash(usage.getApplicationId(), usage.getZoneId(), tenantPlan.id().value()) - )) - .values().stream() - .map(snapshots -> resourceUsageFromSnapshots(tenantPlan, snapshots)) - .map(usages -> usages.stream().reduce(this::resourceUsageAdd)) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - - return snapshotsPerDeployment; - } - - @Override - public void refreshMaterializedView() { - hasRefreshedMaterializedView = true; - } - - public void setPlan(TenantName tenant, Plan plan) { - planMap.put(tenant, plan); - } - - public boolean hasRefreshedMaterializedView() { - return hasRefreshedMaterializedView; - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java index 319b9239ae4..a8d9c4b1f8a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java @@ -31,10 +31,6 @@ public class ResourceSnapshot { this.zoneId = zoneId; } - public static ResourceSnapshot from(ApplicationId applicationId, int nodes, double cpuCores, double memoryGb, double diskGb, Instant timestamp, ZoneId zoneId) { - return new ResourceSnapshot(applicationId, cpuCores * nodes, memoryGb * nodes, diskGb * nodes, timestamp, zoneId); - } - public static ResourceSnapshot from(List nodes, Instant timestamp, ZoneId zoneId) { Set applicationIds = nodes.stream() .filter(node -> node.owner().isPresent()) -- cgit v1.2.3