diff options
author | Morten Tokle <mortent@verizonmedia.com> | 2022-05-18 09:37:25 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-18 09:37:25 +0200 |
commit | fa46c60b0203b0d8b869a338f497662b8f03444f (patch) | |
tree | 0591b851b4692db56a34b406117b002f2d3e1530 | |
parent | f05f60b1ec40be6f3e60349d65da60bc41555db3 (diff) | |
parent | b5888fc62eb9bf8bcb9e42335480d4c18cd233f0 (diff) |
Merge pull request #22594 from vespa-engine/mpolden/node-cloud-account
Use cloud account in node-repository provisioning
29 files changed, 311 insertions, 185 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java index 71d325adb7d..640bede6b62 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.ConfigModelContext; import com.yahoo.config.provision.Capacity; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -56,12 +57,16 @@ public class NodesSpecification { /** The ID of the cluster referencing this node specification, if any */ private final Optional<String> combinedId; + /** The cloud account to use for nodes in this spec, if any */ + private final Optional<CloudAccount> cloudAccount; + private NodesSpecification(ClusterResources min, ClusterResources max, boolean dedicated, Version version, boolean required, boolean canFail, boolean exclusive, Optional<DockerImage> dockerImageRepo, - Optional<String> combinedId) { + Optional<String> combinedId, + Optional<CloudAccount> cloudAccount) { if (max.smallerThan(min)) throw new IllegalArgumentException("Min resources must be larger or equal to max resources, but " + max + " is smaller than " + min); @@ -83,10 +88,12 @@ public class NodesSpecification { this.exclusive = exclusive; this.dockerImageRepo = dockerImageRepo; this.combinedId = combinedId; + this.cloudAccount = cloudAccount; } private static NodesSpecification create(boolean dedicated, boolean canFail, Version version, - ModelElement nodesElement, Optional<DockerImage> dockerImageRepo) { + ModelElement nodesElement, Optional<DockerImage> dockerImageRepo, + Optional<CloudAccount> cloudAccount) { var resolvedElement = resolveElement(nodesElement); var combinedId = findCombinedId(nodesElement, resolvedElement); var resources = toResources(resolvedElement); @@ -98,7 +105,8 @@ public class NodesSpecification { canFail, resolvedElement.booleanAttribute("exclusive", false), dockerImageToUse(resolvedElement, dockerImageRepo), - combinedId); + combinedId, + cloudAccount); } private static Pair<ClusterResources, ClusterResources> toResources(ModelElement nodesElement) { @@ -125,7 +133,8 @@ public class NodesSpecification { ! context.getDeployState().getProperties().isBootstrap(), context.getDeployState().getWantedNodeVespaVersion(), nodesElement, - context.getDeployState().getWantedDockerImageRepo()); + context.getDeployState().getWantedDockerImageRepo(), + context.getDeployState().getProperties().cloudAccount()); } /** @@ -154,7 +163,8 @@ public class NodesSpecification { ! context.getDeployState().getProperties().isBootstrap(), context.getDeployState().getWantedNodeVespaVersion(), nodesElement, - context.getDeployState().getWantedDockerImageRepo())); + context.getDeployState().getWantedDockerImageRepo(), + context.getDeployState().getProperties().cloudAccount())); } /** @@ -169,7 +179,8 @@ public class NodesSpecification { ! context.getDeployState().getProperties().isBootstrap(), false, context.getDeployState().getWantedDockerImageRepo(), - Optional.empty()); + Optional.empty(), + context.getDeployState().getProperties().cloudAccount()); } /** Returns a requirement from <code>count</code> dedicated nodes in one group */ @@ -182,7 +193,8 @@ public class NodesSpecification { ! context.getDeployState().getProperties().isBootstrap(), false, context.getDeployState().getWantedDockerImageRepo(), - Optional.empty()); + Optional.empty(), + context.getDeployState().getProperties().cloudAccount()); } /** @@ -206,7 +218,8 @@ public class NodesSpecification { ! context.getDeployState().getProperties().isBootstrap(), false, context.getDeployState().getWantedDockerImageRepo(), - Optional.empty()); + Optional.empty(), + context.getDeployState().getProperties().cloudAccount()); } public ClusterResources minResources() { return min; } @@ -239,7 +252,7 @@ public class NodesSpecification { .dockerImageRepository(dockerImageRepo) .stateful(stateful) .build(); - return hostSystem.allocateHosts(cluster, Capacity.from(min, max, required, canFail), logger); + return hostSystem.allocateHosts(cluster, Capacity.from(min, max, required, canFail, cloudAccount), logger); } private static Pair<NodeResources, NodeResources> nodeResources(ModelElement nodesElement) { diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java index 958a37e1432..70e88418fb7 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java @@ -1,6 +1,9 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.provision; +import java.util.Objects; +import java.util.Optional; + /** * A capacity request. * @@ -11,14 +14,12 @@ public final class Capacity { /** Resources should stay between these values, inclusive */ private final ClusterResources min, max; - private final boolean required; - private final boolean canFail; - private final NodeType type; + private final Optional<CloudAccount> cloudAccount; - private Capacity(ClusterResources min, ClusterResources max, boolean required, boolean canFail, NodeType type) { + private Capacity(ClusterResources min, ClusterResources max, boolean required, boolean canFail, NodeType type, Optional<CloudAccount> cloudAccount) { validate(min); validate(max); if (max.smallerThan(min)) @@ -29,6 +30,7 @@ public final class Capacity { this.required = required; this.canFail = canFail; this.type = type; + this.cloudAccount = Objects.requireNonNull(cloudAccount); } private static void validate(ClusterResources resources) { @@ -58,8 +60,13 @@ public final class Capacity { */ public NodeType type() { return type; } + /** Returns the cloud account where this capacity is requested */ + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + public Capacity withLimits(ClusterResources min, ClusterResources max) { - return new Capacity(min, max, required, canFail, type); + return new Capacity(min, max, required, canFail, type, cloudAccount); } @Override @@ -82,8 +89,13 @@ public final class Capacity { return from(resources, required, canFail, NodeType.tenant); } + // TODO(mpolden): Remove when config models < 7.590 are gone public static Capacity from(ClusterResources min, ClusterResources max, boolean required, boolean canFail) { - return new Capacity(min, max, required, canFail, NodeType.tenant); + return from(min, max, required, canFail, Optional.empty()); + } + + public static Capacity from(ClusterResources min, ClusterResources max, boolean required, boolean canFail, Optional<CloudAccount> cloudAccount) { + return new Capacity(min, max, required, canFail, NodeType.tenant, cloudAccount); } /** Creates this from a node type */ @@ -92,7 +104,7 @@ public final class Capacity { } private static Capacity from(ClusterResources resources, boolean required, boolean canFail, NodeType type) { - return new Capacity(resources, resources, required, canFail, type); + return new Capacity(resources, resources, required, canFail, type, Optional.empty()); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index e6408a0345b..c3c4771ec1d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; @@ -55,6 +56,7 @@ public final class Node implements Nodelike { private final Optional<ClusterSpec.Type> exclusiveToClusterType; private final Optional<String> switchHostname; private final List<TrustStoreItem> trustStoreItems; + private final Optional<CloudAccount> cloudAccount; /** Record of the last event of each type happening to this node */ private final History history; @@ -84,7 +86,8 @@ public final class Node implements Nodelike { Flavor flavor, Status status, State state, Optional<Allocation> allocation, History history, NodeType type, Reports reports, Optional<String> modelName, Optional<TenantName> reservedTo, Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, - Optional<String> switchHostname, List<TrustStoreItem> trustStoreItems) { + Optional<String> switchHostname, List<TrustStoreItem> trustStoreItems, + Optional<CloudAccount> cloudAccount) { this.id = Objects.requireNonNull(id, "A node must have an ID"); this.hostname = requireNonEmptyString(hostname, "A node must have a hostname"); this.ipConfig = Objects.requireNonNull(ipConfig, "A node must a have an IP config"); @@ -101,7 +104,8 @@ public final class Node implements Nodelike { this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId cannot be null"); this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType cannot be null"); this.switchHostname = requireNonEmptyString(switchHostname, "switchHostname cannot be null"); - this.trustStoreItems = trustStoreItems.stream().distinct().collect(Collectors.toUnmodifiableList()); + this.trustStoreItems = Objects.requireNonNull(trustStoreItems).stream().distinct().collect(Collectors.toUnmodifiableList()); + this.cloudAccount = Objects.requireNonNull(cloudAccount); if (state == State.active) requireNonEmpty(ipConfig.primary(), "Active node " + hostname + " must have at least one valid IP address"); @@ -115,6 +119,11 @@ public final class Node implements Nodelike { if (!ipConfig.pool().ipSet().isEmpty()) throw new IllegalArgumentException("A child node cannot have an IP address pool"); if (modelName.isPresent()) throw new IllegalArgumentException("A child node cannot have model name set"); if (switchHostname.isPresent()) throw new IllegalArgumentException("A child node cannot have switch hostname set"); + if (cloudAccount().isPresent()) throw new IllegalArgumentException("A child node cannot have cloud account set"); + } + + if (cloudAccount.isPresent() && exclusiveToApplicationId.isEmpty()) { + throw new IllegalArgumentException("Host in a custom cloud account must be exclusive to an application"); } if (type != NodeType.host && reservedTo.isPresent()) @@ -218,6 +227,11 @@ public final class Node implements Nodelike { return trustStoreItems; } + /** Returns the cloud account of this host. This is empty if the host is in the zone's default account */ + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + /** * Returns a copy of this where wantToFail is set to true and history is updated to reflect this. */ @@ -306,13 +320,15 @@ public final class Node implements Nodelike { /** Returns a node with the status assigned to the given value */ public Node with(Status status) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems, cloudAccount); } /** Returns a node with the type assigned to the given value */ public Node with(NodeType type) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems, cloudAccount); } /** Returns a node with the flavor assigned to the given value */ @@ -320,31 +336,37 @@ public final class Node implements Nodelike { if (flavor.equals(this.flavor)) return this; History updateHistory = history.with(new History.Event(History.Event.Type.resized, agent, instant)); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, updateHistory, type, - reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, + trustStoreItems, cloudAccount); } /** Returns a copy of this with the reboot generation set to generation */ public Node withReboot(Generation generation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status.withReboot(generation), state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this with given id set */ public Node withId(String id) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this with model name set to given value */ public Node withModelName(String modelName) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this with model name cleared */ public Node withoutModelName() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, Optional.empty(), reservedTo, + exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems, + cloudAccount); } /** Returns a copy of this with a history record saying it was detected to be down at this instant */ @@ -378,55 +400,66 @@ public final class Node implements Nodelike { */ public Node with(Allocation allocation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a new Node without an allocation. */ public Node withoutAllocation() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + Optional.empty(), history, type, reports, modelName, reservedTo, + exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems, + cloudAccount); } /** Returns a copy of this node with IP config set to the given value. */ public Node with(IP.Config ipConfig) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this node with the parent hostname assigned to the given value. */ public Node withParentHostname(String parentHostname) { return new Node(id, ipConfig, hostname, Optional.of(parentHostname), flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } public Node withReservedTo(TenantName tenant) { if (type != NodeType.host) throw new IllegalArgumentException("Only host nodes can be reserved, " + hostname + " has type " + type); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this node which is not reserved to a tenant */ public Node withoutReservedTo() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, Optional.empty(), + exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems, + cloudAccount); } public Node withExclusiveToApplicationId(ApplicationId exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } public Node withExclusiveToClusterType(ClusterSpec.Type exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, Optional.ofNullable(exclusiveTo), switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + Optional.ofNullable(exclusiveTo), switchHostname, trustStoreItems, cloudAccount); } /** Returns a copy of this node with switch hostname set to given value */ public Node withSwitchHostname(String switchHostname) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname), trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, Optional.ofNullable(switchHostname), trustStoreItems, cloudAccount); } /** Returns a copy of this node with switch hostname unset */ @@ -469,18 +502,20 @@ public final class Node implements Nodelike { /** Returns a copy of this node with the given history. */ public Node with(History history) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } public Node with(Reports reports) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } public Node with(List<TrustStoreItem> trustStoreItems) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname, - trustStoreItems); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, + exclusiveToClusterType, switchHostname, trustStoreItems, cloudAccount); } private static Optional<String> requireNonEmptyString(Optional<String> value, String message) { @@ -602,6 +637,7 @@ public final class Node implements Nodelike { } public static class Builder { + private final String id; private final String hostname; private final Flavor flavor; @@ -620,6 +656,7 @@ public final class Node implements Nodelike { private Reports reports; private History history; private List<TrustStoreItem> trustStoreItems; + private CloudAccount cloudAccount; private Builder(String id, String hostname, Flavor flavor, State state, NodeType type) { this.id = id; @@ -694,14 +731,21 @@ public final class Node implements Nodelike { return this; } + public Builder cloudAccount(CloudAccount cloudAccount) { + this.cloudAccount = cloudAccount; + return this; + } + public Node build() { return new Node(id, Optional.ofNullable(ipConfig).orElse(IP.Config.EMPTY), hostname, Optional.ofNullable(parentHostname), flavor, Optional.ofNullable(status).orElseGet(Status::initial), state, Optional.ofNullable(allocation), Optional.ofNullable(history).orElseGet(History::empty), type, Optional.ofNullable(reports).orElseGet(Reports::new), Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId), Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname), - Optional.ofNullable(trustStoreItems).orElseGet(List::of)); + Optional.ofNullable(trustStoreItems).orElseGet(List::of), + Optional.ofNullable(cloudAccount)); } + } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java index 1548038e19d..edffa817f64 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.lb; import ai.vespa.http.DomainName; import com.google.common.collect.ImmutableSortedSet; +import com.yahoo.config.provision.CloudAccount; import java.util.Objects; import java.util.Optional; @@ -21,14 +22,16 @@ public class LoadBalancerInstance { private final Set<Integer> ports; private final Set<String> networks; private final Set<Real> reals; + private final Optional<CloudAccount> cloudAccount; public LoadBalancerInstance(DomainName hostname, Optional<DnsZone> dnsZone, Set<Integer> ports, Set<String> networks, - Set<Real> reals) { + Set<Real> reals, Optional<CloudAccount> cloudAccount) { this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); this.dnsZone = Objects.requireNonNull(dnsZone, "dnsZone must be non-null"); this.ports = ImmutableSortedSet.copyOf(requirePorts(ports)); this.networks = ImmutableSortedSet.copyOf(Objects.requireNonNull(networks, "networks must be non-null")); this.reals = ImmutableSortedSet.copyOf(Objects.requireNonNull(reals, "targets must be non-null")); + this.cloudAccount = Objects.requireNonNull(cloudAccount, "cloudAccount must be non-null"); } /** Fully-qualified domain name of this load balancer. This hostname can be used for query and feed */ @@ -56,9 +59,14 @@ public class LoadBalancerInstance { return reals; } + /** Cloud account of this load balancer. This is empty if the load balancer is in the zone's default account */ + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + /** Returns a copy of this with reals set to given reals */ public LoadBalancerInstance withReals(Set<Real> reals) { - return new LoadBalancerInstance(hostname, dnsZone, ports, networks, reals); + return new LoadBalancerInstance(hostname, dnsZone, ports, networks, reals, cloudAccount); } private static Set<Integer> requirePorts(Set<Integer> ports) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java index ef127e04fba..0e388dac9dd 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerService.java @@ -1,7 +1,6 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.lb; -import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeType; @@ -22,8 +21,8 @@ public interface LoadBalancerService { */ LoadBalancerInstance create(LoadBalancerSpec spec, boolean force); - /** Permanently remove load balancer for given application cluster */ - void remove(ApplicationId application, ClusterSpec.Id cluster); + /** Permanently remove given load balancer */ + void remove(LoadBalancer loadBalancer); /** Returns the protocol supported by this load balancer service */ Protocol protocol(); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java index 7da28bff930..df92a6ee44d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.provision.lb; import ai.vespa.http.DomainName; import com.google.common.collect.ImmutableSet; -import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeType; @@ -60,14 +59,15 @@ public class LoadBalancerServiceMock implements LoadBalancerService { Optional.of(new DnsZone("zone-id-1")), Collections.singleton(4443), ImmutableSet.of("10.2.3.0/24", "10.4.5.0/24"), - spec.reals()); + spec.reals(), + spec.cloudAccount()); instances.put(id, instance); return instance; } @Override - public void remove(ApplicationId application, ClusterSpec.Id cluster) { - instances.remove(new LoadBalancerId(application, cluster)); + public void remove(LoadBalancer loadBalancer) { + instances.remove(loadBalancer.id()); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java index f29cdeaa768..19739111415 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java @@ -3,9 +3,11 @@ package com.yahoo.vespa.hosted.provision.lb; import com.google.common.collect.ImmutableSortedSet; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import java.util.Objects; +import java.util.Optional; import java.util.Set; /** @@ -18,11 +20,14 @@ public class LoadBalancerSpec { private final ApplicationId application; private final ClusterSpec.Id cluster; private final Set<Real> reals; + private final Optional<CloudAccount> cloudAccount; - public LoadBalancerSpec(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals) { + public LoadBalancerSpec(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals, + Optional<CloudAccount> cloudAccount) { this.application = Objects.requireNonNull(application); this.cluster = Objects.requireNonNull(cluster); this.reals = ImmutableSortedSet.copyOf(Objects.requireNonNull(reals)); + this.cloudAccount = Objects.requireNonNull(cloudAccount); } /** Owner of the load balancer */ @@ -40,4 +45,8 @@ public class LoadBalancerSpec { return reals; } + /** Cloud account to use when satisfying this */ + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java index fa9db9cd800..3d0cd3ef1e1 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java @@ -2,7 +2,6 @@ package com.yahoo.vespa.hosted.provision.lb; import ai.vespa.http.DomainName; -import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeType; @@ -11,11 +10,11 @@ import java.util.Optional; import java.util.Set; /** - * This implementation of {@link LoadBalancerService} returns the load balancer(s) that exists by default in the shared + * This implementation of {@link LoadBalancerService} returns the load balancer(s) that exist by default in the shared * routing layer. * - * Since such load balancers always exist, we can return the hostname of the routing layer VIP and the networks of the - * proxy nodes directly. Nothing has to be provisioned. + * Since such load balancers always exist, we can return the hostname of the routing layer VIP directly. Nothing has to + * be provisioned. * * @author ogronnesby */ @@ -33,11 +32,12 @@ public class SharedLoadBalancerService implements LoadBalancerService { Optional.empty(), Set.of(4443), Set.of(), - spec.reals()); + spec.reals(), + spec.cloudAccount()); } @Override - public void remove(ApplicationId application, ClusterSpec.Id cluster) { + public void remove(LoadBalancer loadBalancer) { // Do nothing, we have no external state to modify } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java index 6eaee7b33de..0fa406ab0ef 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java @@ -245,10 +245,11 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { Version osVersion = nodeRepository().osVersions().targetFor(NodeType.host).orElse(Version.emptyVersion); List<Integer> provisionIndices = nodeRepository().database().readProvisionIndices(count); List<Node> hosts = hostProvisioner.provisionHosts(provisionIndices, NodeType.host, nodeResources, - ApplicationId.defaultId(), osVersion, HostSharing.shared, Optional.empty()) - .stream() - .map(ProvisionedHost::generateHost) - .collect(Collectors.toList()); + ApplicationId.defaultId(), osVersion, HostSharing.shared, + Optional.empty(), Optional.empty()) + .stream() + .map(ProvisionedHost::generateHost) + .collect(Collectors.toList()); nodeRepository().nodes().addNodes(hosts, Agent.DynamicProvisioningMaintainer); return hosts; } catch (NodeAllocationException | IllegalArgumentException | IllegalStateException e) { @@ -293,7 +294,7 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { // build() requires a version, even though it is not (should not be) used .vespaVersion(Vtag.currentVersion) .build(); - NodeSpec nodeSpec = NodeSpec.from(clusterCapacity.count(), nodeResources, false, true); + NodeSpec nodeSpec = NodeSpec.from(clusterCapacity.count(), nodeResources, false, true, Optional.empty()); int wantedGroups = 1; NodePrioritizer prioritizer = new NodePrioritizer(nodesAndHosts, applicationId, clusterSpec, nodeSpec, wantedGroups, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java index fbc3d236421..36ca58e6ccd 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirer.java @@ -80,7 +80,7 @@ public class LoadBalancerExpirer extends NodeRepositoryMaintainer { try { attempts.add(1); log.log(Level.INFO, () -> "Removing expired inactive " + lb.id()); - service.remove(lb.id().application(), lb.id().cluster()); + service.remove(lb); db.removeLoadBalancer(lb.id()); } catch (Exception e){ failed.add(lb.id()); @@ -113,7 +113,7 @@ public class LoadBalancerExpirer extends NodeRepositoryMaintainer { try { attempts.add(1); LOG.log(Level.INFO, () -> "Removing reals from inactive load balancer " + lb.id() + ": " + Sets.difference(lb.instance().get().reals(), reals)); - service.create(new LoadBalancerSpec(lb.id().application(), lb.id().cluster(), reals), true); + service.create(new LoadBalancerSpec(lb.id().application(), lb.id().cluster(), reals, lb.instance().get().cloudAccount()), true); db.writeLoadBalancer(lb.with(lb.instance().map(instance -> instance.withReals(reals))), lb.state()); } catch (Exception e) { failed.add(lb.id()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java index 13dd458c041..b4e304155a6 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java @@ -208,7 +208,8 @@ public class CuratorDatabaseClient { toState.isAllocated() ? node.allocation() : Optional.empty(), node.history().recordStateTransition(node.state(), toState, agent, clock.instant()), node.type(), node.reports(), node.modelName(), node.reservedTo(), - node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname(), node.trustedCertificates()); + node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname(), + node.trustedCertificates(), node.cloudAccount()); writeNode(toState, curatorTransaction, node, newNode); writtenNodes.add(newNode); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java index d953ab5b543..83180b2b136 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.provision.persistence; import ai.vespa.http.DomainName; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; @@ -45,6 +46,7 @@ public class LoadBalancerSerializer { private static final String realsField = "reals"; private static final String ipAddressField = "ipAddress"; private static final String portField = "port"; + private static final String cloudAccountField = "cloudAccount"; public static byte[] toJson(LoadBalancer loadBalancer) { Slime slime = new Slime(); @@ -66,6 +68,7 @@ public class LoadBalancerSerializer { realObject.setString(ipAddressField, real.ipAddress()); realObject.setLong(portField, real.port()); })); + loadBalancer.instance().flatMap(LoadBalancerInstance::cloudAccount).ifPresent(cloudAccount -> root.setString(cloudAccountField, cloudAccount.value())); try { return SlimeUtils.toJsonBytes(slime); } catch (IOException e) { @@ -92,8 +95,9 @@ public class LoadBalancerSerializer { Optional<DomainName> hostname = optionalString(object.field(hostnameField), Function.identity()).filter(s -> !s.isEmpty()).map(DomainName::of); Optional<DnsZone> dnsZone = optionalString(object.field(dnsZoneField), DnsZone::new); + Optional<CloudAccount> cloudAccount = optionalString(object.field(cloudAccountField), CloudAccount::new); Optional<LoadBalancerInstance> instance = hostname.map(h -> new LoadBalancerInstance(h, dnsZone, ports, - networks, reals)); + networks, reals, cloudAccount)); return new LoadBalancer(LoadBalancerId.fromSerializedForm(object.field(idField).asString()), instance, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java index 7bbf5efba92..083b707b3c5 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java @@ -9,6 +9,7 @@ import com.google.common.util.concurrent.UncheckedExecutionException; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.DockerImage; @@ -25,7 +26,6 @@ import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; -import com.yahoo.slime.Type; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.node.Address; import com.yahoo.vespa.hosted.provision.node.Agent; @@ -99,6 +99,7 @@ public class NodeSerializer { private static final String exclusiveToClusterTypeKey = "exclusiveToClusterType"; private static final String switchHostnameKey = "switchHostname"; private static final String trustedCertificatesKey = "trustedCertificates"; + private static final String cloudAccountKey = "cloudAccount"; // Node resource fields private static final String flavorKey = "flavor"; @@ -192,6 +193,7 @@ public class NodeSerializer { node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString(exclusiveToApplicationIdKey, applicationId.serializedForm())); node.exclusiveToClusterType().ifPresent(clusterType -> object.setString(exclusiveToClusterTypeKey, clusterType.name())); trustedCertificatesToSlime(node.trustedCertificates(), object.setArray(trustedCertificatesKey)); + node.cloudAccount().ifPresent(cloudAccount -> object.setString(cloudAccountKey, cloudAccount.value())); } private void toSlime(Flavor flavor, Cursor object) { @@ -275,7 +277,7 @@ public class NodeSerializer { ipAddressesFromSlime(object, ipAddressPoolKey), addressesFromSlime(object)), object.field(hostnameKey).asString(), - parentHostnameFromSlime(object), + SlimeUtils.optionalString(object.field(parentHostnameKey)), flavor, statusFromSlime(object), state, @@ -283,12 +285,13 @@ public class NodeSerializer { historyFromSlime(object), nodeTypeFromString(object.field(nodeTypeKey).asString()), Reports.fromSlime(object.field(reportsKey)), - modelNameFromSlime(object), - reservedToFromSlime(object.field(reservedToKey)), - exclusiveToApplicationIdFromSlime(object.field(exclusiveToApplicationIdKey)), - exclusiveToClusterTypeFromSlime(object.field(exclusiveToClusterTypeKey)), - switchHostnameFromSlime(object.field(switchHostnameKey)), - trustedCertificatesFromSlime(object)); + SlimeUtils.optionalString(object.field(modelNameKey)), + SlimeUtils.optionalString(object.field(reservedToKey)).map(TenantName::from), + SlimeUtils.optionalString(object.field(exclusiveToApplicationIdKey)).map(ApplicationId::fromSerializedForm), + SlimeUtils.optionalString(object.field(exclusiveToClusterTypeKey)).map(ClusterSpec.Type::from), + SlimeUtils.optionalString(object.field(switchHostnameKey)), + trustedCertificatesFromSlime(object), + SlimeUtils.optionalString(object.field(cloudAccountKey)).map(CloudAccount::new)); } private Status statusFromSlime(Inspector object) { @@ -303,12 +306,7 @@ public class NodeSerializer { object.field(wantToFailKey).asBool(), new OsVersion(versionFromSlime(object.field(osVersionKey)), versionFromSlime(object.field(wantedOsVersionKey))), - instantFromSlime(object.field(firmwareCheckKey))); - } - - private Optional<String> switchHostnameFromSlime(Inspector field) { - if (!field.valid()) return Optional.empty(); - return Optional.of(field.asString()); + SlimeUtils.optionalInstant(object.field(firmwareCheckKey))); } private Flavor flavorFromSlime(Inspector object) { @@ -373,35 +371,15 @@ public class NodeSerializer { private ClusterMembership clusterMembershipFromSlime(Inspector object) { return ClusterMembership.from(object.field(serviceIdKey).asString(), versionFromSlime(object.field(wantedVespaVersionKey)).get(), - containerImageRepoFromSlime(object.field(wantedContainerImageRepoKey))); + containerImageFromSlime(object.field(wantedContainerImageRepoKey))); } private Optional<Version> versionFromSlime(Inspector object) { - if ( ! object.valid()) return Optional.empty(); - return Optional.of(Version.fromString(object.asString())); - } - - private Optional<DockerImage> containerImageRepoFromSlime(Inspector object) { - if ( ! object.valid() || object.asString().isEmpty()) return Optional.empty(); - return Optional.of(DockerImage.fromString(object.asString())); + return object.valid() ? Optional.of(Version.fromString(object.asString())) : Optional.empty(); } private Optional<DockerImage> containerImageFromSlime(Inspector object) { - if ( ! object.valid()) return Optional.empty(); - return Optional.of(DockerImage.fromString(object.asString())); - } - - private Optional<Instant> instantFromSlime(Inspector object) { - if ( ! object.valid()) - return Optional.empty(); - return Optional.of(Instant.ofEpochMilli(object.asLong())); - } - - private Optional<String> parentHostnameFromSlime(Inspector object) { - if (object.field(parentHostnameKey).valid()) - return Optional.of(object.field(parentHostnameKey).asString()); - else - return Optional.empty(); + return SlimeUtils.optionalString(object).map(DockerImage::fromString); } private Set<String> ipAddressesFromSlime(Inspector object, String key) { @@ -412,43 +390,15 @@ public class NodeSerializer { private List<Address> addressesFromSlime(Inspector object) { return SlimeUtils.entriesStream(object.field(containersKey)) - .map(elem -> new Address(elem.field(containerHostnameKey).asString())) - .collect(Collectors.toList()); - } - - private Optional<String> modelNameFromSlime(Inspector object) { - if (object.field(modelNameKey).valid()) { - return Optional.of(object.field(modelNameKey).asString()); - } - return Optional.empty(); - } - - private Optional<TenantName> reservedToFromSlime(Inspector object) { - if (! object.valid()) return Optional.empty(); - if (object.type() != Type.STRING) - throw new IllegalArgumentException("Expected 'reservedTo' to be a string but is " + object); - return Optional.of(TenantName.from(object.asString())); - } - - private Optional<ApplicationId> exclusiveToApplicationIdFromSlime(Inspector object) { - if (! object.valid()) return Optional.empty(); - if (object.type() != Type.STRING) - throw new IllegalArgumentException("Expected 'exclusiveTo' to be a string but is " + object); - return Optional.of(ApplicationId.fromSerializedForm(object.asString())); - } - - private Optional<ClusterSpec.Type> exclusiveToClusterTypeFromSlime(Inspector object) { - if (! object.valid()) return Optional.empty(); - if (object.type() != Type.STRING) - throw new IllegalArgumentException("Expected 'exclusiveToClusterType' to be a string but is " + object); - return Optional.of(ClusterSpec.Type.from(object.asString())); + .map(elem -> new Address(elem.field(containerHostnameKey).asString())) + .collect(Collectors.toList()); } private List<TrustStoreItem> trustedCertificatesFromSlime(Inspector object) { return SlimeUtils.entriesStream(object.field(trustedCertificatesKey)) - .map(elem -> new TrustStoreItem(elem.field(fingerprintKey).asString(), - Instant.ofEpochMilli(elem.field(expiresKey).asLong()))) - .collect(Collectors.toList()); + .map(elem -> new TrustStoreItem(elem.field(fingerprintKey).asString(), + Instant.ofEpochMilli(elem.field(expiresKey).asLong()))) + .collect(Collectors.toList()); } // ----------------- Enum <-> string mappings ---------------------------------------- diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java index 763fe1cab6f..16ee8281b9a 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java @@ -107,7 +107,8 @@ public class GroupPreparer { application, osVersion, sharing, - Optional.of(cluster.type()))) + Optional.of(cluster.type()), + requestedNodes.cloudAccount())) .orElseGet(List::of); // At this point we have started provisioning of the hosts, the first priority is to make sure that diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java index ce6ac9d5f5f..b849fccfaa5 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -41,6 +42,8 @@ public interface HostProvisioner { * @param osVersion the OS version to use. If this version does not exist, implementations may choose a suitable * fallback version. * @param sharing puts requirements on sharing or exclusivity of the host to be provisioned. + * @param clusterType provision host exclusively for this cluster type + * @param cloudAccount the cloud account to use * @return list of {@link ProvisionedHost} describing the provisioned nodes */ List<ProvisionedHost> provisionHosts(List<Integer> provisionIndices, @@ -49,7 +52,8 @@ public interface HostProvisioner { ApplicationId applicationId, Version osVersion, HostSharing sharing, - Optional<ClusterSpec.Type> clusterType); + Optional<ClusterSpec.Type> clusterType, + Optional<CloudAccount> cloudAccount); /** * Continue provisioning of given list of Nodes. diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java index f76968d67fd..a62edec52e4 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.ApplicationTransaction; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeType; @@ -86,7 +87,7 @@ public class LoadBalancerProvisioner { ClusterSpec.Id clusterId = effectiveId(cluster); LoadBalancerId loadBalancerId = requireNonClashing(new LoadBalancerId(application, clusterId)); NodeList nodes = nodesOf(clusterId, application); - prepare(loadBalancerId, nodes); + prepare(loadBalancerId, nodes, requestedNodes.cloudAccount()); } } @@ -177,10 +178,10 @@ public class LoadBalancerProvisioner { return loadBalancerId; } - private void prepare(LoadBalancerId id, NodeList nodes) { + private void prepare(LoadBalancerId id, NodeList nodes, Optional<CloudAccount> cloudAccount) { Instant now = nodeRepository.clock().instant(); Optional<LoadBalancer> loadBalancer = db.readLoadBalancer(id); - Optional<LoadBalancerInstance> instance = provisionInstance(id, nodes, loadBalancer); + Optional<LoadBalancerInstance> instance = provisionInstance(id, nodes, loadBalancer, cloudAccount); LoadBalancer newLoadBalancer; LoadBalancer.State fromState = null; if (loadBalancer.isEmpty()) { @@ -199,8 +200,9 @@ public class LoadBalancerProvisioner { LoadBalancerId id = new LoadBalancerId(transaction.application(), cluster); Optional<LoadBalancer> loadBalancer = db.readLoadBalancer(id); if (loadBalancer.isEmpty()) throw new IllegalArgumentException("Could not active load balancer that was never prepared: " + id); + if (loadBalancer.get().instance().isEmpty()) throw new IllegalArgumentException("Activating " + id + ", but prepare never provisioned a load balancer instance"); - Optional<LoadBalancerInstance> instance = provisionInstance(id, nodes, loadBalancer); + Optional<LoadBalancerInstance> instance = provisionInstance(id, nodes, loadBalancer, loadBalancer.get().instance().get().cloudAccount()); LoadBalancer.State state = instance.isPresent() ? LoadBalancer.State.active : loadBalancer.get().state(); LoadBalancer newLoadBalancer = loadBalancer.get().with(instance).with(state, now); db.writeLoadBalancers(List.of(newLoadBalancer), loadBalancer.get().state(), transaction.nested()); @@ -208,7 +210,9 @@ public class LoadBalancerProvisioner { } /** Provision or reconfigure a load balancer instance, if necessary */ - private Optional<LoadBalancerInstance> provisionInstance(LoadBalancerId id, NodeList nodes, Optional<LoadBalancer> currentLoadBalancer) { + private Optional<LoadBalancerInstance> provisionInstance(LoadBalancerId id, NodeList nodes, + Optional<LoadBalancer> currentLoadBalancer, + Optional<CloudAccount> cloudAccount) { boolean shouldDeactivateRouting = deactivateRouting.with(FetchVector.Dimension.APPLICATION_ID, id.application().serializedForm()) .value(); @@ -221,7 +225,7 @@ public class LoadBalancerProvisioner { if (hasReals(currentLoadBalancer, reals)) return currentLoadBalancer.get().instance(); log.log(Level.INFO, () -> "Provisioning instance for " + id + ", targeting: " + reals); try { - return Optional.of(service.create(new LoadBalancerSpec(id.application(), id.cluster(), reals), + return Optional.of(service.create(new LoadBalancerSpec(id.application(), id.cluster(), reals, cloudAccount), shouldDeactivateRouting || allowEmptyReals(currentLoadBalancer))); } catch (Exception e) { log.log(Level.WARNING, e, () -> "Could not (re)configure " + id + ", targeting: " + diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java index 5dce931427d..24743b47c8e 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java @@ -17,9 +17,7 @@ import com.yahoo.config.provision.ProvisionLogger; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.Zone; import com.yahoo.transaction.Mutex; -import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; -import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -107,7 +105,7 @@ public class NodeRepositoryProvisioner implements Provisioner { groups = target.groups(); resources = getNodeResources(cluster, target.nodeResources(), application); - nodeSpec = NodeSpec.from(target.nodes(), resources, exclusive, actual.canFail()); + nodeSpec = NodeSpec.from(target.nodes(), resources, exclusive, actual.canFail(), requested.cloudAccount()); } else { groups = 1; // type request with multiple groups is not supported diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java index b6c392c84db..cbd15a979d1 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.provisioning; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeFlavors; @@ -68,6 +69,9 @@ public interface NodeSpec { /** Returns true if nodes with non-active parent hosts should be rejected */ boolean rejectNonActiveParent(); + /** Returns the cloud account to use when fulfilling this spec or empty if none is explicitly requested */ + Optional<CloudAccount> cloudAccount(); + /** * Returns true if a node with given current resources and current spare host resources can be resized * in-place to resources in this spec. @@ -77,8 +81,8 @@ public interface NodeSpec { return false; } - static NodeSpec from(int nodeCount, NodeResources resources, boolean exclusive, boolean canFail) { - return new CountNodeSpec(nodeCount, resources, exclusive, canFail); + static NodeSpec from(int nodeCount, NodeResources resources, boolean exclusive, boolean canFail, Optional<CloudAccount> cloudAccount) { + return new CountNodeSpec(nodeCount, resources, exclusive, canFail, cloudAccount); } static NodeSpec from(NodeType type) { @@ -92,12 +96,14 @@ public interface NodeSpec { private final NodeResources requestedNodeResources; private final boolean exclusive; private final boolean canFail; + private final Optional<CloudAccount> cloudAccount; - private CountNodeSpec(int count, NodeResources resources, boolean exclusive, boolean canFail) { + private CountNodeSpec(int count, NodeResources resources, boolean exclusive, boolean canFail, Optional<CloudAccount> cloudAccount) { this.count = count; this.requestedNodeResources = Objects.requireNonNull(resources, "Resources must be specified"); - this.exclusive = exclusive; + this.exclusive = exclusive || cloudAccount.isPresent(); // Implicitly exclusive if using a custom cloud account this.canFail = canFail; + this.cloudAccount = Objects.requireNonNull(cloudAccount); } @Override @@ -144,7 +150,7 @@ public interface NodeSpec { @Override public NodeSpec fraction(int divisor) { - return new CountNodeSpec(count/divisor, requestedNodeResources, exclusive, canFail); + return new CountNodeSpec(count/divisor, requestedNodeResources, exclusive, canFail, cloudAccount); } @Override @@ -175,6 +181,11 @@ public interface NodeSpec { } @Override + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + + @Override public String toString() { return "request for " + count + " nodes with " + requestedNodeResources; } } @@ -243,6 +254,11 @@ public interface NodeSpec { } @Override + public Optional<CloudAccount> cloudAccount() { + return Optional.empty(); // Type spec does not support custom cloud accounts + } + + @Override public String toString() { return "request for all nodes of type '" + type + "'"; } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java index dbc92b1dbdf..ef6c0da9169 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java @@ -76,8 +76,8 @@ class Preparer { if (requestedNodes.rejectNonActiveParent()) { NodeList activeHosts = allNodesAndHosts.nodes().state(Node.State.active).parents().nodeType(requestedNodes.type().hostType()); accepted = accepted.stream() - .filter(node -> node.parentHostname().isEmpty() || activeHosts.parentOf(node).isPresent()) - .collect(Collectors.toList()); + .filter(node -> node.parentHostname().isEmpty() || activeHosts.parentOf(node).isPresent()) + .collect(Collectors.toList()); } replace(acceptedNodes, accepted); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java index b5fd8c8111f..65071ad848d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; @@ -34,10 +35,11 @@ public class ProvisionedHost { private final List<Address> nodeAddresses; private final NodeResources nodeResources; private final Version osVersion; + private final Optional<CloudAccount> cloudAccount; public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType, Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, - List<Address> nodeAddresses, NodeResources nodeResources, Version osVersion) { + List<Address> nodeAddresses, NodeResources nodeResources, Version osVersion, Optional<CloudAccount> cloudAccount) { this.id = Objects.requireNonNull(id, "Host id must be set"); this.hostHostname = Objects.requireNonNull(hostHostname, "Host hostname must be set"); this.hostFlavor = Objects.requireNonNull(hostFlavor, "Host flavor must be set"); @@ -47,6 +49,7 @@ public class ProvisionedHost { this.nodeAddresses = validateNodeAddresses(nodeAddresses); this.nodeResources = Objects.requireNonNull(nodeResources, "Node resources must be set"); this.osVersion = Objects.requireNonNull(osVersion, "OS version must be set"); + this.cloudAccount = Objects.requireNonNull(cloudAccount, "Cloud account must be set"); if (!hostType.isHost()) throw new IllegalArgumentException(hostType + " is not a host"); } @@ -60,11 +63,12 @@ public class ProvisionedHost { /** Generate {@link Node} instance representing the provisioned physical host */ public Node generateHost() { - Node.Builder builder = Node - .create(id, IP.Config.of(Set.of(), Set.of(), nodeAddresses), hostHostname, hostFlavor, hostType) - .status(Status.initial().withOsVersion(OsVersion.EMPTY.withCurrent(Optional.of(osVersion)))); + Node.Builder builder = Node.create(id, IP.Config.of(Set.of(), Set.of(), nodeAddresses), hostHostname, hostFlavor, + hostType) + .status(Status.initial().withOsVersion(OsVersion.EMPTY.withCurrent(Optional.of(osVersion)))); exclusiveToApplicationId.ifPresent(builder::exclusiveToApplicationId); exclusiveToClusterType.ifPresent(builder::exclusiveToClusterType); + cloudAccount.ifPresent(builder::cloudAccount); return builder.build(); } @@ -82,6 +86,7 @@ public class ProvisionedHost { public List<Address> nodeAddresses() { return nodeAddresses; } public NodeResources nodeResources() { return nodeResources; } public Version osVersion() { return osVersion; } + public Optional<CloudAccount> cloudAccount() { return cloudAccount; } public String nodeHostname() { return nodeAddresses.get(0).hostname(); } @@ -98,12 +103,13 @@ public class ProvisionedHost { exclusiveToClusterType.equals(that.exclusiveToClusterType) && nodeAddresses.equals(that.nodeAddresses) && nodeResources.equals(that.nodeResources) && - osVersion.equals(that.osVersion); + osVersion.equals(that.osVersion) && + cloudAccount.equals(that.cloudAccount); } @Override public int hashCode() { - return Objects.hash(id, hostHostname, hostFlavor, hostType, exclusiveToApplicationId, exclusiveToClusterType, nodeAddresses, nodeResources, osVersion); + return Objects.hash(id, hostHostname, hostFlavor, hostType, exclusiveToApplicationId, exclusiveToClusterType, nodeAddresses, nodeResources, osVersion, cloudAccount); } @Override @@ -118,6 +124,7 @@ public class ProvisionedHost { ", nodeAddresses=" + nodeAddresses + ", nodeResources=" + nodeResources + ", osVersion=" + osVersion + + ", cloudAccount=" + cloudAccount + '}'; } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java index f5b0c791d0a..7686a9a4885 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java @@ -3,33 +3,30 @@ package com.yahoo.vespa.hosted.provision.restapi; import com.yahoo.config.provision.ApplicationId; import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; -import com.yahoo.slime.JsonFormat; -import com.yahoo.slime.Slime; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.lb.LoadBalancer; import com.yahoo.vespa.hosted.provision.lb.LoadBalancerInstance; import com.yahoo.vespa.hosted.provision.lb.LoadBalancerList; import com.yahoo.vespa.hosted.provision.node.filter.ApplicationFilter; -import java.io.IOException; -import java.io.OutputStream; import java.util.List; import java.util.Optional; /** * @author mpolden */ -public class LoadBalancersResponse extends HttpResponse { +public class LoadBalancersResponse extends SlimeJsonResponse { private final NodeRepository nodeRepository; private final HttpRequest request; public LoadBalancersResponse(HttpRequest request, NodeRepository nodeRepository) { - super(200); this.request = request; this.nodeRepository = nodeRepository; + Cursor root = slime.setObject(); + toSlime(loadBalancers(), root); } private Optional<ApplicationId> application() { @@ -48,16 +45,9 @@ public class LoadBalancersResponse extends HttpResponse { return loadBalancers.asList(); } - @Override - public String getContentType() { return "application/json"; } - - @Override - public void render(OutputStream stream) throws IOException { - Slime slime = new Slime(); - Cursor root = slime.setObject(); - Cursor loadBalancerArray = root.setArray("loadBalancers"); - - loadBalancers().forEach(lb -> { + private void toSlime(List<LoadBalancer> loadBalancers, Cursor object) { + Cursor loadBalancerArray = object.setArray("loadBalancers"); + loadBalancers.forEach(lb -> { Cursor lbObject = loadBalancerArray.addObject(); lbObject.setString("id", lb.id().serializedForm()); lbObject.setString("state", lb.state().name()); @@ -84,9 +74,10 @@ public class LoadBalancersResponse extends HttpResponse { realObject.setLong("port", real.port()); }); }); + lb.instance() + .flatMap(LoadBalancerInstance::cloudAccount) + .ifPresent(cloudAccount -> lbObject.setString("cloudAccount", cloudAccount.value())); }); - - new JsonFormat(true).encode(stream, slime); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java index 729ea97d627..3659166c9da 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java @@ -183,6 +183,7 @@ class NodesResponse extends SlimeJsonResponse { node.switchHostname().ifPresent(switchHostname -> object.setString("switchHostname", switchHostname)); nodeRepository.archiveUris().archiveUriFor(node).ifPresent(uri -> object.setString("archiveUri", uri)); trustedCertsToSlime(node.trustedCertificates(), object); + node.cloudAccount().ifPresent(cloudAccount -> object.setString("cloudAccount", cloudAccount.value())); } private void toSlime(ApplicationId id, Cursor object) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java index 0a125c5fb95..c09376ff103 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.testutils; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; @@ -56,7 +57,8 @@ public class MockHostProvisioner implements HostProvisioner { @Override public List<ProvisionedHost> provisionHosts(List<Integer> provisionIndices, NodeType hostType, NodeResources resources, ApplicationId applicationId, Version osVersion, HostSharing sharing, - Optional<ClusterSpec.Type> clusterType) { + Optional<ClusterSpec.Type> clusterType, + Optional<CloudAccount> cloudAccount) { Flavor hostFlavor = this.hostFlavor.orElseGet(() -> flavors.stream().filter(f -> compatible(f, resources)) .findFirst() .orElseThrow(() -> new NodeAllocationException("No host flavor matches " + resources))); @@ -67,11 +69,12 @@ public class MockHostProvisioner implements HostProvisioner { hostHostname, hostFlavor, hostType, - Optional.empty(), + sharing == HostSharing.exclusive ? Optional.of(applicationId) : Optional.empty(), Optional.empty(), createAddressesForHost(hostType, hostFlavor, index), resources, - osVersion)); + osVersion, + cloudAccount)); } provisionedHosts.addAll(hosts); return hosts; diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java index d54b41bfcdd..825e46865fe 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java @@ -26,7 +26,7 @@ public class SharedLoadBalancerServiceTest { @Test public void test_create_lb() { - var lb = loadBalancerService.create(new LoadBalancerSpec(applicationId, clusterId, reals), false); + var lb = loadBalancerService.create(new LoadBalancerSpec(applicationId, clusterId, reals, Optional.empty()), false); assertEquals(HostName.of("vip.example.com"), lb.hostname()); assertEquals(Optional.empty(), lb.dnsZone()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java index 64cee87d942..30d0f673fe1 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java @@ -5,10 +5,13 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.Cloud; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterResources; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.Flavor; +import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -542,6 +545,35 @@ public class DynamicProvisioningMaintainerTest { assertEquals(nodesBefore, nodesAfter); } + @Test + public void custom_cloud_account() { + DynamicProvisioningTester tester = new DynamicProvisioningTester(Cloud.builder().dynamicProvisioning(true).build(), + new MockNameResolver().mockAnyLookup()); + ProvisioningTester provisioningTester = tester.provisioningTester; + ApplicationId applicationId = ApplicationId.from("t1", "a1", "i1"); + + // Deployment requests capacity in custom account + ClusterSpec spec = ProvisioningTester.contentClusterSpec(); + ClusterResources resources = new ClusterResources(2, 1, new NodeResources(16, 24, 100, 1)); + CloudAccount cloudAccount = new CloudAccount("012345678912"); + Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount)); + List<HostSpec> prepared = provisioningTester.prepare(applicationId, spec, capacity); + + // Hosts are provisioned in requested account + tester.maintainer.maintain(); + List<ProvisionedHost> newHosts = tester.hostProvisioner.provisionedHosts(); + assertEquals(2, newHosts.size()); + assertTrue(newHosts.stream().allMatch(host -> host.cloudAccount().get().equals(cloudAccount))); + for (var host : newHosts) { + provisioningTester.nodeRepository().nodes().setReady(host.hostHostname(), Agent.operator, getClass().getSimpleName()); + } + provisioningTester.prepareAndActivateInfraApplication(DynamicProvisioningTester.tenantHostApp, NodeType.host); + NodeList activeHosts = provisioningTester.nodeRepository().nodes().list(Node.State.active).nodeType(NodeType.host); + assertEquals(2, activeHosts.size()); + assertTrue(activeHosts.stream().allMatch(host -> host.cloudAccount().get().equals(cloudAccount))); + assertEquals(2, provisioningTester.activate(applicationId, prepared).size()); + } + private void assertCfghost3IsActive(DynamicProvisioningTester tester) { assertEquals(5, tester.nodeRepository.nodes().list(Node.State.active).size()); assertEquals(3, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.confighost).size()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java index 1eecb442dc0..ce8fc2e9d03 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.persistence; import ai.vespa.http.DomainName; import com.google.common.collect.ImmutableSet; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.vespa.hosted.provision.lb.DnsZone; import com.yahoo.vespa.hosted.provision.lb.LoadBalancer; @@ -40,7 +41,8 @@ public class LoadBalancerSerializerTest { 4080), new Real(DomainName.of("real-2"), "127.0.0.2", - 4080)))), + 4080)), + Optional.of(new CloudAccount("012345678912")))), LoadBalancer.State.active, now); @@ -53,6 +55,7 @@ public class LoadBalancerSerializerTest { assertEquals(loadBalancer.state(), serialized.state()); assertEquals(loadBalancer.changedAt().truncatedTo(MILLIS), serialized.changedAt()); assertEquals(loadBalancer.instance().get().reals(), serialized.instance().get().reals()); + assertEquals(loadBalancer.instance().get().cloudAccount(), serialized.instance().get().cloudAccount()); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java index 67b9569d8bf..bc7d3104a2f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.component.Vtag; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.InstanceName; @@ -488,6 +489,17 @@ public class NodeSerializerTest { assertEquals(trustStoreItems, node.trustedCertificates()); } + @Test + public void cloud_account_serialization() { + CloudAccount account = new CloudAccount("012345678912"); + Node node = Node.create("id", "host1.example.com", nodeFlavors.getFlavorOrThrow("default"), State.provisioned, NodeType.host) + .cloudAccount(account) + .exclusiveToApplicationId(ApplicationId.defaultId()) + .build(); + node = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(node)); + assertEquals(account, node.cloudAccount().get()); + } + private byte[] createNodeJson(String hostname, String... ipAddress) { String ipAddressJsonPart = ""; if (ipAddress.length > 0) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java index 97b664b6ba4..436cf880f1c 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java @@ -73,7 +73,7 @@ public class DynamicProvisioningTest { mockHostProvisioner(hostProvisioner, "large", 3, null); // Provision shared hosts prepareAndActivate(application1, clusterSpec("mycluster"), 4, 1, resources); verify(hostProvisioner).provisionHosts(List.of(100, 101, 102, 103), NodeType.host, resources, application1, - Version.emptyVersion, HostSharing.any, Optional.of(ClusterSpec.Type.content)); + Version.emptyVersion, HostSharing.any, Optional.of(ClusterSpec.Type.content), Optional.empty()); // Total of 8 nodes should now be in node-repo, 4 active hosts and 4 active nodes assertEquals(8, tester.nodeRepository().nodes().list().size()); @@ -97,7 +97,7 @@ public class DynamicProvisioningTest { mockHostProvisioner(hostProvisioner, "large", 3, application3); prepareAndActivate(application3, clusterSpec("mycluster", true), 4, 1, resources); verify(hostProvisioner).provisionHosts(List.of(104, 105, 106, 107), NodeType.host, resources, application3, - Version.emptyVersion, HostSharing.exclusive, Optional.of(ClusterSpec.Type.content)); + Version.emptyVersion, HostSharing.exclusive, Optional.of(ClusterSpec.Type.content), Optional.empty()); // Total of 20 nodes should now be in node-repo, 8 active hosts and 12 active nodes assertEquals(20, tester.nodeRepository().nodes().list().size()); @@ -477,7 +477,7 @@ public class DynamicProvisioningTest { return provisionedHost; }) .collect(Collectors.toList()); - }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any(), any()); + }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any(), any(), any()); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java index 08058ef7b04..15e07f92292 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java @@ -5,6 +5,7 @@ import ai.vespa.http.DomainName; import com.google.common.collect.Iterators; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostSpec; @@ -17,6 +18,7 @@ import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.lb.LoadBalancer; +import com.yahoo.vespa.hosted.provision.lb.LoadBalancerList; import com.yahoo.vespa.hosted.provision.lb.Real; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.IP; @@ -308,6 +310,17 @@ public class LoadBalancerProvisionerTest { assertReals(app1, container1, Node.State.active); } + @Test + public void load_balancer_with_custom_cloud_account() { + ClusterResources resources = new ClusterResources(3, 1, nodeResources); + CloudAccount cloudAccount = new CloudAccount("012345678912"); + Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount)); + tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1")))); + LoadBalancerList loadBalancers = tester.nodeRepository().loadBalancers().list(); + assertEquals(1, loadBalancers.size()); + assertEquals(cloudAccount, loadBalancers.first().get().instance().get().cloudAccount().get()); + } + private void assertReals(ApplicationId application, ClusterSpec.Id cluster, Node.State... states) { List<LoadBalancer> loadBalancers = tester.nodeRepository().loadBalancers().list(application).cluster(cluster).asList(); assertEquals(1, loadBalancers.size()); |