diff options
57 files changed, 921 insertions, 248 deletions
diff --git a/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java b/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java index 081e9ad6036..9696e9c9848 100644 --- a/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java +++ b/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java @@ -3,14 +3,21 @@ package com.yahoo.application.preprocessor; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.application.provider.BaseDeployLogger; +import com.yahoo.config.model.application.provider.DeployData; import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; +import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.yolean.Exceptions; import java.io.File; import java.io.IOException; import java.util.Optional; +import java.util.Set; /** * Main entry for preprocessing an application package. @@ -21,36 +28,75 @@ public class ApplicationPreprocessor { private final File applicationDir; private final Optional<File> outputDir; + private final Optional<InstanceName> instance; private final Optional<Environment> environment; private final Optional<RegionName> region; + private final Tags tags; - public ApplicationPreprocessor(File applicationDir, Optional<File> outputDir, Optional<Environment> environment, Optional<RegionName> region) { + public ApplicationPreprocessor(File applicationDir, + Optional<File> outputDir, + Optional<InstanceName> instance, + Optional<Environment> environment, + Optional<RegionName> region, + Tags tags) { this.applicationDir = applicationDir; this.outputDir = outputDir; + this.instance = instance; this.environment = environment; this.region = region; + this.tags = tags; } public void run() throws IOException { FilesApplicationPackage.Builder applicationPackageBuilder = new FilesApplicationPackage.Builder(applicationDir); outputDir.ifPresent(applicationPackageBuilder::preprocessedDir); - ApplicationPackage preprocessed = applicationPackageBuilder.build().preprocess( - new Zone(environment.orElse(Environment.defaultEnvironment()), region.orElse(RegionName.defaultName())), - new BaseDeployLogger()); + applicationPackageBuilder.deployData(new DeployData(applicationDir.getAbsolutePath(), + ApplicationId.from(TenantName.defaultName(), + ApplicationName.defaultName(), + instance.orElse(InstanceName.defaultName())), + tags, + System.currentTimeMillis(), + false, + 0L, + -1)); + + ApplicationPackage preprocessed = applicationPackageBuilder.build().preprocess(new Zone(environment.orElse(Environment.defaultEnvironment()), + region.orElse(RegionName.defaultName())), + new BaseDeployLogger()); preprocessed.validateXML(); } public static void main(String[] args) { - int argCount = args.length; - if (argCount < 1) { - System.out.println("Usage: vespa-application-preprocessor <application package path> [environment] [region] [output path]"); + if (args.length < 1) { + System.out.println("Usage: vespa-application-preprocessor <application package path> [instance] [environment] [region] [tag] [output path]"); System.exit(1); } File applicationDir = new File(args[0]); - Optional<Environment> environment = (argCount > 1) ? Optional.of(Environment.valueOf(args[1])) : Optional.empty(); - Optional<RegionName> region = (argCount > 2) ? Optional.of(RegionName.from(args[2])) : Optional.empty(); - Optional<File> outputDir = (argCount > 3) ? Optional.of(new File(args[3])) : Optional.empty(); - ApplicationPreprocessor preprocessor = new ApplicationPreprocessor(applicationDir, outputDir, environment, region); + Optional<InstanceName> instance; + Optional<Environment> environment; + Optional<RegionName> region; + Tags tags; + Optional<File> outputDir; + if (args.length <= 4) { // Legacy: No instance and tags + instance = Optional.empty(); + environment = args.length > 1 ? Optional.of(Environment.valueOf(args[1])) : Optional.empty(); + region = args.length > 2 ? Optional.of(RegionName.from(args[2])) : Optional.empty(); + tags = Tags.empty(); + outputDir = args.length > 3 ? Optional.of(new File(args[3])) : Optional.empty(); + } + else { + instance = Optional.of(InstanceName.from(args[1])); + environment = Optional.of(Environment.valueOf(args[2])); + region = Optional.of(RegionName.from(args[3])); + tags = Tags.fromString(args[4]); + outputDir = args.length > 5 ? Optional.of(new File(args[5])) : Optional.empty(); + } + ApplicationPreprocessor preprocessor = new ApplicationPreprocessor(applicationDir, + outputDir, + instance, + environment, + region, + tags); try { preprocessor.run(); System.out.println("Application preprocessed successfully and written to " + diff --git a/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java b/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java index 0e5d8fb2784..4de4312c099 100644 --- a/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java +++ b/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.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.application.preprocessor; +import com.yahoo.config.provision.Tags; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.xml.sax.SAXException; @@ -18,11 +19,13 @@ public class ApplicationPreprocessorTest { // Basic test just to check that instantiation and run() works. Unit testing is in config-application-package @Test - void basic() throws ParserConfigurationException, TransformerException, SAXException, IOException { + void basic() throws IOException { ApplicationPreprocessor preprocessor = new ApplicationPreprocessor(new File("src/test/resources/simple"), - Optional.of(newFolder(outputDir, "basic")), - Optional.empty(), - Optional.empty()); + Optional.of(newFolder(outputDir, "basic")), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Tags.empty()); preprocessor.run(); } diff --git a/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java b/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java index d7efca3b723..21bb193ef93 100644 --- a/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java +++ b/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.text.XML; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -23,7 +24,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; /** - * Handles overrides in a XML document according to the rules defined for multi environment application packages. + * Handles overrides in a XML document according to the rules defined for multi-environment application packages. * * Rules: * @@ -41,16 +42,19 @@ class OverrideProcessor implements PreProcessor { private final InstanceName instance; private final Environment environment; private final RegionName region; + private final Tags tags; private static final String ID_ATTRIBUTE = "id"; private static final String INSTANCE_ATTRIBUTE = "instance"; private static final String ENVIRONMENT_ATTRIBUTE = "environment"; private static final String REGION_ATTRIBUTE = "region"; + private static final String TAGS_ATTRIBUTE = "tags"; - public OverrideProcessor(InstanceName instance, Environment environment, RegionName region) { + public OverrideProcessor(InstanceName instance, Environment environment, RegionName region, Tags tags) { this.instance = instance; this.environment = environment; this.region = region; + this.tags = tags; } public Document process(Document input) throws TransformerException { @@ -80,6 +84,7 @@ class OverrideProcessor implements PreProcessor { child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, INSTANCE_ATTRIBUTE); child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, ENVIRONMENT_ATTRIBUTE); child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, REGION_ATTRIBUTE); + child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, TAGS_ATTRIBUTE); } } @@ -87,13 +92,16 @@ class OverrideProcessor implements PreProcessor { Set<InstanceName> instances = context.instances; Set<Environment> environments = context.environments; Set<RegionName> regions = context.regions; + Tags tags = context.tags; if (instances.isEmpty()) instances = getInstances(parent); if (environments.isEmpty()) environments = getEnvironments(parent); if (regions.isEmpty()) regions = getRegions(parent); - return Context.create(instances, environments, regions); + if (tags.isEmpty()) + tags = getTags(parent); + return Context.create(instances, environments, regions, tags); } /** @@ -128,17 +136,21 @@ class OverrideProcessor implements PreProcessor { throw new IllegalArgumentException("Regions in child (" + regions + ") are not a subset of those of the parent (" + context.regions + ") at " + child); } + + Tags tags = getTags(child); + if ( ! tags.isEmpty() && ! context.tags.isEmpty() && ! context.tags.containsAll(tags)) { + throw new IllegalArgumentException("Tags in child (" + environments + + ") are not a subset of those of the parent (" + context.tags + ") at " + child); + } } } - /** - * Prune elements that are not matching our environment and region - */ + /** Prune elements that are not matching our environment and region. */ private void pruneNonMatching(Element parent, List<Element> children) { Iterator<Element> elemIt = children.iterator(); while (elemIt.hasNext()) { Element child = elemIt.next(); - if ( ! matches(getInstances(child), getEnvironments(child), getRegions(child))) { + if ( ! matches(getInstances(child), getEnvironments(child), getRegions(child), getTags(child))) { parent.removeChild(child); elemIt.remove(); } @@ -147,7 +159,8 @@ class OverrideProcessor implements PreProcessor { private boolean matches(Set<InstanceName> elementInstances, Set<Environment> elementEnvironments, - Set<RegionName> elementRegions) { + Set<RegionName> elementRegions, + Tags elementTags) { if ( ! elementInstances.isEmpty()) { // match instance if ( ! elementInstances.contains(instance)) return false; } @@ -164,12 +177,14 @@ class OverrideProcessor implements PreProcessor { if ( ! environment.isMultiRegion() && elementEnvironments.isEmpty() ) return false; } + if ( ! elementTags.isEmpty()) { // match tags + if ( ! elementTags.intersects(tags)) return false; + } + return true; } - /** - * Find the most specific element and remove all others. - */ + /** Find the most specific element and remove all others. */ private void retainMostSpecific(Element parent, List<Element> children, Context context) { // Keep track of elements with highest number of matches (might be more than one element with same tag, need a list) List<Element> bestMatches = new ArrayList<>(); @@ -205,12 +220,15 @@ class OverrideProcessor implements PreProcessor { Set<InstanceName> elementInstances = hasInstance(child) ? getInstances(child) : context.instances; Set<Environment> elementEnvironments = hasEnvironment(child) ? getEnvironments(child) : context.environments; Set<RegionName> elementRegions = hasRegion(child) ? getRegions(child) : context.regions; + Tags elementTags = hasTag(child) ? getTags(child) : context.tags; if ( ! elementInstances.isEmpty() && elementInstances.contains(instance)) currentMatch++; if ( ! elementEnvironments.isEmpty() && elementEnvironments.contains(environment)) currentMatch++; if ( ! elementRegions.isEmpty() && elementRegions.contains(region)) currentMatch++; + if ( elementTags.intersects(tags)) + currentMatch++; return currentMatch; } @@ -233,16 +251,14 @@ class OverrideProcessor implements PreProcessor { return false; } - /** - * Retains all elements where at least one element is overridden. Removes non-overridden elements from map. - */ + /** Retains all elements where at least one element is overridden. Removes non-overridden elements from map. */ private void retainOverriddenElements(Map<String, List<Element>> elementsByTagName) { Iterator<Map.Entry<String, List<Element>>> it = elementsByTagName.entrySet().iterator(); while (it.hasNext()) { List<Element> elements = it.next().getValue(); boolean hasOverrides = false; for (Element element : elements) { - if (hasEnvironment(element) || hasRegion(element)) { + if (hasInstance(element) || hasEnvironment(element) || hasRegion(element) || hasTag(element)) { hasOverrides = true; } } @@ -264,24 +280,34 @@ class OverrideProcessor implements PreProcessor { return element.hasAttributeNS(XmlPreProcessor.deployNamespaceUri, ENVIRONMENT_ATTRIBUTE); } + private boolean hasTag(Element element) { + return element.hasAttributeNS(XmlPreProcessor.deployNamespaceUri, TAGS_ATTRIBUTE); + } + private Set<InstanceName> getInstances(Element element) { String instance = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, INSTANCE_ATTRIBUTE); - if (instance == null || instance.isEmpty()) return Collections.emptySet(); + if (instance == null || instance.isEmpty()) return Set.of(); return Arrays.stream(instance.split(" ")).map(InstanceName::from).collect(Collectors.toSet()); } private Set<Environment> getEnvironments(Element element) { String env = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, ENVIRONMENT_ATTRIBUTE); - if (env == null || env.isEmpty()) return Collections.emptySet(); + if (env == null || env.isEmpty()) return Set.of(); return Arrays.stream(env.split(" ")).map(Environment::from).collect(Collectors.toSet()); } private Set<RegionName> getRegions(Element element) { String reg = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, REGION_ATTRIBUTE); - if (reg == null || reg.isEmpty()) return Collections.emptySet(); + if (reg == null || reg.isEmpty()) return Set.of(); return Arrays.stream(reg.split(" ")).map(RegionName::from).collect(Collectors.toSet()); } + private Tags getTags(Element element) { + String env = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, TAGS_ATTRIBUTE); + if (env == null || env.isEmpty()) return Tags.empty(); + return Tags.fromString(env); + } + private Map<String, List<Element>> elementsByTagNameAndId(List<Element> children) { Map<String, List<Element>> elementsByTagName = new LinkedHashMap<>(); // Index by tag name @@ -336,21 +362,27 @@ class OverrideProcessor implements PreProcessor { final Set<InstanceName> instances; final Set<Environment> environments; final Set<RegionName> regions; + final Tags tags; - private Context(Set<InstanceName> instances, Set<Environment> environments, Set<RegionName> regions) { + private Context(Set<InstanceName> instances, + Set<Environment> environments, + Set<RegionName> regions, + Tags tags) { this.instances = Set.copyOf(instances); this.environments = Set.copyOf(environments); this.regions = Set.copyOf(regions); + this.tags = tags; } static Context empty() { - return new Context(Set.of(), Set.of(), Set.of()); + return new Context(Set.of(), Set.of(), Set.of(), Tags.empty()); } public static Context create(Set<InstanceName> instances, Set<Environment> environments, - Set<RegionName> regions) { - return new Context(instances, environments, regions); + Set<RegionName> regions, + Tags tags) { + return new Context(instances, environments, regions, tags); } } diff --git a/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java b/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java index ba68894c9f9..42333ea7662 100644 --- a/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java +++ b/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java @@ -5,6 +5,7 @@ import com.yahoo.config.application.FileSystemWrapper.FileWrapper; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.text.XML; import org.w3c.dom.Document; import org.xml.sax.InputSource; @@ -19,6 +20,7 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * A preprocessor for services.xml files that handles deploy:environment, deploy:region, preprocess:properties, preprocess:include @@ -38,22 +40,53 @@ public class XmlPreProcessor { private final InstanceName instance; private final Environment environment; private final RegionName region; + private final Tags tags; private final List<PreProcessor> chain; - public XmlPreProcessor(File applicationDir, File xmlInput, InstanceName instance, Environment environment, RegionName region) throws IOException { - this(applicationDir, new FileReader(xmlInput), instance, environment, region); + // TODO: Remove after November 2022 + public XmlPreProcessor(File applicationDir, + File xmlInput, + InstanceName instance, + Environment environment, + RegionName region) throws IOException { + this(applicationDir, new FileReader(xmlInput), instance, environment, region, Tags.empty()); } - public XmlPreProcessor(File applicationDir, Reader xmlInput, InstanceName instance, Environment environment, RegionName region) { - this(FileSystemWrapper.getDefault(applicationDir.toPath()).wrap(applicationDir.toPath()), xmlInput, instance, environment, region); + public XmlPreProcessor(File applicationDir, + File xmlInput, + InstanceName instance, + Environment environment, + RegionName region, + Tags tags) throws IOException { + this(applicationDir, new FileReader(xmlInput), instance, environment, region, tags); } - public XmlPreProcessor(FileWrapper applicationDir, Reader xmlInput, InstanceName instance, Environment environment, RegionName region) { + public XmlPreProcessor(File applicationDir, + Reader xmlInput, + InstanceName instance, + Environment environment, + RegionName region, + Tags tags) { + this(FileSystemWrapper.getDefault(applicationDir.toPath()).wrap(applicationDir.toPath()), + xmlInput, + instance, + environment, + region, + tags); + } + + public XmlPreProcessor(FileWrapper applicationDir, + Reader xmlInput, + InstanceName instance, + Environment environment, + RegionName region, + Tags tags) { this.applicationDir = applicationDir; this.xmlInput = xmlInput; this.instance = instance; this.environment = environment; this.region = region; + this.tags = tags; this.chain = setupChain(); } @@ -73,7 +106,7 @@ public class XmlPreProcessor { private List<PreProcessor> setupChain() { List<PreProcessor> chain = new ArrayList<>(); chain.add(new IncludeProcessor(applicationDir)); - chain.add(new OverrideProcessor(instance, environment, region)); + chain.add(new OverrideProcessor(instance, environment, region, tags)); chain.add(new PropertiesProcessor()); chain.add(new ValidationProcessor()); // must be last in chain return chain; diff --git a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java index 279af646a8c..c3e9b99f562 100644 --- a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java +++ b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java @@ -2,6 +2,9 @@ package com.yahoo.config.model.application.provider; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; + +import java.util.Set; /** * Data generated or computed during deployment @@ -12,6 +15,8 @@ public class DeployData { private final ApplicationId applicationId; + private final Tags tags; + /** The absolute path to the directory holding the application */ private final String deployedFromDir; @@ -25,25 +30,16 @@ public class DeployData { private final long generation; private final long currentlyActiveGeneration; - // TODO: Remove when oldest version in use is 8.13 - public DeployData(String ignored, - String deployedFromDir, - ApplicationId applicationId, - Long deployTimestamp, - boolean internalRedeploy, - Long generation, - long currentlyActiveGeneration) { - this(deployedFromDir, applicationId, deployTimestamp, internalRedeploy, generation, currentlyActiveGeneration); - } - public DeployData(String deployedFromDir, ApplicationId applicationId, + Tags tags, Long deployTimestamp, boolean internalRedeploy, Long generation, long currentlyActiveGeneration) { this.deployedFromDir = deployedFromDir; this.applicationId = applicationId; + this.tags = tags; this.deployTimestamp = deployTimestamp; this.internalRedeploy = internalRedeploy; this.generation = generation; @@ -62,4 +58,6 @@ public class DeployData { public ApplicationId getApplicationId() { return applicationId; } + public Tags getTags() { return tags; } + } diff --git a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java index 7b483d0603c..e61ea01a99a 100644 --- a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java +++ b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java @@ -17,6 +17,7 @@ import com.yahoo.config.model.application.AbstractApplicationPackage; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.io.HexDump; @@ -138,6 +139,7 @@ public class FilesApplicationPackage extends AbstractApplicationPackage { deployData.getDeployTimestamp(), deployData.isInternalRedeploy(), deployData.getApplicationId(), + deployData.getTags(), computeCheckSum(appDir), deployData.getGeneration(), deployData.getCurrentlyActiveGeneration()); @@ -484,6 +486,7 @@ public class FilesApplicationPackage extends AbstractApplicationPackage { ApplicationId.from(TenantName.defaultName(), ApplicationName.from(originalAppDir), InstanceName.defaultName()), + Tags.empty(), "", 0L, 0L); @@ -583,7 +586,8 @@ public class FilesApplicationPackage extends AbstractApplicationPackage { inputXml, metaData.getApplicationId().instance(), zone.environment(), - zone.region()) + zone.region(), + metaData.getTags()) .run(); try (FileOutputStream outputStream = new FileOutputStream(destination)) { diff --git a/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTagsTest.java b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTagsTest.java new file mode 100644 index 00000000000..8d7431d33b6 --- /dev/null +++ b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTagsTest.java @@ -0,0 +1,124 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.application; + +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; +import org.custommonkey.xmlunit.XMLUnit; +import org.junit.Test; +import org.w3c.dom.Document; + +import javax.xml.transform.TransformerException; +import java.io.StringReader; + +/** + * @author bratseth + */ +public class HostedOverrideProcessorTagsTest { + + static { + XMLUnit.setIgnoreWhitespace(true); + } + + private static final String input = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='5' deploy:tags='a' deploy:environment='perf'/>" + + " <nodes count='10' deploy:tags='a b'/>" + + " <nodes count='20' deploy:tags='c'/>" + + " <search deploy:tags='b'/>" + + " <document-api deploy:tags='d'/>" + + " </container>" + + "</services>"; + + @Test + public void testParsingTagAPerf() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='5' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.perf, + RegionName.defaultName(), + Tags.fromString("a"), + expected); + } + + @Test + public void testParsingTagAProd() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='10' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("a"), + expected); + } + + @Test + public void testParsingTagB() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='10' required='true'/>" + + " <search/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("b"), + expected); + } + + @Test + public void testParsingTagC() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='20' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("c"), + expected); + } + + @Test + public void testParsingTagCAndD() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='20' required='true'/>" + + " <document-api/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("c d"), + expected); + } + + private void assertOverride(InstanceName instance, Environment environment, RegionName region, Tags tags, String expected) throws TransformerException { + Document inputDoc = Xml.getDocument(new StringReader(input)); + Document newDoc = new OverrideProcessor(instance, environment, region, tags).process(inputDoc); + TestBase.assertDocument(expected, newDoc); + } + +} diff --git a/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java index 1a4dab01930..451c7a3c217 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; import org.w3c.dom.Document; @@ -14,6 +15,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.transform.TransformerException; import java.io.IOException; import java.io.StringReader; +import java.util.List; /** * @author bratseth @@ -48,7 +50,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1'/>" + " </container>" + "</services>"; - assertOverride(Environment.test, RegionName.defaultName(), expected); + assertOverride(InstanceName.defaultName(), + Environment.test, + RegionName.defaultName(), + Tags.empty(), + expected); } @Test @@ -60,7 +66,27 @@ public class HostedOverrideProcessorTest { " <nodes count='4' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("prod"), RegionName.from("us-west"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty(), + expected); + } + + @Test + public void testParsingSpecificTag() throws TransformerException { + String expected = + "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + + "<services xmlns:deploy=\"vespa\" xmlns:preprocess=\"?\" version=\"1.0\">" + + " <container id=\"foo\" version=\"1.0\">" + + " <nodes count='4' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty(), + expected); } @Test @@ -72,7 +98,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1' required='true'/>" + " </container>" + "</services>"; - assertOverride(InstanceName.from("myinstance"), Environment.from("prod"), RegionName.from("us-west"), expected); + assertOverride(InstanceName.from("myinstance"), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty(), + expected); } @Test @@ -84,7 +114,11 @@ public class HostedOverrideProcessorTest { " <nodes count='5' flavor='v-8-8-100' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("prod"), RegionName.from("us-east-3"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.from("us-east-3"), + Tags.empty(), + expected); } @Test @@ -96,7 +130,11 @@ public class HostedOverrideProcessorTest { " <nodes count='3' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("perf"), RegionName.from("us-east-3"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("perf"), + RegionName.from("us-east-3"), + Tags.empty(), + expected); } @Test @@ -108,7 +146,11 @@ public class HostedOverrideProcessorTest { " <nodes count='3' flavor='v-4-8-100' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.valueOf("prod"), RegionName.from("unknown"), expected); + assertOverride(InstanceName.defaultName(), + Environment.valueOf("prod"), + RegionName.from("unknown"), + Tags.empty(), + expected); } @Test @@ -120,7 +162,11 @@ public class HostedOverrideProcessorTest { " <nodes count='3' flavor='v-4-8-100' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("prod"), RegionName.defaultName(), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.defaultName(), + Tags.empty(), + expected); } @Test @@ -132,7 +178,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("dev"), RegionName.defaultName(), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("dev"), + RegionName.defaultName(), + Tags.empty(), + expected); } @Test @@ -144,7 +194,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("test"), RegionName.from("us-west"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("test"), + RegionName.from("us-west"), + Tags.empty(), + expected); } @Test @@ -156,16 +210,16 @@ public class HostedOverrideProcessorTest { " <nodes count='2' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("staging"), RegionName.from("us-west"), expected); - } - - private void assertOverride(Environment environment, RegionName region, String expected) throws TransformerException { - assertOverride(InstanceName.from("default"), environment, region, expected); + assertOverride(InstanceName.defaultName(), + Environment.from("staging"), + RegionName.from("us-west"), + Tags.empty(), + expected); } - private void assertOverride(InstanceName instance, Environment environment, RegionName region, String expected) throws TransformerException { + private void assertOverride(InstanceName instance, Environment environment, RegionName region, Tags tags, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(input)); - Document newDoc = new OverrideProcessor(instance, environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(instance, environment, region, tags).process(inputDoc); TestBase.assertDocument(expected, newDoc); } diff --git a/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java index 44bcb12957a..debde6c1438 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; import org.w3c.dom.Document; @@ -124,13 +125,13 @@ public class MultiOverrideProcessorTest { private void assertOverride(Environment environment, RegionName region, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(input)); - Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region, Tags.empty()).process(inputDoc); TestBase.assertDocument(expected, newDoc); } private void assertOverrideWithIds(Environment environment, RegionName region, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(inputWithIds)); - Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region, Tags.empty()).process(inputDoc); TestBase.assertDocument(expected, newDoc); } diff --git a/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java index e9ee7c97876..150999390d8 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; import org.w3c.dom.Document; @@ -332,7 +333,10 @@ public class OverrideProcessorTest { " </admin>" + "</services>"; Document inputDoc = Xml.getDocument(new StringReader(in)); - new OverrideProcessor(InstanceName.from("default"), Environment.from("prod"), RegionName.from("us-west")).process(inputDoc); + new OverrideProcessor(InstanceName.from("default"), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty()).process(inputDoc); } @Test(expected = IllegalArgumentException.class) @@ -344,7 +348,10 @@ public class OverrideProcessorTest { " </admin>" + "</services>"; Document inputDoc = Xml.getDocument(new StringReader(in)); - new OverrideProcessor(InstanceName.from("default"), Environment.defaultEnvironment(), RegionName.from("us-west")).process(inputDoc); + new OverrideProcessor(InstanceName.from("default"), + Environment.defaultEnvironment(), + RegionName.from("us-west"), + Tags.empty()).process(inputDoc); } @Test @@ -399,7 +406,7 @@ public class OverrideProcessorTest { private void assertOverride(String input, Environment environment, RegionName region, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(input)); - Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region, Tags.empty()).process(inputDoc); TestBase.assertDocument(expected, newDoc); } diff --git a/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java index 92c2c2a820f..0da94b69e58 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java @@ -4,11 +4,13 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.junit.Test; import org.w3c.dom.Document; import java.io.File; import java.io.StringReader; +import java.util.Set; /** * @author hmusum @@ -44,7 +46,13 @@ public class XmlPreprocessorTest { " </nodes>\n" + " </container>\n" + "</services>"; - TestBase.assertDocument(expectedDev, new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.dev, RegionName.defaultName()).run()); + TestBase.assertDocument(expectedDev, + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.dev, + RegionName.defaultName(), + Tags.empty()).run()); String expectedStaging = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + @@ -70,7 +78,13 @@ public class XmlPreprocessorTest { " </nodes>\n" + " </container>\n" + "</services>"; - TestBase.assertDocument(expectedStaging, new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.staging, RegionName.defaultName()).run()); + TestBase.assertDocument(expectedStaging, + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.staging, + RegionName.defaultName(), + Tags.empty()).run()); String expectedUsWest = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + @@ -104,7 +118,13 @@ public class XmlPreprocessorTest { " </nodes>\n" + " </container>\n" + "</services>"; - TestBase.assertDocument(expectedUsWest, new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.prod, RegionName.from("us-west")).run()); + TestBase.assertDocument(expectedUsWest, + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.prod, + RegionName.from("us-west"), + Tags.empty()).run()); String expectedUsEastAndCentral = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + @@ -138,9 +158,19 @@ public class XmlPreprocessorTest { " </container>\n" + "</services>"; TestBase.assertDocument(expectedUsEastAndCentral, - new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.prod, RegionName.from("us-east")).run()); + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.prod, + RegionName.from("us-east"), + Tags.empty()).run()); TestBase.assertDocument(expectedUsEastAndCentral, - new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.prod, RegionName.from("us-central")).run()); + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.prod, + RegionName.from("us-central"), + Tags.empty()).run()); } @Test @@ -184,7 +214,12 @@ public class XmlPreprocessorTest { " <adminserver hostalias=\"node0\"/>" + " </admin>" + "</services>"; - Document docDev = (new XmlPreProcessor(appDir, new StringReader(input), InstanceName.defaultName(), Environment.prod, RegionName.defaultName()).run()); + Document docDev = (new XmlPreProcessor(appDir, + new StringReader(input), + InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.empty()).run()); TestBase.assertDocument(expectedProd, docDev); } diff --git a/config-model-api/abi-spec.json b/config-model-api/abi-spec.json index 7e17cd0f600..4f66ded727b 100644 --- a/config-model-api/abi-spec.json +++ b/config-model-api/abi-spec.json @@ -71,11 +71,13 @@ "public" ], "methods": [ + "public void <init>(java.lang.String, java.lang.Long, boolean, com.yahoo.config.provision.ApplicationId, com.yahoo.config.provision.Tags, java.lang.String, java.lang.Long, long)", "public void <init>(java.lang.String, java.lang.Long, boolean, com.yahoo.config.provision.ApplicationId, java.lang.String, java.lang.Long, long)", "public void <init>(java.lang.String, java.lang.String, java.lang.Long, boolean, com.yahoo.config.provision.ApplicationId, java.lang.String, java.lang.Long, long)", "public java.lang.String getDeployedByUser()", "public java.lang.String getDeployPath()", "public com.yahoo.config.provision.ApplicationId getApplicationId()", + "public com.yahoo.config.provision.Tags getTags()", "public java.lang.Long getDeployTimestamp()", "public java.lang.Long getGeneration()", "public boolean isInternalRedeploy()", @@ -194,8 +196,9 @@ "public" ], "methods": [ - "public void <init>(com.yahoo.config.provision.InstanceName, java.util.List, com.yahoo.config.application.api.DeploymentSpec$UpgradePolicy, com.yahoo.config.application.api.DeploymentSpec$RevisionTarget, com.yahoo.config.application.api.DeploymentSpec$RevisionChange, com.yahoo.config.application.api.DeploymentSpec$UpgradeRollout, int, int, int, java.util.List, java.util.Optional, java.util.Optional, java.util.Optional, com.yahoo.config.application.api.Notifications, java.util.List, java.time.Instant)", + "public void <init>(com.yahoo.config.provision.InstanceName, com.yahoo.config.provision.Tags, java.util.List, com.yahoo.config.application.api.DeploymentSpec$UpgradePolicy, com.yahoo.config.application.api.DeploymentSpec$RevisionTarget, com.yahoo.config.application.api.DeploymentSpec$RevisionChange, com.yahoo.config.application.api.DeploymentSpec$UpgradeRollout, int, int, int, java.util.List, java.util.Optional, java.util.Optional, java.util.Optional, com.yahoo.config.application.api.Notifications, java.util.List, java.time.Instant)", "public com.yahoo.config.provision.InstanceName name()", + "public com.yahoo.config.provision.Tags tags()", "public com.yahoo.config.application.api.DeploymentSpec$UpgradePolicy upgradePolicy()", "public com.yahoo.config.application.api.DeploymentSpec$RevisionTarget revisionTarget()", "public com.yahoo.config.application.api.DeploymentSpec$RevisionChange revisionChange()", diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java index a23afd994f2..c830c2baa1f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java @@ -2,6 +2,7 @@ package com.yahoo.config.application.api; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -9,6 +10,9 @@ import com.yahoo.slime.SlimeUtils; import com.yahoo.text.Utf8; import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; /** * Metadata about an application package. @@ -21,32 +25,40 @@ public class ApplicationMetaData { private final long deployTimestamp; private final boolean internalRedeploy; private final ApplicationId applicationId; + private final Tags tags; private final String checksum; private final long generation; private final long previousActiveGeneration; public ApplicationMetaData(String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, - ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { - this("unknown", deployedFromDir, deployTimestamp, internalRedeploy, applicationId, checksum, generation, previousActiveGeneration); - } - - @Deprecated - // TODO: Remove in Vespa 9 - public ApplicationMetaData(String ignored, String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, - ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { + ApplicationId applicationId, Tags tags, + String checksum, Long generation, long previousActiveGeneration) { this.deployedFromDir = deployedFromDir; this.deployTimestamp = deployTimestamp; this.internalRedeploy = internalRedeploy; this.applicationId = applicationId; + this.tags = tags; this.checksum = checksum; this.generation = generation; this.previousActiveGeneration = previousActiveGeneration; } + @Deprecated // TODO: Remove on Vespa 9 + public ApplicationMetaData(String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, + ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { + this(deployedFromDir, deployTimestamp, internalRedeploy, applicationId, Tags.empty(), checksum, generation, previousActiveGeneration); + } + + @Deprecated // TODO: Remove on Vespa 9 + public ApplicationMetaData(String ignored, String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, + ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { + this(deployedFromDir, deployTimestamp, internalRedeploy, applicationId, Tags.empty(), checksum, generation, previousActiveGeneration); + } + /** * Gets the user who deployed the application. * - * @return user name for the user who ran "deploy-application" + * @return username of the user who ran "deploy-application" */ @Deprecated // TODO: Remove in Vespa 9 public String getDeployedByUser() { return "unknown"; } @@ -61,6 +73,8 @@ public class ApplicationMetaData { public ApplicationId getApplicationId() { return applicationId; } + public Tags getTags() { return tags; } + /** * Gets the time the application was deployed. * Will return null if a problem occurred while getting metadata. @@ -103,6 +117,7 @@ public class ApplicationMetaData { deploy.field("timestamp").asLong(), booleanField("internalRedeploy", false, deploy), ApplicationId.fromSerializedForm(app.field("id").asString()), + Tags.fromString(deploy.field("tags").asString()), app.field("checksum").asString(), app.field("generation").asLong(), app.field("previousActiveGeneration").asLong()); @@ -118,6 +133,7 @@ public class ApplicationMetaData { deploy.setString("from", deployedFromDir); deploy.setLong("timestamp", deployTimestamp); deploy.setBool("internalRedeploy", internalRedeploy); + deploy.setString("tags", tags.asString()); Cursor app = meta.setObject("application"); app.setString("id", applicationId.serializedForm()); app.setString("checksum", checksum); diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java index cd20b5b8910..01a20cc158d 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java @@ -6,6 +6,7 @@ import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import java.time.Duration; import java.time.Instant; @@ -40,6 +41,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { /** The name of the instance this step deploys */ private final InstanceName name; + private final Tags tags; private final DeploymentSpec.UpgradePolicy upgradePolicy; private final DeploymentSpec.RevisionTarget revisionTarget; private final DeploymentSpec.RevisionChange revisionChange; @@ -55,6 +57,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { private final List<Endpoint> endpoints; public DeploymentInstanceSpec(InstanceName name, + Tags tags, List<DeploymentSpec.Step> steps, DeploymentSpec.UpgradePolicy upgradePolicy, DeploymentSpec.RevisionTarget revisionTarget, @@ -70,6 +73,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { Instant now) { super(steps); this.name = Objects.requireNonNull(name); + this.tags = Objects.requireNonNull(tags); this.upgradePolicy = Objects.requireNonNull(upgradePolicy); Objects.requireNonNull(revisionTarget); Objects.requireNonNull(revisionChange); @@ -94,6 +98,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { public InstanceName name() { return name; } + public Tags tags() { return tags; } + /** * Throws an IllegalArgumentException if any production deployment or test is declared multiple times, * or if any production test is declared not after its corresponding deployment. diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index b85150356e3..2df55ffce95 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -194,7 +194,7 @@ public class DeploymentSpec { /** Returns the instance names declared in this */ public List<InstanceName> instanceNames() { - return instances().stream().map(DeploymentInstanceSpec::name).collect(Collectors.toUnmodifiableList()); + return instances().stream().map(DeploymentInstanceSpec::name).toList(); } /** Returns the step descendants of this which are instances */ diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java index 83fba75325e..77894a8cf1f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java @@ -25,6 +25,7 @@ import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.io.IOUtils; import com.yahoo.text.XML; import org.w3c.dom.Element; @@ -55,6 +56,7 @@ public class DeploymentSpecXmlReader { private static final String deploymentTag = "deployment"; private static final String instanceTag = "instance"; + private static final String tagsTag = "tags"; private static final String testTag = "test"; private static final String stagingTag = "staging"; private static final String devTag = "dev"; @@ -155,48 +157,50 @@ public class DeploymentSpecXmlReader { * Reads the content of an (implicit or explicit) instance tag producing an instances step * * @param instanceNameString a comma-separated list of the names of the instances this is for - * @param instanceTag the element having the content of this instance + * @param instanceElement the element having the content of this instance * @param parentTag the parent of instanceTag (or the same, if this instance is implicitly defined, which means instanceTag is the root) * @return the instances specified, one for each instance name element */ private List<DeploymentInstanceSpec> readInstanceContent(String instanceNameString, - Element instanceTag, + Element instanceElement, Map<String, String> prodAttributes, Element parentTag) { if (instanceNameString.isBlank()) illegal("<instance> attribute 'id' must be specified, and not be blank"); // If this is an absolutely empty instance, or the implicit "default" instance but without content, ignore it - if (XML.getChildren(instanceTag).isEmpty() && (instanceTag.getAttributes().getLength() == 0 || instanceTag == parentTag)) + if (XML.getChildren(instanceElement).isEmpty() && (instanceElement.getAttributes().getLength() == 0 || instanceElement == parentTag)) return List.of(); if (validate) - validateTagOrder(instanceTag); + validateTagOrder(instanceElement); // Values where the parent may provide a default - DeploymentSpec.UpgradePolicy upgradePolicy = getWithFallback(instanceTag, parentTag, upgradeTag, "policy", this::readUpgradePolicy, UpgradePolicy.defaultPolicy); - DeploymentSpec.RevisionTarget revisionTarget = getWithFallback(instanceTag, parentTag, upgradeTag, "revision-target", this::readRevisionTarget, RevisionTarget.latest); - DeploymentSpec.RevisionChange revisionChange = getWithFallback(instanceTag, parentTag, upgradeTag, "revision-change", this::readRevisionChange, RevisionChange.whenFailing); - DeploymentSpec.UpgradeRollout upgradeRollout = getWithFallback(instanceTag, parentTag, upgradeTag, "rollout", this::readUpgradeRollout, UpgradeRollout.separate); - int minRisk = getWithFallback(instanceTag, parentTag, upgradeTag, "min-risk", Integer::parseInt, 0); - int maxRisk = getWithFallback(instanceTag, parentTag, upgradeTag, "max-risk", Integer::parseInt, 0); - int maxIdleHours = getWithFallback(instanceTag, parentTag, upgradeTag, "max-idle-hours", Integer::parseInt, 8); - List<DeploymentSpec.ChangeBlocker> changeBlockers = readChangeBlockers(instanceTag, parentTag); - Optional<AthenzService> athenzService = mostSpecificAttribute(instanceTag, athenzServiceAttribute).map(AthenzService::from); - Optional<CloudAccount> cloudAccount = mostSpecificAttribute(instanceTag, cloudAccountAttribute).map(CloudAccount::new); - Notifications notifications = readNotifications(instanceTag, parentTag); + DeploymentSpec.UpgradePolicy upgradePolicy = getWithFallback(instanceElement, parentTag, upgradeTag, "policy", this::readUpgradePolicy, UpgradePolicy.defaultPolicy); + DeploymentSpec.RevisionTarget revisionTarget = getWithFallback(instanceElement, parentTag, upgradeTag, "revision-target", this::readRevisionTarget, RevisionTarget.latest); + DeploymentSpec.RevisionChange revisionChange = getWithFallback(instanceElement, parentTag, upgradeTag, "revision-change", this::readRevisionChange, RevisionChange.whenFailing); + DeploymentSpec.UpgradeRollout upgradeRollout = getWithFallback(instanceElement, parentTag, upgradeTag, "rollout", this::readUpgradeRollout, UpgradeRollout.separate); + int minRisk = getWithFallback(instanceElement, parentTag, upgradeTag, "min-risk", Integer::parseInt, 0); + int maxRisk = getWithFallback(instanceElement, parentTag, upgradeTag, "max-risk", Integer::parseInt, 0); + int maxIdleHours = getWithFallback(instanceElement, parentTag, upgradeTag, "max-idle-hours", Integer::parseInt, 8); + List<DeploymentSpec.ChangeBlocker> changeBlockers = readChangeBlockers(instanceElement, parentTag); + Optional<AthenzService> athenzService = mostSpecificAttribute(instanceElement, athenzServiceAttribute).map(AthenzService::from); + Optional<CloudAccount> cloudAccount = mostSpecificAttribute(instanceElement, cloudAccountAttribute).map(CloudAccount::new); + Notifications notifications = readNotifications(instanceElement, parentTag); // Values where there is no default + Tags tags = XML.attribute(tagsTag, instanceElement).map(value -> Tags.fromString(value)).orElse(Tags.empty()); List<Step> steps = new ArrayList<>(); - for (Element instanceChild : XML.getChildren(instanceTag)) + for (Element instanceChild : XML.getChildren(instanceElement)) steps.addAll(readNonInstanceSteps(instanceChild, prodAttributes, instanceChild)); - List<Endpoint> endpoints = readEndpoints(instanceTag, Optional.of(instanceNameString), steps); + List<Endpoint> endpoints = readEndpoints(instanceElement, Optional.of(instanceNameString), steps); // Build and return instances with these values Instant now = clock.instant(); return Arrays.stream(instanceNameString.split(",")) .map(name -> name.trim()) .map(name -> new DeploymentInstanceSpec(InstanceName.from(name), + tags, steps, upgradePolicy, revisionTarget, diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java index 87b0f709125..2e51e283eb9 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.test.ManualClock; import org.junit.Test; @@ -133,6 +134,29 @@ public class DeploymentSpecTest { } @Test + public void specWithTags() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <instance id='a' tags='tag1 tag2'>" + + " <prod>" + + " <region active='false'>us-east1</region>" + + " <region active='true'>us-west1</region>" + + " </prod>" + + " </instance>" + + " <instance id='b' tags='tag3'>" + + " <prod>" + + " <region active='false'>us-east1</region>" + + " <region active='true'>us-west1</region>" + + " </prod>" + + " </instance>" + + "</deployment>" + ); + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals(Tags.fromString("tag1 tag2"), spec.requireInstance("a").tags()); + assertEquals(Tags.fromString("tag3"), spec.requireInstance("b").tags()); + } + + @Test public void maximalProductionSpec() { StringReader r = new StringReader( "<deployment version='1.0'>" + diff --git a/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java b/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java index 9ee279c68d3..2d98c8b35c0 100644 --- a/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java +++ b/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java @@ -10,6 +10,7 @@ import com.yahoo.config.application.api.UnparsedConfigDefinition; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.io.IOUtils; import com.yahoo.io.reader.NamedReader; @@ -85,6 +86,7 @@ public class MockApplicationPackage implements ApplicationPackage { ApplicationId.from(TenantName.defaultName(), ApplicationName.from(APPLICATION_NAME), InstanceName.defaultName()), + Tags.empty(), "checksum", APPLICATION_GENERATION, 0L); diff --git a/config-model/src/main/resources/schema/deployment.rnc b/config-model/src/main/resources/schema/deployment.rnc index 9723e531bd2..bf94ee3b750 100644 --- a/config-model/src/main/resources/schema/deployment.rnc +++ b/config-model/src/main/resources/schema/deployment.rnc @@ -35,6 +35,7 @@ PrimitiveStep = Instance = element instance { attribute id { xsd:string } & + attribute tags { xsd:string }? & attribute athenz-service { xsd:string }? & attribute cloud-account { xsd:string }? & StepExceptInstance diff --git a/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java b/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java index c1dd62316db..6507341670f 100644 --- a/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java +++ b/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java @@ -10,6 +10,7 @@ import com.yahoo.config.model.application.provider.DeployData; import com.yahoo.config.model.application.provider.FilesApplicationPackage; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.document.DataType; import com.yahoo.io.IOUtils; import com.yahoo.path.Path; @@ -225,17 +226,19 @@ public class ApplicationDeployTest { IOUtils.copyDirectory(new File(appPkg), tmp); ApplicationId applicationId = ApplicationId.from("tenant1", "application1", "instance1"); DeployData deployData = new DeployData("bar", - applicationId, - 13L, - false, - 1337L, - 3L); + applicationId, + Tags.fromString("tag1 tag2"), + 13L, + false, + 1337L, + 3L); FilesApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData); app.writeMetaData(); FilesApplicationPackage newApp = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData); ApplicationMetaData meta = newApp.getMetaData(); assertEquals("bar", meta.getDeployPath()); assertEquals(applicationId, meta.getApplicationId()); + assertEquals(Tags.fromString("tag1 tag2"), meta.getTags()); assertEquals(13L, (long) meta.getDeployTimestamp()); assertEquals(1337L, (long) meta.getGeneration()); assertEquals(3L, meta.getPreviousActiveGeneration()); diff --git a/config-model/src/test/schema-test-files/deployment-with-instances.xml b/config-model/src/test/schema-test-files/deployment-with-instances.xml index 39771ca9d41..f37ff9f6cc6 100644 --- a/config-model/src/test/schema-test-files/deployment-with-instances.xml +++ b/config-model/src/test/schema-test-files/deployment-with-instances.xml @@ -35,7 +35,7 @@ <delay hours='2'/> <parallel> - <instance id="three"> + <instance id="three" tags="a b"> <test/> <staging/> </instance> diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Tags.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Tags.java new file mode 100644 index 00000000000..3a54a0b9ebb --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Tags.java @@ -0,0 +1,64 @@ +// 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.Set; +import java.util.stream.Collectors; + +/** + * A deployment may have a list of tags associated with it. Config files may have variants for these tags similar + * to how they may have variants for instance and zone. + * + * @author bratseth + */ +public class Tags { + + private final Set<String> tags; + + public Tags(Set<String> tags) { + this.tags = Set.copyOf(tags); + } + + public boolean contains(String tag) { + return tags.contains(tag); + } + + public boolean intersects(Tags other) { + return this.tags.stream().anyMatch(other::contains); + } + + public boolean isEmpty() { return tags.isEmpty(); } + + public boolean containsAll(Tags other) { return tags.containsAll(other.tags); } + + /** Returns this as a space-separated string which can be used to recreate this by calling fromString(). */ + public String asString() { + return tags.stream().sorted().collect(Collectors.joining(" ")); + } + + @Override + public String toString() { + return asString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (other == null || other.getClass() != getClass()) return false; + return tags.equals(((Tags)other).tags); + } + + @Override + public int hashCode() { + return tags.hashCode(); + } + + public static Tags empty() { return new Tags(Set.of()); } + + /** + * Creates this from a space-separated string or null. */ + public static Tags fromString(String tagsString) { + if (tagsString == null || tagsString.isBlank()) return empty(); + return new Tags(Set.of(tagsString.trim().split(" +"))); + } + +} diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/TagsTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/TagsTest.java new file mode 100644 index 00000000000..a20e674c66e --- /dev/null +++ b/config-provisioning/src/test/java/com/yahoo/config/provision/TagsTest.java @@ -0,0 +1,57 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bratseth + */ +public class TagsTest { + + @Test + public void testEmpty() { + assertEquals(Tags.empty(), Tags.fromString(null)); + assertEquals(Tags.empty(), Tags.fromString("")); + assertEquals(Tags.empty(), Tags.fromString(" ")); + } + + @Test + public void testDeserialization() { + assertEquals(new Tags(Set.of("tag1", "tag2")), Tags.fromString(" tag1 tag2 ")); + } + + @Test + public void testSerialization() { + Tags tags = new Tags(Set.of("a", "tag2", "3")); + assertEquals(tags, Tags.fromString(tags.toString())); // Required by automatic serialization + assertEquals(tags, Tags.fromString(tags.asString())); // Required by automatic serialization + } + + @Test + public void testContains() { + Tags tags = new Tags(Set.of("a", "tag2", "3")); + assertTrue(tags.contains("a")); + assertTrue(tags.contains("tag2")); + assertTrue(tags.contains("3")); + assertFalse(tags.contains("other")); + + Tags subTags = new Tags(Set.of("a", "3")); + assertTrue(tags.containsAll(subTags)); + assertFalse(subTags.containsAll(tags)); + } + + @Test + public void testIntersects() { + Tags tags1 = new Tags(Set.of("a", "tag2", "3")); + Tags tags2 = new Tags(Set.of("a", "tag3")); + assertTrue(tags1.intersects(tags2)); + assertTrue(tags2.intersects(tags1)); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java index 2a15f724b29..cf79005b1ab 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java @@ -23,6 +23,7 @@ import com.yahoo.config.provision.InfraDeployer; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.config.provision.exception.ActivationConflictException; @@ -361,8 +362,11 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye } private PrepareResult deploy(File applicationDir, PrepareParams prepareParams, DeployHandlerLogger logger) { - ApplicationId applicationId = prepareParams.getApplicationId(); - long sessionId = createSession(applicationId, prepareParams.getTimeoutBudget(), applicationDir, logger); + long sessionId = createSession(prepareParams.getApplicationId(), + prepareParams.tags(), + prepareParams.getTimeoutBudget(), + applicationDir, + logger); Deployment deployment = prepare(sessionId, prepareParams, logger); if ( ! prepareParams.isDryRun()) @@ -870,21 +874,21 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye return sessionRepository.createSessionFromExisting(fromSession, internalRedeploy, timeoutBudget, deployLogger).getSessionId(); } - public long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, InputStream in, + public long createSession(ApplicationId applicationId, Tags tags, TimeoutBudget timeoutBudget, InputStream in, String contentType, DeployLogger logger) { File tempDir = uncheck(() -> Files.createTempDirectory("deploy")).toFile(); long sessionId; try { - sessionId = createSession(applicationId, timeoutBudget, decompressApplication(in, contentType, tempDir), logger); + sessionId = createSession(applicationId, tags, timeoutBudget, decompressApplication(in, contentType, tempDir), logger); } finally { cleanupTempDirectory(tempDir, logger); } return sessionId; } - public long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, File applicationDirectory, DeployLogger deployLogger) { + public long createSession(ApplicationId applicationId, Tags tags, TimeoutBudget timeoutBudget, File applicationDirectory, DeployLogger deployLogger) { SessionRepository sessionRepository = getTenant(applicationId).getSessionRepository(); - Session session = sessionRepository.createSessionFromApplicationPackage(applicationDirectory, applicationId, timeoutBudget, deployLogger); + Session session = sessionRepository.createSessionFromApplicationPackage(applicationDirectory, applicationId, tags, timeoutBudget, deployLogger); return session.getSessionId(); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java index f29b05b66af..71702e2926c 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java @@ -6,6 +6,7 @@ import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; @@ -46,7 +47,7 @@ public class SessionCreateHandler extends SessionHandler { @Override protected HttpResponse handlePOST(HttpRequest request) { - final TenantName tenantName = Utils.getTenantNameFromSessionRequest(request); + TenantName tenantName = Utils.getTenantNameFromSessionRequest(request); Utils.checkThatTenantExists(applicationRepository.tenantRepository(), tenantName); TimeoutBudget timeoutBudget = SessionHandler.getTimeoutBudget(request, zookeeperBarrierTimeout); boolean verbose = request.getBooleanProperty("verbose"); @@ -62,8 +63,12 @@ public class SessionCreateHandler extends SessionHandler { logger = DeployHandlerLogger.forTenant(tenantName, verbose); // TODO: Avoid using application id here at all ApplicationId applicationId = ApplicationId.from(tenantName, ApplicationName.defaultName(), InstanceName.defaultName()); - sessionId = applicationRepository.createSession(applicationId, timeoutBudget, request.getData(), - request.getHeader(ApplicationApiHandler.contentTypeHeader), logger); + sessionId = applicationRepository.createSession(applicationId, + Tags.empty(), + timeoutBudget, + request.getData(), + request.getHeader(ApplicationApiHandler.contentTypeHeader), + logger); } return new SessionCreateResponse(logger.slime(), tenantName, request.getHost(), request.getPort(), sessionId); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java index 175b6f6457f..ec673377af9 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -10,6 +10,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.security.X509CertificateUtils; @@ -41,6 +42,7 @@ public final class PrepareParams { static final String APPLICATION_NAME_PARAM_NAME = "applicationName"; static final String INSTANCE_PARAM_NAME = "instance"; + static final String TAGS_PARAM_NAME = "tags"; static final String IGNORE_VALIDATION_PARAM_NAME = "ignoreValidationErrors"; static final String DRY_RUN_PARAM_NAME = "dryRun"; static final String VERBOSE_PARAM_NAME = "verbose"; @@ -57,6 +59,7 @@ public final class PrepareParams { static final String CLOUD_ACCOUNT = "cloudAccount"; private final ApplicationId applicationId; + private final Tags tags; private final TimeoutBudget timeoutBudget; private final boolean ignoreValidationErrors; private final boolean dryRun; @@ -74,16 +77,27 @@ public final class PrepareParams { private final List<X509Certificate> operatorCertificates; private final Optional<CloudAccount> cloudAccount; - private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors, - boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion, + private PrepareParams(ApplicationId applicationId, + Tags tags, + TimeoutBudget timeoutBudget, + boolean ignoreValidationErrors, + boolean dryRun, + boolean verbose, + boolean isBootstrap, + Optional<Version> vespaVersion, List<ContainerEndpoint> containerEndpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, - Optional<DockerImage> dockerImageRepository, Optional<AthenzDomain> athenzDomain, - Optional<Quota> quota, List<TenantSecretStore> tenantSecretStores, - boolean force, boolean waitForResourcesInPrepare, List<X509Certificate> operatorCertificates, + Optional<DockerImage> dockerImageRepository, + Optional<AthenzDomain> athenzDomain, + Optional<Quota> quota, + List<TenantSecretStore> tenantSecretStores, + boolean force, + boolean waitForResourcesInPrepare, + List<X509Certificate> operatorCertificates, Optional<CloudAccount> cloudAccount) { this.timeoutBudget = timeoutBudget; this.applicationId = Objects.requireNonNull(applicationId); + this.tags = tags; this.ignoreValidationErrors = ignoreValidationErrors; this.dryRun = dryRun; this.verbose = verbose; @@ -110,6 +124,7 @@ public final class PrepareParams { private boolean force = false; private boolean waitForResourcesInPrepare = false; private ApplicationId applicationId = null; + private Tags tags = Tags.empty(); private TimeoutBudget timeoutBudget = new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(60)); private Optional<Version> vespaVersion = Optional.empty(); private List<ContainerEndpoint> containerEndpoints = null; @@ -128,6 +143,11 @@ public final class PrepareParams { return this; } + public Builder tags(Tags tags) { + this.tags = tags; + return this; + } + public Builder ignoreValidationErrors(boolean ignoreValidationErrors) { this.ignoreValidationErrors = ignoreValidationErrors; return this; @@ -258,11 +278,24 @@ public final class PrepareParams { } public PrepareParams build() { - return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun, - verbose, isBootstrap, vespaVersion, containerEndpoints, - endpointCertificateMetadata, dockerImageRepository, athenzDomain, - quota, tenantSecretStores, force, waitForResourcesInPrepare, - operatorCertificates, cloudAccount); + return new PrepareParams(applicationId, + tags, + timeoutBudget, + ignoreValidationErrors, + dryRun, + verbose, + isBootstrap, + vespaVersion, + containerEndpoints, + endpointCertificateMetadata, + dockerImageRepository, + athenzDomain, + quota, + tenantSecretStores, + force, + waitForResourcesInPrepare, + operatorCertificates, + cloudAccount); } } @@ -273,6 +306,7 @@ public final class PrepareParams { .verbose(request.getBooleanProperty(VERBOSE_PARAM_NAME)) .timeoutBudget(SessionHandler.getTimeoutBudget(request, barrierTimeout)) .applicationId(createApplicationId(request, tenant)) + .tags(Tags.fromString(request.getProperty(TAGS_PARAM_NAME))) .vespaVersion(request.getProperty(VESPA_VERSION_PARAM_NAME)) .containerEndpoints(request.getProperty(CONTAINER_ENDPOINTS_PARAM_NAME)) .endpointCertificateMetadata(request.getProperty(ENDPOINT_CERTIFICATE_METADATA_PARAM_NAME)) @@ -295,6 +329,7 @@ public final class PrepareParams { .verbose(booleanValue(params, VERBOSE_PARAM_NAME)) .timeoutBudget(SessionHandler.getTimeoutBudget(getTimeout(params, barrierTimeout))) .applicationId(createApplicationId(params, tenant)) + .tags(Tags.fromString(params.field(TAGS_PARAM_NAME).asString())) .vespaVersion(SlimeUtils.optionalString(params.field(VESPA_VERSION_PARAM_NAME)).orElse(null)) .containerEndpointList(deserialize(params.field(CONTAINER_ENDPOINTS_PARAM_NAME), ContainerEndpointSerializer::endpointListFromSlime, List.of())) .endpointCertificateMetadata(deserialize(params.field(ENDPOINT_CERTIFICATE_METADATA_PARAM_NAME), EndpointCertificateMetadataSerializer::fromSlime)) @@ -367,9 +402,9 @@ public final class PrepareParams { return applicationId.application().value(); } - public ApplicationId getApplicationId() { - return applicationId; - } + public ApplicationId getApplicationId() { return applicationId; } + + public Tags tags() { return tags; } /** Returns the Vespa version the nodes running the prepared system should have, or empty to use the system version */ public Optional<Version> vespaVersion() { return vespaVersion; } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java index 82faeae01e8..e4bbe120c11 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java @@ -12,6 +12,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; import com.yahoo.transaction.Transaction; @@ -119,6 +120,10 @@ public abstract class Session implements Comparable<Session> { sessionZooKeeperClient.writeApplicationId(applicationId); } + public void setTags(Tags tags) { + sessionZooKeeperClient.writeTags(tags); + } + void setApplicationPackageReference(FileReference applicationPackageReference) { sessionZooKeeperClient.writeApplicationPackageReference(Optional.ofNullable(applicationPackageReference)); } @@ -153,6 +158,10 @@ public abstract class Session implements Comparable<Session> { .orElseThrow(() -> new RuntimeException("Unable to read application id for session " + sessionId)); } + public Tags getTags() { + return sessionZooKeeperClient.readTags(); + } + /** Returns application id read from ZooKeeper. Will return Optional.empty() if not found */ public Optional<ApplicationId> getOptionalApplicationId() { try { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java index 71df5a8829e..8d023cac88a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -24,6 +24,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.net.HostName; @@ -157,6 +158,7 @@ public class SessionPreparer { final PrepareParams params; final ApplicationId applicationId; + final Tags tags; /** The repository part of docker image to be used for this deployment */ final Optional<DockerImage> dockerImageRepository; @@ -188,6 +190,7 @@ public class SessionPreparer { this.applicationPackage = applicationPackage; this.sessionZooKeeperClient = sessionZooKeeperClient; this.applicationId = params.getApplicationId(); + this.tags = params.tags(); this.dockerImageRepository = params.dockerImageRepository(); this.vespaVersion = params.vespaVersion().orElse(Vtag.currentVersion); this.containerEndpointsCache = new ContainerEndpointsCache(tenantPath, curator); @@ -224,7 +227,7 @@ public class SessionPreparer { if (! timeoutBudget.hasTimeLeft(step)) { String used = timeoutBudget.timesUsed(); throw new UncheckedTimeoutException("prepare timed out " + used + " after " + step + - " step (timeout " + timeoutBudget.timeout() + "): " + applicationId); + " step (timeout " + timeoutBudget.timeout() + "): " + applicationId); } } @@ -248,7 +251,7 @@ public class SessionPreparer { this.preprocessedApplicationPackage = applicationPackage.preprocess(zone, logger); } catch (IOException | RuntimeException e) { throw new IllegalArgumentException("Error preprocessing application package for " + applicationId + - ", session " + sessionZooKeeperClient.sessionId(), e); + ", session " + sessionZooKeeperClient.sessionId(), e); } checkTimeout("preprocess"); } @@ -318,10 +321,11 @@ public class SessionPreparer { void vespaPreprocess(File appDir, File inputXml, ApplicationMetaData metaData) { try { new XmlPreProcessor(appDir, - inputXml, - metaData.getApplicationId().instance(), - zone.environment(), - zone.region()) + inputXml, + metaData.getApplicationId().instance(), + zone.environment(), + zone.region(), + metaData.getTags()) .run(); } catch (ParserConfigurationException | IOException | SAXException | TransformerException e) { throw new RuntimeException(e); @@ -346,6 +350,7 @@ public class SessionPreparer { writeStateToZooKeeper(sessionZooKeeperClient, preprocessedApplicationPackage, applicationId, + tags, filereference, dockerImageRepository, vespaVersion, @@ -387,6 +392,7 @@ public class SessionPreparer { private void writeStateToZooKeeper(SessionZooKeeperClient zooKeeperClient, ApplicationPackage applicationPackage, ApplicationId applicationId, + Tags tags, FileReference fileReference, Optional<DockerImage> dockerImageRepository, Version vespaVersion, @@ -403,6 +409,7 @@ public class SessionPreparer { zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts); // Note: When changing the below you need to also change similar calls in SessionRepository.createSessionFromExisting() zooKeeperClient.writeApplicationId(applicationId); + zooKeeperClient.writeTags(tags); zooKeeperClient.writeApplicationPackageReference(Optional.of(fileReference)); zooKeeperClient.writeVespaVersion(vespaVersion); zooKeeperClient.writeDockerImageRepository(dockerImageRepository); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java index 97cebbbe174..15be909c069 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java @@ -13,6 +13,7 @@ import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.model.application.provider.DeployData; import com.yahoo.config.model.application.provider.FilesApplicationPackage; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.secretstore.SecretStore; @@ -283,10 +284,17 @@ public class SessionRepository { TimeoutBudget timeoutBudget, DeployLogger deployLogger) { ApplicationId existingApplicationId = existingSession.getApplicationId(); + Tags existingTags = existingSession.getTags(); File existingApp = getSessionAppDir(existingSession.getSessionId()); - LocalSession session = createSessionFromApplication(existingApp, existingApplicationId, internalRedeploy, timeoutBudget, deployLogger); + LocalSession session = createSessionFromApplication(existingApp, + existingApplicationId, + existingTags, + internalRedeploy, + timeoutBudget, + deployLogger); // Note: Setters below need to be kept in sync with calls in SessionPreparer.writeStateToZooKeeper() session.setApplicationId(existingApplicationId); + session.setTags(existingTags); session.setApplicationPackageReference(existingSession.getApplicationPackageReference()); session.setVespaVersion(existingSession.getVespaVersion()); session.setDockerImageRepository(existingSession.getDockerImageRepository()); @@ -307,19 +315,20 @@ public class SessionRepository { */ public LocalSession createSessionFromApplicationPackage(File applicationDirectory, ApplicationId applicationId, + Tags tags, TimeoutBudget timeoutBudget, DeployLogger deployLogger) { applicationRepo.createApplication(applicationId); - return createSessionFromApplication(applicationDirectory, applicationId, false, timeoutBudget, deployLogger); + return createSessionFromApplication(applicationDirectory, applicationId, tags, false, timeoutBudget, deployLogger); } /** * Creates a local session based on a remote session and the distributed application package. * Does not wait for session being created on other servers. */ - private void createLocalSession(File applicationFile, ApplicationId applicationId, long sessionId) { + private void createLocalSession(File applicationFile, ApplicationId applicationId, Tags tags, long sessionId) { try { - ApplicationPackage applicationPackage = createApplicationPackage(applicationFile, applicationId, sessionId, false, Optional.empty()); + ApplicationPackage applicationPackage = createApplicationPackage(applicationFile, applicationId, tags, sessionId, false, Optional.empty()); createLocalSession(sessionId, applicationPackage); } catch (Exception e) { throw new RuntimeException("Error creating session " + sessionId, e); @@ -707,12 +716,13 @@ public class SessionRepository { private ApplicationPackage createApplication(File userDir, File configApplicationDir, ApplicationId applicationId, + Tags tags, long sessionId, Optional<Long> currentlyActiveSessionId, boolean internalRedeploy, Optional<DeployLogger> deployLogger) { long deployTimestamp = System.currentTimeMillis(); - DeployData deployData = new DeployData(userDir.getAbsolutePath(), applicationId, deployTimestamp, internalRedeploy, + DeployData deployData = new DeployData(userDir.getAbsolutePath(), applicationId, tags, deployTimestamp, internalRedeploy, sessionId, currentlyActiveSessionId.orElse(nonExistingActiveSessionId)); FilesApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(configApplicationDir, deployData); validateFileExtensions(applicationId, deployLogger, app); @@ -740,13 +750,14 @@ public class SessionRepository { private LocalSession createSessionFromApplication(File applicationDirectory, ApplicationId applicationId, + Tags tags, boolean internalRedeploy, TimeoutBudget timeoutBudget, DeployLogger deployLogger) { long sessionId = getNextSessionId(); try { ensureSessionPathDoesNotExist(sessionId); - ApplicationPackage app = createApplicationPackage(applicationDirectory, applicationId, sessionId, internalRedeploy, Optional.of(deployLogger)); + ApplicationPackage app = createApplicationPackage(applicationDirectory, applicationId, tags, sessionId, internalRedeploy, Optional.of(deployLogger)); log.log(Level.FINE, () -> TenantRepository.logPre(tenantName) + "Creating session " + sessionId + " in ZooKeeper"); SessionZooKeeperClient sessionZKClient = createSessionZooKeeperClient(sessionId); sessionZKClient.createNewSession(clock.instant()); @@ -762,6 +773,7 @@ public class SessionRepository { private ApplicationPackage createApplicationPackage(File applicationDirectory, ApplicationId applicationId, + Tags tags, long sessionId, boolean internalRedeploy, Optional<DeployLogger> deployLogger) throws IOException { @@ -774,6 +786,7 @@ public class SessionRepository { ApplicationPackage applicationPackage = createApplication(applicationDirectory, userApplicationDir, applicationId, + tags, sessionId, activeSessionId, internalRedeploy, @@ -885,7 +898,7 @@ public class SessionRepository { ApplicationId applicationId = sessionZKClient.readApplicationId() .orElseThrow(() -> new RuntimeException("Could not find application id for session " + sessionId)); log.log(Level.FINE, () -> "Creating local session for tenant '" + tenantName + "' with session id " + sessionId); - createLocalSession(sessionDir, applicationId, sessionId); + createLocalSession(sessionDir, applicationId, sessionZKClient.readTags(), sessionId); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java index 988d13b1978..9218b03af1e 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java @@ -14,6 +14,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; import com.yahoo.slime.SlimeUtils; @@ -57,6 +58,7 @@ public class SessionZooKeeperClient { // NOTE: Any state added here MUST also be propagated in com.yahoo.vespa.config.server.deploy.Deployment.prepare() static final String APPLICATION_ID_PATH = "applicationId"; + static final String TAGS_PATH = "tags"; static final String APPLICATION_PACKAGE_REFERENCE_PATH = "applicationPackageReference"; private static final String VERSION_PATH = "version"; private static final String CREATE_TIME_PATH = "createTime"; @@ -171,6 +173,20 @@ public class SessionZooKeeperClient { return curator.getData(applicationIdPath()).map(d -> ApplicationId.fromSerializedForm(Utf8.toString(d))); } + private Path tagsPath() { + return sessionPath.append(TAGS_PATH); + } + + public void writeTags(Tags tags) { + curator.set(tagsPath(), Utf8.toBytes(tags.asString())); + } + + public Tags readTags() { + Optional<byte[]> data = curator.getData(tagsPath()); + if (data.isEmpty()) return Tags.empty(); + return Tags.fromString(Utf8.toString(data.get())); + } + void writeApplicationPackageReference(Optional<FileReference> applicationPackageReference) { applicationPackageReference.ifPresent( reference -> curator.set(applicationPackageReferencePath(), Utf8.toBytes(reference.value()))); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java index 99487230c5d..b185bb5915d 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java @@ -17,6 +17,7 @@ import com.yahoo.config.provision.HostFilter; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NetworkPorts; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.exception.ActivationConflictException; import com.yahoo.container.jdisc.HttpResponse; @@ -847,7 +848,7 @@ public class ApplicationRepositoryTest { } private long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, File app) { - return applicationRepository.createSession(applicationId, timeoutBudget, app, new BaseDeployLogger()); + return applicationRepository.createSession(applicationId, Tags.empty(), timeoutBudget, app, new BaseDeployLogger()); } private long createSessionFromExisting(ApplicationId applicationId, TimeoutBudget timeoutBudget) { diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java index fd6440a9632..653753c97e7 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java @@ -13,6 +13,7 @@ import com.yahoo.config.model.application.provider.MockFileRegistry; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.Tags; import com.yahoo.path.Path; import com.yahoo.text.Utf8; import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage; @@ -59,6 +60,7 @@ public class ZooKeeperClientTest { ApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(new File("src/test/apps/zkfeed"), new DeployData("/bar/baz", ApplicationId.from("default", "appName", "default"), + Tags.fromString("tag1 tag2"), 1345L, true, 3L, @@ -121,6 +123,7 @@ public class ZooKeeperClientTest { assertTrue(metaData.getChecksum().length() > 0); assertTrue(metaData.isInternalRedeploy()); assertEquals("/bar/baz", metaData.getDeployPath()); + assertEquals(Tags.fromString("tag1 tag2"), metaData.getTags()); assertEquals(1345, metaData.getDeployTimestamp().longValue()); assertEquals(3, metaData.getGeneration().longValue()); assertEquals(2, metaData.getPreviousActiveGeneration()); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java index 1c71ef0b7fb..816f7e3dcec 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.config.application.api.ApplicationMetaData; import com.yahoo.config.model.application.provider.BaseDeployLogger; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.HttpResponse; @@ -131,6 +132,7 @@ public class SessionActiveHandlerTest { void invoke() { long sessionId = applicationRepository.createSession(applicationId(), + Tags.empty(), new TimeoutBudget(clock, Duration.ofSeconds(10)), testApp, new BaseDeployLogger()); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java index 8e39460db71..525a969ed1e 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java @@ -10,6 +10,7 @@ import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NodeAllocationException; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.http.HttpRequest; @@ -187,7 +188,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { } @Test - public void require_that_preparing_with_multiple_tenants_work() throws Exception { + public void prepare_with_multiple_tenants() throws Exception { SessionHandler handler = createHandler(); TenantName defaultTenant = TenantName.from("test2"); @@ -206,7 +207,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { assertEquals(sessionId, sessionId2); // Want to test when they are equal (but for different tenants) pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId2 + - "/prepared?applicationName=" + applicationName; + "/prepared?applicationName=" + applicationName; response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix)); assertNotNull(response); assertEquals(SessionHandlerTest.getRenderedString(response), OK, response.getStatus()); @@ -214,7 +215,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { ApplicationId applicationId3 = ApplicationId.from(tenant.value(), applicationName, "quux"); long sessionId3 = createSession(applicationId3); pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId3 + - "/prepared?applicationName=" + applicationName + "&instance=quux"; + "/prepared?applicationName=" + applicationName + "&instance=quux"; response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix)); assertNotNull(response); assertEquals(SessionHandlerTest.getRenderedString(response), OK, response.getStatus()); @@ -324,7 +325,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { } private long createSession(ApplicationId applicationId) { - return applicationRepository.createSession(applicationId, timeoutBudget, app, new BaseDeployLogger()); + return applicationRepository.createSession(applicationId, Tags.empty(), timeoutBudget, app, new BaseDeployLogger()); } private static class FailingSessionPrepareHandler extends SessionPrepareHandler { diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java index e5b550fdc1a..34921db1bb7 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.model.api.EndpointCertificateMetadata; import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; @@ -62,6 +63,7 @@ public class PrepareParamsTest { PrepareParams prepareParams = createParams("http://foo:19071/application/v2/", TenantName.defaultName()); assertEquals(ApplicationId.defaultId(), prepareParams.getApplicationId()); + assertTrue(prepareParams.tags().isEmpty()); assertFalse(prepareParams.isDryRun()); assertFalse(prepareParams.isVerbose()); assertFalse(prepareParams.ignoreValidationErrors()); @@ -72,6 +74,18 @@ public class PrepareParamsTest { } @Test + public void testTagsParsing() throws IOException { + var prepareParams = createParams(request + "&" + PrepareParams.TAGS_PARAM_NAME + "=tag1%20tag2", TenantName.from("foo")); + assertEquals(Tags.fromString("tag1 tag2"), prepareParams.tags()); + + // Verify using json object + var slime = SlimeUtils.jsonToSlime(json); + slime.get().setString(PrepareParams.TAGS_PARAM_NAME, "tag1 tag2"); + PrepareParams prepareParamsJson = PrepareParams.fromJson(SlimeUtils.toJsonBytes(slime), TenantName.from("foo"), Duration.ofSeconds(60)); + assertPrepareParamsEqual(prepareParams, prepareParamsJson); + } + + @Test public void testCorrectParsingWithContainerEndpoints() throws IOException { var endpoints = List.of(new ContainerEndpoint("qrs1", ApplicationClusterEndpoint.Scope.global, List.of("c1.example.com", @@ -207,6 +221,7 @@ public class PrepareParamsTest { assertEquals(urlParams.force(), jsonParams.force()); assertEquals(urlParams.waitForResourcesInPrepare(), jsonParams.waitForResourcesInPrepare()); assertEquals(urlParams.getApplicationId(), jsonParams.getApplicationId()); + assertEquals(urlParams.tags(), jsonParams.tags()); assertEquals(urlParams.getTimeoutBudget().timeout(), jsonParams.getTimeoutBudget().timeout()); assertEquals(urlParams.vespaVersion(), jsonParams.vespaVersion()); assertEquals(urlParams.containerEndpoints(), jsonParams.containerEndpoints()); diff --git a/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java b/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java index bf0272f4f66..7dd1772a53e 100644 --- a/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java +++ b/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java @@ -22,9 +22,11 @@ import java.util.Map; * @author baldersheim */ class Json2SingleLevelMap { + private static final ObjectMapper jsonMapper = new ObjectMapper().configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); private final byte [] buf; private final JsonParser parser; + Json2SingleLevelMap(InputStream data) { try { buf = data.readAllBytes(); @@ -33,6 +35,7 @@ class Json2SingleLevelMap { throw new RuntimeException("Problem reading POSTed data", e); } } + Map<String, String> parse() { try { Map<String, String> map = new HashMap<>(); @@ -47,16 +50,17 @@ class Json2SingleLevelMap { throw new RuntimeException("Problem reading POSTed data", e); } } + void parse(Map<String, String> map, String parent) throws IOException { for (parser.nextToken(); parser.getCurrentToken() != JsonToken.END_OBJECT; parser.nextToken()) { String fieldName = parent + parser.getCurrentName(); JsonToken token = parser.nextToken(); if ((token == JsonToken.VALUE_STRING) || - (token == JsonToken.VALUE_NUMBER_FLOAT) || - (token == JsonToken.VALUE_NUMBER_INT) || - (token == JsonToken.VALUE_TRUE) || - (token == JsonToken.VALUE_FALSE) || - (token == JsonToken.VALUE_NULL)) { + (token == JsonToken.VALUE_NUMBER_FLOAT) || + (token == JsonToken.VALUE_NUMBER_INT) || + (token == JsonToken.VALUE_TRUE) || + (token == JsonToken.VALUE_FALSE) || + (token == JsonToken.VALUE_NULL)) { map.put(fieldName, parser.getText()); } else if (token == JsonToken.START_ARRAY) { map.put(fieldName, skipChildren(parser, buf)); @@ -71,6 +75,7 @@ class Json2SingleLevelMap { } } } + private String skipChildren(JsonParser parser, byte [] input) throws IOException { JsonLocation start = parser.getCurrentLocation(); parser.skipChildren(); @@ -78,4 +83,5 @@ class Json2SingleLevelMap { int offset = (int)start.getByteOffset() - 1; return new String(input, offset, (int)(end.getByteOffset() - offset), StandardCharsets.UTF_8); } + } diff --git a/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java b/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java index afd25132510..bcdc84c1808 100644 --- a/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java +++ b/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java @@ -69,8 +69,7 @@ public class NGramSearcher extends Searcher { private boolean rewriteToNGramMatching(Item item, int indexInParent, IndexFacts.Session indexFacts, Query query) { boolean rewritten = false; - if (item instanceof SegmentItem) { // handle CJK segmented terms which should be grams instead - SegmentItem segments = (SegmentItem)item; + if (item instanceof SegmentItem segments) { // handle CJK segmented terms which should be grams instead Index index = indexFacts.getIndex(segments.getIndexName()); if (index.isNGram()) { Item grams = splitToGrams(segments, toLowerCase(segments.getRawWord()), index.getGramSize(), query); @@ -78,13 +77,11 @@ public class NGramSearcher extends Searcher { rewritten = true; } } - else if (item instanceof CompositeItem) { - CompositeItem composite = (CompositeItem)item; + else if (item instanceof CompositeItem composite) { for (int i=0; i<composite.getItemCount(); i++) rewritten = rewriteToNGramMatching(composite.getItem(i), i, indexFacts, query) || rewritten; } - else if (item instanceof TermItem) { - TermItem term = (TermItem)item; + else if (item instanceof TermItem term) { Index index = indexFacts.getIndex(term.getIndexName()); if (index.isNGram()) { Item grams = splitToGrams(term,term.stringValue(), index.getGramSize(), query); @@ -149,11 +146,10 @@ public class NGramSearcher extends Searcher { } private void replaceItemByGrams(Item item, Item grams, int indexInParent) { - if (!(grams instanceof CompositeItem) || !(item.getParent() instanceof PhraseItem)) { // usually, simply replace + if (!(grams instanceof CompositeItem) || !(item.getParent() instanceof PhraseItem phraseParent)) { // usually, simply replace item.getParent().setItem(indexInParent, grams); } else { // but if the parent is a phrase, we cannot add the AND to it, so add each gram to the phrase - PhraseItem phraseParent = (PhraseItem)item.getParent(); phraseParent.removeItem(indexInParent); int addedTerms = 0; for (Iterator<Item> i = ((CompositeItem)grams).getItemIterator(); i.hasNext(); ) { diff --git a/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java b/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java index adab4d59ec0..205c65a6256 100644 --- a/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java +++ b/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java @@ -67,7 +67,7 @@ public class AsyncExecution { * Creates an async execution. * * @param chain the chain to execute - * @param context the the context of this + * @param context the context of this */ public AsyncExecution(Chain<? extends Searcher> chain, Execution.Context context) { this(context, chain); diff --git a/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java b/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java index ae2a8800bbc..5b972e40774 100644 --- a/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java @@ -65,7 +65,7 @@ import static org.junit.jupiter.api.Assertions.*; public class QueryTestCase { @Test - void testIt() throws Exception { + void testIt() { JSONObject newroot = new JSONObject("{\"key\": 3}"); var hit = new FastHit(); hit.setField("data", (JsonProducer) s -> s.append(newroot)); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java index a35d01f6891..d3331c3cfd4 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; @@ -22,12 +23,14 @@ import static java.util.Objects.requireNonNull; /** * Data pertaining to a deployment to be done on a config server. + * Accessor names must match the names in com.yahoo.vespa.config.server.session.PrepareParams. * * @author jonmv */ public class DeploymentData { private final ApplicationId instance; + private final Tags tags; private final ZoneId zone; private final byte[] applicationPackage; private final Version platform; @@ -41,7 +44,7 @@ public class DeploymentData { private final Optional<CloudAccount> cloudAccount; private final boolean dryRun; - public DeploymentData(ApplicationId instance, ZoneId zone, byte[] applicationPackage, Version platform, + public DeploymentData(ApplicationId instance, Tags tags, ZoneId zone, byte[] applicationPackage, Version platform, Set<ContainerEndpoint> containerEndpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, Optional<DockerImage> dockerImageRepo, @@ -51,6 +54,7 @@ public class DeploymentData { List<X509Certificate> operatorCertificates, Optional<CloudAccount> cloudAccount, boolean dryRun) { this.instance = requireNonNull(instance); + this.tags = requireNonNull(tags); this.zone = requireNonNull(zone); this.applicationPackage = requireNonNull(applicationPackage); this.platform = requireNonNull(platform); @@ -69,6 +73,8 @@ public class DeploymentData { return instance; } + public Tags tags() { return tags; } + public ZoneId zone() { return zone; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java index e3a0ad7cb84..66e62ff7b95 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java @@ -65,7 +65,7 @@ public class Application { Set.of(), OptionalLong.empty(), RevisionHistory.empty(), List.of()); } - // DO NOT USE! For serialization purposes, only. + // Do not use directly - edit through LockedApplication. public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId, @@ -230,11 +230,8 @@ public class Application { @Override public boolean equals(Object o) { if (this == o) return true; - if (! (o instanceof Application)) return false; - - Application that = (Application) o; - - return id.equals(that.id); + if (! (o instanceof Application other)) return false; + return id.equals(other.id); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 19775ef420d..d8234e4f269 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -12,6 +12,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.log.LogLevel; @@ -166,10 +167,11 @@ public class ApplicationController { int count = 0; for (TenantAndApplicationId id : curator.readApplicationIds()) { lockApplicationIfPresent(id, application -> { - for (InstanceName instance : application.get().deploymentSpec().instanceNames()) - if ( ! application.get().instances().containsKey(instance)) - application = withNewInstance(application, id.instance(instance)); - + for (var declaredInstance : application.get().deploymentSpec().instances()) + if ( ! application.get().instances().containsKey(declaredInstance.name())) + application = withNewInstance(application, + id.instance(declaredInstance.name()), + declaredInstance.tags()); store(application); }); count++; @@ -451,14 +453,14 @@ public class ApplicationController { * * @throws IllegalArgumentException if the instance already exists, or has an invalid instance name. */ - public void createInstance(ApplicationId id) { + public void createInstance(ApplicationId id, Tags tags) { lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { - store(withNewInstance(application, id)); + store(withNewInstance(application, id, tags)); }); } /** Returns given application with a new instance */ - public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance) { + public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance, Tags tags) { if (instance.instance().isTester()) throw new IllegalArgumentException("'" + instance + "' is a tester application!"); InstanceId.validate(instance.instance().value()); @@ -469,7 +471,7 @@ public class ApplicationController { throw new IllegalArgumentException("Could not create '" + instance + "': Instance " + dashToUnderscore(instance) + " already exists"); log.info("Created " + instance); - return application.withNewInstance(instance.instance()); + return application.withNewInstance(instance.instance(), tags); } /** Deploys an application package for an existing application instance. */ @@ -496,10 +498,11 @@ public class ApplicationController { ApplicationPackage applicationPackage = new ApplicationPackage(applicationStore.get(deployment, revision)); AtomicReference<RevisionId> lastRevision = new AtomicReference<>(); + Instance instance; try (Mutex lock = lock(applicationId)) { LockedApplication application = new LockedApplication(requireApplication(applicationId), lock); application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set); - Instance instance = application.get().require(job.application().instance()); + instance = application.get().require(job.application().instance()); if ( ! applicationPackage.trustedCertificates().isEmpty() && run.testerCertificate().isPresent()) @@ -512,7 +515,7 @@ public class ApplicationController { } // Release application lock while doing the deployment, which is a lengthy task. // Carry out deployment without holding the application lock. - ActivateResult result = deploy(job.application(), applicationPackage, zone, platform, containerEndpoints, + ActivateResult result = deploy(job.application(), instance.tags(), applicationPackage, zone, platform, containerEndpoints, endpointCertificateMetadata, run.isDryRun()); endpointCertificateMetadata.ifPresent(e -> deployLogger.accept("Using CA signed certificate version %s".formatted(e.version()))); @@ -543,9 +546,9 @@ public class ApplicationController { lockApplicationOrThrow(applicationId, application -> store(application.with(job.application().instance(), - instance -> instance.withNewDeployment(zone, revision, platform, - clock.instant(), warningsFrom(result), - quotaUsage)))); + i -> i.withNewDeployment(zone, revision, platform, + clock.instant(), warningsFrom(result), + quotaUsage)))); return result; } } @@ -557,16 +560,19 @@ public class ApplicationController { application = application.with(applicationPackage.deploymentSpec()); application = application.with(applicationPackage.validationOverrides()); - var existingInstances = application.get().instances().keySet(); - var declaredInstances = applicationPackage.deploymentSpec().instanceNames(); - for (var name : declaredInstances) - if ( ! existingInstances.contains(name)) - application = withNewInstance(application, application.get().id().instance(name)); + var existingInstances = application.get().instances(); + var declaredInstances = applicationPackage.deploymentSpec().instances(); + for (var declaredInstance : declaredInstances) { + if ( ! existingInstances.containsKey(declaredInstance.name())) + application = withNewInstance(application, application.get().id().instance(declaredInstance.name()), declaredInstance.tags()); + else if ( ! existingInstances.get(declaredInstance.name()).tags().equals(declaredInstance.tags())) + application = application.with(declaredInstance.name(), instance -> instance.with(declaredInstance.tags())); + } // Delete zones not listed in DeploymentSpec, if allowed // We do this at deployment time for externally built applications, and at submission time // for internally built ones, to be able to return a validation failure message when necessary - for (InstanceName name : existingInstances) { + for (InstanceName name : existingInstances.keySet()) { application = withoutDeletedDeployments(application, name); } @@ -599,7 +605,7 @@ public class ApplicationController { ApplicationPackage applicationPackage = new ApplicationPackage( artifactRepository.getSystemApplicationPackage(application.id(), zone, version) ); - return deploy(application.id(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty(), false); + return deploy(application.id(), Tags.empty(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty(), false); } else { throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString()); } @@ -607,10 +613,10 @@ public class ApplicationController { /** Deploys the given tester application to the given zone. */ public ActivateResult deployTester(TesterId tester, ApplicationPackage applicationPackage, ZoneId zone, Version platform) { - return deploy(tester.id(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty(), false); + return deploy(tester.id(), Tags.empty(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty(), false); } - private ActivateResult deploy(ApplicationId application, ApplicationPackage applicationPackage, + private ActivateResult deploy(ApplicationId application, Tags tags, ApplicationPackage applicationPackage, ZoneId zone, Version platform, Set<ContainerEndpoint> endpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, boolean dryRun) { @@ -646,7 +652,7 @@ public class ApplicationController { .collect(toList()); Optional<CloudAccount> cloudAccount = decideCloudAccountOf(deployment, applicationPackage.deploymentSpec()); ConfigServer.PreparedApplication preparedApplication = - configServer.deploy(new DeploymentData(application, zone, applicationPackage.zippedContent(), platform, + configServer.deploy(new DeploymentData(application, tags, zone, applicationPackage.zippedContent(), platform, endpoints, endpointCertificateMetadata, dockerImageRepo, domain, deploymentQuota, tenantSecretStores, operatorCertificates, cloudAccount, dryRun)); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java index d66d1491f73..430bafe5c44 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; @@ -40,6 +41,7 @@ import java.util.stream.Collectors; public class Instance { private final ApplicationId id; + private final Tags tags; private final Map<ZoneId, Deployment> deployments; private final List<AssignedRotation> rotations; private final RotationStatus rotationStatus; @@ -47,14 +49,15 @@ public class Instance { private final Change change; /** Creates an empty instance */ - public Instance(ApplicationId id) { - this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty()); + public Instance(ApplicationId id, Tags tags) { + this(id, tags, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty()); } /** Creates an empty instance*/ - public Instance(ApplicationId id, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses, + public Instance(ApplicationId id, Tags tags, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses, List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change) { this.id = Objects.requireNonNull(id, "id cannot be null"); + this.tags = Objects.requireNonNull(tags, "tags cannot be null"); this.deployments = Objects.requireNonNull(deployments, "deployments cannot be null").stream() .collect(Collectors.toUnmodifiableMap(Deployment::zone, Function.identity())); this.jobPauses = Map.copyOf(Objects.requireNonNull(jobPauses, "deploymentJobs cannot be null")); @@ -63,6 +66,10 @@ public class Instance { this.change = Objects.requireNonNull(change, "change cannot be null"); } + public Instance with(Tags tags) { + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); + } + public Instance withNewDeployment(ZoneId zone, RevisionId revision, Version version, Instant instant, Map<DeploymentMetrics.Warning, Integer> warnings, QuotaUsage quotaUsage) { // Use info from previous deployment if available, otherwise create a new one. @@ -87,7 +94,7 @@ public class Instance { else jobPauses.remove(jobType); - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } public Instance recordActivityAt(Instant instant, ZoneId zone) { @@ -118,15 +125,15 @@ public class Instance { } public Instance with(List<AssignedRotation> assignedRotations) { - return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, assignedRotations, rotationStatus, change); } public Instance with(RotationStatus rotationStatus) { - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } public Instance withChange(Change change) { - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } private Instance with(Deployment deployment) { @@ -136,13 +143,15 @@ public class Instance { } private Instance with(Map<ZoneId, Deployment> deployments) { - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } public ApplicationId id() { return id; } public InstanceName name() { return id.instance(); } + public Tags tags() { return tags; } + /** Returns an immutable map of the current deployments of this */ public Map<ZoneId, Deployment> deployments() { return deployments; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java index 3e822415e96..fa702a166d2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; @@ -88,9 +89,9 @@ public class LockedApplication { projectId, revisions, instances.values()); } - LockedApplication withNewInstance(InstanceName instance) { + LockedApplication withNewInstance(InstanceName instance, Tags tags) { var instances = new HashMap<>(this.instances); - instances.put(instance, new Instance(id.instance(instance))); + instances.put(instance, new Instance(id.instance(instance), tags)); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, instances, revisions); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java index 4bc9aeb00e4..c2c95a0c4bf 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java @@ -16,6 +16,7 @@ import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -202,7 +203,8 @@ public class ApplicationPackage { new InputStreamReader(new ByteArrayInputStream(servicesXml.content()), UTF_8), InstanceName.defaultName(), Environment.prod, - RegionName.defaultName()) + RegionName.defaultName(), + Tags.empty()) .run(); // Populates the zip archive cache with files that would be included. } catch (IllegalArgumentException e) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index 4c84f311458..edddaa5dee7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -7,6 +7,7 @@ import com.yahoo.component.VersionCompatibility; import com.yahoo.concurrent.UncheckedTimeoutException; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.controller.Application; @@ -320,15 +321,13 @@ public class JobController { public List<ApplicationId> instances() { return controller.applications().readable().stream() .flatMap(application -> application.instances().values().stream()) - .map(Instance::id) - .collect(toUnmodifiableList()); + .map(Instance::id).toList(); } /** Returns all job types which have been run for the given application. */ private List<JobType> jobs(ApplicationId id) { return JobType.allIn(controller.zoneRegistry()).stream() - .filter(type -> last(id, type).isPresent()) - .collect(toUnmodifiableList()); + .filter(type -> last(id, type).isPresent()).toList(); } /** Returns an immutable map of all known runs for the given application and job type. */ @@ -339,9 +338,8 @@ public class JobController { /** Lists the start time of non-redeployment runs of the given job, in order of increasing age. */ public List<Instant> jobStarts(JobId id) { return runs(id).descendingMap().values().stream() - .filter(run -> ! run.isRedeployment()) - .map(Run::start) - .collect(toUnmodifiableList()); + .filter(run -> !run.isRedeployment()) + .map(Run::start).toList(); } /** Returns when given deployment last started deploying, falling back to time of deployment if it cannot be determined from job runs */ @@ -697,7 +695,7 @@ public class JobController { controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { if ( ! application.get().instances().containsKey(id.instance())) - application = controller.applications().withNewInstance(application, id); + application = controller.applications().withNewInstance(application, id, Tags.empty()); // TODO(mpolden): Enable for public CD once all tests have been updated if (controller.system() != SystemName.PublicCd) { controller.applications().validatePackage(applicationPackage, application.get()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java index 5aa847f648a..37c45f38e36 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java @@ -9,6 +9,7 @@ import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyUtils; import com.yahoo.slime.ArrayTraverser; @@ -97,6 +98,7 @@ public class ApplicationSerializer { // Instance fields private static final String instanceNameField = "instanceName"; + private static final String tagsField = "tags"; private static final String deploymentsField = "deployments"; private static final String deploymentJobsField = "deploymentJobs"; // TODO jonmv: clean up serialisation format private static final String assignedRotationsField = "assignedRotations"; @@ -184,6 +186,7 @@ public class ApplicationSerializer { for (Instance instance : application.instances().values()) { Cursor instanceObject = array.addObject(); instanceObject.setString(instanceNameField, instance.name().value()); + instanceObject.setString(tagsField, instance.tags().asString()); deploymentsToSlime(instance.deployments().values(), instanceObject.setArray(deploymentsField)); toSlime(instance.jobPauses(), instanceObject.setObject(deploymentJobsField)); assignedRotationsToSlime(instance.rotations(), instanceObject); @@ -380,12 +383,14 @@ public class ApplicationSerializer { List<Instance> instances = new ArrayList<>(); field.traverse((ArrayTraverser) (name, object) -> { InstanceName instanceName = InstanceName.from(object.field(instanceNameField).asString()); - List<Deployment> deployments = deploymentsFromSlime(object.field(deploymentsField), id.instance(instanceName)); + Tags tags = Tags.fromString(object.field(tagsField).asString()); + List < Deployment > deployments = deploymentsFromSlime(object.field(deploymentsField), id.instance(instanceName)); Map<JobType, Instant> jobPauses = jobPausesFromSlime(object.field(deploymentJobsField)); List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(object); RotationStatus rotationStatus = rotationStatusFromSlime(object); Change change = changeFromSlime(object.field(deployingField)); instances.add(new Instance(id.instance(instanceName), + tags, deployments, jobPauses, assignedRotations, diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index e8fe7b7d5f0..81fb72e19fd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -22,6 +22,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NodeResources; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneId; @@ -2035,7 +2036,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (controller.applications().getApplication(applicationId).isEmpty()) createApplication(tenantName, applicationName, request); - controller.applications().createInstance(applicationId.instance(instanceName)); + controller.applications().createInstance(applicationId.instance(instanceName), Tags.empty()); Slime slime = new Slime(); toSlime(applicationId.instance(instanceName), slime.setObject(), request); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java index 5380cf4ee27..0c908b1f035 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneApi; @@ -370,7 +371,7 @@ public final class ControllerTester { public Application createApplication(String tenant, String applicationName, String instanceName) { Application application = createApplication(tenant, applicationName); - controller().applications().createInstance(application.id().instance(instanceName)); + controller().applications().createInstance(application.id().instance(instanceName), Tags.empty()); return application; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java index 3163c8b6439..db45933f498 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; @@ -63,8 +64,8 @@ public class DeploymentQuotaCalculatorTest { var existing_dev_deployment = new Application(TenantAndApplicationId.from(ApplicationId.defaultId()), Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(1, 1), Set.of(), OptionalLong.empty(), RevisionHistory.empty(), - List.of(new Instance(ApplicationId.defaultId()).withNewDeployment(ZoneId.from(Environment.dev, RegionName.defaultName()), - RevisionId.forProduction(1), Version.emptyVersion, Instant.EPOCH, Map.of(), QuotaUsage.create(0.53d)))); + List.of(new Instance(ApplicationId.defaultId(), Tags.empty()).withNewDeployment(ZoneId.from(Environment.dev, RegionName.defaultName()), + RevisionId.forProduction(1), Version.emptyVersion, Instant.EPOCH, Map.of(), QuotaUsage.create(0.53d)))); Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited().withBudget(2), List.of(existing_dev_deployment), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.fromXml( diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java index 9a2d9eddba7..274cf3c5867 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyAlgorithm; @@ -100,7 +101,7 @@ public class EndpointCertificatesTest { return x509CertificateBuilder.build(); } - private final Instance testInstance = new Instance(ApplicationId.defaultId()); + private final Instance testInstance = new Instance(ApplicationId.defaultId(), Tags.empty()); private final String testKeyName = "testKeyName"; private final String testCertName = "testCertName"; private ZoneId testZone; @@ -234,7 +235,7 @@ public class EndpointCertificatesTest { @Test void includes_application_endpoint_when_declared() { - Instance instance = new Instance(ApplicationId.from("t1", "a1", "default")); + Instance instance = new Instance(ApplicationId.from("t1", "a1", "default"), Tags.empty()); ZoneId zone1 = ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c")); ZoneId zone2 = ZoneId.from(Environment.prod, RegionName.from("aws-us-west-2a")); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java index 6bc99b865e4..73630488969 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; @@ -996,7 +997,7 @@ public class DeploymentTriggerTest { @Test void testUserInstancesNotInDeploymentSpec() { var app = tester.newDeploymentContext(); - tester.controller().applications().createInstance(app.application().id().instance("user")); + tester.controller().applications().createInstance(app.application().id().instance("user"), Tags.empty()); app.submit().deploy(); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index 849261d5ae4..40217890351 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -6,6 +6,7 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyUtils; import com.yahoo.slime.SlimeUtils; @@ -127,18 +128,21 @@ public class ApplicationSerializerTest { RevisionHistory revisions = RevisionHistory.ofRevisions(List.of(applicationVersion1), Map.of(new JobId(id1, DeploymentContext.productionUsEast3), List.of(applicationVersion2))); - List<Instance> instances = List.of(new Instance(id1, - deployments, - Map.of(DeploymentContext.systemTest, Instant.ofEpochMilli(333)), - List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of("us-west-1"))), - rotationStatus, - Change.of(new Version("6.1"))), - new Instance(id3, - List.of(), - Map.of(), - List.of(), - RotationStatus.EMPTY, - Change.of(Version.fromString("6.7")).withPin())); + List<Instance> instances = + List.of(new Instance(id1, + Tags.fromString("tag1 tag2"), + deployments, + Map.of(DeploymentContext.systemTest, Instant.ofEpochMilli(333)), + List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of("us-west-1"))), + rotationStatus, + Change.of(new Version("6.1"))), + new Instance(id3, + Tags.empty(), + List.of(), + Map.of(), + List.of(), + RotationStatus.EMPTY, + Change.of(Version.fromString("6.7")).withPin())); Application original = new Application(TenantAndApplicationId.from(id1), Instant.now().truncatedTo(ChronoUnit.MILLIS), @@ -177,6 +181,9 @@ public class ApplicationSerializerTest { assertEquals(original.revisions().production(), serialized.revisions().production()); assertEquals(original.revisions().development(), serialized.revisions().development()); + assertEquals(original.require(id1.instance()).tags(), serialized.require(id1.instance()).tags()); + assertEquals(original.require(id3.instance()).tags(), serialized.require(id3.instance()).tags()); + assertEquals(original.deploymentSpec().xmlForm(), serialized.deploymentSpec().xmlForm()); assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java index 3a539987443..2f4b4154c08 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.restapi.athenz; import com.yahoo.application.container.handler.Request; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; @@ -25,8 +26,8 @@ public class AthenzApiTest extends ControllerContainerTest { controllerTester.createTenant("sandbox", AthenzApiHandler.sandboxDomainIn(tester.controller().system()), 123L); controllerTester.createApplication("sandbox", "app", "default"); - tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName())); - tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName())); + tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName()), Tags.empty()); + tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName()), Tags.empty()); controllerTester.createApplication("sandbox", "opp", "default"); controllerTester.createTenant("tenant1", "domain1", 123L); diff --git a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java index aaf67f6b8ea..ce85b7d6f32 100644 --- a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java +++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java @@ -43,6 +43,9 @@ public abstract class AbstractVespaMojo extends AbstractMojo { @Parameter(property = "instance") protected String instance; + @Parameter(property = "tags") + protected String tags; + @Parameter(property = "apiKey") protected String apiKey; diff --git a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java index 556af8b6f85..377975b3d01 100644 --- a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java +++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java @@ -3,6 +3,7 @@ package ai.vespa.hosted.plugin; import com.yahoo.config.application.XmlPreProcessor; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -44,16 +45,17 @@ public class EffectiveServicesMojo extends AbstractVespaDeploymentMojo { ZoneId zone = zoneOf(environment, region); Path output = Paths.get(outputDirectory).resolve("services-" + zone.environment().value() + "-" + zone.region().value() + ".xml"); - Files.write(output, effectiveServices(services, zone, InstanceName.from(instance)).getBytes(StandardCharsets.UTF_8)); + Files.write(output, effectiveServices(services, zone, InstanceName.from(instance), Tags.fromString(tags)).getBytes(StandardCharsets.UTF_8)); getLog().info("Effective services for " + zone + " written to " + output); } - static String effectiveServices(File servicesFile, ZoneId zone, InstanceName instance) throws Exception { + static String effectiveServices(File servicesFile, ZoneId zone, InstanceName instance, Tags tags) throws Exception { Document processedServicesXml = new XmlPreProcessor(servicesFile.getParentFile(), servicesFile, instance, zone.environment(), - zone.region()) + zone.region(), + tags) .run(); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); diff --git a/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java b/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java index 3cb08f5f2b6..fb7376b13cb 100644 --- a/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java +++ b/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java @@ -2,6 +2,7 @@ package ai.vespa.hosted.plugin; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,21 +27,21 @@ class EffectiveServicesMojoTest { @DisplayName("when zone matches environment-only directive") void devServices() throws Exception { assertEquals(Files.readString(Paths.get("src/test/resources/effective-services/dev.xml")), - effectiveServices(servicesFile, ZoneId.from("dev", "us-east-3"), InstanceName.defaultName())); + effectiveServices(servicesFile, ZoneId.from("dev", "us-east-3"), InstanceName.defaultName(), Tags.empty())); } @Test @DisplayName("when zone matches region-and-environment directive") void prodUsEast3() throws Exception { assertEquals(Files.readString(Paths.get("src/test/resources/effective-services/prod_us-east-3.xml")), - effectiveServices(servicesFile, ZoneId.from("prod", "us-east-3"), InstanceName.defaultName())); + effectiveServices(servicesFile, ZoneId.from("prod", "us-east-3"), InstanceName.defaultName(), Tags.empty())); } @Test @DisplayName("when zone doesn't match any directives") void prodUsWest1Services() throws Exception { assertEquals(Files.readString(Paths.get("src/test/resources/effective-services/prod_us-west-1.xml")), - effectiveServices(servicesFile, ZoneId.from("prod", "us-west-1"), InstanceName.defaultName())); + effectiveServices(servicesFile, ZoneId.from("prod", "us-west-1"), InstanceName.defaultName(), Tags.empty())); } } |