summaryrefslogtreecommitdiffstats
path: root/controller-api
diff options
context:
space:
mode:
authorØyvind Grønnesby <ogr@ogr.no>2021-10-11 09:13:12 +0200
committerGitHub <noreply@github.com>2021-10-11 09:13:12 +0200
commit5e9f649da988f171e34952f6626096dbd4c2093b (patch)
tree19d3c12ac06c76249d1187ffec9c13209a36c7d7 /controller-api
parent9ae8365988df61354b9a87d16cd3ddaff21fb102 (diff)
Revert "Revert "Billing refactoring and cleanup""
Diffstat (limited to 'controller-api')
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java (renamed from controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java)18
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java71
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java135
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java178
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java60
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java122
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java146
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java4
11 files changed, 771 insertions, 46 deletions
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 4714b74ba94..93e3f5585c8 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,6 +7,8 @@ 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;
@@ -25,6 +27,7 @@ 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;
@@ -88,6 +91,10 @@ public interface ServiceRegistry {
BillingController billingController();
+ ResourceDatabaseClient resourceDatabase();
+
+ BillingDatabaseClient billingDatabase();
+
ContainerRegistry containerRegistry();
TenantSecretService tenantSecretService();
@@ -99,4 +106,6 @@ 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/Invoice.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java
index 3789021ae8e..d1af5b428de 100644
--- 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/Bill.java
@@ -20,18 +20,18 @@ 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
+ * 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.
* <p>
* All line items have a Plan associated with them - which was used to map from utilization to an actual price.
* <p>
- * The invoice has a status history, but only the latest status is exposed through this API.
+ * The bill has a status history, but only the latest status is exposed through this API.
*
* @author ogronnesby
*/
-public class Invoice {
+public class Bill {
private static final BigDecimal SCALED_ZERO = new BigDecimal("0.00");
private final Id id;
@@ -41,7 +41,7 @@ public class Invoice {
private final ZonedDateTime startTime;
private final ZonedDateTime endTime;
- public Invoice(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems, ZonedDateTime startTime, ZonedDateTime endTime) {
+ public Bill(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems, ZonedDateTime startTime, ZonedDateTime endTime) {
this.id = id;
this.tenant = tenant;
this.lineItems = List.copyOf(lineItems);
@@ -141,8 +141,8 @@ public class Invoice {
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);
+ Id billId = (Id) o;
+ return value.equals(billId.value);
}
@Override
@@ -152,14 +152,14 @@ public class Invoice {
@Override
public String toString() {
- return "InvoiceId{" +
+ return "BillId{" +
"value='" + value + '\'' +
'}';
}
}
/**
- * Represents a chargeable line on an invoice.
+ * Represents a chargeable line on a bill.
*/
public static class LineItem {
private final String id;
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 91916975146..61f8844482c 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,55 +12,112 @@ 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<TenantName> tenantsWithPlan(List<TenantName> 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);
- Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
+ /**
+ * 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 createUncommittedInvoice(TenantName tenant, LocalDate until);
+ /**
+ * 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);
- Map<TenantName, Invoice> createUncommittedInvoices(LocalDate until);
+ /** Run {createUncommittedBill} for all tenants with unbilled use */
+ Map<TenantName, Bill> createUncommittedBills(LocalDate until);
- List<Invoice.LineItem> getUnusedLineItems(TenantName tenant);
+ /** Get line items that have been manually added to a tenant, but is not yet part of a bill */
+ List<Bill.LineItem> getUnusedLineItems(TenantName tenant);
+ /** Get the payment instrument for the given tenant */
Optional<PaymentInstrument> 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);
- void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status);
+ /** Change the status of the given bill */
+ void updateBillStatus(Bill.Id billId, 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);
- List<Invoice> getInvoicesForTenant(TenantName tenant);
+ /** Get all bills for the given tenant */
+ List<Bill> getBillsForTenant(TenantName tenant);
- List<Invoice> getInvoices();
+ /** Get all bills from the system */
+ List<Bill> getBills();
+ /** Delete billing contact information from the tenant */
void deleteBillingInfo(TenantName tenant, Set<User> 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
new file mode 100644
index 00000000000..4891fe0ffa7
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java
@@ -0,0 +1,135 @@
+// 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<InstrumentOwner> 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<Bill> 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<Bill> readBillsForTenant(TenantName tenant);
+
+ /**
+ * Read all bills, ordered by date
+ * @return List of all bills ordered by date
+ */
+ List<Bill> 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<Bill.Id> 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<Bill.LineItem> 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<Plan> 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<TenantName, Optional<Plan>> getPlans(List<TenantName> 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<CollectionMethod> 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
new file mode 100644
index 00000000000..f53025a2e6d
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java
@@ -0,0 +1,178 @@
+// 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<TenantName, Plan> tenantPlans = new HashMap<>();
+ private final Map<Bill.Id, TenantName> invoices = new HashMap<>();
+ 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, ZonedDateTime> startTimes = new HashMap<>();
+ private final Map<Bill.Id, ZonedDateTime> 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<InstrumentOwner> paymentInstruments = new ArrayList<>();
+ private final Map<TenantName, CollectionMethod> 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<InstrumentOwner> 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<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 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<Bill.Id> 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<Bill.LineItem> 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<Plan> getPlan(TenantName tenantName) {
+ return Optional.ofNullable(tenantPlans.get(tenantName));
+ }
+
+ @Override
+ public Map<TenantName, Optional<Plan>> getPlans(List<TenantName> 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<CollectionMethod> getCollectionMethod(TenantName tenantName) {
+ return Optional.ofNullable(collectionMethods.get(tenantName));
+ }
+
+ @Override
+ public void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod) {
+ collectionMethods.put(tenantName, collectionMethod);
+ }
+
+ @Override
+ public List<Bill> 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<Bill> 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/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
index 8816c4eb57b..f4d3577aeec 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<TenantName, PlanId> plans = new HashMap<>();
Map<TenantName, PaymentInstrument> activeInstruments = new HashMap<>();
- Map<TenantName, List<Invoice>> committedInvoices = new HashMap<>();
- Map<TenantName, Invoice> uncommittedInvoices = new HashMap<>();
- Map<TenantName, List<Invoice.LineItem>> unusedLineItems = new HashMap<>();
+ Map<TenantName, List<Bill>> committedBills = new HashMap<>();
+ Map<TenantName, Bill> uncommittedBills = new HashMap<>();
+ Map<TenantName, List<Bill.LineItem>> unusedLineItems = new HashMap<>();
Map<TenantName, CollectionMethod> collectionMethod = new HashMap<>();
public MockBillingController(Clock clock) {
@@ -64,32 +64,32 @@ public class MockBillingController implements BillingController {
}
@Override
- 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,
+ 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,
tenant,
- Invoice.StatusHistory.open(clock),
+ Bill.StatusHistory.open(clock),
List.of(),
startTime,
endTime
));
- return invoiceId;
+ return billId;
}
@Override
- public Invoice createUncommittedInvoice(TenantName tenant, LocalDate until) {
- return uncommittedInvoices.getOrDefault(tenant, emptyInvoice());
+ public Bill createUncommittedBill(TenantName tenant, LocalDate until) {
+ return uncommittedBills.getOrDefault(tenant, emptyBill());
}
@Override
- public Map<TenantName, Invoice> createUncommittedInvoices(LocalDate until) {
- return uncommittedInvoices;
+ public Map<TenantName, Bill> createUncommittedBills(LocalDate until) {
+ return uncommittedBills;
}
@Override
- public List<Invoice.LineItem> getUnusedLineItems(TenantName tenant) {
+ public List<Bill.LineItem> getUnusedLineItems(TenantName tenant) {
return unusedLineItems.getOrDefault(tenant, List.of());
}
@@ -110,18 +110,18 @@ public class MockBillingController implements BillingController {
}
@Override
- public void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status) {
+ public void updateBillStatus(Bill.Id billId, String agent, String status) {
var now = clock.instant().atZone(ZoneOffset.UTC);
- committedInvoices.values().stream()
+ committedBills.values().stream()
.flatMap(List::stream)
- .filter(invoice -> invoiceId.equals(invoice.id()))
- .forEach(invoice -> invoice.statusHistory().history.put(now, status));
+ .filter(bill -> billId.equals(bill.id()))
+ .forEach(bill -> bill.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 Invoice.LineItem(
+ .add(new Bill.LineItem(
"line-item-id",
description,
amount,
@@ -152,13 +152,13 @@ public class MockBillingController implements BillingController {
}
@Override
- public List<Invoice> getInvoicesForTenant(TenantName tenant) {
- return committedInvoices.getOrDefault(tenant, List.of());
+ public List<Bill> getBillsForTenant(TenantName tenant) {
+ return committedBills.getOrDefault(tenant, List.of());
}
@Override
- public List<Invoice> getInvoices() {
- return committedInvoices.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
+ public List<Bill> getBills() {
+ return committedBills.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
}
@Override
@@ -191,17 +191,17 @@ public class MockBillingController implements BillingController {
"country");
}
- public void addInvoice(TenantName tenantName, Invoice invoice, boolean committed) {
+ public void addBill(TenantName tenantName, Bill bill, boolean committed) {
if (committed)
- committedInvoices.computeIfAbsent(tenantName, i -> new ArrayList<>())
- .add(invoice);
+ committedBills.computeIfAbsent(tenantName, i -> new ArrayList<>())
+ .add(bill);
else
- uncommittedInvoices.put(tenantName, invoice);
+ uncommittedBills.put(tenantName, bill);
}
- private Invoice emptyInvoice() {
+ private Bill emptyBill() {
var start = clock.instant().atZone(ZoneOffset.UTC);
var end = clock.instant().atZone(ZoneOffset.UTC);
- return new Invoice(Invoice.Id.of("empty"), TenantName.defaultName(), Invoice.StatusHistory.open(clock), List.of(), start, end);
+ return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), Bill.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
new file mode 100644
index 00000000000..d64d6e3ea04
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java
@@ -0,0 +1,23 @@
+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> plan(PlanId planId);
+
+ /** Get a plan give a plan ID */
+ default Optional<Plan> 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
new file mode 100644
index 00000000000..60eddbd24ff
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java
@@ -0,0 +1,122 @@
+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> 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
new file mode 100644
index 00000000000..2f277193231
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
@@ -0,0 +1,51 @@
+// 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<ResourceSnapshot> snapshots);
+
+ List<ResourceSnapshot> getResourceSnapshotsForMonth(TenantName tenantName, ApplicationName applicationName, YearMonth month);
+
+ List<ResourceUsage> getResourceSnapshotsForPeriod(TenantName tenantName, long startTimestamp, long endTimestamp);
+
+ void refreshMaterializedView();
+
+ Set<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName);
+
+ List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth);
+
+ Set<TenantName> getTenants();
+
+ default List<ResourceUsage> 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
new file mode 100644
index 00000000000..5a4d250ea9d
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
@@ -0,0 +1,146 @@
+// 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<TenantName, Plan> planMap = new HashMap<>();
+ List<ResourceSnapshot> resourceSnapshots = new ArrayList<>();
+ private boolean hasRefreshedMaterializedView = false;
+
+ public ResourceDatabaseClientMock(PlanRegistry planRegistry) {
+ this.planRegistry = planRegistry;
+ }
+
+ @Override
+ public void writeResourceSnapshots(Collection<ResourceSnapshot> items) {
+ this.resourceSnapshots.addAll(items);
+ }
+
+ @Override
+ public List<ResourceSnapshot> 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<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth) {
+ return resourceSnapshots;
+ }
+
+ @Override
+ public Set<TenantName> getTenants() {
+ return resourceSnapshots.stream()
+ .map(snapshot -> snapshot.getApplicationId().tenant())
+ .collect(Collectors.toSet());
+ }
+
+ private List<ResourceUsage> resourceUsageFromSnapshots(Plan plan, List<ResourceSnapshot> 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<ResourceUsage> 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 a8d9c4b1f8a..319b9239ae4 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,6 +31,10 @@ 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<Node> nodes, Instant timestamp, ZoneId zoneId) {
Set<ApplicationId> applicationIds = nodes.stream()
.filter(node -> node.owner().isPresent())