diff options
author | Håkon Hallingstad <hakon@yahooinc.com> | 2022-08-18 16:57:55 +0200 |
---|---|---|
committer | Håkon Hallingstad <hakon@yahooinc.com> | 2022-08-18 16:57:55 +0200 |
commit | b679206de2431f11e52e6734abfcaefa40554037 (patch) | |
tree | bde9c884a71f552165d5554596c48bcd3f63fdf1 | |
parent | 7a65553cd4efc3574cb1bd859b17008ecdf878d6 (diff) |
Allow overriding wanted docker tag and vespa version
7 files changed, 108 insertions, 9 deletions
diff --git a/component/abi-spec.json b/component/abi-spec.json index d990a9077b4..cfbbdf4b306 100644 --- a/component/abi-spec.json +++ b/component/abi-spec.json @@ -138,6 +138,7 @@ "public void <init>(java.lang.String)", "public void <init>(com.yahoo.text.Utf8Array)", "public static com.yahoo.component.Version fromString(java.lang.String)", + "public com.yahoo.component.Version withQualifier(java.lang.String)", "public java.lang.String toFullString()", "public int getMajor()", "public int getMinor()", diff --git a/component/src/main/java/com/yahoo/component/Version.java b/component/src/main/java/com/yahoo/component/Version.java index 1d4546c0c58..db8606d31fa 100644 --- a/component/src/main/java/com/yahoo/component/Version.java +++ b/component/src/main/java/com/yahoo/component/Version.java @@ -202,6 +202,12 @@ public final class Version implements Comparable<Version> { return (versionString == null) ? emptyVersion :new Version(versionString); } + public Version withQualifier(String qualifier) { + if (qualifier.indexOf('.') != -1) + throw new IllegalArgumentException("Qualifier cannot contain '.'"); + return new Version(major, minor, micro, qualifier); + } + /** * Must be called on construction after the component values are set * diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java index 9f40013f627..c1877373ce2 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java @@ -20,7 +20,7 @@ public class FetchVector { * Note: If this enum is changed, you must also change {@link DimensionHelper}. */ public enum Dimension { - /** A legal value for TenantName, e.g. vespa-team */ + /** Value from TenantName::value, e.g. vespa-team */ TENANT_ID, /** Value from ApplicationId::serializedForm of the form tenant:applicationName:instance. */ @@ -29,7 +29,7 @@ public class FetchVector { /** Node type from com.yahoo.config.provision.NodeType::name, e.g. tenant, host, confighost, controller, etc. */ NODE_TYPE, - /** Cluster type from com.yahoo.config.provision.ClusterSpec.Type::value, e.g. content, container, admin */ + /** Cluster type from com.yahoo.config.provision.ClusterSpec.Type::name, e.g. content, container, admin */ CLUSTER_TYPE, /** Cluster ID from com.yahoo.config.provision.ClusterSpec.Id::value, e.g. cluster-controllers, logserver. */ diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index e349165b855..9552e1a961d 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -11,6 +11,7 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; import java.util.TreeMap; +import java.util.function.Predicate; import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID; import static com.yahoo.vespa.flags.FetchVector.Dimension.CONSOLE_USER_EMAIL; @@ -490,7 +491,19 @@ public class Flags { public static UnboundStringFlag defineStringFlag(String flagId, String defaultValue, List<String> owners, String createdAt, String expiresAt, String description, String modificationEffect, FetchVector.Dimension... dimensions) { - return define(UnboundStringFlag::new, flagId, defaultValue, owners, createdAt, expiresAt, description, modificationEffect, dimensions); + return defineStringFlag(flagId, defaultValue, owners, + createdAt, expiresAt, description, + modificationEffect, value -> true, + dimensions); + } + + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ + public static UnboundStringFlag defineStringFlag(String flagId, String defaultValue, List<String> owners, + String createdAt, String expiresAt, String description, + String modificationEffect, Predicate<String> validator, + FetchVector.Dimension... dimensions) { + return define((i, d, v) -> new UnboundStringFlag(i, d, v, validator), + flagId, defaultValue, owners, createdAt, expiresAt, description, modificationEffect, dimensions); } /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ @@ -532,7 +545,7 @@ public class Flags { @FunctionalInterface private interface TypedUnboundFlagFactory<T, U extends UnboundFlag<?, ?, ?>> { - U create(FlagId id, T defaultVale, FetchVector defaultFetchVector); + U create(FlagId id, T defaultValue, FetchVector defaultFetchVector); } /** diff --git a/flags/src/main/java/com/yahoo/vespa/flags/PermanentFlags.java b/flags/src/main/java/com/yahoo/vespa/flags/PermanentFlags.java index b5a292a554d..0003d5e42c7 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/PermanentFlags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/PermanentFlags.java @@ -9,6 +9,8 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; import static com.yahoo.vespa.flags.FetchVector.Dimension.APPLICATION_ID; import static com.yahoo.vespa.flags.FetchVector.Dimension.CLUSTER_ID; @@ -17,6 +19,7 @@ import static com.yahoo.vespa.flags.FetchVector.Dimension.CONSOLE_USER_EMAIL; import static com.yahoo.vespa.flags.FetchVector.Dimension.HOSTNAME; import static com.yahoo.vespa.flags.FetchVector.Dimension.NODE_TYPE; import static com.yahoo.vespa.flags.FetchVector.Dimension.TENANT_ID; +import static com.yahoo.vespa.flags.FetchVector.Dimension.VESPA_VERSION; import static com.yahoo.vespa.flags.FetchVector.Dimension.ZONE_ID; /** @@ -113,6 +116,28 @@ public class PermanentFlags { "Takes effect on next deployment from controller", ZONE_ID, APPLICATION_ID); + private static final String VERSION_QUALIFIER_REGEX = "[a-zA-Z0-9_-]+"; + private static final Pattern QUALIFIER_PATTERN = Pattern.compile("^" + VERSION_QUALIFIER_REGEX + "$"); + private static final Pattern VERSION_PATTERN = Pattern.compile("^\\d\\.\\d\\.\\d(\\." + VERSION_QUALIFIER_REGEX + ")?$"); + + public static final UnboundStringFlag WANTED_DOCKER_TAG = defineStringFlag( + "wanted-docker-tag", "", + "If non-empty the flag value overrides the docker image tag of the wantedDockerImage of the node object. " + + "If the flag value contains '.', it must specify a valid Vespa version like '8.83.42'. " + + "Otherwise a '.' + the flag value will be appended.", + "Takes effect on the next host admin tick. The upgrade to the new wanted docker image is orchestrated.", + value -> value.isEmpty() || QUALIFIER_PATTERN.matcher(value).find() || VERSION_PATTERN.matcher(value).find(), + HOSTNAME, NODE_TYPE, TENANT_ID, APPLICATION_ID, CLUSTER_TYPE, CLUSTER_ID, VESPA_VERSION); + + public static final UnboundStringFlag WANTED_VESPA_VERSION = defineStringFlag( + "wanted-vespa-version", "", + "If non-empty the flag value overrides the wantedVespaVersion of the node object." + + "If the flag value contains '.', it must specify a valid Vespa version like '8.83.42'. " + + "Otherwise a '.' + the flag value will be appended.", + "Takes effect on the next host admin tick. The upgrade to the new wanted docker image is orchestrated.", + value -> value.isEmpty() || QUALIFIER_PATTERN.matcher(value).find() || VERSION_PATTERN.matcher(value).find(), + HOSTNAME, NODE_TYPE, TENANT_ID, APPLICATION_ID, CLUSTER_TYPE, CLUSTER_ID, VESPA_VERSION); + public static final UnboundStringFlag ZOOKEEPER_SERVER_VERSION = defineStringFlag( "zookeeper-server-version", "3.7.1", // Note: Nodes running Vespa 7 have 3.7.1 as the only available version "ZooKeeper server version, a jar file zookeeper-server-<ZOOKEEPER_SERVER_VERSION>-jar-with-dependencies.jar must exist", @@ -280,6 +305,11 @@ public class PermanentFlags { return Flags.defineStringFlag(flagId, defaultValue, OWNERS, toString(CREATED_AT), toString(EXPIRES_AT), description, modificationEffect, dimensions); } + private static UnboundStringFlag defineStringFlag( + String flagId, String defaultValue, String description, String modificationEffect, Predicate<String> validator, FetchVector.Dimension... dimensions) { + return Flags.defineStringFlag(flagId, defaultValue, OWNERS, toString(CREATED_AT), toString(EXPIRES_AT), description, modificationEffect, validator, dimensions); + } + private static UnboundIntFlag defineIntFlag( String flagId, int defaultValue, String description, String modificationEffect, FetchVector.Dimension... dimensions) { return Flags.defineIntFlag(flagId, defaultValue, OWNERS, toString(CREATED_AT), toString(EXPIRES_AT), description, modificationEffect, dimensions); diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java index f96be55e2eb..9c69e917fa6 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundStringFlag.java @@ -4,6 +4,10 @@ package com.yahoo.vespa.flags; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + /** * @author hakonhall */ @@ -13,8 +17,26 @@ public class UnboundStringFlag extends UnboundFlagImpl<String, StringFlag, Unbou } public UnboundStringFlag(FlagId id, String defaultValue, FetchVector defaultFetchVector) { - super(id, defaultValue, defaultFetchVector, - new SimpleFlagSerializer<>(TextNode::new, JsonNode::isTextual, JsonNode::asText), - UnboundStringFlag::new, StringFlag::new); + this(id, defaultValue, defaultFetchVector, + new SimpleFlagSerializer<>(TextNode::new, JsonNode::isTextual, JsonNode::asText)); + } + + public UnboundStringFlag(FlagId id, String defaultValue, Predicate<String> validator) { + this(id, defaultValue, new FetchVector(), validator); + } + + public UnboundStringFlag(FlagId id, String defaultValue, FetchVector fetchVector, Predicate<String> validator) { + this(id, defaultValue, fetchVector, + new SimpleFlagSerializer<>(stringValue -> { + if (!validator.test(stringValue)) + throw new IllegalArgumentException("Invalid value: '" + stringValue + "'"); + return new TextNode(stringValue); + }, + JsonNode::isTextual, JsonNode::asText)); + } + + public UnboundStringFlag(FlagId id, String defaultValue, FetchVector defaultFetchVector, + FlagSerializer<String> serializer) { + super(id, defaultValue, defaultFetchVector, serializer, UnboundStringFlag::new, StringFlag::new); } } 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 87a9735f91e..2ecf73a645b 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 @@ -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.restapi; +import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.DockerImage; @@ -10,10 +11,14 @@ import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.PermanentFlags; +import com.yahoo.vespa.flags.StringFlag; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Address; +import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.History; import com.yahoo.vespa.hosted.provision.node.TrustStoreItem; import com.yahoo.vespa.hosted.provision.node.filter.NodeFilter; @@ -47,6 +52,8 @@ class NodesResponse extends SlimeJsonResponse { private final boolean recursive; private final Function<HostName, Optional<HostInfo>> orchestrator; private final NodeRepository nodeRepository; + private final StringFlag wantedDockerTagFlag; + private final StringFlag wantedVespaVersionFlag; public NodesResponse(ResponseType responseType, HttpRequest request, Orchestrator orchestrator, NodeRepository nodeRepository) { @@ -56,6 +63,8 @@ class NodesResponse extends SlimeJsonResponse { this.recursive = request.getBooleanProperty("recursive"); this.orchestrator = orchestrator.getHostResolver(); this.nodeRepository = nodeRepository; + this.wantedDockerTagFlag = PermanentFlags.WANTED_DOCKER_TAG.bindTo(nodeRepository.flagSource()); + this.wantedVespaVersionFlag = PermanentFlags.WANTED_VESPA_VERSION.bindTo(nodeRepository.flagSource()); Cursor root = slime.setObject(); switch (responseType) { @@ -146,8 +155,8 @@ class NodesResponse extends SlimeJsonResponse { toSlime(allocation.membership(), object.setObject("membership")); object.setLong("restartGeneration", allocation.restartGeneration().wanted()); object.setLong("currentRestartGeneration", allocation.restartGeneration().current()); - object.setString("wantedDockerImage", nodeRepository.containerImages().get(node).withTag(allocation.membership().cluster().vespaVersion()).asString()); - object.setString("wantedVespaVersion", allocation.membership().cluster().vespaVersion().toFullString()); + object.setString("wantedDockerImage", nodeRepository.containerImages().get(node).withTag(resolveVersionFlag(wantedDockerTagFlag, node, allocation)).asString()); + object.setString("wantedVespaVersion", resolveVersionFlag(wantedVespaVersionFlag, node, allocation).toFullString()); NodeResourcesSerializer.toSlime(allocation.requestedResources(), object.setObject("requestedResources")); allocation.networkPorts().ifPresent(ports -> NetworkPortsSerializer.toSlime(ports, object.setArray("networkPorts"))); orchestrator.apply(new HostName(node.hostname())) @@ -189,6 +198,24 @@ class NodesResponse extends SlimeJsonResponse { node.cloudAccount().ifPresent(cloudAccount -> object.setString("cloudAccount", cloudAccount.value())); } + private Version resolveVersionFlag(StringFlag flag, Node node, Allocation allocation) { + String value = flag + .with(FetchVector.Dimension.HOSTNAME, node.hostname()) + .with(FetchVector.Dimension.NODE_TYPE, node.type().name()) + .with(FetchVector.Dimension.TENANT_ID, allocation.owner().tenant().value()) + .with(FetchVector.Dimension.APPLICATION_ID, allocation.owner().serializedForm()) + .with(FetchVector.Dimension.CLUSTER_TYPE, allocation.membership().cluster().type().name()) + .with(FetchVector.Dimension.CLUSTER_ID, allocation.membership().cluster().id().value()) + .with(FetchVector.Dimension.VESPA_VERSION, allocation.membership().cluster().vespaVersion().toFullString()) + .value(); + + return value.isEmpty() ? + allocation.membership().cluster().vespaVersion() : + value.indexOf('.') == -1 ? + allocation.membership().cluster().vespaVersion().withQualifier(value) : + new Version(value); + } + private void toSlime(ApplicationId id, Cursor object) { object.setString("tenant", id.tenant().value()); object.setString("application", id.application().value()); |