summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2019-01-25 15:29:54 +0100
committerGitHub <noreply@github.com>2019-01-25 15:29:54 +0100
commite7f859874ac9133e81d8b93743d8e9fb56bb0822 (patch)
treeef4be34d45f43699bc49a06b4a43160807778b9b /controller-server
parent888dcfc48fcebb73cf45a3c4763473eedb0490fc (diff)
parent8d90a1fd5a0e6b08ab03ab9497e2508a8e15fcae (diff)
Merge pull request #8173 from vespa-engine/mortent/loadbalancer-maintainer
Maintainer updating name service for load balancers
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java18
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAlias.java93
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainer.java147
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java18
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializer.java52
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAliasTest.java33
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainerTest.java146
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializerTest.java35
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json3
14 files changed, 616 insertions, 17 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
index 6ac62284661..130519335ce 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
@@ -10,11 +10,12 @@ import com.yahoo.config.provision.HostName;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
@@ -27,10 +28,12 @@ import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import java.time.Instant;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
+import java.util.stream.Collectors;
/**
* An application that has been locked for modification. Provides methods for modifying an application's fields.
@@ -68,8 +71,7 @@ public class LockedApplication {
application.deployments(),
application.deploymentJobs(), application.change(), application.outstandingChange(),
application.ownershipIssueId(), application.owner(), application.majorVersion(), application.metrics(),
- application.rotation(),
- application.rotationStatus());
+ application.rotation(), application.rotationStatus());
}
private LockedApplication(Lock lock, ApplicationId id, Instant createdAt,
@@ -147,7 +149,8 @@ public class LockedApplication {
previousDeployment.clusterUtils(),
previousDeployment.clusterInfo(),
previousDeployment.metrics(),
- previousDeployment.activity());
+ previousDeployment.activity(),
+ previousDeployment.loadBalancers());
return with(newDeployment);
}
@@ -248,6 +251,13 @@ public class LockedApplication {
outstandingChange, ownershipIssueId, owner, majorVersion, metrics, rotation, rotationStatus);
}
+ public LockedApplication withLoadBalancersIn(ZoneId zoneId, List<LoadBalancer> loadBalancers) {
+ Map<ClusterSpec.Id, HostName> loadBalancersByCluster = loadBalancers.stream()
+ .collect(Collectors.toUnmodifiableMap(LoadBalancer::cluster,
+ LoadBalancer::hostname));
+ return with(deployments.get(zoneId).withLoadBalancers(loadBalancersByCluster));
+ }
+
/** Don't expose non-leaf sub-objects. */
private LockedApplication with(Deployment deployment) {
Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
index ed06ff5bddc..3fd2932c3e1 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
@@ -1,8 +1,10 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.application;
+import com.google.common.collect.ImmutableMap;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ClusterSpec.Id;
+import com.yahoo.config.provision.HostName;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
@@ -28,24 +30,26 @@ public class Deployment {
private final Map<Id, ClusterInfo> clusterInfo;
private final DeploymentMetrics metrics;
private final DeploymentActivity activity;
+ private final Map<Id, HostName> loadBalancers;
public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime) {
this(zone, applicationVersion, version, deployTime, Collections.emptyMap(), Collections.emptyMap(),
- DeploymentMetrics.none, DeploymentActivity.none);
+ DeploymentMetrics.none, DeploymentActivity.none, Collections.emptyMap());
}
public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime,
Map<Id, ClusterUtilization> clusterUtilization, Map<Id, ClusterInfo> clusterInfo,
DeploymentMetrics metrics,
- DeploymentActivity activity) {
+ DeploymentActivity activity, Map<Id, HostName> loadBalancers) {
this.zone = Objects.requireNonNull(zone, "zone cannot be null");
this.applicationVersion = Objects.requireNonNull(applicationVersion, "applicationVersion cannot be null");
this.version = Objects.requireNonNull(version, "version cannot be null");
this.deployTime = Objects.requireNonNull(deployTime, "deployTime cannot be null");
- this.clusterUtilization = Objects.requireNonNull(clusterUtilization, "clusterUtilization cannot be null");
- this.clusterInfo = Objects.requireNonNull(clusterInfo, "clusterInfo cannot be null");
+ this.clusterUtilization = ImmutableMap.copyOf(Objects.requireNonNull(clusterUtilization, "clusterUtilization cannot be null"));
+ this.clusterInfo = ImmutableMap.copyOf(Objects.requireNonNull(clusterInfo, "clusterInfo cannot be null"));
this.metrics = Objects.requireNonNull(metrics, "deploymentMetrics cannot be null");
this.activity = Objects.requireNonNull(activity, "activity cannot be null");
+ this.loadBalancers = ImmutableMap.copyOf(Objects.requireNonNull(loadBalancers, "loadBalancers cannot be null"));
}
/** Returns the zone this was deployed to */
@@ -78,24 +82,33 @@ public class Deployment {
return clusterUtilization;
}
+ public Map<Id, HostName> loadBalancers() {
+ return loadBalancers;
+ }
+
public Deployment recordActivityAt(Instant instant) {
return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics,
- activity.recordAt(instant, metrics));
+ activity.recordAt(instant, metrics), loadBalancers);
}
public Deployment withClusterUtils(Map<Id, ClusterUtilization> clusterUtilization) {
return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics,
- activity);
+ activity, loadBalancers);
}
public Deployment withClusterInfo(Map<Id, ClusterInfo> newClusterInfo) {
return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, newClusterInfo, metrics,
- activity);
+ activity, loadBalancers);
}
public Deployment withMetrics(DeploymentMetrics metrics) {
return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics,
- activity);
+ activity, loadBalancers);
+ }
+
+ public Deployment withLoadBalancers(Map<Id, HostName> loadBalancers) {
+ return new Deployment(zone, applicationVersion, version, deployTime, clusterUtilization, clusterInfo, metrics,
+ activity, loadBalancers);
}
/**
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAlias.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAlias.java
new file mode 100644
index 00000000000..4e1c79248a9
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAlias.java
@@ -0,0 +1,93 @@
+// 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.application;
+
+import com.google.common.base.Strings;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.HostName;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a DNS alias for a load balancer.
+ *
+ * @author mortent
+ */
+public class LoadBalancerAlias {
+
+ private static final String ignoredEndpointPart = "default";
+ private final ApplicationId owner;
+ private final String id;
+ private final HostName alias;
+ private final HostName canonicalName;
+
+ public LoadBalancerAlias(ApplicationId owner, String id, HostName alias, HostName canonicalName) {
+ this.owner = Objects.requireNonNull(owner, "owner must be non-null");
+ this.id = Objects.requireNonNull(id, "id must be non-null");
+ this.alias = Objects.requireNonNull(alias, "alias must be non-null");
+ this.canonicalName = Objects.requireNonNull(canonicalName, "canonicalName must be non-null");
+ }
+
+ /** The application owning this */
+ public ApplicationId owner() {
+ return owner;
+ }
+
+ /** The ID of the DNS record represented by this */
+ public String id() {
+ return id;
+ }
+
+ /** This alias (lhs of the CNAME record) */
+ public HostName alias() {
+ return alias;
+ }
+
+ /** The canonical name of this (rhs of the CNAME record) */
+ public HostName canonicalName() {
+ return canonicalName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LoadBalancerAlias that = (LoadBalancerAlias) o;
+ return Objects.equals(owner, that.owner) &&
+ Objects.equals(id, that.id) &&
+ Objects.equals(alias, that.alias) &&
+ Objects.equals(canonicalName, that.canonicalName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(owner, id, alias, canonicalName);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s: %s -> %s, owned by %s", id, alias, canonicalName, owner.toShortString());
+ }
+
+ public static String createAlias(ClusterSpec.Id clusterId, ApplicationId applicationId, ZoneId zoneId) {
+ List<String> parts = Arrays.asList(ignorePartIfDefault(clusterId.value()),
+ ignorePartIfDefault(applicationId.instance().value()),
+ applicationId.application().value(),
+ applicationId.tenant().value(),
+ zoneId.value(),
+ "vespa.oath.cloud"
+ );
+ return parts.stream()
+ .filter(s -> !Strings.isNullOrEmpty((s)))
+ .collect(Collectors.joining("--"));
+ }
+
+ private static String ignorePartIfDefault(String s) {
+ return ignoredEndpointPart.equalsIgnoreCase(s) ? "" : s;
+ }
+
+}
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 08e5be2ea1f..b38f55826d6 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
@@ -52,6 +52,7 @@ public class ControllerMaintenance extends AbstractComponent {
private final JobRunner jobRunner;
private final ContactInformationMaintainer contactInformationMaintainer;
private final CostReportMaintainer costReportMaintainer;
+ private final LoadBalancerAliasMaintainer loadBalancerAliasMaintainer;
@SuppressWarnings("unused") // instantiated by Dependency Injection
public ControllerMaintenance(MaintainerConfig maintainerConfig, ApiAuthorityConfig apiAuthorityConfig, Controller controller, CuratorDb curator,
@@ -81,6 +82,7 @@ public class ControllerMaintenance extends AbstractComponent {
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(), selfHostedCostConfig);
+ loadBalancerAliasMaintainer = new LoadBalancerAliasMaintainer(controller, Duration.ofMinutes(5), jobControl, nameService, curator);
}
public Upgrader upgrader() { return upgrader; }
@@ -108,6 +110,7 @@ public class ControllerMaintenance extends AbstractComponent {
jobRunner.deconstruct();
contactInformationMaintainer.deconstruct();
costReportMaintainer.deconstruct();
+ loadBalancerAliasMaintainer.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/LoadBalancerAliasMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainer.java
new file mode 100644
index 00000000000..6c5000e3e3d
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainer.java
@@ -0,0 +1,147 @@
+// 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.maintenance;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.HostName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.curator.Lock;
+import com.yahoo.vespa.hosted.controller.Application;
+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.configserver.LoadBalancer;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordId;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.application.Deployment;
+import com.yahoo.vespa.hosted.controller.application.LoadBalancerAlias;
+import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Maintains DNS aliases for all load balancers in this system.
+ *
+ * @author mortent
+ */
+public class LoadBalancerAliasMaintainer extends Maintainer {
+
+ private static final Logger log = Logger.getLogger(LoadBalancerAliasMaintainer.class.getName());
+
+ private final NameService nameService;
+ private final CuratorDb db;
+ private final ApplicationController applications;
+
+ public LoadBalancerAliasMaintainer(Controller controller,
+ Duration interval,
+ JobControl jobControl,
+ NameService nameService,
+ CuratorDb db) {
+ super(controller, interval, jobControl);
+ this.nameService = nameService;
+ this.db = db;
+ this.applications = controller.applications();
+ }
+
+ @Override
+ protected void maintain() {
+ updateDnsRecords();
+ removeObsoleteDnsRecords();
+ }
+
+ /** Create DNS records for all exclusive load balancers */
+ private void updateDnsRecords() {
+ for (Application application : applications.asList()) {
+ for (ZoneId zone : application.deployments().keySet()) {
+ List<LoadBalancer> loadBalancers = findLoadBalancersIn(zone, application.id());
+ if (loadBalancers.isEmpty()) continue;
+
+ applications.lockIfPresent(application.id(), (locked) -> {
+ applications.store(locked.withLoadBalancersIn(zone, loadBalancers));
+ });
+
+ try (Lock lock = db.lockLoadBalancerAliases()) {
+ Set<LoadBalancerAlias> aliases = new LinkedHashSet<>(db.readLoadBalancerAliases(application.id()));
+ for (LoadBalancer loadBalancer : loadBalancers) {
+ try {
+ aliases.add(registerDnsAlias(application.id(), zone, loadBalancer));
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Failed to create or update DNS record for load balancer " +
+ loadBalancer.hostname() + ". Retrying in " + maintenanceInterval(),
+ e);
+ }
+ }
+ db.writeLoadBalancerAliases(application.id(), aliases);
+ }
+ }
+ }
+ }
+
+ /** Register DNS alias for given load balancer */
+ private LoadBalancerAlias registerDnsAlias(ApplicationId application, ZoneId zone, LoadBalancer loadBalancer) {
+ HostName alias = HostName.from(LoadBalancerAlias.createAlias(loadBalancer.cluster(), application, zone));
+ RecordName name = RecordName.from(alias.value());
+ RecordData data = RecordData.fqdn(loadBalancer.hostname().value());
+ Optional<Record> existingRecord = nameService.findRecord(Record.Type.CNAME, name);
+ RecordId id;
+ if(existingRecord.isPresent()) {
+ id = existingRecord.get().id();
+ nameService.updateRecord(existingRecord.get().id(), data);
+ } else {
+ id = nameService.createCname(name, data);
+ }
+ return new LoadBalancerAlias(application, id.asString(), alias, loadBalancer.hostname());
+ }
+
+ /** Find all load balancers assigned to application in given zone */
+ private List<LoadBalancer> findLoadBalancersIn(ZoneId zone, ApplicationId application) {
+ try {
+ return controller().applications().configServer().getLoadBalancers(new DeploymentId(application, zone));
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING,
+ String.format("Got exception fetching load balancers for application: %s, in zone: %s. Retrying in %s",
+ application.toShortString(), zone.value(), maintenanceInterval()), e);
+ }
+ return Collections.emptyList();
+ }
+
+ /** Remove all DNS records that point to non-existing load balancers */
+ private void removeObsoleteDnsRecords() {
+ try (Lock lock = db.lockLoadBalancerAliases()) {
+ List<LoadBalancerAlias> removalCandidates = new ArrayList<>(db.readLoadBalancerAliases());
+ Set<HostName> activeLoadBalancers = controller().applications().asList().stream()
+ .map(Application::deployments)
+ .map(Map::values)
+ .flatMap(Collection::stream)
+ .map(Deployment::loadBalancers)
+ .map(Map::values)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toUnmodifiableSet());
+
+ // Remove any active load balancers
+ removalCandidates.removeIf(lb -> activeLoadBalancers.contains(lb.canonicalName()));
+ for (LoadBalancerAlias alias : removalCandidates) {
+ try {
+ nameService.removeRecord(new RecordId(alias.id()));
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Failed to remove DNS record with ID '" + alias.id() +
+ "'. Retrying in " + maintenanceInterval());
+ }
+ }
+ }
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
index fa474f5c5e0..74edfcea6f7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
@@ -88,6 +88,7 @@ public class ApplicationSerializer {
private final String lastWrittenField = "lastWritten";
private final String lastQueriesPerSecondField = "lastQueriesPerSecond";
private final String lastWritesPerSecondField = "lastWritesPerSecond";
+ private final String loadBalancers = "loadBalancers";
// DeploymentJobs fields
private final String projectIdField = "projectId";
@@ -179,6 +180,7 @@ public class ApplicationSerializer {
deployment.activity().lastWritten().ifPresent(instant -> object.setLong(lastWrittenField, instant.toEpochMilli()));
deployment.activity().lastQueriesPerSecond().ifPresent(value -> object.setDouble(lastQueriesPerSecondField, value));
deployment.activity().lastWritesPerSecond().ifPresent(value -> object.setDouble(lastWritesPerSecondField, value));
+ loadBalancersToSlime(deployment.loadBalancers(), object);
}
private void deploymentMetricsToSlime(DeploymentMetrics metrics, Cursor object) {
@@ -301,6 +303,13 @@ public class ApplicationSerializer {
});
}
+ private void loadBalancersToSlime(Map<ClusterSpec.Id, HostName> loadBalancerMap, Cursor parentObject) {
+ Cursor root = parentObject.setObject(loadBalancers);
+ for (Map.Entry<ClusterSpec.Id, HostName> entry : loadBalancerMap.entrySet()) {
+ root.setString(entry.getKey().value(), entry.getValue().value());
+ }
+ }
+
// ------------------ Deserialization
public Application fromSlime(Slime slime) {
@@ -343,7 +352,8 @@ public class ApplicationSerializer {
DeploymentActivity.create(optionalInstant(deploymentObject.field(lastQueriedField)),
optionalInstant(deploymentObject.field(lastWrittenField)),
optionalDouble(deploymentObject.field(lastQueriesPerSecondField)),
- optionalDouble(deploymentObject.field(lastWritesPerSecondField))));
+ optionalDouble(deploymentObject.field(lastWritesPerSecondField))),
+ loadBalancerMapFromSlime(deploymentObject.field(loadBalancers)));
}
private DeploymentMetrics deploymentMetricsFromSlime(Inspector object) {
@@ -499,6 +509,12 @@ public class ApplicationSerializer {
return field.valid() ? optionalString(field).map(RotationId::new) : Optional.empty();
}
+ private Map<ClusterSpec.Id, HostName> loadBalancerMapFromSlime(Inspector object) {
+ Map<ClusterSpec.Id, HostName> loadBalancers = new HashMap<>();
+ object.traverse((String name, Inspector value) -> loadBalancers.put(new ClusterSpec.Id(name), HostName.from(value.asString())));
+ return loadBalancers;
+ }
+
private OptionalLong optionalLong(Inspector field) {
return field.valid() ? OptionalLong.of(field.asLong()) : OptionalLong.empty();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
index 74e60a40b4c..9c498233809 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
@@ -18,6 +18,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.Step;
+import com.yahoo.vespa.hosted.controller.application.LoadBalancerAlias;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
@@ -71,6 +72,7 @@ public class CuratorDb {
private static final Path applicationRoot = root.append("applications");
private static final Path jobRoot = root.append("jobs");
private static final Path controllerRoot = root.append("controllers");
+ private static final Path loadBalancerAliasesRoot = root.append("loadBalancerAliases");
private final StringSetSerializer stringSetSerializer = new StringSetSerializer();
private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer();
@@ -81,6 +83,7 @@ public class CuratorDb {
private final RunSerializer runSerializer = new RunSerializer();
private final OsVersionSerializer osVersionSerializer = new OsVersionSerializer();
private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer);
+ private final LoadBalancerAliasSerializer loadBalancerAliasSerializer = new LoadBalancerAliasSerializer();
private final Curator curator;
private final Duration tryLockTimeout;
@@ -174,6 +177,9 @@ public class CuratorDb {
return lock(lockRoot.append("osVersionStatus"), defaultLockTimeout);
}
+ public Lock lockLoadBalancerAliases() {
+ return lock(lockRoot.append("loadBalancerAliases"), defaultLockTimeout);
+ }
// -------------- Helpers ------------------------------------------
/** Try locking with a low timeout, meaning it is OK to fail lock acquisition.
@@ -455,6 +461,24 @@ public class CuratorDb {
curator.set(openStackServerPoolPath(), data);
}
+ // -------------- Load balancer aliases------------------------------------
+
+ public void writeLoadBalancerAliases(ApplicationId application, Set<LoadBalancerAlias> aliases) {
+ curator.set(loadBalancerAliasPath(application), asJson(loadBalancerAliasSerializer.toSlime(aliases)));
+ }
+
+ public Set<LoadBalancerAlias> readLoadBalancerAliases() {
+ return curator.getChildren(loadBalancerAliasesRoot).stream()
+ .map(ApplicationId::fromSerializedForm)
+ .flatMap(application -> readLoadBalancerAliases(application).stream())
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public Set<LoadBalancerAlias> readLoadBalancerAliases(ApplicationId application) {
+ return readSlime(loadBalancerAliasPath(application)).map(slime -> loadBalancerAliasSerializer.fromSlime(application, slime))
+ .orElseGet(Collections::emptySet);
+ }
+
// -------------- Paths ---------------------------------------------------
private Path lockPath(TenantName tenant) {
@@ -530,6 +554,10 @@ public class CuratorDb {
return root.append("versionStatus");
}
+ private static Path loadBalancerAliasPath(ApplicationId application) {
+ return loadBalancerAliasesRoot.append(application.serializedForm());
+ }
+
private static Path provisionStatePath() {
return root.append("provisioning").append("states");
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializer.java
new file mode 100644
index 00000000000..0e7682eaf96
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializer.java
@@ -0,0 +1,52 @@
+// 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.persistence;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.HostName;
+import com.yahoo.slime.ArrayTraverser;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.hosted.controller.application.LoadBalancerAlias;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Serializer and deserializer for a {@link LoadBalancerAlias}.
+ *
+ * @author mortent
+ */
+public class LoadBalancerAliasSerializer {
+
+ private static final String aliasesField = "aliases";
+ private static final String idField = "id";
+ private static final String aliasField = "alias";
+ private static final String canonicalNameField = "canonicalName";
+
+ public Slime toSlime(Set<LoadBalancerAlias> aliases) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor aliasArray = root.setArray(aliasesField);
+ aliases.forEach(alias -> {
+ Cursor nameObject = aliasArray.addObject();
+ nameObject.setString(idField, alias.id());
+ nameObject.setString(aliasField, alias.alias().value());
+ nameObject.setString(canonicalNameField, alias.canonicalName().value());
+ });
+ return slime;
+ }
+
+ public Set<LoadBalancerAlias> fromSlime(ApplicationId owner, Slime slime) {
+ Set<LoadBalancerAlias> names = new LinkedHashSet<>();
+ slime.get().field(aliasesField).traverse((ArrayTraverser) (i, inspect) -> {
+ names.add(new LoadBalancerAlias(owner,
+ inspect.field(idField).asString(),
+ HostName.from(inspect.field(aliasField).asString()),
+ HostName.from(inspect.field(canonicalNameField).asString())));
+ });
+
+ return Collections.unmodifiableSet(names);
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAliasTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAliasTest.java
new file mode 100644
index 00000000000..965a639a68c
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/LoadBalancerAliasTest.java
@@ -0,0 +1,33 @@
+// 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.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
+import org.junit.Test;
+
+import static com.yahoo.vespa.hosted.controller.application.LoadBalancerAlias.createAlias;
+import static org.junit.Assert.*;
+
+/**
+ * @author mpolden
+ */
+public class LoadBalancerAliasTest {
+
+ @Test
+ public void test_endpoint_names() {
+ ZoneId zoneId = ZoneId.from("prod", "us-north-1");
+ ApplicationId withInstanceName = ApplicationId.from("tenant", "application", "instance");
+ testAlias("instance--application--tenant--prod.us-north-1--vespa.oath.cloud", "default", withInstanceName, zoneId);
+ testAlias("cluster--instance--application--tenant--prod.us-north-1--vespa.oath.cloud", "cluster", withInstanceName, zoneId);
+
+ ApplicationId withDefaultInstance = ApplicationId.from("tenant", "application", "default");
+ testAlias("application--tenant--prod.us-north-1--vespa.oath.cloud", "default", withDefaultInstance, zoneId);
+ testAlias("cluster--application--tenant--prod.us-north-1--vespa.oath.cloud", "cluster", withDefaultInstance, zoneId);
+ }
+
+ private void testAlias(String expected, String clusterName, ApplicationId applicationId, ZoneId zoneId) {
+ assertEquals(expected, createAlias(ClusterSpec.Id.from(clusterName), applicationId, zoneId));
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
index f011baef39a..45e87859576 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
@@ -7,7 +7,6 @@ import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeType;
-import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ConfigChangeActions;
@@ -16,6 +15,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.Hostname;
import com.yahoo.vespa.hosted.controller.api.identifiers.Identifier;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Logs;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
@@ -28,8 +28,6 @@ import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import com.yahoo.vespa.serviceview.bindings.ClusterView;
import com.yahoo.vespa.serviceview.bindings.ServiceView;
-import java.io.IOException;
-import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -55,6 +53,7 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
private final Map<DeploymentId, ServiceConvergence> serviceStatus = new HashMap<>();
private final Version initialVersion = new Version(6, 1, 0);
private final Set<DeploymentId> suspendedApplications = new HashSet<>();
+ private final Map<DeploymentId, List<LoadBalancer>> loadBalancers = new HashMap<>();
private Version lastPrepareVersion = null;
private RuntimeException prepareException = null;
@@ -173,6 +172,19 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
}
@Override
+ public List<LoadBalancer> getLoadBalancers(DeploymentId deployment) {
+ return loadBalancers.getOrDefault(deployment, Collections.emptyList());
+ }
+
+ public void addLoadBalancers(ZoneId zoneId, ApplicationId applicationId, List<LoadBalancer> loadBalancers) {
+ this.loadBalancers.put(new DeploymentId(applicationId, zoneId), loadBalancers);
+ }
+
+ public void removeLoadBalancers(DeploymentId deployment) {
+ this.loadBalancers.remove(deployment);
+ }
+
+ @Override
public PreparedApplication deploy(DeploymentId deployment, DeployOptions deployOptions, Set<String> rotationCnames,
Set<String> rotationNames, byte[] content) {
lastPrepareVersion = deployOptions.vespaVersion.map(Version::fromString).orElse(null);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainerTest.java
new file mode 100644
index 00000000000..fa3fe505f3c
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/LoadBalancerAliasMaintainerTest.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.maintenance;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.HostName;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
+import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordId;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.application.LoadBalancerAlias;
+import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author mortent
+ */
+public class LoadBalancerAliasMaintainerTest {
+
+ @Test
+ public void maintains_load_balancer_records_correctly() {
+ DeploymentTester tester = new DeploymentTester();
+ Application application = tester.createApplication("app1", "tenant1", 1, 1L);
+ LoadBalancerAliasMaintainer maintainer = new LoadBalancerAliasMaintainer(tester.controller(), Duration.ofHours(12),
+ new JobControl(new MockCuratorDb()),
+ tester.controllerTester().nameService(),
+ tester.controllerTester().curator());
+
+ ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .region("us-west-1")
+ .region("us-central-1")
+ .build();
+
+ int numberOfClustersPerZone = 2;
+
+ // Deploy application
+ tester.deployCompletely(application, applicationPackage);
+ setupClustersWithLoadBalancers(tester, application, numberOfClustersPerZone);
+
+ maintainer.maintain();
+ Map<RecordId, Record> records = tester.controllerTester().nameService().records();
+ long recordCount = records.entrySet().stream().filter(entry -> entry.getValue().data().asString().contains("loadbalancer")).count();
+ assertEquals(4, recordCount);
+
+ Set<LoadBalancerAlias> loadBalancerAliases = tester.controller().curator().readLoadBalancerAliases(application.id());
+ assertEquals(4, loadBalancerAliases.size());
+
+
+ // no update
+ maintainer.maintain();
+ Map<RecordId, Record> records2 = tester.controllerTester().nameService().records();
+ long recordCount2 = records2.entrySet().stream().filter(entry -> entry.getValue().data().asString().contains("loadbalancer")).count();
+ assertEquals(recordCount, recordCount2);
+ assertEquals(records, records2);
+
+
+ // add 1 cluster per zone
+ setupClustersWithLoadBalancers(tester, application, numberOfClustersPerZone + 1);
+
+ maintainer.maintain();
+ Map<RecordId, Record> records3 = tester.controllerTester().nameService().records();
+ long recordCount3 = records3.entrySet().stream().filter(entry -> entry.getValue().data().asString().contains("loadbalancer")).count();
+ assertEquals(6,recordCount3);
+
+ Set<LoadBalancerAlias> aliases3 = tester.controller().curator().readLoadBalancerAliases(application.id());
+ assertEquals(6, aliases3.size());
+
+
+ // Add application
+ Application application2 = tester.createApplication("app2", "tenant1", 1, 1L);
+ tester.deployCompletely(application2, applicationPackage);
+ setupClustersWithLoadBalancers(tester, application2, numberOfClustersPerZone);
+
+ maintainer.maintain();
+ Map<RecordId, Record> records4 = tester.controllerTester().nameService().records();
+ long recordCount4 = records4.entrySet().stream().filter(entry -> entry.getValue().data().asString().contains("loadbalancer")).count();
+ assertEquals(10,recordCount4);
+
+ Set<LoadBalancerAlias> aliases4 = tester.controller().curator().readLoadBalancerAliases(application2.id());
+ assertEquals(4, aliases4.size());
+
+
+ // Remove cluster in app1
+ setupClustersWithLoadBalancers(tester, application, numberOfClustersPerZone);
+
+ maintainer.maintain();
+ Map<RecordId, Record> records5 = tester.controllerTester().nameService().records();
+ long recordCount5 = records5.entrySet().stream().filter(entry -> entry.getValue().data().asString().contains("loadbalancer")).count();
+ assertEquals(8, recordCount5);
+
+ // Remove application app2
+ tester.controller().applications().get(application2.id())
+ .map(app -> app.deployments().keySet())
+ .orElse(Collections.emptySet())
+ .forEach(zone -> tester.controller().applications().deactivate(application2.id(), zone));
+
+ maintainer.maintain();
+ Map<RecordId, Record> records6 = tester.controllerTester().nameService().records();
+ long recordCount6 = records6.entrySet().stream().filter(entry -> entry.getValue().data().asString().contains("loadbalancer")).count();
+ assertEquals(4, recordCount6);
+ }
+
+ private void setupClustersWithLoadBalancers(DeploymentTester tester, Application application, int numberOfClustersPerZone) {
+ tester.controller().applications().get(application.id()).orElseThrow(()->new RuntimeException("No deployments")).deployments().keySet()
+ .forEach(zone -> tester.configServer()
+ .removeLoadBalancers(new DeploymentId(application.id(), zone)));
+ tester.controller().applications().get(application.id()).orElseThrow(()->new RuntimeException("No deployments")).deployments().keySet()
+ .forEach(zone -> tester.configServer()
+ .addLoadBalancers(zone, application.id(), makeLoadBalancers(zone, application.id(), numberOfClustersPerZone)));
+
+ }
+
+ private List<LoadBalancer> makeLoadBalancers(ZoneId zone, ApplicationId applicationId, int count) {
+ List<LoadBalancer> loadBalancers = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ loadBalancers.add(
+ new LoadBalancer("LB-" + i + "-Z-" + zone.value(),
+ new TenantId(applicationId.tenant().value()),
+ new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(applicationId.application().value()),
+ new InstanceId(applicationId.instance().value()),
+ ClusterSpec.Id.from("cluster-" + i),
+ HostName.from("loadbalancer-" + i + "-" + applicationId.serializedForm() + "-zone-" + zone.value())
+ ));
+ }
+ return loadBalancers;
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
index d4cd7c0fe85..e46119dc0b5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
@@ -1,6 +1,7 @@
// 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.persistence;
+import com.google.common.collect.ImmutableMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
@@ -78,7 +79,7 @@ public class ApplicationSerializerTest {
createClusterUtils(3, 0.2), createClusterInfo(3, 4),
new DeploymentMetrics(2, 3, 4, 5, 6, Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS))),
DeploymentActivity.create(Optional.of(activityAt), Optional.of(activityAt),
- OptionalDouble.of(200), OptionalDouble.of(10))));
+ OptionalDouble.of(200), OptionalDouble.of(10)), createLoadBalancers("default", "foo.bar")));
OptionalLong projectId = OptionalLong.of(123L);
List<JobStatus> statusList = new ArrayList<>();
@@ -198,6 +199,9 @@ public class ApplicationSerializerTest {
Application original6 = writable(original).withOutstandingChange(Change.of(ApplicationVersion.from(new SourceRevision("a", "b", "c"), 42))).get();
Application serialized6 = applicationSerializer.fromSlime(applicationSerializer.toSlime(original6));
assertEquals(original6.outstandingChange(), serialized6.outstandingChange());
+
+ assertEquals(1, serialized.deployments().get(zone2).loadBalancers().size());
+ assertEquals(original.deployments().get(zone2).loadBalancers(), serialized.deployments().get(zone2).loadBalancers());
}
}
@@ -231,6 +235,10 @@ public class ApplicationSerializerTest {
return result;
}
+ private Map<ClusterSpec.Id, HostName> createLoadBalancers(String clusterId, String hostName) {
+ return ImmutableMap.of(ClusterSpec.Id.from(clusterId), HostName.from(hostName));
+ }
+
@Test
public void testCompleteApplicationDeserialization() throws Exception {
byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-application.json"));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializerTest.java
new file mode 100644
index 00000000000..6ef15be775e
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LoadBalancerAliasSerializerTest.java
@@ -0,0 +1,35 @@
+// 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.persistence;
+
+import com.google.common.collect.ImmutableSet;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.HostName;
+import com.yahoo.vespa.hosted.controller.application.LoadBalancerAlias;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author mortent
+ */
+public class LoadBalancerAliasSerializerTest {
+
+ @Test
+ public void test_serialization() {
+ LoadBalancerAliasSerializer serializer = new LoadBalancerAliasSerializer();
+ ApplicationId owner = ApplicationId.defaultId();
+ Set<LoadBalancerAlias> names = ImmutableSet.of(new LoadBalancerAlias(owner,
+ "record-id-1",
+ HostName.from("my-pretty-alias"),
+ HostName.from("long-and-ugly-name")),
+ new LoadBalancerAlias(owner,
+ "record-id-2",
+ HostName.from("my-pretty-alias-2"),
+ HostName.from("long-and-ugly-name-2")));
+ Set<LoadBalancerAlias> serialized = serializer.fromSlime(owner, serializer.toSlime(names));
+ assertEquals(names, serialized);
+ }
+
+}
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 dcee9717ecc..169c86fabbe 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
@@ -34,6 +34,9 @@
"name": "JobRunner"
},
{
+ "name": "LoadBalancerAliasMaintainer"
+ },
+ {
"name": "MetricsReporter"
},
{