// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.application.api; import com.google.common.collect.ImmutableList; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; import com.yahoo.io.IOUtils; import com.yahoo.text.XML; import org.w3c.dom.Element; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; /** * Specifies the environments and regions to which an application should be deployed. * This may be used both for inspection as part of an application model and to answer * queries about deployment from the command line. A main method is included for the latter usage. * * This is immutable. * * @author bratseth */ public class DeploymentSpec { /** The empty deployment spec, specifying no zones or rotation, and defaults for all settings */ public static final DeploymentSpec empty = new DeploymentSpec(Optional.empty(), UpgradePolicy.defaultPolicy, ImmutableList.of(), ""); private final Optional globalServiceId; private final UpgradePolicy upgradePolicy; private final List zones; private final String xmlForm; public DeploymentSpec(Optional globalServiceId, UpgradePolicy upgradePolicy, List zones) { this(globalServiceId, upgradePolicy, zones, null); } private DeploymentSpec(Optional globalServiceId, UpgradePolicy upgradePolicy, List zones, String xmlForm) { this.globalServiceId = globalServiceId; this.upgradePolicy = upgradePolicy; this.zones = ImmutableList.copyOf(zones); this.xmlForm = xmlForm; } /** Returns the ID of the service to expose through global routing, if present */ public Optional globalServiceId() { return globalServiceId; } /** Returns the upgrade policy of this, which is defaultPolicy if none is specified */ public UpgradePolicy upgradePolicy() { return upgradePolicy; } /** Returns the zones this declares as a read-only list. */ public List zones() { return zones; } /** Returns the XML form of this spec, or null if it was not created by fromXml or is the empty spec */ public String xmlForm() { return xmlForm; } /** Returns whether this deployment spec specifies the given zone, either implicitly or explicitly */ public boolean includes(Environment environment, Optional region) { for (DeclaredZone declaredZone : zones) if (declaredZone.matches(environment, region)) return true; return false; } /** * Creates a deployment spec from XML. * * @throws IllegalArgumentException if the XML is invalid */ public static DeploymentSpec fromXml(Reader reader) { try { return fromXml(IOUtils.readAll(reader)); } catch (IOException e) { throw new IllegalArgumentException("Could not read deployment spec", e); } } /** * Creates a deployment spec from XML. * * @throws IllegalArgumentException if the XML is invalid */ public static DeploymentSpec fromXml(String xmlForm) { List zones = new ArrayList<>(); Optional globalServiceId = Optional.empty(); Element root = XML.getDocument(xmlForm).getDocumentElement(); for (Element environmentTag : XML.getChildren(root)) { if (!isEnvironmentName(environmentTag.getTagName())) continue; Environment environment = Environment.from(environmentTag.getTagName()); List regionTags = XML.getChildren(environmentTag, "region"); if (regionTags.isEmpty()) { zones.add(new DeclaredZone(environment, Optional.empty(), false)); } else { for (Element regionTag : regionTags) { RegionName region = RegionName.from(XML.getValue(regionTag).trim()); boolean active = environment == Environment.prod && readActive(regionTag); zones.add(new DeclaredZone(environment, Optional.of(region), active)); } } if (Environment.prod.equals(environment)) { globalServiceId = readGlobalServiceId(environmentTag); } else if (readGlobalServiceId(environmentTag).isPresent()) { throw new IllegalArgumentException("Attribute 'global-service-id' is only valid on 'prod' tag."); } } return new DeploymentSpec(globalServiceId, readUpgradePolicy(root), zones, xmlForm); } private static boolean isEnvironmentName(String tagName) { return tagName.equals("test") || tagName.equals("staging") || tagName.equals("prod"); } private static Optional readGlobalServiceId(Element environmentTag) { String globalServiceId = environmentTag.getAttribute("global-service-id"); if (globalServiceId == null || globalServiceId.isEmpty()) { return Optional.empty(); } else { return Optional.of(globalServiceId); } } private static UpgradePolicy readUpgradePolicy(Element root) { Element upgradeElement = XML.getChild(root, "upgrade"); if (upgradeElement == null) return UpgradePolicy.defaultPolicy; String policy = upgradeElement.getAttribute("policy"); switch (policy) { case "canary" : return UpgradePolicy.canary; case "default" : return UpgradePolicy.defaultPolicy; case "conservative" : return UpgradePolicy.conservative; default : throw new IllegalArgumentException("Illegal upgrade policy '" + policy + "': " + "Must be one of " + Arrays.toString(UpgradePolicy.values())); } } private static boolean readActive(Element regionTag) { String activeValue = regionTag.getAttribute("active"); if ("true".equals(activeValue)) return true; if ("false".equals(activeValue)) return false; throw new IllegalArgumentException("Region tags must have an 'active' attribute set to 'true' or 'false' " + "to control whether the region should receive production traffic"); } public static String toMessageString(Throwable t) { StringBuilder b = new StringBuilder(); String lastMessage = null; String message; for (; t != null; t = t.getCause()) { message = t.getMessage(); if (message == null) continue; if (message.equals(lastMessage)) continue; if (b.length() > 0) { b.append(": "); } b.append(message); lastMessage = message; } return b.toString(); } /** This may be invoked by a continuous build */ public static void main (String[] args) { if (args.length != 2 && args.length != 3) { System.err.println("Usage: DeploymentSpec [file] [environment] [region]?" + "Returns 0 if the specified zone matches the deployment spec, 1 otherwise"); System.exit(1); } try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) { DeploymentSpec spec = DeploymentSpec.fromXml(reader); Environment environment = Environment.from(args[1]); Optional region = args.length == 3 ? Optional.of(RegionName.from(args[2])) : Optional.empty(); if (spec.includes(environment, region)) System.exit(0); else System.exit(1); } catch (Exception e) { System.err.println("Exception checking deployment spec: " + toMessageString(e)); System.exit(1); } } public static class DeclaredZone { private final Environment environment; private Optional region; private final boolean active; public DeclaredZone(Environment environment, Optional region, boolean active) { this.environment = environment; this.region = region; this.active = active; } public Environment environment() { return environment; } /** The region name, or empty if not declared */ public Optional region() { return region; } /** Returns whether this zone should receive production traffic */ public boolean active() { return active; } public boolean matches(Environment environment, Optional region) { if (environment.equals(this.environment) && region.equals(this.region)) return true; if ( ! region.isPresent() && prerequisite(environment)) return true; return false; } /** * Returns whether deployment in the given environment is a prerequisite of deployment in this environment * * The required progression leading to prerequisites is test, staging, prod. */ private boolean prerequisite(Environment environment) { if (this.environment == Environment.prod) return environment == Environment.staging || environment == Environment.test; if (this.environment == Environment.staging) return environment == Environment.test; return false; } } /** Controls when this application will be upgraded to new Vespa versions */ public enum UpgradePolicy { /** Canary: Applications with this policy will upgrade before any other */ canary, /** Default: Will upgrade after all canary applications upgraded successfully. The default. */ defaultPolicy, /** Will upgrade after most default applications upgraded successfully */ conservative } }