// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.model; import com.yahoo.config.application.api.ApplicationFile; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.ConfigModelContext.ApplicationType; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.builder.xml.ConfigModelBuilder; import com.yahoo.config.model.builder.xml.ConfigModelId; import com.yahoo.config.model.builder.xml.XmlHelper; import com.yahoo.config.model.graph.ModelGraphBuilder; import com.yahoo.config.model.graph.ModelNode; import com.yahoo.config.model.provision.HostsXmlProvisioner; import com.yahoo.log.LogLevel; import com.yahoo.path.Path; import com.yahoo.text.XML; import com.yahoo.config.model.producer.AbstractConfigProducer; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.builder.VespaModelBuilder; import com.yahoo.vespa.model.clients.Clients; import com.yahoo.vespa.model.content.Content; import com.yahoo.vespa.model.routing.Routing; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import java.io.IOException; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; import java.util.*; import java.util.logging.Logger; /** * A collection of config model instances owned by a system model * * @author gjoranv */ public class ConfigModelRepo implements ConfigModelRepoAdder, Serializable, Iterable { private static final long serialVersionUID = 1L; private static final Logger log = Logger.getLogger(ConfigModelRepo.class.getPackage().toString()); private final Map configModelMap = new TreeMap<>(); private final List configModels = new ArrayList<>(); /** * Returns a config model for a given id * * @param id the id of the model to return * @return the model, or none if a model with this id is not present in this */ public ConfigModel get(String id) { return configModelMap.get(id); } /** Adds a new config model instance in this */ @Override public void add(ConfigModel model) { configModelMap.put(model.getId(), model); configModels.add(model); } /** Returns the models in this as an iterator */ public Iterator iterator() { return configModels.iterator(); } /** Returns a read-only view of the config model instances of this */ public Map asMap() { return Collections.unmodifiableMap(configModelMap); } /** Initialize part 1.: Reads the config models used in the application package. */ public void readConfigModels(DeployState deployState, VespaModel vespaModel, VespaModelBuilder builder, ApplicationConfigProducerRoot root, ConfigModelRegistry configModelRegistry) throws IOException, SAXException { Element userServicesElement = getServicesFromApp(deployState.getApplicationPackage()); readConfigModels(root, userServicesElement, deployState, vespaModel, configModelRegistry); builder.postProc(deployState.getDeployLogger(), root, this); } private Element getServicesFromApp(ApplicationPackage applicationPackage) throws IOException, SAXException { try (Reader servicesFile = applicationPackage.getServices()) { return getServicesFromReader(servicesFile); } } /** * If the top level is <services>, it contains a list of services elements, * otherwise, the top level tag is a single service. */ private List getServiceElements(Element servicesRoot) { if (servicesRoot.getTagName().equals("services")) return XML.getChildren(servicesRoot); List singleServiceList = new ArrayList<>(1); singleServiceList.add(servicesRoot); return singleServiceList; } /** * Creates all the config models specified in the given XML element and * passes their respective XML node as parameter. * * @param root The Root to set as parent for all plugins * @param servicesRoot XML root node of the services file */ @SuppressWarnings("deprecation") private void readConfigModels(ApplicationConfigProducerRoot root, Element servicesRoot, DeployState deployState, VespaModel vespaModel, ConfigModelRegistry configModelRegistry) throws IOException, SAXException { final Map> model2Element = new LinkedHashMap<>(); ModelGraphBuilder graphBuilder = new ModelGraphBuilder(); final List children = getServiceElements(servicesRoot); if (XML.getChild(servicesRoot, "admin") == null) children.add(getImplicitAdmin(deployState)); children.addAll(getPermanentServices(deployState)); for (Element servicesElement : children) { String tagName = servicesElement.getTagName(); if (tagName.equals("config")) continue; // TODO: Remove on Vespa 6 if (tagName.equals("cluster")) continue; // TODO: Remove on Vespa 6 if ((tagName.equals("clients")) && deployState.isHosted()) throw new IllegalArgumentException("<" + tagName + "> is not allowed when running Vespa in a hosted environment"); String tagVersion = servicesElement.getAttribute("version"); ConfigModelId xmlId = ConfigModelId.fromNameAndVersion(tagName, tagVersion); Collection builders = configModelRegistry.resolve(xmlId); if (builders.isEmpty()) throw new RuntimeException("Could not resolve tag <" + tagName + " version=\"" + tagVersion + "\"> to a config model component"); for (ConfigModelBuilder builder : builders) { if ( ! model2Element.containsKey(builder)) { model2Element.put(builder, new ArrayList<>()); graphBuilder.addBuilder(builder); } model2Element.get(builder).add(servicesElement); } } for (ModelNode node : graphBuilder.build().topologicalSort()) buildModels(node, getApplicationType(servicesRoot), deployState, vespaModel, root, model2Element.get(node.builder)); for (ConfigModel model : configModels) model.initialize(ConfigModelRepo.this); // XXX deprecated } private ApplicationType getApplicationType(Element servicesRoot) { return XmlHelper.getOptionalAttribute(servicesRoot, "application-type") .map(ApplicationType::fromString) .orElse(ApplicationType.DEFAULT); } private Collection getPermanentServices(DeployState deployState) throws IOException, SAXException { List permanentServices = new ArrayList<>(); Optional applicationPackage = deployState.getPermanentApplicationPackage(); if (applicationPackage.isPresent()) { ApplicationFile file = applicationPackage.get().getFile(Path.fromString(ApplicationPackage.PERMANENT_SERVICES)); if (file.exists()) { try (Reader reader = file.createReader()) { Element permanentServicesRoot = getServicesFromReader(reader); permanentServices.addAll(getServiceElements(permanentServicesRoot)); } } } return permanentServices; } private Element getServicesFromReader(Reader reader) throws IOException, SAXException { Document doc = XmlHelper.getDocumentBuilder().parse(new InputSource(reader)); return doc.getDocumentElement(); } private void buildModels(ModelNode node, ApplicationType applicationType, DeployState deployState, VespaModel vespaModel, AbstractConfigProducer parent, List elements) { for (Element servicesElement : elements) { ConfigModel model = buildModel(node, applicationType, deployState, vespaModel, parent, servicesElement); if (model.isServing()) add(model); } } private ConfigModel buildModel(ModelNode node, ApplicationType applicationType, DeployState deployState, VespaModel vespaModel, AbstractConfigProducer parent, Element servicesElement) { ConfigModelBuilder builder = node.builder; ConfigModelContext context = ConfigModelContext.create(applicationType, deployState, vespaModel, this, parent, getIdString(servicesElement)); return builder.build(node, servicesElement, context); } private static String getIdString(Element spec) { String idString = XmlHelper.getIdString(spec); if (idString == null || idString.isEmpty()) { idString = spec.getTagName(); } return idString; } /** * Initialize part 2.: * Prepare all config models for starting. Must be called after plugins are loaded and frozen. */ public void prepareConfigModels(DeployState deployState) { for (ConfigModel model : configModels) { model.prepare(this, deployState); } } @SuppressWarnings("unchecked") public List getModels(Class modelClass) { List modelsOfModelClass = new ArrayList<>(); for (ConfigModel model : asMap().values()) { if (modelClass.isInstance(model)) modelsOfModelClass.add((T)model); } return modelsOfModelClass; } public Clients getClients() { for (ConfigModel m : configModels) { if (m instanceof Clients) { return (Clients)m; } } return null; } public Routing getRouting() { for (ConfigModel m : configModels) { if (m instanceof Routing) { return (Routing)m; } } return null; } public Content getContent() { for (ConfigModel m : configModels) { if (m instanceof Content) { return (Content)m; } } return null; } // TODO: Doctoring on the XML is the wrong level for this. We should be able to mark a model as default instead -Jon private static Element getImplicitAdmin(DeployState deployState) throws IOException, SAXException { String defaultAdminElement = deployState.isHosted() ? getImplicitAdminV4() : getImplicitAdminV2(); log.log(LogLevel.DEBUG, "No defined, using " + defaultAdminElement); return XmlHelper.getDocumentBuilder().parse(new InputSource(new StringReader(defaultAdminElement))).getDocumentElement(); } private static String getImplicitAdminV2() { return "\n" + " \n" + "\n"; } private static String getImplicitAdminV4() { return "\n" + " \n" + "\n"; } }