summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorLeandro Alves <ldalves@gmail.com>2018-12-10 11:20:07 +0100
committerAndreas Eriksen <andreer@pvv.ntnu.no>2018-12-10 11:20:07 +0100
commitbec7ce30bf9263ab2d8b212b935e1514b105a8b5 (patch)
tree1fa60f963c3fd03d261ecae76a657a939f425f52 /controller-server
parentd23084a0a2d4447d3fa9748c3239fa4ec9773e6b (diff)
cost resources by property (#7789)
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java47
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiHandler.java41
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java95
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostReportConsumer.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/NoopCostReportConsumer.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/package-info.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryClientMock.java21
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java52
13 files changed, 326 insertions, 4 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
index f6978ef70ac..fafd27aaded 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
@@ -14,7 +14,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipI
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.maintenance.config.MaintainerConfig;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
+import com.yahoo.vespa.hosted.controller.restapi.cost.CostReportConsumer;
+import java.time.Clock;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
@@ -48,13 +50,15 @@ public class ControllerMaintenance extends AbstractComponent {
private final OsVersionStatusUpdater osVersionStatusUpdater;
private final JobRunner jobRunner;
private final ContactInformationMaintainer contactInformationMaintainer;
+ private final CostReportMaintainer costReportMaintainer;
@SuppressWarnings("unused") // instantiated by Dependency Injection
public ControllerMaintenance(MaintainerConfig maintainerConfig, ApiAuthorityConfig apiAuthorityConfig, Controller controller, CuratorDb curator,
JobControl jobControl, Metric metric, Chef chefClient,
DeploymentIssues deploymentIssues, OwnershipIssues ownershipIssues,
NameService nameService, NodeRepositoryClientInterface nodeRepositoryClient,
- ContactRetriever contactRetriever) {
+ ContactRetriever contactRetriever,
+ CostReportConsumer reportConsumer) {
Duration maintenanceInterval = Duration.ofMinutes(maintainerConfig.intervalMinutes());
this.jobControl = jobControl;
deploymentExpirer = new DeploymentExpirer(controller, maintenanceInterval, jobControl);
@@ -74,6 +78,7 @@ public class ControllerMaintenance extends AbstractComponent {
osUpgraders = osUpgraders(controller, jobControl);
osVersionStatusUpdater = new OsVersionStatusUpdater(controller, maintenanceInterval, jobControl);
contactInformationMaintainer = new ContactInformationMaintainer(controller, Duration.ofHours(12), jobControl, contactRetriever);
+ costReportMaintainer = new CostReportMaintainer(controller, Duration.ofHours(2), reportConsumer, jobControl, nodeRepositoryClient, Clock.systemUTC());
}
public Upgrader upgrader() { return upgrader; }
@@ -100,6 +105,7 @@ public class ControllerMaintenance extends AbstractComponent {
osVersionStatusUpdater.deconstruct();
jobRunner.deconstruct();
contactInformationMaintainer.deconstruct();
+ costReportMaintainer.deconstruct();
}
/** Create one OS upgrader per cloud found in the zone registry of controller */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java
new file mode 100644
index 00000000000..77febb71ca6
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java
@@ -0,0 +1,47 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryClientInterface;
+import com.yahoo.vespa.hosted.controller.restapi.cost.CostCalculator;
+import com.yahoo.vespa.hosted.controller.restapi.cost.CostReportConsumer;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Periodically calculate and store cost allocation for properties.
+ *
+ * @author ldalves
+ * @author andreer
+ */
+public class CostReportMaintainer extends Maintainer {
+
+ private static final Logger log = Logger.getLogger(CostReportMaintainer.class.getName());
+
+ private final CostReportConsumer consumer;
+ private final NodeRepositoryClientInterface nodeRepository;
+ private final Clock clock;
+
+ @Inject
+ @SuppressWarnings("WeakerAccess")
+ public CostReportMaintainer(Controller controller, Duration interval,
+ CostReportConsumer consumer,
+ JobControl jobControl,
+ NodeRepositoryClientInterface nodeRepository,
+ Clock clock) {
+ super(controller, interval, jobControl, "CostReportMaintainer", EnumSet.of(SystemName.main));
+ this.consumer = consumer;
+ this.nodeRepository = Objects.requireNonNull(nodeRepository, "node repository must be non-null");
+ this.clock = clock;
+ }
+
+ @Override
+ protected void maintain() {
+ consumer.Consume(CostCalculator.toCsv(CostCalculator.calculateCost(nodeRepository, controller(), clock)));
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiHandler.java
new file mode 100644
index 00000000000..cdec0f8da74
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiHandler.java
@@ -0,0 +1,41 @@
+package com.yahoo.vespa.hosted.controller.restapi.cost;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.restapi.Path;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryClientInterface;
+import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
+import com.yahoo.vespa.hosted.controller.restapi.StringResponse;
+
+import java.time.Clock;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
+
+public class CostApiHandler extends LoggingRequestHandler {
+
+ private final Controller controller;
+ private final NodeRepositoryClientInterface nodeRepository;
+
+ public CostApiHandler(Context ctx, Controller controller, NodeRepositoryClientInterface nodeRepository) {
+ super(ctx);
+ this.controller = controller;
+ this.nodeRepository = nodeRepository;
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ if (request.getMethod() != GET) {
+ return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
+ }
+
+ Path path = new Path(request.getUri().getPath());
+
+ if (path.matches("/cost/v1/csv")) {
+ return new StringResponse(CostCalculator.toCsv(CostCalculator.calculateCost(nodeRepository, controller, Clock.systemUTC())));
+ }
+
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java
new file mode 100644
index 00000000000..d6e77e6d381
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostCalculator.java
@@ -0,0 +1,95 @@
+package com.yahoo.vespa.hosted.controller.restapi.cost;
+
+import com.yahoo.config.provision.Environment;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryClientInterface;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.CloudName;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+
+import java.time.Clock;
+import java.time.LocalDate;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+public class CostCalculator {
+ public static Map<Property, ResourceAllocation> calculateCost(NodeRepositoryClientInterface nodeRepository, Controller controller, Clock clock) {
+
+ String date = LocalDate.now(clock).toString();
+
+ List<NodeRepositoryNode> nodes = controller.zoneRegistry().zones()
+ .reachable().in(Environment.prod).ofCloud(CloudName.from("yahoo")).ids().stream()
+ .flatMap(zoneId -> uncheck(() -> nodeRepository.listNodes(zoneId, true).nodes().stream()))
+ .filter(node -> node.getOwner() != null && !node.getOwner().getTenant().equals("hosted-vespa"))
+ .collect(Collectors.toList());
+
+ ResourceAllocation total = ResourceAllocation.from(date, nodes, null);
+
+ Map<String, Property> propertyByTenantName = controller.tenants().asList().stream()
+ .filter(AthenzTenant.class::isInstance)
+ .collect(Collectors.toMap(
+ tenant -> tenant.name().value(),
+ tenant -> ((AthenzTenant) tenant).property()
+ ));
+
+ return nodes.stream()
+ .filter(node -> propertyByTenantName.containsKey(node.getOwner().tenant))
+ .collect(Collectors.groupingBy(
+ node -> propertyByTenantName.get(node.getOwner().tenant),
+ Collectors.collectingAndThen(
+ Collectors.toList(),
+ (tenantNodes) -> ResourceAllocation.from(date, tenantNodes, total)
+ )
+ ));
+ }
+
+ static class ResourceAllocation {
+ final double cpuCores;
+ final double memoryGb;
+ final double diskGb;
+ final String date;
+ final ResourceAllocation total;
+
+ private ResourceAllocation(String date, double cpuCores, double memoryGb, double diskGb, ResourceAllocation total) {
+ this.date = date;
+ this.cpuCores = cpuCores;
+ this.memoryGb = memoryGb;
+ this.diskGb = diskGb;
+ this.total = total;
+ }
+
+ private static ResourceAllocation from(String date, List<NodeRepositoryNode> nodes, ResourceAllocation total) {
+ return new ResourceAllocation(
+ date,
+ nodes.stream().mapToDouble(NodeRepositoryNode::getMinCpuCores).sum(),
+ nodes.stream().mapToDouble(NodeRepositoryNode::getMinMainMemoryAvailableGb).sum(),
+ nodes.stream().mapToDouble(NodeRepositoryNode::getMinDiskAvailableGb).sum(),
+ total
+ );
+ }
+
+ private double usageFraction() {
+ return (cpuCores / total.cpuCores + memoryGb / total.memoryGb + diskGb / total.diskGb) / 3;
+ }
+ }
+
+ public static String toCsv(Map<Property, ResourceAllocation> resourceShareByProperty) {
+ String header = "Date,Property,Reserved Cpu Cores,Reserved Memory GB,Reserved Disk Space GB,Usage Fraction\n";
+ String entries = resourceShareByProperty.entrySet().stream()
+ .sorted((Comparator.comparingDouble(entry -> entry.getValue().usageFraction())))
+ .map(propertyEntry -> {
+ ResourceAllocation r = propertyEntry.getValue();
+ return Stream.of(r.date, propertyEntry.getKey(), r.cpuCores, r.memoryGb, r.diskGb, r.usageFraction())
+ .map(Object::toString).collect(Collectors.joining(","));
+ })
+ .collect(Collectors.joining("\n"));
+ return header + entries;
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostReportConsumer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostReportConsumer.java
new file mode 100644
index 00000000000..51a664160ab
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostReportConsumer.java
@@ -0,0 +1,5 @@
+package com.yahoo.vespa.hosted.controller.restapi.cost;
+
+public interface CostReportConsumer {
+ void Consume(String csv);
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/NoopCostReportConsumer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/NoopCostReportConsumer.java
new file mode 100644
index 00000000000..f1406da1d4b
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/NoopCostReportConsumer.java
@@ -0,0 +1,15 @@
+package com.yahoo.vespa.hosted.controller.restapi.cost;
+
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+
+public class NoopCostReportConsumer implements CostReportConsumer {
+
+ @Inject
+ public NoopCostReportConsumer() {}
+
+ @Override
+ public void Consume(String csv) {
+ // discard into the void
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/package-info.java
new file mode 100644
index 00000000000..a96ae5488fa
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/cost/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.hosted.controller.restapi.cost;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index 639511959df..f966fea8f7a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -248,7 +248,7 @@ public final class ControllerTester {
TenantName name = TenantName.from(tenantName);
Optional<Tenant> existing = controller().tenants().tenant(name);
if (existing.isPresent()) return name;
- AthenzTenant tenant = AthenzTenant.create(name, createDomain(domainName), new Property("app1Property"),
+ AthenzTenant tenant = AthenzTenant.create(name, createDomain(domainName), new Property("Property"+propertyId),
Optional.ofNullable(propertyId)
.map(Object::toString)
.map(PropertyId::new), contact);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryClientMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryClientMock.java
index 1b12b441272..14de73e3f75 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryClientMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryClientMock.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.integration;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.MaintenanceJobList;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeList;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeMembership;
+import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeOwner;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryClientInterface;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeState;
@@ -34,7 +35,9 @@ public class NodeRepositoryClientMock implements NodeRepositoryClientInterface {
@Override
public NodeList listNodes(ZoneId zone, boolean recursive) {
- throw new UnsupportedOperationException();
+ NodeRepositoryNode nodeA = createNodeA();
+ NodeRepositoryNode nodeB = createNodeB();
+ return new NodeList(Arrays.asList(nodeA, nodeB));
}
@Override
@@ -49,6 +52,14 @@ public class NodeRepositoryClientMock implements NodeRepositoryClientInterface {
node.setHostname("hostA");
node.setCost(10);
node.setFlavor("C-2B/24/500");
+ node.setMinCpuCores(24d);
+ node.setMinDiskAvailableGb(500d);
+ node.setMinMainMemoryAvailableGb(24d);
+ NodeOwner owner = new NodeOwner();
+ owner.tenant = "lsbe";
+ owner.application = "local-search";
+ owner.instance = "default";
+ node.setOwner(owner);
NodeMembership membership = new NodeMembership();
membership.clusterid = "clusterA";
membership.clustertype = "container";
@@ -61,6 +72,14 @@ public class NodeRepositoryClientMock implements NodeRepositoryClientInterface {
node.setHostname("hostB");
node.setCost(20);
node.setFlavor("C-2C/24/500");
+ node.setMinCpuCores(40d);
+ node.setMinDiskAvailableGb(500d);
+ node.setMinMainMemoryAvailableGb(24d);
+ NodeOwner owner = new NodeOwner();
+ owner.tenant = "mediasearch";
+ owner.application = "imagesearch";
+ owner.instance = "default";
+ node.setOwner(owner);
NodeMembership membership = new NodeMembership();
membership.clusterid = "clusterB";
membership.clustertype = "content";
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java
new file mode 100644
index 00000000000..f8da14aec56
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java
@@ -0,0 +1,30 @@
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryClientMock;
+import com.yahoo.vespa.hosted.controller.restapi.cost.CostReportConsumer;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+
+public class CostReportMaintainerTest {
+
+ @Test
+ public void maintain() {
+ ControllerTester tester = new ControllerTester();
+
+ CostReportConsumer mockConsumer = csv -> Assert.assertEquals(csv,
+ "Date,Property,Reserved Cpu Cores,Reserved Memory GB,Reserved Disk Space GB,Usage Fraction\n" +
+ "1970-01-01,Property1,120.0,120.0,2500.0,0.4583333333333333\n" +
+ "1970-01-01,Property2,200.0,120.0,2500.0,0.5416666666666666");
+
+ tester.createTenant("lsbe", "local-search", 1L);
+ tester.createTenant("mediasearch", "msbe", 2L);
+ CostReportMaintainer maintainer = new CostReportMaintainer(tester.controller(), Duration.ofDays(1), mockConsumer, new JobControl(tester.curator()), new NodeRepositoryClientMock(), Clock.fixed(Instant.EPOCH, ZoneId.of("UTC")));
+ maintainer.maintain();
+ }
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index ce69f32a21e..a2dea41b444 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -70,6 +70,7 @@ public class ControllerContainerTest {
" <component id='com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.github.GitHubMock'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.restapi.cost.NoopCostReportConsumer'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockRunDataStore'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever'/>\n" +
@@ -109,6 +110,9 @@ public class ControllerContainerTest {
" <handler id='com.yahoo.vespa.hosted.controller.restapi.os.OsApiHandler'>\n" +
" <binding>http://*/os/v1/*</binding>\n" +
" </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.cost.CostApiHandler'>\n" +
+ " <binding>http://*/cost/v1/*</binding>\n" +
+ " </handler>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.screwdriver.ScrewdriverApiHandler'>\n" +
" <binding>http://*/screwdriver/v1/*</binding>\n" +
" </handler>\n" +
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
index 6a71e524ae4..dcee9717ecc 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
@@ -13,7 +13,10 @@
"name": "ContactInformationMaintainer"
},
{
- "name": "DefaultOsUpgrader"
+ "name": "CostReportMaintainer"
+ },
+ {
+ "name":"DefaultOsUpgrader"
},
{
"name": "DeploymentExpirer"
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java
new file mode 100644
index 00000000000..bc03c9e87b3
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java
@@ -0,0 +1,52 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.cost;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.api.AthenzUser;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.CloudName;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author andreer
+ */
+public class CostApiTest extends ControllerContainerTest {
+
+ private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/responses/";
+ private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser");
+ private static final CloudName cloud1 = CloudName.from("yahoo");
+ private static final CloudName cloud2 = CloudName.from("cloud2");
+ private static final ZoneId zone1 = ZoneId.from("prod", "us-east-3", cloud1.value());
+ private static final ZoneId zone2 = ZoneId.from("prod", "us-west-1", cloud1.value());
+ private static final ZoneId zone3 = ZoneId.from("prod", "eu-west-1", cloud2.value());
+
+ private ContainerControllerTester tester;
+
+ @Before
+ public void before() {
+ tester = new ContainerControllerTester(container, responses);
+ zoneRegistryMock().setSystemName(SystemName.cd)
+ .setZones(zone1, zone2, zone3);
+ }
+
+ @Test
+ public void test_api() {
+ assertResponse(new Request("http://localhost:8080/cost/v1/csv"), "Date,Property,Reserved Cpu Cores,Reserved Memory GB,Reserved Disk Space GB,Usage Fraction\n", 200);
+ }
+
+ private ZoneRegistryMock zoneRegistryMock() {
+ return (ZoneRegistryMock) tester.containerTester().container().components()
+ .getComponent(ZoneRegistryMock.class.getName());
+ }
+
+ private void assertResponse(Request request, String body, int statusCode) {
+ addIdentityToRequest(request, operator);
+ tester.assertResponse(request, body, statusCode);
+ }
+}