From e59bfa22dfc2d3448541c8eb443049fb9bb7002a Mon Sep 17 00:00:00 2001 From: Harald Musum Date: Sat, 27 Jun 2020 16:10:55 +0200 Subject: Revert "Add BillingApiHandler" --- .../api/integration/ServiceRegistry.java | 4 +- .../api/integration/billing/BillingController.java | 49 --- .../api/integration/billing/CostCalculator.java | 19 + .../api/integration/billing/InstrumentList.java | 39 -- .../api/integration/billing/InstrumentOwner.java | 67 ---- .../api/integration/billing/Invoice.java | 266 -------------- .../integration/billing/MockBillingController.java | 144 -------- .../api/integration/billing/PaymentInstrument.java | 51 --- .../controller/api/integration/billing/Plan.java | 23 ++ .../api/integration/billing/PlanController.java | 10 + .../controller/api/integration/billing/PlanId.java | 43 --- .../api/integration/billing/ResourceUsage.java | 54 +++ .../hosted/controller/api/role/PathGroup.java | 4 - .../vespa/hosted/controller/api/role/Policy.java | 5 - .../hosted/controller/api/role/RoleDefinition.java | 1 - .../restapi/billing/BillingApiHandler.java | 396 --------------------- .../controller/security/CloudAccessControl.java | 8 +- .../integration/ServiceRegistryMock.java | 16 +- .../restapi/billing/BillingApiHandlerTest.java | 214 ----------- .../restapi/billing/responses/billing-all-tenants | 48 --- .../billing/responses/invoice-creation-response | 1 - .../restapi/billing/responses/line-item-list | 9 - .../restapi/billing/responses/tenant-billing-view | 38 -- 23 files changed, 120 insertions(+), 1389 deletions(-) delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java delete 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/MockBillingController.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanController.java delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/ResourceUsage.java delete mode 100644 controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java delete mode 100644 controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java delete mode 100644 controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/billing-all-tenants delete mode 100644 controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/invoice-creation-response delete mode 100644 controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/line-item-list delete mode 100644 controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/tenant-billing-view 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 a522e26a46d..0b5f2538892 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 @@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.controller.api.integration; import com.yahoo.vespa.hosted.controller.api.integration.aws.ApplicationRoleService; import com.yahoo.vespa.hosted.controller.api.integration.aws.AwsEventFetcher; 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.PlanController; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore; @@ -77,6 +77,6 @@ public interface ServiceRegistry { SystemMonitor systemMonitor(); - BillingController billingController(); + PlanController planController(); } 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 deleted file mode 100644 index bd9568fe891..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java +++ /dev/null @@ -1,49 +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 com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public interface BillingController { - - PlanId getPlan(TenantName tenant); - - /** - * Returns true if plan was changed - */ - boolean setPlan(TenantName tenant, PlanId planId, boolean hasApplications); - - Invoice.Id createInvoiceForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent); - - Invoice createUncommittedInvoice(TenantName tenant, LocalDate until); - - Map createUncommittedInvoices(LocalDate until); - - List getUnusedLineItems(TenantName tenant); - - Optional getDefaultInstrument(TenantName tenant); - - String createClientToken(String tenant, String userId); - - boolean deleteInstrument(TenantName tenant, String userId, String instrumentId); - - void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status); - - void addLineItem(TenantName tenant, String description, BigDecimal amount, String agent); - - void deleteLineItem(String lineItemId); - - boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument); - - InstrumentList listInstruments(TenantName tenant, String userId); - - List getInvoices(TenantName tenant); - -} \ No newline at end of file diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java new file mode 100644 index 00000000000..628beec8450 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java @@ -0,0 +1,19 @@ +// 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.NodeResources; +import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo; + + +/** + * @author ogronnesby + */ +public interface CostCalculator { + + /** Calculate the cost for the given usage */ + CostInfo calculate(ResourceUsage usage); + + /** Estimate the cost for the given resources */ + double calculate(NodeResources resources); + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java deleted file mode 100644 index f26261cd157..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentList.java +++ /dev/null @@ -1,39 +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 java.util.ArrayList; -import java.util.List; - -/** - * @author olaa - */ -public class InstrumentList { - - private String activeInstrumentId; - private List instruments; - - - public InstrumentList(List instruments) { - this.instruments = instruments; - } - - public void setActiveInstrumentId(String activeInstrumentId) { - this.activeInstrumentId = activeInstrumentId; - } - - public void addInstrument(PaymentInstrument instrument) { - instruments.add(instrument); - } - - public void addInstruments(List instruments) { - instruments.addAll(instruments); - } - - public String getActiveInstrumentId() { - return activeInstrumentId; - } - - public List getInstruments() { - return instruments; - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java deleted file mode 100644 index 45e06b11b2a..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java +++ /dev/null @@ -1,67 +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.util.Objects; - -/** - * @author olaa - */ -public class InstrumentOwner { - - private final TenantName tenantName; - private final String userId; - private final String paymentInstrumentId; - private final boolean isDefault; - - public InstrumentOwner(TenantName tenantName, String userId, String paymentInstrumentId, boolean isDefault) { - this.tenantName = tenantName; - this.userId = userId; - this.paymentInstrumentId = paymentInstrumentId; - this.isDefault = isDefault; - } - - public TenantName getTenantName() { - return tenantName; - } - - public String getUserId() { - return userId; - } - - public String getPaymentInstrumentId() { - return paymentInstrumentId; - } - - public boolean isDefault() { - return isDefault; - } - - @Override - public String toString() { - return String.format( - "Tenant: %s\nCusomer ID: %s\nPayment Instrument ID: %s\nIs default: %s", - tenantName.value(), - userId, - paymentInstrumentId, - isDefault - ); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - InstrumentOwner other = (InstrumentOwner) o; - return this.tenantName.equals(other.getTenantName()) && - this.userId.equals(other.getUserId()) && - this.paymentInstrumentId.equals(other.getPaymentInstrumentId()) && - this.isDefault() == other.isDefault(); - } - - @Override - public int hashCode() { - return Objects.hash(tenantName, userId, paymentInstrumentId, isDefault); - } -} 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 deleted file mode 100644 index 31388d24e2e..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Invoice.java +++ /dev/null @@ -1,266 +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.ApplicationId; -import com.yahoo.config.provision.zone.ZoneId; - -import java.math.BigDecimal; -import java.time.ZonedDateTime; -import java.util.Collections; -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; - - -/** - * 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 List lineItems; - private final StatusHistory statusHistory; - private final ZonedDateTime startTime; - private final ZonedDateTime endTime; - - public Invoice(Id id, StatusHistory statusHistory, List lineItems, ZonedDateTime startTime, ZonedDateTime endTime) { - this.id = id; - this.lineItems = List.copyOf(lineItems); - this.statusHistory = statusHistory; - this.startTime = startTime; - this.endTime = endTime; - } - - public Id id() { - return id; - } - - 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 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 final Optional startedAt; - private final Optional endedAt; - private final Optional applicationId; - private final Optional zoneId; - - public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt, ZonedDateTime startedAt, ZonedDateTime endedAt, ApplicationId applicationId, ZoneId zoneId) { - this.id = id; - this.description = description; - this.amount = amount; - this.plan = plan; - this.agent = agent; - this.addedAt = addedAt; - this.startedAt = Optional.ofNullable(startedAt); - this.endedAt = Optional.ofNullable(endedAt); - - if (applicationId == null && zoneId != null) - throw new IllegalArgumentException("Must supply applicationId if zoneId is supplied"); - - this.applicationId = Optional.ofNullable(applicationId); - this.zoneId = Optional.ofNullable(zoneId); - } - - public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt) { - this(id, description, amount, plan, agent, addedAt, null, null, null, null); - } - - /** 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 startedAt; - } - - /** What time period is this line item for - time end */ - public Optional endedAt() { - return endedAt; - } - - /** Optionally - what application is this line item about */ - public Optional applicationId() { - return applicationId; - } - - /** Optionally - what zone deployment is this line item about */ - public Optional zoneId() { - return zoneId; - } - - @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() { - return new StatusHistory( - new TreeMap<>(Map.of(ZonedDateTime.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 deleted file mode 100644 index a4c25e301ba..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java +++ /dev/null @@ -1,144 +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 com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * @author olaa - */ -public class MockBillingController implements BillingController { - - Map plans = new HashMap<>(); - Map activeInstruments = new HashMap<>(); - Map> committedInvoices = new HashMap<>(); - Map uncommittedInvoices = new HashMap<>(); - Map> unusedLineItems = new HashMap<>(); - - @Override - public PlanId getPlan(TenantName tenant) { - return plans.get(tenant); - } - - @Override - public boolean setPlan(TenantName tenant, PlanId planId, boolean hasApplications) { - plans.put(tenant, planId); - return true; - } - - @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, - Invoice.StatusHistory.open(), - List.of(), - startTime, - endTime - )); - return invoiceId; - } - - @Override - public Invoice createUncommittedInvoice(TenantName tenant, LocalDate until) { - return uncommittedInvoices.get(tenant); - } - - @Override - public Map createUncommittedInvoices(LocalDate until) { - return uncommittedInvoices; - } - - @Override - public List getUnusedLineItems(TenantName tenant) { - return unusedLineItems.getOrDefault(tenant, List.of()); - } - - @Override - public Optional getDefaultInstrument(TenantName tenant) { - return Optional.ofNullable(activeInstruments.get(tenant)); - } - - @Override - public String createClientToken(String tenant, String userId) { - return "some-token"; - } - - @Override - public boolean deleteInstrument(TenantName tenant, String userId, String instrumentId) { - activeInstruments.remove(tenant); - return true; - } - - @Override - public void updateInvoiceStatus(Invoice.Id invoiceId, String agent, String status) { - committedInvoices.values().stream() - .flatMap(List::stream) - .filter(invoice -> invoiceId.equals(invoice.id())) - .forEach(invoice -> invoice.statusHistory().history.put(ZonedDateTime.now(), status)); - } - - @Override - public void addLineItem(TenantName tenant, String description, BigDecimal amount, String agent) { - unusedLineItems.computeIfAbsent(tenant, l -> new ArrayList<>()) - .add(new Invoice.LineItem( - "line-item-id", - description, - amount, - "some-plan", - agent, - ZonedDateTime.now() - )); - } - - @Override - public void deleteLineItem(String lineItemId) { - - } - - @Override - public boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument) { - var instrumentId = paymentInstrument.getPaymentInstrumentId(); - activeInstruments.put(paymentInstrument.getTenantName(), createInstrument(instrumentId)); - return true; - } - - @Override - public InstrumentList listInstruments(TenantName tenant, String userId) { - return null; - } - - @Override - public List getInvoices(TenantName tenant) { - return committedInvoices.getOrDefault(tenant, List.of()); - } - - private PaymentInstrument createInstrument(String id) { - return new PaymentInstrument(id, - "name", - "displayText", - "brand", - "type", - "endingWith", - "expiryDate" - ); - } - - public void addInvoice(TenantName tenantName, Invoice invoice, boolean committed) { - if (committed) - committedInvoices.computeIfAbsent(tenantName, i -> new ArrayList<>()) - .add(invoice); - else - uncommittedInvoices.put(tenantName, invoice); - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java deleted file mode 100644 index 7b8d36f3d4f..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java +++ /dev/null @@ -1,51 +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; - -/** - * @author olaa - */ -public class PaymentInstrument { - - private final String id; - private final String nameOnCard; - private final String displayText; - private final String brand; - private final String type; - private final String endingWith; - private final String expiryDate; - - - public PaymentInstrument(String id, String nameOnCard, String displayText, String brand, String type, String endingWith, String expiryDate) { - this.id = id; - this.nameOnCard = nameOnCard; - this.displayText = displayText; - this.brand = brand; - this.type = type; - this.endingWith = endingWith; - this.expiryDate = expiryDate; - } - - public String getId() { - return id; - } - - public String getNameOnCard() { - return nameOnCard; - } - - public String getDisplayText() { - return displayText; - } - - public String getBrand() { - return brand; - } - - public String getType() { - return type; - } - - public String getEndingWith() { - return endingWith; - } -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java new file mode 100644 index 00000000000..75a88136c45 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java @@ -0,0 +1,23 @@ +// 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; + +/** + * A Plan decides two different things: + * + * - How to map from usage to a sum of money that is owed. + * - Limits on how much resources can be used. + * + * @author ogronnesby + */ +public interface Plan { + + /** The ID of the plan as used in APIs and storage systems */ + String id(); + + /** The calculator used to calculate a bill for usage */ + CostCalculator calculator(); + + /** The quota limits associated with the plan */ + Object quota(); + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanController.java new file mode 100644 index 00000000000..f13c251d212 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanController.java @@ -0,0 +1,10 @@ +// 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; + +public interface PlanController { + + Plan getPlan(TenantName tenant); + +} \ No newline at end of file diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java deleted file mode 100644 index 68a897c904f..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java +++ /dev/null @@ -1,43 +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 java.util.Objects; - -/** - * @author olaa - */ -public class PlanId { - - private final String value; - - public PlanId(String value) { - if (value.isBlank()) - throw new IllegalArgumentException("Id must be non-blank."); - this.value = value; - } - - public static PlanId from(String value) { - return new PlanId(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; - PlanId id = (PlanId) o; - return Objects.equals(value, id.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } - - @Override - public String toString() { - return "plan '" + value + "'"; - } - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/ResourceUsage.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/ResourceUsage.java new file mode 100644 index 00000000000..cbfd2b6ff50 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/ResourceUsage.java @@ -0,0 +1,54 @@ +// 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.ApplicationId; +import com.yahoo.config.provision.zone.ZoneId; + +import java.math.BigDecimal; + +/** + * @author olaa + */ +public class ResourceUsage { + + private final ApplicationId applicationId; + private final ZoneId zoneId; + private final Plan plan; + private final BigDecimal cpuMillis; + private final BigDecimal memoryMillis; + private final BigDecimal diskMillis; + + public ResourceUsage(ApplicationId applicationId, ZoneId zoneId, Plan plan, + BigDecimal cpuMillis, BigDecimal memoryMillis, BigDecimal diskMillis) { + this.applicationId = applicationId; + this.zoneId = zoneId; + this.cpuMillis = cpuMillis; + this.memoryMillis = memoryMillis; + this.diskMillis = diskMillis; + this.plan = plan; + } + + public ApplicationId getApplicationId() { + return applicationId; + } + + public ZoneId getZoneId() { + return zoneId; + } + + public BigDecimal getCpuMillis() { + return cpuMillis; + } + + public BigDecimal getMemoryMillis() { + return memoryMillis; + } + + public BigDecimal getDiskMillis() { + return diskMillis; + } + + public Plan getPlan() { + return plan; + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index aaddd3811bc..2fdf442dbe0 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -72,10 +72,6 @@ enum PathGroup { PathPrefix.api, "/billing/v1/tenant/{tenant}/instrument/{*}"), - billingPlan(Matcher.tenant, - PathPrefix.api, - "/billing/v1/tenant/{tenant}/plan/{*}"), - billingList(Matcher.tenant, PathPrefix.api, "/billing/v1/tenant/{tenant}/billing/{*}"), diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java index 548ad0af484..83adba6f59b 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java @@ -158,11 +158,6 @@ enum Policy { .on(PathGroup.billingToken) .in(SystemName.PublicCd)), - /** Ability to update tenant payment instrument */ - planUpdate(Privilege.grant(Action.update) - .on(PathGroup.billingPlan) - .in(SystemName.PublicCd)), - /** Read the generated bills */ billingInformationRead(Privilege.grant(Action.read) .on(PathGroup.billingList) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index 801661f454e..b9d534019db 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -67,7 +67,6 @@ public enum RoleDefinition { Policy.paymentInstrumentUpdate, Policy.paymentInstrumentDelete, Policy.paymentInstrumentCreate, - Policy.planUpdate, Policy.billingInformationRead), /** Headless — the application specific role identified by deployment keys for production */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java deleted file mode 100644 index ccbee15d2c5..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java +++ /dev/null @@ -1,396 +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.restapi.billing; - -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.TenantName; -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.container.jdisc.LoggingRequestHandler; -import com.yahoo.container.logging.AccessLog; -import com.yahoo.restapi.ErrorResponse; -import com.yahoo.restapi.JacksonJsonResponse; -import com.yahoo.restapi.MessageResponse; -import com.yahoo.restapi.Path; -import com.yahoo.restapi.SlimeJsonResponse; -import com.yahoo.restapi.StringResponse; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Inspector; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.vespa.hosted.controller.ApplicationController; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Invoice; -import com.yahoo.vespa.hosted.controller.api.integration.billing.InstrumentOwner; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; -import com.yahoo.yolean.Exceptions; - -import javax.ws.rs.BadRequestException; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.NotFoundException; -import java.io.IOException; -import java.math.BigDecimal; -import java.security.Principal; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Executor; -import java.util.logging.Level; - -/** - * @author andreer - * @author olaa - */ -public class BillingApiHandler extends LoggingRequestHandler { - - private static final String OPTIONAL_PREFIX = "/api"; - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - - private final BillingController billingController; - private final ApplicationController applicationController; - - public BillingApiHandler(Executor executor, - AccessLog accessLog, - Controller controller) { - super(executor, accessLog); - this.billingController = controller.serviceRegistry().billingController(); - this.applicationController = controller.applications(); - } - - @Override - public HttpResponse handle(HttpRequest request) { - try { - Path path = new Path(request.getUri(), OPTIONAL_PREFIX); - String userId = userIdOrThrow(request); - switch (request.getMethod()) { - case GET: - return handleGET(request, path, userId); - case PATCH: - return handlePATCH(request, path, userId); - case DELETE: - return handleDELETE(path, userId); - case POST: - return handlePOST(path, request, userId); - default: - return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported"); - } - } catch (NotFoundException e) { - return ErrorResponse.notFoundError(Exceptions.toMessageString(e)); - } catch (IllegalArgumentException e) { - return ErrorResponse.badRequest(Exceptions.toMessageString(e)); - } catch (Exception e) { - log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e); - // Don't expose internal billing details in error message to user - return ErrorResponse.internalServerError("Internal problem while handling billing API request"); - } - } - - private HttpResponse handleGET(HttpRequest request, Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/token")) return getToken(path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/instrument")) return getInstruments(path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/billing")) return getBilling(path.get("tenant"), request.getProperty("until")); - if (path.matches("/billing/v1/tenant/{tenant}/plan")) return getPlan(path.get("tenant")); - if (path.matches("/billing/v1/billing")) return getBillingAllTenants(request.getProperty("until")); - if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return getLineItems(path.get("tenant")); - return ErrorResponse.notFoundError("Nothing at " + path); - } - - private HttpResponse handlePATCH(HttpRequest request, Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/instrument")) return patchActiveInstrument(request, path.get("tenant"), userId); - if (path.matches("/billing/v1/tenant/{tenant}/plan")) return patchPlan(request, path.get("tenant")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse handleDELETE(Path path, String userId) { - if (path.matches("/billing/v1/tenant/{tenant}/instrument/{instrument}")) return deleteInstrument(path.get("tenant"), userId, path.get("instrument")); - if (path.matches("/billing/v1/invoice/line-item/{line-item-id}")) return deleteLineItem(path.get("line-item-id")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse handlePOST(Path path, HttpRequest request, String userId) { - if (path.matches("/billing/v1/invoice")) return createInvoice(request, userId); - if (path.matches("/billing/v1/invoice/{invoice-id}/status")) return setInvoiceStatus(request, path.get("invoice-id")); - if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return addLineItem(request, path.get("tenant")); - return ErrorResponse.notFoundError("Nothing at " + path); - - } - - private HttpResponse getPlan(String tenant) { - var plan = billingController.getPlan(TenantName.from(tenant)); - var slime = new Slime(); - var root = slime.setObject(); - root.setString("tenant", tenant); - root.setString("plan", plan.value()); - return new SlimeJsonResponse(slime); - } - - private HttpResponse patchPlan(HttpRequest request, String tenant) { - var tenantName = TenantName.from(tenant); - var slime = inspectorOrThrow(request); - var planId = PlanId.from(slime.field("plan").asString()); - var hasApplications = applicationController.asList(tenantName).size() > 0; - - if (billingController.setPlan(tenantName, planId, hasApplications)) { - return new StringResponse("Plan: " + planId.value()); - } else { - return ErrorResponse.forbidden("Invalid plan change with active deployments"); - } - - } - - private HttpResponse getBillingAllTenants(String until) { - try { - var untilDate = untilParameter(until); - var uncommittedInvoices = billingController.createUncommittedInvoices(untilDate); - - var slime = new Slime(); - var root = slime.setObject(); - root.setString("until", untilDate.format(DateTimeFormatter.ISO_DATE)); - var tenants = root.setArray("tenants"); - - uncommittedInvoices.forEach((tenant, invoice) -> { - var tc = tenants.addObject(); - tc.setString("tenant", tenant.value()); - getPlanForTenant(tc, tenant); - renderCurrentUsage(tc.setObject("current"), invoice); - renderAdditionalItems(tc.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenant)); - - billingController.getDefaultInstrument(tenant).ifPresent(card -> - renderInstrument(tc.setObject("payment"), card) - ); - }); - - return new SlimeJsonResponse(slime); - } catch (DateTimeParseException e) { - return ErrorResponse.badRequest("Could not parse date: " + until); - } - } - - private HttpResponse addLineItem(HttpRequest request, String tenant) { - Inspector inspector = inspectorOrThrow(request); - billingController.addLineItem( - TenantName.from(tenant), - getInspectorFieldOrThrow(inspector, "description"), - new BigDecimal(getInspectorFieldOrThrow(inspector, "amount")), - userIdOrThrow(request)); - return new MessageResponse("Added line item for tenant " + tenant); - } - - private HttpResponse setInvoiceStatus(HttpRequest request, String invoiceId) { - Inspector inspector = inspectorOrThrow(request); - String status = getInspectorFieldOrThrow(inspector, "status"); - billingController.updateInvoiceStatus(Invoice.Id.of(invoiceId), userIdOrThrow(request), status); - return new MessageResponse("Updated status of invoice " + invoiceId); - } - - private HttpResponse createInvoice(HttpRequest request, String userId) { - Inspector inspector = inspectorOrThrow(request); - TenantName tenantName = TenantName.from(getInspectorFieldOrThrow(inspector, "tenant")); - - LocalDate startDate = LocalDate.parse(getInspectorFieldOrThrow(inspector, "startTime")); - LocalDate endDate = LocalDate.parse(getInspectorFieldOrThrow(inspector, "endTime")); - ZonedDateTime startTime = startDate.atStartOfDay(ZoneId.of("UTC")); - ZonedDateTime endTime = endDate.atStartOfDay(ZoneId.of("UTC")); - - var invoiceId = billingController.createInvoiceForPeriod(tenantName, startTime, endTime, userId); - - return new MessageResponse("Created invoice with ID " + invoiceId.value()); - } - - private HttpResponse getInstruments(String tenant, String userId) { - var instrumentListResponse = billingController.listInstruments(TenantName.from(tenant), userId); - return new JacksonJsonResponse<>(200, instrumentListResponse); - } - - private HttpResponse getToken(String tenant, String userId) { - return new StringResponse(billingController.createClientToken(tenant, userId)); - } - - private HttpResponse getBilling(String tenant, String until) { - try { - var untilDate = untilParameter(until); - var tenantId = TenantName.from(tenant); - var slimeResponse = new Slime(); - var root = slimeResponse.setObject(); - - root.setString("until", untilDate.format(DateTimeFormatter.ISO_DATE)); - - getPlanForTenant(root, tenantId); - renderCurrentUsage(root.setObject("current"), getCurrentUsageForTenant(tenantId, untilDate)); - renderAdditionalItems(root.setObject("additional").setArray("items"), billingController.getUnusedLineItems(tenantId)); - renderInvoices(root.setArray("bills"), getInvoicesForTenant(tenantId)); - - billingController.getDefaultInstrument(tenantId).ifPresent( card -> - renderInstrument(root.setObject("payment"), card) - ); - - return new SlimeJsonResponse(slimeResponse); - } catch (DateTimeParseException e) { - return ErrorResponse.badRequest("Could not parse date: " + until); - } - } - - private HttpResponse getLineItems(String tenant) { - var slimeResponse = new Slime(); - var root = slimeResponse.setObject(); - var lineItems = root.setArray("lineItems"); - - billingController.getUnusedLineItems(TenantName.from(tenant)) - .forEach(lineItem -> { - var itemCursor = lineItems.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - - return new SlimeJsonResponse(slimeResponse); - } - - private void getPlanForTenant(Cursor cursor, TenantName tenant) { - cursor.setString("plan", billingController.getPlan(tenant).value()); - } - - private void renderInstrument(Cursor cursor, PaymentInstrument instrument) { - cursor.setString("type", instrument.getType()); - cursor.setString("brand", instrument.getBrand()); - cursor.setString("endingWith", instrument.getEndingWith()); - } - - private void renderCurrentUsage(Cursor cursor, Invoice currentUsage) { - cursor.setString("amount", currentUsage.sum().toPlainString()); - cursor.setString("status", "accrued"); - cursor.setString("from", currentUsage.getStartTime().format(DATE_TIME_FORMATTER)); - var itemsCursor = cursor.setArray("items"); - currentUsage.lineItems().forEach(lineItem -> { - var itemCursor = itemsCursor.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - } - - private void renderAdditionalItems(Cursor cursor, List items) { - items.forEach(item -> { - renderLineItemToCursor(cursor.addObject(), item); - }); - } - - private Invoice getCurrentUsageForTenant(TenantName tenant, LocalDate until) { - return billingController.createUncommittedInvoice(tenant, until); - } - - private List getInvoicesForTenant(TenantName tenant) { - return billingController.getInvoices(tenant); - } - - private void renderInvoices(Cursor cursor, List invoices) { - invoices.forEach(invoice -> { - var invoiceCursor = cursor.addObject(); - renderInvoiceToCursor(invoiceCursor, invoice); - }); - } - - private void renderInvoiceToCursor(Cursor invoiceCursor, Invoice invoice) { - invoiceCursor.setString("id", invoice.id().value()); - invoiceCursor.setString("from", invoice.getStartTime().format(DATE_TIME_FORMATTER)); - invoiceCursor.setString("to", invoice.getEndTime().format(DATE_TIME_FORMATTER)); - - invoiceCursor.setString("amount", invoice.sum().toString()); - invoiceCursor.setString("status", invoice.status()); - var statusCursor = invoiceCursor.setArray("statusHistory"); - renderStatusHistory(statusCursor, invoice.statusHistory()); - - - var lineItemsCursor = invoiceCursor.setArray("items"); - invoice.lineItems().forEach(lineItem -> { - var itemCursor = lineItemsCursor.addObject(); - renderLineItemToCursor(itemCursor, lineItem); - }); - } - - private void renderStatusHistory(Cursor cursor, Invoice.StatusHistory statusHistory) { - statusHistory.getHistory() - .entrySet() - .stream() - .forEach(entry -> { - var c = cursor.addObject(); - c.setString("at", entry.getKey().format(DATE_TIME_FORMATTER)); - c.setString("status", entry.getValue()); - }); - } - - private void renderLineItemToCursor(Cursor cursor, Invoice.LineItem lineItem) { - cursor.setString("id", lineItem.id()); - cursor.setString("description", lineItem.description()); - cursor.setString("amount", lineItem.amount().toString()); - lineItem.applicationId().ifPresent(appId -> { - cursor.setString("application", appId.application().value()); - }); - } - - private HttpResponse deleteInstrument(String tenant, String userId, String instrument) { - if (billingController.deleteInstrument(TenantName.from(tenant), userId, instrument)) { - return new StringResponse("OK"); - } else { - return ErrorResponse.forbidden("Cannot delete payment instrument you don't own"); - } - } - - private HttpResponse deleteLineItem(String lineItemId) { - billingController.deleteLineItem(lineItemId); - return new MessageResponse("Succesfully deleted line item " + lineItemId); - } - - private HttpResponse patchActiveInstrument(HttpRequest request, String tenant, String userId) { - var inspector = inspectorOrThrow(request); - String instrumentId = getInspectorFieldOrThrow(inspector, "active"); - InstrumentOwner paymentInstrument = new InstrumentOwner(TenantName.from(tenant), userId, instrumentId, true); - boolean success = billingController.setActivePaymentInstrument(paymentInstrument); - return success ? new StringResponse("OK") : ErrorResponse.internalServerError("Failed to patch active instrument"); - } - - private Inspector inspectorOrThrow(HttpRequest request) { - try { - return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get(); - } catch (IOException e) { - throw new BadRequestException("Failed to parse request body"); - } - } - - private static String userIdOrThrow(HttpRequest request) { - return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()) - .map(Principal::getName) - .orElseThrow(() -> new ForbiddenException("Must be authenticated to use this API")); - } - - private static String getInspectorFieldOrThrow(Inspector inspector, String field) { - if (!inspector.field(field).valid()) - throw new BadRequestException("Field " + field + " cannot be null"); - return inspector.field(field).asString(); - } - - private DeploymentId getDeploymentIdOrNull(Inspector inspector) { - if (inspector.field("applicationId").valid() != inspector.field("zoneId").valid() ) { - throw new BadRequestException("Either both application id and zone id should be set, or neither."); - } - if (inspector.field("applicationId").valid()) { - return new DeploymentId( - ApplicationId.fromSerializedForm(inspector.field("applicationId").asString()), - com.yahoo.config.provision.zone.ZoneId.from(inspector.field("zoneId").asString()) - ); - } - return null; - } - - private LocalDate untilParameter(String until) { - if (until == null || until.isEmpty() || until.isBlank()) - return LocalDate.now().plusDays(1); - return LocalDate.parse(until); - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java index 13cf992cd52..bd0143ef879 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java @@ -9,7 +9,7 @@ import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry; -import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; +import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanController; import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo; import com.yahoo.vespa.hosted.controller.api.integration.user.Roles; import com.yahoo.vespa.hosted.controller.api.integration.user.UserId; @@ -36,13 +36,13 @@ public class CloudAccessControl implements AccessControl { private final UserManagement userManagement; private final BooleanFlag enablePublicSignup; - private final BillingController planController; + private final PlanController planController; @Inject public CloudAccessControl(UserManagement userManagement, FlagSource flagSource, ServiceRegistry serviceRegistry) { this.userManagement = userManagement; this.enablePublicSignup = Flags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource); - planController = serviceRegistry.billingController(); + planController = serviceRegistry.planController(); } @Override @@ -109,7 +109,7 @@ public class CloudAccessControl implements AccessControl { } private boolean isTrial(TenantName tenant) { - return planController.getPlan(tenant).value().equals("trial"); + return planController.getPlan(tenant).id().equals("trial"); } @Override diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java index 1b21f7db7c4..b7e7c9814e3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java @@ -12,14 +12,14 @@ import com.yahoo.vespa.hosted.controller.api.integration.aws.MockAwsEventFetcher import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger; import com.yahoo.vespa.hosted.controller.api.integration.aws.NoopApplicationRoleService; 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.MockBillingController; +import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanController; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer; import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService; import com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueHandler; +import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor; import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock; import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService; import com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService; @@ -60,7 +60,7 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final MockRunDataStore mockRunDataStore = new MockRunDataStore(); private final MockResourceTagger mockResourceTagger = new MockResourceTagger(); private final ApplicationRoleService applicationRoleService = new NoopApplicationRoleService(); - private final BillingController billingController = new MockBillingController(); + private final PlanController planController = (tenantName) -> null; public ServiceRegistryMock(SystemName system) { this.zoneRegistryMock = new ZoneRegistryMock(system); @@ -187,11 +187,6 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg return systemMonitor; } - @Override - public BillingController billingController() { - return billingController; - } - public ConfigServerMock configServerMock() { return configServerMock; } @@ -208,4 +203,9 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg return endpointCertificateMock; } + @Override + public PlanController planController() { + return planController; + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java deleted file mode 100644 index 19cfa95c682..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.yahoo.vespa.hosted.controller.restapi.billing; - -import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.api.integration.billing.Invoice; -import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController; -import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; -import com.yahoo.vespa.hosted.controller.api.role.Role; -import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; -import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -import static com.yahoo.application.container.handler.Request.Method.*; -import static org.junit.Assert.*; - -/** - * @author olaa - */ -public class BillingApiHandlerTest extends ControllerContainerCloudTest { - - private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/"; - private static final TenantName tenant = TenantName.from("tenant1"); - private static final TenantName tenant2 = TenantName.from("tenant2"); - private static final Set tenantRole = Set.of(Role.administrator(tenant)); - private static final Set financeAdmin = Set.of(Role.hostedAccountant()); - private MockBillingController billingController; - - private ContainerTester tester; - - @Before - public void setup() { - tester = new ContainerTester(container, responseFiles); - billingController = (MockBillingController) tester.serviceRegistry().billingController(); - } - - @Override - protected SystemName system() { - return SystemName.PublicCd; - } - - @Override - protected String variablePartXml() { - return " \n" + - " \n" + - - " \n" + - " http://*/billing/v1/*\n" + - " \n" + - - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " http://*/*\n" + - " \n" + - " \n" + - " \n"; - } - - @Test - public void setting_and_deleting_instrument() { - assertTrue(billingController.getDefaultInstrument(tenant).isEmpty()); - - var instrumentRequest = request("/billing/v1/tenant/tenant1/instrument", PATCH) - .data("{\"active\": \"id-1\"}") - .roles(tenantRole); - - tester.assertResponse(instrumentRequest,"OK"); - assertEquals("id-1", billingController.getDefaultInstrument(tenant).get().getId()); - - var deleteInstrumentRequest = request("/billing/v1/tenant/tenant1/instrument/id-1", DELETE) - .roles(tenantRole); - - tester.assertResponse(deleteInstrumentRequest,"OK"); - assertTrue(billingController.getDefaultInstrument(tenant).isEmpty()); - } - - @Test - public void response_list_bills() { - var invoice = createInvoice(); - - billingController.addInvoice(tenant, invoice, true); - billingController.addInvoice(tenant, invoice, false); - billingController.setPlan(tenant, PlanId.from("some-plan"), true); - - var request = request("/billing/v1/tenant/tenant1/billing?until=2020-05-28").roles(tenantRole); - tester.assertResponse(request, new File("tenant-billing-view")); - - } - - @Test - public void test_invoice_creation() { - var invoices = billingController.getInvoices(tenant); - assertEquals(0, invoices.size()); - - String requestBody = "{\"tenant\":\"tenant1\", \"startTime\":\"2020-04-20\", \"endTime\":\"2020-05-20\"}"; - var request = request("/billing/v1/invoice", POST) - .data(requestBody) - .roles(tenantRole); - - tester.assertResponse(request, accessDenied, 403); - request.roles(financeAdmin); - tester.assertResponse(request, new File("invoice-creation-response")); - - invoices = billingController.getInvoices(tenant); - assertEquals(1, invoices.size()); - Invoice invoice = invoices.get(0); - assertEquals(invoice.getStartTime().toString(), "2020-04-20T00:00Z[UTC]"); - assertEquals(invoice.getEndTime().toString(), "2020-05-20T00:00Z[UTC]"); - } - - @Test - public void adding_and_listing_line_item() { - - var requestBody = "{" + - "\"description\":\"some description\"," + - "\"amount\":\"123.45\" " + - "}"; - - var request = request("/billing/v1/invoice/tenant/tenant1/line-item", POST) - .data(requestBody) - .roles(financeAdmin); - - tester.assertResponse(request, "{\"message\":\"Added line item for tenant tenant1\"}"); - - var lineItems = billingController.getUnusedLineItems(tenant); - Assert.assertEquals(1, lineItems.size()); - Invoice.LineItem lineItem = lineItems.get(0); - assertEquals("some description", lineItem.description()); - assertEquals(new BigDecimal("123.45"), lineItem.amount()); - - request = request("/billing/v1/invoice/tenant/tenant1/line-item") - .roles(financeAdmin); - - tester.assertResponse(request, new File("line-item-list")); - } - - @Test - public void adding_new_status() { - billingController.addInvoice(tenant, createInvoice(), true); - - var requestBody = "{\"status\":\"DONE\"}"; - var request = request("/billing/v1/invoice/id-1/status", POST) - .data(requestBody) - .roles(financeAdmin); - tester.assertResponse(request, "{\"message\":\"Updated status of invoice id-1\"}"); - - var invoice = billingController.getInvoices(tenant).get(0); - assertEquals("DONE", invoice.status()); - } - - @Test - public void list_all_uninvoiced_items() { - var invoice = createInvoice(); - billingController.setPlan(tenant, PlanId.from("some-plan"), true); - billingController.setPlan(tenant2, PlanId.from("some-plan"), true); - billingController.addInvoice(tenant, invoice, false); - billingController.addLineItem(tenant, "support", new BigDecimal("42"), "Smith"); - billingController.addInvoice(tenant2, invoice, false); - - - var request = request("/billing/v1/billing?until=2020-05-28").roles(financeAdmin); - - tester.assertResponse(request, new File("billing-all-tenants")); - } - - @Test - public void setting_plans() { - var planRequest = request("/billing/v1/tenant/tenant1/plan", PATCH) - .data("{\"plan\": \"new-plan\"}") - .roles(tenantRole); - tester.assertResponse(planRequest, "Plan: new-plan"); - assertEquals("new-plan", billingController.getPlan(tenant).value()); - } - - private Invoice createInvoice() { - var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneId.systemDefault()); - var end = start.plusDays(5); - var statusHistory = new Invoice.StatusHistory(new TreeMap<>(Map.of(start, "OPEN"))); - return new Invoice( - Invoice.Id.of("id-1"), - statusHistory, - List.of(createLineItem(start)), - start, - end - ); - } - - - private Invoice.LineItem createLineItem(ZonedDateTime addedAt) { - return new Invoice.LineItem( - "some-id", - "description", - new BigDecimal("123.00"), - "plan", - "Smith", - addedAt - ); - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/billing-all-tenants b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/billing-all-tenants deleted file mode 100644 index c5bf0c88c2c..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/billing-all-tenants +++ /dev/null @@ -1,48 +0,0 @@ -{ - "until":"2020-05-28", - "tenants":[ - { - "tenant":"tenant2", - "plan":"some-plan", - "current":{ - "amount":"123.00", - "status":"accrued", - "from":"2020-05-23", - "items":[ - { - "id":"some-id", - "description":"description", - "amount":"123.00" - } - ] - }, - "additional":{"items":[]} - }, - { - "tenant":"tenant1", - "plan":"some-plan", - "current":{ - "amount":"123.00", - "status":"accrued", - "from":"2020-05-23", - "items":[ - { - "id":"some-id", - "description":"description", - "amount":"123.00" - } - ] - }, - "additional": - { - "items":[ - { - "id":"line-item-id", - "description":"support", - "amount":"42.00" - } - ] - } - } - ] -} \ No newline at end of file diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/invoice-creation-response b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/invoice-creation-response deleted file mode 100644 index 0a92229025b..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/invoice-creation-response +++ /dev/null @@ -1 +0,0 @@ -{"message":"Created invoice with ID id-123"} \ No newline at end of file diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/line-item-list b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/line-item-list deleted file mode 100644 index cd5aec2f8f4..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/line-item-list +++ /dev/null @@ -1,9 +0,0 @@ -{ - "lineItems":[ - { - "id":"line-item-id", - "description":"some description", - "amount":"123.45" - } - ] -} \ No newline at end of file diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/tenant-billing-view b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/tenant-billing-view deleted file mode 100644 index 8bc39771b31..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/tenant-billing-view +++ /dev/null @@ -1,38 +0,0 @@ -{ - "until":"2020-05-28", - "plan":"some-plan", - "current":{ - "amount":"123.00", - "status":"accrued", - "from":"2020-05-23", - "items":[ - { - "id":"some-id", - "description":"description", - "amount":"123.00" - } - ] - }, - "additional":{"items":[]}, - "bills":[ - { - "id":"id-1", - "from":"2020-05-23", - "to":"2020-05-28","amount":"123.00", - "status":"OPEN", - "statusHistory":[ - { - "at":"2020-05-23", - "status":"OPEN" - } - ], - "items":[ - { - "id":"some-id", - "description":"description", - "amount":"123.00" - } - ] - } - ] -} \ No newline at end of file -- cgit v1.2.3