// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.container.xml; import com.yahoo.cloud.config.CuratorConfig; import com.yahoo.cloud.config.ZookeeperServerConfig; import com.yahoo.component.ComponentId; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.NullConfigModelRegistry; import com.yahoo.config.model.api.ApplicationClusterEndpoint; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.ModelContext; import com.yahoo.config.model.builder.xml.test.DomBuilderTest; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.config.model.producer.AbstractConfigProducerRoot; import com.yahoo.config.model.provision.InMemoryProvisioner; import com.yahoo.config.model.provision.SingleNodeProvisioner; import com.yahoo.config.model.test.MockApplicationPackage; import com.yahoo.config.model.test.MockRoot; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.Zone; import com.yahoo.config.provision.ZoneEndpoint; import com.yahoo.config.provision.ZoneEndpoint.AccessType; import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn; import com.yahoo.config.provisioning.FlavorsConfig; import com.yahoo.container.ComponentsConfig; import com.yahoo.container.QrConfig; import com.yahoo.container.core.ChainsConfig; import com.yahoo.container.core.VipStatusConfig; import com.yahoo.container.di.config.PlatformBundlesConfig; import com.yahoo.container.handler.VipStatusHandler; import com.yahoo.container.handler.metrics.MetricsV2Handler; import com.yahoo.container.handler.observability.ApplicationStatusHandler; import com.yahoo.container.jdisc.JdiscBindingsConfig; import com.yahoo.container.usability.BindingsOverviewHandler; import com.yahoo.net.HostName; import com.yahoo.prelude.cluster.QrMonitorConfig; import com.yahoo.search.config.QrStartConfig; import com.yahoo.vespa.model.AbstractService; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.container.ApplicationContainer; import com.yahoo.vespa.model.container.ApplicationContainerCluster; import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.ContainerModelEvaluation; import com.yahoo.vespa.model.container.component.Component; import com.yahoo.vespa.model.container.component.Handler; import com.yahoo.vespa.model.content.utils.ContentClusterUtils; import com.yahoo.vespa.model.test.VespaModelTester; import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg; import org.junit.jupiter.api.Test; import org.w3c.dom.Element; import org.xml.sax.SAXException; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.logging.Level; import static com.yahoo.config.model.test.TestUtil.joinLines; import static com.yahoo.test.LinePatternMatcher.containsLineWithPattern; import static com.yahoo.vespa.defaults.Defaults.getDefaults; import static com.yahoo.vespa.model.container.component.chain.ProcessingHandler.PROCESSING_HANDLER_CLASS; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** * Tests for "core functionality" of the container model, e.g. ports, or the 'components' and 'bundles' configs. * * Before adding a new test to this class, check if the test fits into one of the other existing subclasses * of {@link ContainerModelBuilderTestBase}. If not, consider creating a new subclass. * * @author gjoranv */ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { @Test void model_evaluation_bundles_are_deployed() { createBasicContainerModel(); PlatformBundlesConfig config = root.getConfig(PlatformBundlesConfig.class, "default"); assertTrue(config.bundlePaths().contains(ContainerModelEvaluation.MODEL_EVALUATION_BUNDLE_FILE.toString())); assertTrue(config.bundlePaths().contains(ContainerModelEvaluation.MODEL_INTEGRATION_BUNDLE_FILE.toString())); } @Test void default_port_is_4080() { Element clusterElem = DomBuilderTest.parse( "", nodesXml, ""); createModel(root, clusterElem); AbstractService container = (AbstractService) root.getProducer("container/container.0"); assertEquals(getDefaults().vespaWebServicePort(), container.getRelativePort(0)); } @Test void http_server_port_is_configurable_and_does_not_affect_other_ports() { Element clusterElem = DomBuilderTest.parse( "", " ", " ", " ", nodesXml, ""); createModel(root, clusterElem); AbstractService container = (AbstractService) root.getProducer("container/container.0"); assertEquals(9000, container.getRelativePort(0)); assertNotEquals(9001, container.getRelativePort(1)); } @Test void omitting_http_server_port_gives_default() { Element clusterElem = DomBuilderTest.parse( "", " ", " ", " ", nodesXml, ""); createModel(root, clusterElem); AbstractService container = (AbstractService) root.getProducer("container/container.0"); assertEquals(getDefaults().vespaWebServicePort(), container.getRelativePort(0)); } @Test void fail_if_http_port_is_not_default_in_hosted_vespa() throws Exception { try { String servicesXml = "" + "" + " " + "" + "" + " " + " " + " " + nodesXml + "" + ""; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build(); // Need to create VespaModel to make deploy properties have effect TestLogger logger = new TestLogger(); new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .applicationPackage(applicationPackage) .deployLogger(logger) .properties(new TestProperties().setHostedVespa(true)) .build()); fail("Expected exception"); } catch (IllegalArgumentException e) { // Success assertEquals("Illegal port 9000 in http server 'foo': Port must be set to " + getDefaults().vespaWebServicePort(), e.getMessage()); } } @Test void one_cluster_with_explicit_port_and_one_without_is_ok() { Element cluster1Elem = DomBuilderTest.parse( ""); Element cluster2Elem = DomBuilderTest.parse( "", " ", " ", " ", ""); createModel(root, cluster1Elem, cluster2Elem); } @Test void container_cluster_with_invalid_name_throws_exception_when_hosted() throws IOException, SAXException { String servicesXml = """ """; assertEquals("container cluster name must match '([a-z0-9]|[a-z0-9][a-z0-9_-]{0,61}[a-z0-9])', but got: 'C-1'", assertThrows(IllegalArgumentException.class, () -> new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(new InMemoryProvisioner(4, false)) .applicationPackage(new MockApplicationPackage.Builder().withServices(servicesXml).build()) .properties(new TestProperties().setHostedVespa(true)) .build())) .getMessage()); new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(new InMemoryProvisioner(4, false)) .applicationPackage(new MockApplicationPackage.Builder().withServices(servicesXml).build()) .properties(new TestProperties().setHostedVespa(false)) .build()); } @Test void two_clusters_with_clashing_cluster_names_throws_exception_when_hosted() throws IOException, SAXException { String servicesXml = """ """; assertEquals("container clusters 'c-1' and 'c_1' have clashing endpoint names, when '_' is replaced with '-' to form valid domain names", assertThrows(IllegalArgumentException.class, () -> new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(new InMemoryProvisioner(4, false)) .applicationPackage(new MockApplicationPackage.Builder().withServices(servicesXml).build()) .properties(new TestProperties().setHostedVespa(true)) .build())) .getMessage()); new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(new InMemoryProvisioner(4, false)) .applicationPackage(new MockApplicationPackage.Builder().withServices(servicesXml).build()) .properties(new TestProperties().setHostedVespa(false)) .build()); } @Test void two_clusters_without_explicit_port_throws_exception() { Element cluster1Elem = DomBuilderTest.parse( "", nodesXml, ""); Element cluster2Elem = DomBuilderTest.parse( "", nodesXml, ""); try { createModel(root, cluster1Elem, cluster2Elem); fail("Expected exception"); } catch (RuntimeException e) { assertThat(e.getMessage(), containsString("cannot reserve port")); } } @Test void load_balancers_can_be_set() throws IOException, SAXException { // No endpoints verifyAllowedUrns("", Environment.prod, "eu", ZoneEndpoint.defaultEndpoint); // No non-default settings verifyAllowedUrns(""" """, Environment.prod, "eu", ZoneEndpoint.defaultEndpoint); // No allowed urns verifyAllowedUrns(""" """, Environment.prod, "eu", new ZoneEndpoint(true, true, List.of())); // Various settings verifyAllowedUrns(""" eu """, Environment.prod, "eu", new ZoneEndpoint(false, true, List.of(new AllowedUrn(AccessType.awsPrivateLink, "barn"), new AllowedUrn(AccessType.gcpServiceConnect, "nine")))); // Various settings, but wrong region verifyAllowedUrns(""" eu """, Environment.prod, "us", ZoneEndpoint.defaultEndpoint); // Various settings, but wrong environment verifyAllowedUrns(""" eu """, Environment.dev, "eu", ZoneEndpoint.defaultEndpoint); } private void verifyAllowedUrns(String endpointsTag, Environment environment, String region, ZoneEndpoint expected) throws IOException, SAXException { String servicesXml = """ """; String deploymentXml = """ eu %s """.formatted(endpointsTag); ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).withDeploymentSpec(deploymentXml).build(); InMemoryProvisioner provisioner = new InMemoryProvisioner(true, false, "host1.yahoo.com", "host2.yahoo.com"); VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(provisioner) .provisioned(provisioner.startProvisionedRecording()) .applicationPackage(applicationPackage) .properties(new TestProperties().setMultitenant(true) .setHostedVespa(true) .setZone(new Zone(environment, RegionName.from(region)))) .build()); assertEquals(2, model.hostSystem().getHosts().size()); assertEquals(1, provisioner.provisionedClusters().size()); assertEquals(expected, provisioner.provisionedClusters().iterator().next().zoneEndpoint()); } @Test void builtin_handlers_get_default_threadpool() { createBasicContainerModel(); Handler h1 = getHandler("default", ApplicationStatusHandler.class.getName()); assertTrue(h1.getInjectedComponentIds().contains("threadpool@default-handler-common")); Handler h2 = getHandler("default", BindingsOverviewHandler.class.getName()); assertTrue(h2.getInjectedComponentIds().contains("threadpool@default-handler-common")); } @Test void verify_bindings_for_builtin_handlers() { createBasicContainerModel(); JdiscBindingsConfig config = root.getConfig(JdiscBindingsConfig.class, "default/container.0"); JdiscBindingsConfig.Handlers defaultRootHandler = config.handlers(BindingsOverviewHandler.class.getName()); assertThat(defaultRootHandler.serverBindings(), contains("http://*/")); JdiscBindingsConfig.Handlers applicationStatusHandler = config.handlers(ApplicationStatusHandler.class.getName()); assertThat(applicationStatusHandler.serverBindings(), contains("http://*/ApplicationStatus")); JdiscBindingsConfig.Handlers fileRequestHandler = config.handlers(VipStatusHandler.class.getName()); assertThat(fileRequestHandler.serverBindings(), contains("http://*/status.html")); JdiscBindingsConfig.Handlers metricsV2Handler = config.handlers(MetricsV2Handler.class.getName()); assertThat(metricsV2Handler.serverBindings(), contains("http://*/metrics/v2", "http://*/metrics/v2/*")); } @Test void processing_handler_bindings_can_be_overridden() { Element clusterElem = DomBuilderTest.parse( "", " ", " http://*/binding0", " http://*/binding1", " ", ""); createModel(root, clusterElem); String discBindingsConfig = root.getConfig(JdiscBindingsConfig.class, "default").toString(); assertThat(discBindingsConfig, containsString(".serverBindings[0] \"http://*/binding0\"")); assertThat(discBindingsConfig, containsString(".serverBindings[1] \"http://*/binding1\"")); assertThat(discBindingsConfig, not(containsString("/processing/*"))); } @Test void serverProviders_are_included_in_components_config() { Element clusterElem = DomBuilderTest.parse( "" + " " + ""); createModel(root, clusterElem); String componentsConfig = componentsConfig().toString(); assertThat(componentsConfig, containsString(".id \"discServer\"")); } private String getChainsConfig(String configId) { return root.getConfig(ChainsConfig.class, configId).toString(); } @Test void searchHandler_gets_only_search_chains_in_chains_config() { createClusterWithProcessingAndSearchChains(); String searchHandlerConfigId = "default/component/com.yahoo.search.handler.SearchHandler"; String chainsConfig = getChainsConfig(searchHandlerConfigId); assertThat(chainsConfig, containsLineWithPattern(".*\\.id \"testSearcher@default\"$")); assertThat(chainsConfig, not(containsLineWithPattern(".*\\.id \"testProcessor@default\"$"))); } @Test void processingHandler_gets_only_processing_chains_in_chains_config() { createClusterWithProcessingAndSearchChains(); String processingHandlerConfigId = "default/component/" + PROCESSING_HANDLER_CLASS; String chainsConfig = getChainsConfig(processingHandlerConfigId); assertThat(chainsConfig, containsLineWithPattern(".*\\.id \"testProcessor@default\"$")); assertThat(chainsConfig, not(containsLineWithPattern(".*\\.id \"testSearcher@default\"$"))); } @Test void processingHandler_is_instantiated_from_the_default_bundle() { createClusterWithProcessingAndSearchChains(); ComponentsConfig.Components config = getComponentInConfig(componentsConfig(), PROCESSING_HANDLER_CLASS); assertEquals(PROCESSING_HANDLER_CLASS, config.bundle()); } private void createClusterWithProcessingAndSearchChains() { Element clusterElem = DomBuilderTest.parse( "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + nodesXml + " "); createModel(root, clusterElem); } @Test void user_config_can_be_overridden_on_node() { Element containerElem = DomBuilderTest.parse( "", " " + " 111", " " + " ", " ", " ", " ", " 222", " ", " ", " ", ""); root = ContentClusterUtils.createMockRoot(new String[]{"host1", "host2"}); createModel(root, containerElem); ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); assertEquals(2, cluster.getContainers().size()); assertEquals(root.getConfig(QrMonitorConfig.class, "default/container.0").requesttimeout(), 111); assertEquals(root.getConfig(QrMonitorConfig.class, "default/container.1").requesttimeout(), 222); } @Test void component_includes_are_added() { VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/application/include_dirs"); VespaModel model = creator.create(true); ContainerCluster cluster = model.getContainerClusters().get("default"); Map> componentsMap = cluster.getComponentsMap(); Component example = componentsMap.get( ComponentId.fromString("test.Exampledocproc")); assertEquals("test.Exampledocproc", example.getComponentId().getName()); } @Test void affinity_is_set() { Element clusterElem = DomBuilderTest.parse( "", " ", " ", " ", " ", " ", " " + ""); createModel(root, clusterElem); assertTrue(getContainerCluster("default").getContainers().get(0).getAffinity().isPresent()); assertEquals(0, getContainerCluster("default").getContainers().get(0).getAffinity().get().cpuSocket()); } @Test void singlenode_servicespec_is_used_with_hosts_xml() throws IOException, SAXException { String servicesXml = ""; String hostsXml = "\n" + " \n" + " node1\n" + " \n" + ""; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder() .withHosts(hostsXml) .withServices(servicesXml) .build(); VespaModel model = new VespaModel(applicationPackage); assertEquals(1, model.hostSystem().getHosts().size()); } @Test void endpoints_are_added_to_containers() throws IOException, SAXException { final var servicesXml = joinLines("", "", " ", " ", " ", "" ); final var deploymentXml = joinLines("", "", " ", "" ); final var applicationPackage = new MockApplicationPackage.Builder() .withServices(servicesXml) .withDeploymentSpec(deploymentXml) .build(); final var deployState = new DeployState.Builder() .applicationPackage(applicationPackage) .zone(new Zone(Environment.prod, RegionName.from("us-east-1"))) .endpoints(Set.of(new ContainerEndpoint("comics-search", ApplicationClusterEndpoint.Scope.global, List.of("nalle", "balle")))) .properties(new TestProperties().setHostedVespa(true)) .build(); final var model = new VespaModel(new NullConfigModelRegistry(), deployState); final var containers = model.getContainerClusters().values().stream() .flatMap(cluster -> cluster.getContainers().stream()) .toList(); assertFalse(containers.isEmpty(), "Missing container objects based on configuration"); containers.forEach(container -> { final var rotations = container.getServicePropertyString("rotations").split(","); final var rotationsSet = Set.of(rotations); assertEquals(Set.of("balle", "nalle"), rotationsSet); }); } @Test void singlenode_servicespec_is_used_with_hosted_vespa() throws IOException, SAXException { String servicesXml = ""; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build(); VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(new InMemoryProvisioner(true, false, "host1.yahoo.com", "host2.yahoo.com")) .applicationPackage(applicationPackage) .properties(new TestProperties() .setMultitenant(true) .setHostedVespa(true)) .build()); assertEquals(2, model.hostSystem().getHosts().size()); } @Test void cloud_account_without_nodes_tag() throws Exception { String servicesXml = ""; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build(); CloudAccount cloudAccount = CloudAccount.from("000000000000"); InMemoryProvisioner provisioner = new InMemoryProvisioner(true, false, "host1.yahoo.com", "host2.yahoo.com"); VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .modelHostProvisioner(provisioner) .provisioned(provisioner.startProvisionedRecording()) .applicationPackage(applicationPackage) .properties(new TestProperties().setMultitenant(true) .setHostedVespa(true) .setCloudAccount(cloudAccount)) .build()); assertEquals(2, model.hostSystem().getHosts().size()); assertEquals(List.of(cloudAccount), model.provisioned().all().values() .stream() .map(capacity -> capacity.cloudAccount().get()) .toList()); } @Test void renderers_named_JsonRenderer_are_not_allowed() { assertThrows(IllegalArgumentException.class, () -> { createModel(root, generateContainerElementWithRenderer("JsonRenderer")); }); } @Test void renderers_named_DefaultRenderer_are_not_allowed() { assertThrows(IllegalArgumentException.class, () -> { createModel(root, generateContainerElementWithRenderer("XmlRenderer")); }); } @Test void renderers_named_something_else_are_allowed() { createModel(root, generateContainerElementWithRenderer("my-little-renderer")); } @Test void vip_status_handler_uses_file_for_hosted_vespa() throws Exception { String servicesXml = "" + "" + nodesXml + "" + ""; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build(); VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .applicationPackage(applicationPackage) .properties(new TestProperties().setHostedVespa(true)) .build()); AbstractConfigProducerRoot modelRoot = model.getRoot(); VipStatusConfig vipStatusConfig = modelRoot.getConfig(VipStatusConfig.class, "container/component/status.html-status-handler"); assertTrue(vipStatusConfig.accessdisk()); assertEquals(ContainerModelBuilder.HOSTED_VESPA_STATUS_FILE, vipStatusConfig.statusfile()); } @Test void qrconfig_is_produced() throws IOException, SAXException { QrConfig qr = getQrConfig(new TestProperties()); String hostname = HostName.getLocalhost(); // Using the same way of getting hostname as filedistribution model assertEquals("default.container.0", qr.discriminator()); assertEquals(19102, qr.rpc().port()); assertEquals("vespa/service/default/container.0", qr.rpc().slobrokId()); assertTrue(qr.rpc().enabled()); assertEquals("", qr.rpc().host()); assertFalse(qr.restartOnDeploy()); assertEquals("filedistribution/" + hostname, qr.filedistributor().configid()); assertEquals(50.0, qr.shutdown().timeout(), 0.00000000000001); assertFalse(qr.shutdown().dumpHeapOnTimeout()); } private QrConfig getQrConfig(ModelContext.Properties properties) throws IOException, SAXException { String servicesXml = "" + " " + " " + " " + " " + " " + " " + " " + " " + ""; ApplicationPackage applicationPackage = new MockApplicationPackage.Builder() .withServices(servicesXml) .build(); VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .applicationPackage(applicationPackage) .properties(properties) .build()); return model.getConfig(QrConfig.class, "default/container.0"); } @Test void control_container_shutdown() throws IOException, SAXException { QrConfig qr = getQrConfig(new TestProperties().containerShutdownTimeout(133).containerDumpHeapOnShutdownTimeout(true)); assertEquals(133.0, qr.shutdown().timeout(), 0.00000000000001); assertTrue(qr.shutdown().dumpHeapOnTimeout()); } @Test void environment_vars_are_honoured() { Element clusterElem = DomBuilderTest.parse( "", " ", " ", " 1", " some value", " granularity=fine,verbose,compact,1,0", " ", " ", " ", ""); createModel(root, clusterElem); var container = (AbstractService) root.getProducer("container/container.0"); var env = container.getEnvVars(); assertEquals("1", env.get("KMP_SETTING")); assertEquals("granularity=fine,verbose,compact,1,0", env.get("KMP_AFFINITY")); } private void verifyAvailableprocessors(boolean isHosted, Flavor flavor, int expectProcessors) { DeployState deployState = new DeployState.Builder() .modelHostProvisioner(flavor != null ? new SingleNodeProvisioner(flavor) : new SingleNodeProvisioner()) .properties(new TestProperties() .setMultitenant(isHosted) .setHostedVespa(isHosted)) .build(); MockRoot myRoot = new MockRoot("root", deployState); Element clusterElem = DomBuilderTest.parse( "", " ", " ", " ", "" ); createModel(myRoot, clusterElem); QrStartConfig.Builder qsB = new QrStartConfig.Builder(); myRoot.getConfig(qsB, "container/container.0"); QrStartConfig qsC= new QrStartConfig(qsB); assertEquals(expectProcessors, qsC.jvm().availableProcessors()); } @Test void requireThatAvailableProcessorsFollowFlavor() { verifyAvailableprocessors(false, null, 0); verifyAvailableprocessors(true, null, 0); verifyAvailableprocessors(true, new Flavor(new FlavorsConfig.Flavor.Builder().name("test-flavor").minCpuCores(9).build()), 9); verifyAvailableprocessors(true, new Flavor(new FlavorsConfig.Flavor.Builder().name("test-flavor").minCpuCores(1).build()), 2); } @Test void cluster_with_zookeeper() { Function servicesXml = (nodeCount) -> "" + "" + "" + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(3); { VespaModel model = tester.createModel(servicesXml.apply(3), true); ApplicationContainerCluster cluster = model.getContainerClusters().get("default"); assertNotNull(cluster); assertComponentConfigured(cluster, "com.yahoo.vespa.curator.Curator"); assertComponentConfigured(cluster, "com.yahoo.vespa.curator.CuratorWrapper"); assertEquals(30, model.getConfig(CuratorConfig.class, cluster.getConfigId()).zookeeperSessionTimeoutSeconds()); cluster.getContainers().forEach(container -> { assertComponentConfigured(container, "com.yahoo.vespa.zookeeper.ReconfigurableVespaZooKeeperServer"); assertComponentConfigured(container, "com.yahoo.vespa.zookeeper.Reconfigurer"); assertComponentConfigured(container, "com.yahoo.vespa.zookeeper.VespaZooKeeperAdminImpl"); ZookeeperServerConfig config = model.getConfig(ZookeeperServerConfig.class, container.getConfigId()); assertEquals(container.index(), config.myid()); assertEquals(3, config.server().size()); }); } { try { tester.createModel(servicesXml.apply(2), true); fail("Expected exception"); } catch (IllegalArgumentException ignored) { } } { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + " " + ""; try { tester.createModel(xmlWithNodes, true); fail("Expected exception"); } catch (IllegalArgumentException ignored) { } } } @Test void logs_deployment_spec_deprecations() throws Exception { String containerService = joinLines("", " ", " ", " ", ""); String deploymentXml = joinLines("", " ", " us-east-1", " ", ""); ApplicationPackage applicationPackage = new MockApplicationPackage.Builder() .withServices(containerService) .withDeploymentSpec(deploymentXml) .build(); TestLogger logger = new TestLogger(); DeployState deployState = new DeployState.Builder() .applicationPackage(applicationPackage) .zone(new Zone(Environment.prod, RegionName.from("us-east-1"))) .properties(new TestProperties().setHostedVespa(true)) .deployLogger(logger) .build(); createModel(root, deployState, null, DomBuilderTest.parse(containerService)); assertFalse(logger.msgs.isEmpty()); assertEquals(Level.WARNING, logger.msgs.get(0).getFirst()); assertEquals(Level.WARNING, logger.msgs.get(1).getFirst()); assertEquals("Element 'prod' contains attribute 'global-service-id' deprecated since major version 7. See https://cloud.vespa.ai/en/reference/routing#deprecated-syntax", logger.msgs.get(0).getSecond()); assertEquals("Element 'region' contains attribute 'active' deprecated since major version 7. See https://cloud.vespa.ai/en/reference/routing#deprecated-syntax", logger.msgs.get(1).getSecond()); } private void assertComponentConfigured(ApplicationContainer container, String id) { assertTrue(container.getComponents().getComponents().stream().anyMatch(component -> id.equals(component.getComponentId().getName()))); } private Element generateContainerElementWithRenderer(String rendererId) { return DomBuilderTest.parse( "", " ", String.format(" ", rendererId), " ", ""); } }