diff options
8 files changed, 191 insertions, 8 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/Rotations.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/Rotations.java new file mode 100644 index 00000000000..429c80f1b2c --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/Rotations.java @@ -0,0 +1,50 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.builder.xml.dom; + +import com.yahoo.config.provision.RotationName; +import com.yahoo.text.XML; +import org.w3c.dom.Element; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +/** + * Read rotations from the <rotations/> element in services.xml. + * + * @author mpolden + */ +public class Rotations { + + /* + * Rotation names must be: + * - lowercase + * - alphanumeric + * - begin with a character + * - have a length between 1 and 12 + */ + private static final Pattern pattern = Pattern.compile("^[a-z][a-z0-9-]{0,11}$"); + + private Rotations() {} + + /** Set the rotations the given cluster should be member of */ + public static Set<RotationName> from(Element rotationsElement) { + Set<RotationName> rotations = new TreeSet<>(); + List<Element> children = XML.getChildren(rotationsElement, "rotation"); + for (Element el : children) { + String name = el.getAttribute("id"); + if (name == null || !pattern.matcher(name).find()) { + throw new IllegalArgumentException("Rotation ID '" + name + "' is missing or has invalid format"); + } + RotationName rotation = RotationName.from(name); + if (rotations.contains(rotation)) { + throw new IllegalArgumentException("Rotation ID '" + name + "' is duplicated"); + } + rotations.add(rotation); + } + return Collections.unmodifiableSet(rotations); + } + +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index bb35b66edef..ab15005ca73 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -22,6 +22,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.RotationName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.Zone; import com.yahoo.search.rendering.RendererRegistry; @@ -33,10 +34,10 @@ import com.yahoo.vespa.model.HostResource; import com.yahoo.vespa.model.HostSystem; import com.yahoo.vespa.model.builder.xml.dom.DomClientProviderBuilder; import com.yahoo.vespa.model.builder.xml.dom.DomComponentBuilder; -import com.yahoo.vespa.model.builder.xml.dom.DomFilterBuilder; import com.yahoo.vespa.model.builder.xml.dom.DomHandlerBuilder; import com.yahoo.vespa.model.builder.xml.dom.ModelElement; import com.yahoo.vespa.model.builder.xml.dom.NodesSpecification; +import com.yahoo.vespa.model.builder.xml.dom.Rotations; import com.yahoo.vespa.model.builder.xml.dom.ServletBuilder; import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder; import com.yahoo.vespa.model.builder.xml.dom.chains.docproc.DomDocprocChainsBuilder; @@ -465,6 +466,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { } private void addNodesFromXml(ContainerCluster cluster, Element containerElement, ConfigModelContext context) { Element nodesElement = XML.getChild(containerElement, "nodes"); + Element rotationsElement = XML.getChild(containerElement, "rotations"); if (nodesElement == null) { // default single node on localhost Container node = new Container(cluster, "container.0", 0, cluster.isHostedVespa()); HostResource host = allocateSingleNodeHost(cluster, log, containerElement, context); @@ -472,7 +474,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { node.initService(context.getDeployLogger()); cluster.addContainers(Collections.singleton(node)); } else { - List<Container> nodes = createNodes(cluster, nodesElement, context); + List<Container> nodes = createNodes(cluster, nodesElement, rotationsElement, context); applyNodesTagJvmArgs(nodes, getJvmOptions(cluster, nodesElement, context.getDeployLogger())); if ( !cluster.getJvmGCOptions().isPresent()) { @@ -506,9 +508,9 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { return sb.toString(); } - private List<Container> createNodes(ContainerCluster cluster, Element nodesElement, ConfigModelContext context) { + private List<Container> createNodes(ContainerCluster cluster, Element nodesElement, Element rotationsElement, ConfigModelContext context) { if (nodesElement.hasAttribute("count")) // regular, hosted node spec - return createNodesFromNodeCount(cluster, nodesElement, context); + return createNodesFromNodeCount(cluster, nodesElement, rotationsElement, context); else if (nodesElement.hasAttribute("type")) // internal use for hosted system infrastructure nodes return createNodesFromNodeType(cluster, nodesElement, context); else if (nodesElement.hasAttribute("of")) // hosted node spec referencing a content cluster @@ -573,8 +575,9 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { } } - private List<Container> createNodesFromNodeCount(ContainerCluster cluster, Element nodesElement, ConfigModelContext context) { + private List<Container> createNodesFromNodeCount(ContainerCluster cluster, Element nodesElement, Element rotationsElement, ConfigModelContext context) { NodesSpecification nodesSpecification = NodesSpecification.from(new ModelElement(nodesElement), context); + Set<RotationName> rotations = Rotations.from(rotationsElement); Map<HostResource, ClusterMembership> hosts = nodesSpecification.provision(cluster.getRoot().getHostSystem(), ClusterSpec.Type.container, ClusterSpec.Id.from(cluster.getName()), diff --git a/config-model/src/main/resources/schema/container.rnc b/config-model/src/main/resources/schema/container.rnc index 3f0e1f626ac..54d030e6bc0 100644 --- a/config-model/src/main/resources/schema/container.rnc +++ b/config-model/src/main/resources/schema/container.rnc @@ -50,3 +50,9 @@ FilterConfig = element filter-config { Renderer = element renderer { ComponentDefinition } + +Rotations = element rotations { + element rotation { + attribute id { xsd:Name } + }+ +} diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc index 76137fd6263..5ac5a0817b6 100644 --- a/config-model/src/main/resources/schema/containercluster.rnc +++ b/config-model/src/main/resources/schema/containercluster.rnc @@ -7,7 +7,8 @@ ContainerCluster = element container | jdisc { ContainerServices & DocumentBinding* & Aliases? & - NodesOfContainerCluster? + NodesOfContainerCluster? & + Rotations? } ContainerServices = diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/RotationsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/RotationsTest.java new file mode 100644 index 00000000000..0c3f31be0f7 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/RotationsTest.java @@ -0,0 +1,54 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.builder.xml.dom; + +import com.yahoo.config.provision.RotationName; +import com.yahoo.text.XML; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author mpolden + */ +public class RotationsTest { + + @Test + public void invalid_ids() { + assertInvalid("<rotation/>"); + assertInvalid("<rotation id=''/>"); // Blank + assertInvalid("<rotation id='FOO'/>"); // Uppercaes + assertInvalid("<rotation id='123'/>"); // Starting with non-character + assertInvalid("<rotation id='foo!'/>"); // Non-alphanumeric + assertInvalid("<rotation id='fooooooooooooo'/>"); // Too long + assertInvalid("<rotation id='foo'/><rotation id='foo'/>"); // Duplicate ID + } + + @Test + public void valid_ids() { + assertEquals(rotations(), xml("")); + assertEquals(rotations("foo"), xml("<rotation id='foo'/>")); + assertEquals(rotations("foo", "bar"), xml("<rotation id='foo'/><rotation id='bar'/>")); + } + + private static Set<RotationName> rotations(String... rotation) { + return Arrays.stream(rotation).map(RotationName::from).collect(Collectors.toSet()); + } + + private static void assertInvalid(String rotations) { + try { + xml(rotations); + fail("Expected exception for input '" + rotations + "'"); + } catch (IllegalArgumentException ignored) {} + } + + private static Set<RotationName> xml(String rotations) { + return Rotations.from(XML.getDocument("<rotations>" + rotations + "</rotations>") + .getDocumentElement()); + } + +} diff --git a/config-model/src/test/schema-test-files/services-hosted.xml b/config-model/src/test/schema-test-files/services-hosted.xml index 90894aa253b..e9b1672ce7d 100644 --- a/config-model/src/test/schema-test-files/services-hosted.xml +++ b/config-model/src/test/schema-test-files/services-hosted.xml @@ -18,6 +18,14 @@ <nodes of="search"/> </container> + <container id="container3" version="1.0"> + <rotations> + <rotation id="r1"/> + <rotation id="r2"/> + </rotations> + <nodes of="search"/> + </container> + <content id="search" version="1.0"> <redundancy>2</redundancy> <nodes count="7" flavor="large" groups="12"/> diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/RotationName.java b/config-provisioning/src/main/java/com/yahoo/config/provision/RotationName.java new file mode 100644 index 00000000000..5d9ac3699b3 --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/RotationName.java @@ -0,0 +1,57 @@ +// Copyright 2019 Oath Inc. 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; + +/** + * Represents a rotation name for a container cluster. Typically created from the rotation element in services.xml. + * + * @author mpolden + */ +public class RotationName implements Comparable<RotationName> { + + private final String name; + + private RotationName(String name) { + this.name = requireNonBlank(name, "name must be non-empty"); + } + + public String value() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RotationName that = (RotationName) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public int compareTo(RotationName o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return "rotation '" + name + "'"; + } + + public static RotationName from(String name) { + return new RotationName(name); + } + + private static String requireNonBlank(String s, String message) { + if (s == null || s.isBlank()) { + throw new IllegalArgumentException(message); + } + return s; + } + +} diff --git a/configserver/src/test/apps/hosted/services.xml b/configserver/src/test/apps/hosted/services.xml index 2025a177430..7c2920958a2 100644 --- a/configserver/src/test/apps/hosted/services.xml +++ b/configserver/src/test/apps/hosted/services.xml @@ -6,7 +6,7 @@ <nodes count='1'/> </admin> - <jdisc version="1.0"> + <container version="1.0"> <http> <filtering> <access-control domain="foo" write="true" /> @@ -15,7 +15,11 @@ </http> <search/> <nodes count='1'/> - </jdisc> + <rotations> + <rotation id='us-cluster'/> + <rotation id='eu-cluster'/> + </rotations> + </container> <content id="music" version="1.0"> <redundancy>1</redundancy> |