diff options
author | Martin Polden <mpolden@mpolden.no> | 2019-01-25 15:29:54 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-25 15:29:54 +0100 |
commit | e7f859874ac9133e81d8b93743d8e9fb56bb0822 (patch) | |
tree | ef4be34d45f43699bc49a06b4a43160807778b9b | |
parent | 888dcfc48fcebb73cf45a3c4763473eedb0490fc (diff) | |
parent | 8d90a1fd5a0e6b08ab03ab9497e2508a8e15fcae (diff) |
Merge pull request #8173 from vespa-engine/mortent/loadbalancer-maintainer
Maintainer updating name service for load balancers
17 files changed, 692 insertions, 17 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java index e3599b3e652..ad52ba48d4e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java @@ -71,4 +71,6 @@ public interface ConfigServer { /** Get service convergence status for given deployment */ Optional<ServiceConvergence> serviceConvergence(DeploymentId deployment); + List<LoadBalancer> getLoadBalancers(DeploymentId deployment); + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java new file mode 100644 index 00000000000..b77b63cab8a --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java @@ -0,0 +1,59 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.configserver; + +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId; +import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId; +import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId; + +import java.util.Objects; + +/** + * Represents an exclusive load balancer, assigned to an application's cluster. + * + * @author mortent + */ +public class LoadBalancer { + + private final String id; + private final TenantId tenant; + private final ApplicationId application; + private final InstanceId instance; + private final ClusterSpec.Id cluster; + private final HostName hostname; + + public LoadBalancer(String id, TenantId tenant, ApplicationId application, InstanceId instance, ClusterSpec.Id cluster, HostName hostname) { + this.id = Objects.requireNonNull(id, "id must be non-null"); + this.tenant = Objects.requireNonNull(tenant, "tenant must be non-null"); + this.application = Objects.requireNonNull(application, "application must be non-null"); + this.instance = Objects.requireNonNull(instance, "instance must be non-null"); + this.cluster = Objects.requireNonNull(cluster, "cluster must be non-null"); + this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); + } + + public String id() { + return id; + } + + public TenantId tenant() { + return tenant; + } + + public ApplicationId application() { + return application; + } + + public InstanceId instance() { + return instance; + } + + public ClusterSpec.Id cluster() { + return cluster; + } + + public HostName hostname() { + return hostname; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordId.java index da42c38252a..2aa3f4a810e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordId.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordId.java @@ -1,6 +1,8 @@ // 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.api.integration.dns; +import java.util.Objects; + /** * Unique identifier for a resource record. * @@ -24,4 +26,17 @@ public class RecordId { "id='" + id + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordId recordId = (RecordId) o; + return id.equals(recordId.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } 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" }, { |