summaryrefslogtreecommitdiffstats
path: root/config-provisioning
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /config-provisioning
Publish
Diffstat (limited to 'config-provisioning')
-rw-r--r--config-provisioning/.gitignore2
-rw-r--r--config-provisioning/OWNERS2
-rw-r--r--config-provisioning/README1
-rw-r--r--config-provisioning/pom.xml99
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationId.java174
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationName.java58
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java69
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java124
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java165
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Deployer.java25
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Deployment.java28
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java57
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/HostFilter.java105
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/HostSpec.java83
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/InstanceName.java54
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/OutOfCapacityException.java17
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionInfo.java100
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionLogger.java15
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Provisioner.java53
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Quota.java23
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/RegionName.java55
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Rotation.java39
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/TenantName.java57
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Version.java173
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Zone.java115
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/package-info.java7
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationIdTest.java115
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationNameTest.java23
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/ClusterMembershipTest.java117
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/ClusterSpecTest.java30
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/HostFilterTest.java76
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/IdentifierTestBase.java44
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/InstanceNameTest.java23
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/ProvisionInfoTest.java74
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/RegionTest.java23
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/TenantTest.java39
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/VersionTest.java70
37 files changed, 2334 insertions, 0 deletions
diff --git a/config-provisioning/.gitignore b/config-provisioning/.gitignore
new file mode 100644
index 00000000000..0b832b112c9
--- /dev/null
+++ b/config-provisioning/.gitignore
@@ -0,0 +1,2 @@
+/target
+pom.xml.build
diff --git a/config-provisioning/OWNERS b/config-provisioning/OWNERS
new file mode 100644
index 00000000000..03193c770cb
--- /dev/null
+++ b/config-provisioning/OWNERS
@@ -0,0 +1,2 @@
+musum
+bratseth
diff --git a/config-provisioning/README b/config-provisioning/README
new file mode 100644
index 00000000000..2c37f2c1230
--- /dev/null
+++ b/config-provisioning/README
@@ -0,0 +1 @@
+Interface for provisioning hosts in Cloud Config
diff --git a/config-provisioning/pom.xml b/config-provisioning/pom.xml
new file mode 100644
index 00000000000..d3d37adeb86
--- /dev/null
+++ b/config-provisioning/pom.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-provisioning</artifactId>
+ <packaging>container-plugin</packaging>
+ <version>6-SNAPSHOT</version>
+ <name>config-provisioning</name>
+ <description>
+Provisioning APIs.
+ </description>
+ <dependencies>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespajlib</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-bundle</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-testlib</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <compilerArgs>
+ <arg>-Xlint:all</arg>
+ <arg>-Xlint:-serial</arg>
+ <arg>-Werror</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-install-plugin</artifactId>
+ <configuration>
+ <updateReleaseInfo>true</updateReleaseInfo>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationId.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationId.java
new file mode 100644
index 00000000000..c326e6d25c1
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationId.java
@@ -0,0 +1,174 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ApplicationIdConfig;
+
+/**
+ * A complete, immutable identification of an application instance.
+ *
+ * @author lulf
+ * @author vegard
+ * @author bratseth
+ * @since 5.1
+ */
+public final class ApplicationId implements Comparable<ApplicationId> {
+
+ private final TenantName tenant;
+ private final ApplicationName application;
+ private final InstanceName instance;
+
+ private final String stringValue;
+ private final String serializedForm;
+
+ public static class Builder {
+
+ private TenantName tenant;
+ private ApplicationName application;
+ private InstanceName instance;
+
+ public Builder() {
+ this.tenant = TenantName.defaultName();
+ this.application = null;
+ this.instance = InstanceName.defaultName();
+ }
+
+ public Builder tenant(TenantName ten) { this.tenant = ten; return this; }
+ public Builder tenant(String ten) { return tenant(TenantName.from(ten)); }
+
+ public Builder applicationName(ApplicationName nam) { this.application = nam; return this; }
+ public Builder applicationName(String nam) { return applicationName(ApplicationName.from(nam)); }
+
+ public Builder instanceName(InstanceName ins) { this.instance = ins; return this; }
+ public Builder instanceName(String ins) { return instanceName(InstanceName.from(ins)); }
+
+ public ApplicationId build() {
+ if (application == null) {
+ throw new IllegalArgumentException("must set application name in builder");
+ }
+ return ApplicationId.from(tenant, application, instance);
+ }
+
+ }
+
+ public ApplicationId(ApplicationIdConfig config) {
+ this(TenantName.from(config.tenant()), ApplicationName.from(config.application()), InstanceName.from(config.instance()));
+ }
+
+ private ApplicationId(TenantName tenant, ApplicationName applicationName, InstanceName instanceName) {
+ this.tenant = tenant;
+ this.application = applicationName;
+ this.instance = instanceName;
+ this.stringValue = toStringValue();
+ this.serializedForm = toSerializedForm();
+ }
+
+ public static final TenantName HOSTED_VESPA_TENANT = TenantName.from("hosted-vespa");
+ // TODO: Remove references to routing application, or rename them to zone
+ // application, once everything (like Chef recipes) refers to the zone
+ // application name.
+ public static final ApplicationName ROUTING_APPLICATION = ApplicationName.from("routing");
+ public static final ApplicationName ZONE_APPLICATION = ApplicationName.from("zone");
+ public static final ApplicationId HOSTED_ZONE_APPLICATION_ID =
+ new ApplicationId.Builder()
+ .tenant(HOSTED_VESPA_TENANT)
+ .applicationName(ROUTING_APPLICATION)
+ .build();
+
+
+ public boolean isHostedVespaRoutingApplication() {
+ return HOSTED_VESPA_TENANT.equals(tenant) &&
+ (ROUTING_APPLICATION.equals(application) ||
+ ZONE_APPLICATION.equals(application));
+ }
+
+ public static ApplicationId from(TenantName tenant, ApplicationName application, InstanceName instanceName) {
+ return new ApplicationId(tenant, application, instanceName);
+ }
+
+ /** Creates an application id from a string on the form application:environment:region:instance */
+ public static ApplicationId fromSerializedForm(TenantName tenant, String idString) {
+ String[] parts = idString.split(":");
+ if (parts.length < 3) {
+ throw new IllegalArgumentException("Illegal id string '" + idString + "'. Id string must consist of at least three parts separated by ':'");
+ }
+
+ // Legacy id from 5.50 and backwards
+ if (parts.length < 4) {
+ return new Builder()
+ .tenant(parts[0])
+ .applicationName(parts[1])
+ .instanceName(parts[2])
+ .build();
+
+ } else {
+ return new Builder()
+ .applicationName(parts[0])
+ .instanceName(parts[3])
+ .tenant(tenant)
+ .build();
+ }
+ }
+
+ @Override
+ public int hashCode() { return stringValue.hashCode(); }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null || getClass() != other.getClass()) return false;
+
+ ApplicationId rhs = (ApplicationId) other;
+ return tenant.equals(rhs.tenant) &&
+ application.equals(rhs.application) &&
+ instance.equals(rhs.instance);
+ }
+
+ /** Returns a serialized form of the content of this: tenant:application:instance */
+ public String serializedForm() { return serializedForm; }
+
+ private String toStringValue() {
+ return "tenant '" + tenant + "', application '" + application + "', instance '" + instance + "'";
+ }
+
+ private String toSerializedForm() {
+ return tenant + ":" + application + ":" + instance;
+ }
+
+ @Override
+ public String toString() { return stringValue; }
+
+ public TenantName tenant() { return tenant; }
+ public ApplicationName application() { return application; }
+ public InstanceName instance() { return instance; }
+
+ @Override
+ public int compareTo(ApplicationId other) {
+ int diff;
+
+ diff = tenant.compareTo(other.tenant);
+ if (diff != 0) { return diff; }
+
+ diff = application.compareTo(other.application);
+ if (diff != 0) { return diff; }
+
+ diff = instance.compareTo(other.instance);
+ if (diff != 0) { return diff; }
+
+ return 0;
+ }
+
+ /** Returns an application id where all fields are "default" */
+ public static ApplicationId defaultId() {
+ return new ApplicationId(TenantName.defaultName(), ApplicationName.defaultName(), InstanceName.defaultName());
+ }
+
+ /** Returns an application id where all fields are "*" */
+ public static ApplicationId global() { // TODO: Sukk ... get rid of this
+ return new Builder().tenant("*")
+ .applicationName("*")
+ .instanceName("*")
+ .build();
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationName.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationName.java
new file mode 100644
index 00000000000..a4dbb65e9ac
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ApplicationName.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Objects;
+
+/**
+ * Represents an applications name, which may be any kind of string or default. This type is defined
+ * in order to provide a type safe API for defining environments.
+ *
+ * @author lulf
+ * @since 5.25
+ */
+public class ApplicationName implements Comparable<ApplicationName> {
+
+ private final String applicationName;
+
+ private ApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ }
+
+ @Override
+ public int hashCode() {
+ return applicationName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ApplicationName)) return false;
+ return Objects.equals(((ApplicationName) obj).applicationName, applicationName);
+ }
+
+ @Override
+ public String toString() {
+ return applicationName;
+ }
+
+ public static ApplicationName from(String name) {
+ return new ApplicationName(name);
+ }
+
+ public static ApplicationName defaultName() {
+ return new ApplicationName("default");
+ }
+
+ public boolean isDefault() {
+ return equals(ApplicationName.defaultName());
+ }
+
+ public String value() {
+ return applicationName;
+ }
+
+ @Override
+ public int compareTo(ApplicationName name) {
+ return this.applicationName.compareTo(name.applicationName);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java
new file mode 100644
index 00000000000..5e02e1105ae
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Optional;
+
+/**
+ * A capacity request.
+ *
+ * @author lulf
+ * @author bratseth
+ */
+public final class Capacity {
+
+ private final int nodeCount;
+
+ private final boolean required;
+
+ private final Optional<String> flavor;
+
+ private Capacity(int nodeCount, boolean required, Optional<String> flavor) {
+ this.nodeCount = nodeCount;
+ this.flavor = flavor;
+ this.required = required;
+ }
+
+ /** Returns the number of nodes requested */
+ public int nodeCount() { return nodeCount; }
+
+ /** Returns whether the requested number of nodes must be met exactly for a request for this to succeed */
+ public boolean isRequired() { return required; }
+
+ /**
+ * The node flavor requested, or empty if no particular flavor is specified.
+ * This may be satisfied by the requested flavor or a suitable replacement
+ */
+ public Optional<String> flavor() { return flavor; }
+
+ @Override
+ public String toString() {
+ return nodeCount + " nodes " + ( flavor.isPresent() ? "of flavor " + flavor.get() : "(default flavor)" );
+ }
+
+ /** Creates this from a desired node count: The request may be satisfied with a smaller number of nodes. */
+ public static Capacity fromNodeCount(int capacity) {
+ return fromNodeCount(capacity, Optional.empty());
+ }
+ /** Creates this from a desired node count: The request may be satisfied with a smaller number of nodes. */
+ public static Capacity fromNodeCount(int nodeCount, String flavor) {
+ return fromNodeCount(nodeCount, Optional.of(flavor));
+ }
+ /** Creates this from a desired node count: The request may be satisfied with a smaller number of nodes. */
+ public static Capacity fromNodeCount(int nodeCount, Optional<String> flavor) {
+ return new Capacity(nodeCount, false, flavor);
+ }
+
+ /** Creates this from a required node count: Requests must fail unless the node count can be satisfied exactly */
+ public static Capacity fromRequiredNodeCount(int nodeCount) {
+ return fromRequiredNodeCount(nodeCount, Optional.empty());
+ }
+ /** Creates this from a required node count: Requests must fail unless the node count can be satisfied exactly */
+ public static Capacity fromRequiredNodeCount(int nodeCount, String flavor) {
+ return fromRequiredNodeCount(nodeCount, Optional.of(flavor));
+ }
+ /** Creates this from a required node count: Requests must fail unless the node count can be satisfied exactly */
+ public static Capacity fromRequiredNodeCount(int nodeCount, Optional<String> flavor) {
+ return new Capacity(nodeCount, true, flavor);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
new file mode 100644
index 00000000000..1bf690749bb
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Optional;
+
+/**
+ * A node's membership in a cluster.
+ * This is a value object.
+ *
+ * @author bratseth
+ */
+public class ClusterMembership {
+
+ private ClusterSpec cluster; // final
+ private int index; // final
+ private boolean retired; // final
+ private String stringValue; // final
+
+ protected ClusterMembership() {}
+
+ private ClusterMembership(String stringValue, Optional<String> dockerImage) {
+ String restValue;
+ if (stringValue.endsWith("/retired")) {
+ retired = true;
+ restValue = stringValue.substring(0, stringValue.length() - "/retired".length());
+ }
+ else {
+ retired = false;
+ restValue = stringValue;
+ }
+
+ String[] components = restValue.split("/");
+
+ if ( components.length == 3)
+ initWithoutGroup(components, dockerImage);
+ else if (components.length == 4)
+ initWithGroup(components, dockerImage);
+ else
+ throw new RuntimeException("Could not parse '" + stringValue + "' to a cluster membership. " +
+ "Expected 'id/type.index[/group]'");
+
+ this.stringValue = toStringValue();
+ }
+
+ private ClusterMembership(ClusterSpec cluster, int index, boolean retired) {
+ this.cluster = cluster;
+ this.index = index;
+ this.retired = retired;
+ this.stringValue = toStringValue();
+ }
+
+ private void initWithoutGroup(String[] components, Optional<String> dockerImage) {
+ this.cluster = ClusterSpec.from(ClusterSpec.Type.valueOf(components[0]), ClusterSpec.Id.from(components[1]),
+ Optional.empty(), dockerImage);
+ this.index = Integer.parseInt(components[2]);
+ }
+
+ private void initWithGroup(String[] components, Optional<String> dockerImage) {
+ this.cluster = ClusterSpec.from(ClusterSpec.Type.valueOf(components[0]), ClusterSpec.Id.from(components[1]),
+ Optional.of(ClusterSpec.Group.from(components[2])), dockerImage);
+ this.index = Integer.parseInt(components[3]);
+ }
+
+ protected String toStringValue() {
+ return cluster.type().name() + "/" + cluster.id().value() +
+ ( cluster.group().isPresent() ? "/" + cluster.group().get().value() : "") + "/" + index +
+ ( retired ? "/retired" : "");
+ }
+
+ /** Returns the cluster this node is a member of */
+ public ClusterSpec cluster() { return cluster; }
+
+ /** Returns the index of this node within the cluster */
+ public int index() { return index; }
+
+ /** Returns whether the cluster should prepare for this node to be removed */
+ public boolean retired() { return retired; }
+
+ /** Returns a copy of this which is retired */
+ public ClusterMembership retire() {
+ return new ClusterMembership(cluster, index, true);
+ }
+
+ /** Returns a copy of this node which is not retired */
+ public ClusterMembership unretire() {
+ return new ClusterMembership(cluster, index, false);
+ }
+
+ public ClusterMembership changeCluster(ClusterSpec newCluster) {
+ return new ClusterMembership(newCluster, index, retired);
+ }
+
+ /**
+ * Returns all the information in this as a string which can be used to construct the same ClusterMembership
+ * instance using {@link #from}. This string is currently stored in ZooKeeper on running instances.
+ */
+ public String stringValue() { return stringValue; }
+
+ @Override
+ public int hashCode() { return stringValue().hashCode(); }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if ( ! (other instanceof ClusterMembership)) return false;
+ return ((ClusterMembership)other).stringValue().equals(stringValue());
+ }
+
+ @Override
+ public String toString() { return stringValue(); }
+
+ public static ClusterMembership from(String stringValue, Optional<String> dockerImage) {
+ return new ClusterMembership(stringValue, dockerImage);
+ }
+
+ public static ClusterMembership from(ClusterSpec cluster, int index) {
+ return new ClusterMembership(cluster, index, false);
+ }
+
+ public static ClusterMembership retiredFrom(ClusterSpec cluster, int index) {
+ return new ClusterMembership(cluster, index, true);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
new file mode 100644
index 00000000000..da6c6baa417
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * A specification of a cluster - or group in a grouped cluster - to be run on a set of hosts.
+ * This is a value object.
+ *
+ * @author bratseth
+ */
+public final class ClusterSpec {
+
+ private final Type type;
+ private final Id id;
+ private final Optional<Group> groupId;
+ private final Optional<String> dockerImage;
+
+ private ClusterSpec(Type type, Id id, Optional<Group> groupId, Optional<String> dockerImage) {
+ this.type = type;
+ this.id = id;
+ this.groupId = groupId;
+ this.dockerImage = dockerImage;
+ }
+
+ /** Returns the cluster type */
+ public Type type() { return type; }
+
+ /** Returns the cluster id */
+ public Id id() { return id; }
+
+ public Optional<String> dockerImage() { return dockerImage; }
+
+ /** Returns the group within the cluster this specifies, or empty to specify the whole cluster */
+ public Optional<Group> group() { return groupId; }
+
+ public ClusterSpec changeGroup(Optional<Group> newGroup) { return new ClusterSpec(type, id, newGroup, dockerImage); }
+
+ public static ClusterSpec from(Type type, Id id) {
+ return new ClusterSpec(type, id, Optional.empty(), Optional.empty());
+ }
+
+ public static ClusterSpec from(Type type, Id id, Optional<Group> groupId) {
+ return new ClusterSpec(type, id, groupId, Optional.empty());
+ }
+
+ public static ClusterSpec from(Type type, Id id, Optional<Group> groupId, Optional<String> dockerImage) {
+ return new ClusterSpec(type, id, groupId, dockerImage);
+ }
+
+ @Override
+ public String toString() {
+ return type + " " + id + (groupId.isPresent() ? " " + groupId.get() : "");
+ }
+
+ @Override
+ public int hashCode() { return type.hashCode() + 17 * id.hashCode() + 31 * groupId.hashCode(); }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof ClusterSpec)) return false;
+ ClusterSpec other = (ClusterSpec)o;
+ if ( ! other.type.equals(this.type)) return false;
+ if ( ! other.id.equals(this.id)) return false;
+ if ( ! other.groupId.equals(this.groupId)) return false;
+ if ( ! other.dockerImage.equals(this.dockerImage)) return false;
+ return true;
+ }
+
+ /** Returns whether this is equal, disregarding the group value */
+ public boolean equalsIgnoringGroup(Object o) {
+ if (o == this) return true;
+ if ( ! (o instanceof ClusterSpec)) return false;
+ ClusterSpec other = (ClusterSpec)o;
+ if ( ! other.type.equals(this.type)) return false;
+ if ( ! other.id.equals(this.id)) return false;
+ if ( ! other.dockerImage.equals(this.dockerImage)) return false;
+ return true;
+ }
+
+ /** A cluster type */
+ public enum Type {
+
+ // These enum values are stored in ZooKeeper - do not change
+ admin,
+ container,
+ content;
+
+ public static Type from(String typeName) {
+ switch (typeName) {
+ case "admin" : return admin;
+ case "container" : return container;
+ case "content" : return content;
+ default: throw new IllegalArgumentException("Illegal cluster type '" + typeName + "'");
+ }
+ }
+
+ }
+
+ public static final class Id {
+
+ private final String id;
+
+ public Id(String id) {
+ Objects.requireNonNull(id, "Id cannot be null");
+ this.id = id;
+ }
+
+ public static Id from(String id) {
+ return new Id(id);
+ }
+
+ public String value() { return id; }
+
+ @Override
+ public String toString() { return "cluster '" + id + "'"; }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ return ((Id)o).id.equals(this.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ }
+
+ /** Identifier of a group within a cluster */
+ public static final class Group {
+
+ private final String id;
+
+ public Group(String id) {
+ Objects.requireNonNull(id, "Group id cannot be null");
+ this.id = id;
+ }
+
+ public static Group from(String id) {
+ return new Group(id);
+ }
+
+ public String value() { return id; }
+
+ @Override
+ public String toString() { return "group '" + id + "'"; }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ return ((Group)o).id.equals(this.id);
+ }
+
+ @Override
+ public int hashCode() { return id.hashCode(); }
+
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Deployer.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Deployer.java
new file mode 100644
index 00000000000..dccce0a8d8d
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Deployer.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * A deployer is used to deploy applications.
+ *
+ * @author bratseth
+ */
+public interface Deployer {
+
+ /**
+ * Creates a new deployment from the active application, if available.
+ *
+ * @param application the active application to be redeployed
+ * @param timeout the timeout to use for each individual deployment operation
+ * @return a new deployment from the local active, or empty if a local active application
+ * was not present for this id (meaning it either is not active or active on another
+ * node in the config server cluster)
+ */
+ public Optional<Deployment> deployFromLocalActive(ApplicationId application, Duration timeout);
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Deployment.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Deployment.java
new file mode 100644
index 00000000000..31718b235e7
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Deployment.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * A deployment of an application
+ *
+ * @author bratseth
+ */
+public interface Deployment {
+
+ /**
+ * Prepares activation of this deployment.
+ * This will do all validation and preparatory steps in the system such that a subsequent activation should
+ * be fast and error free.
+ */
+ void prepare();
+
+ /** Activates this deployment. This will prepare it if necessary. */
+ void activate();
+
+ /**
+ * Request a restart of services of this application on hosts matching the filter.
+ * This is sometimes needed after activation, but can also be requested without
+ * doing prepare and activate in the same session.
+ */
+ void restart(HostFilter filter);
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java
new file mode 100644
index 00000000000..c7b2b84ca58
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * Environments in hosted Vespa.
+ *
+ * @author bratseth
+ * @since 5.11
+ */
+public enum Environment {
+
+ /** The environment in which any external or internal applications serve actual requests */
+ prod,
+
+ /** Production-like environment which runs staging tests before an app is deployed to production */
+ staging,
+
+ /** Environment for running system tests before an app is deployed to staging */
+ test,
+
+ /** Environment used by individual developers to experiment */
+ dev,
+
+ /** Environment used to run performance and stability experiments */
+ perf;
+
+ /** Returns whether deployments to this environment are done manually */
+ public boolean isManuallyDeployed() { return this == dev || this == perf; }
+
+ /** Returns the prod environment. This is useful for non-hosted properties where we just need any consistent value */
+ public static Environment defaultEnvironment() { return prod; }
+
+ /** Returns the environment name from the string value returned by value() */
+ public static Environment from(String value) {
+ switch(value) {
+ case "prod" : return prod;
+ case "staging" : return staging;
+ case "test" : return test;
+ case "dev" : return dev;
+ case "perf" : return perf;
+ default : throw new IllegalStateException("'" + value + "' is not a valid environment identifier");
+ }
+ }
+
+ /** Returns a name of this which is used in external API's and stored in persistent stores */
+ public String value() {
+ switch(this) {
+ case prod : return "prod";
+ case staging : return "staging";
+ case test : return "test";
+ case dev : return "dev";
+ case perf : return "perf";
+ default : throw new IllegalStateException();
+ }
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/HostFilter.java b/config-provisioning/src/main/java/com/yahoo/config/provision/HostFilter.java
new file mode 100644
index 00000000000..8f3127f938f
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/HostFilter.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A filter which matches a host depending on its properties.
+ *
+ * @author bratseth
+ */
+public class HostFilter {
+
+ // Filters. Empty to not filter on this property
+ private final Set<String> hostnames;
+ private final Set<String> flavors;
+ private final Set<ClusterSpec.Type> clusterTypes;
+ private final Set<ClusterSpec.Id> clusterIds;
+
+ private HostFilter(Set<String> hostnames,
+ Set<String> flavors,
+ Set<ClusterSpec.Type> clusterTypes,
+ Set<ClusterSpec.Id> clusterIds) {
+ Objects.requireNonNull(hostnames, "Hostnames cannot be null, use an empty list");
+ Objects.requireNonNull(flavors, "Flavors cannot be null, use an empty list");
+ Objects.requireNonNull(clusterTypes, "clusterTypes cannot be null, use an empty list");
+ Objects.requireNonNull(clusterIds, "clusterIds cannot be null, use an empty list");
+
+ this.hostnames = hostnames;
+ this.flavors = flavors;
+ this.clusterTypes = clusterTypes;
+ this.clusterIds = clusterIds;
+ }
+
+ /** Returns true if this filter matches the given host properties */
+ public boolean matches(String hostname, String flavor, Optional<ClusterMembership> membership) {
+ if ( ! hostnames.isEmpty() && ! hostnames.contains(hostname)) return false;
+ if ( ! flavors.isEmpty() && ! flavors.contains(flavor)) return false;
+ if ( ! clusterTypes.isEmpty() && ! (membership.isPresent() && clusterTypes.contains(membership.get().cluster().type()))) return false;
+ if ( ! clusterIds.isEmpty() && ! (membership.isPresent() && clusterIds.contains(membership.get().cluster().id()))) return false;
+ return true;
+ }
+
+ /** Returns a filter which matches all hosts */
+ public static HostFilter all() {
+ return new HostFilter(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
+ }
+
+ /** Returns a filter which matches a given host only */
+ public static HostFilter hostname(String hostname) {
+ return new HostFilter(Collections.singleton(hostname), Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
+ }
+
+ /** Returns a filter which matches a given flavor only */
+ public static HostFilter flavor(String flavor) {
+ return new HostFilter(Collections.emptySet(), Collections.singleton(flavor), Collections.emptySet(), Collections.emptySet());
+ }
+
+ /** Returns a filter which matches a given cluster type only */
+ public static HostFilter clusterType(ClusterSpec.Type clusterType) {
+ return new HostFilter(Collections.emptySet(), Collections.emptySet(), Collections.singleton(clusterType), Collections.emptySet());
+ }
+
+ /** Returns a filter which matches a given cluster id only */
+ public static HostFilter clusterId(ClusterSpec.Id clusterId) {
+ return new HostFilter(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.singleton(clusterId));
+ }
+
+ /** Returns a host filter from three optional conditions */
+ public static HostFilter from(Collection<String> hostNames,
+ Collection<String> flavors,
+ Collection<ClusterSpec.Type> clusterTypes,
+ Collection<ClusterSpec.Id> clusterIds) {
+ return new HostFilter(ImmutableSet.copyOf(hostNames),
+ ImmutableSet.copyOf(flavors),
+ ImmutableSet.copyOf(clusterTypes),
+ ImmutableSet.copyOf(clusterIds));
+ }
+
+ /** Returns a host filter from three comma and-or space separated string lists. The strings may be null or empty. */
+ public static HostFilter from(String hostNames, String flavors, String clusterTypes, String clusterIds) {
+ return new HostFilter(
+ split(hostNames),
+ split(flavors),
+ split(clusterTypes).stream().map(ClusterSpec.Type::from).collect(Collectors.toSet()),
+ split(clusterIds).stream().map(ClusterSpec.Id::from).collect(Collectors.toSet()));
+ }
+
+ /** Splits a string on space and comma */
+ public static Set<String> split(String s) {
+ if (s == null || s.isEmpty()) return Collections.emptySet();
+ ImmutableSet.Builder<String> b = new ImmutableSet.Builder<>();
+ for (String item : s.split("[\\s\\,]"))
+ if ( ! item.isEmpty())
+ b.add(item);
+ return b.build();
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/HostSpec.java b/config-provisioning/src/main/java/com/yahoo/config/provision/HostSpec.java
new file mode 100644
index 00000000000..dd8ef7c4943
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/HostSpec.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A specification of a host and its role.
+ * The identity of a host is determined by its name.
+ *
+ * @author musum
+ */
+public class HostSpec implements Comparable<HostSpec> {
+
+ /** The name of this host */
+ private final String hostname;
+
+ /** Aliases of this host */
+ private final List<String> aliases;
+
+ /** The current membership role of this host in the cluster it belongs to */
+ private final Optional<ClusterMembership> membership;
+
+ public HostSpec(String hostname, Optional<ClusterMembership> membership) {
+ this(hostname, new ArrayList<>(), membership);
+ }
+
+ public HostSpec(String hostname, ClusterMembership membership) {
+ this(hostname, new ArrayList<>(), Optional.of(membership));
+ }
+
+ public HostSpec(String hostname, List<String> aliases) {
+ this(hostname, aliases, Optional.empty());
+ }
+
+ public HostSpec(String hostname, List<String> aliases, ClusterMembership membership) {
+ this(hostname, aliases, Optional.of(membership));
+ }
+
+ public HostSpec(String hostname, List<String> aliases, Optional<ClusterMembership> membership) {
+ if (hostname == null || hostname.isEmpty()) throw new IllegalArgumentException("Hostname must be specified");
+ this.hostname = hostname;
+ this.aliases = ImmutableList.copyOf(aliases);
+ this.membership = membership;
+ }
+
+ /** Returns the name identifying this host */
+ public String hostname() { return hostname; }
+
+ /** Returns the aliases of this host as an immutable list. This may be empty but never null. */
+ public List<String> aliases() { return aliases; }
+
+ /** Returns the membership of this host, or an empty value if not present */
+ public Optional<ClusterMembership> membership() { return membership; }
+
+ @Override
+ public String toString() {
+ return hostname +
+ (! aliases.isEmpty() ? " (aliases: " + aliases + ")" : "") +
+ (membership.isPresent() ? " (membership: " + membership.get() + ")" : " (no membership)");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( ! (o instanceof HostSpec)) return false;
+ HostSpec other = (HostSpec) o;
+ return this.hostname().equals(other.hostname());
+ }
+
+ @Override
+ public int hashCode() {
+ return hostname.hashCode();
+ }
+
+ @Override
+ public int compareTo(HostSpec other) {
+ return hostname.compareTo(other.hostname);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/InstanceName.java b/config-provisioning/src/main/java/com/yahoo/config/provision/InstanceName.java
new file mode 100644
index 00000000000..d8696098b4b
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/InstanceName.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Objects;
+
+/**
+ * Represents an applications instance name, which may be any kind of string or default. This type is defined
+ * in order to provide a type safe API for defining environments.
+ *
+ * @author lulf
+ * @since 5.25
+ */
+public class InstanceName implements Comparable<InstanceName> {
+ private final String instanceName;
+
+ private InstanceName(String instanceName) {
+ this.instanceName = instanceName;
+ }
+
+ @Override
+ public int hashCode() {
+ return instanceName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof InstanceName)) return false;
+ return Objects.equals(((InstanceName) obj).instanceName, instanceName);
+ }
+
+ @Override
+ public String toString() {
+ return instanceName;
+ }
+
+ public static InstanceName from(String name) {
+ return new InstanceName(name);
+ }
+
+ public static InstanceName defaultName() {
+ return new InstanceName("default");
+ }
+
+ public boolean isDefault() {
+ return equals(InstanceName.defaultName());
+ }
+
+ public String value() { return instanceName; }
+
+ @Override
+ public int compareTo(InstanceName instance) {
+ return instanceName.compareTo(instance.instanceName);
+ }
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/OutOfCapacityException.java b/config-provisioning/src/main/java/com/yahoo/config/provision/OutOfCapacityException.java
new file mode 100644
index 00000000000..4f37e54dca8
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/OutOfCapacityException.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ *
+ * Exception thrown when we are unable to fulfill the request due to
+ * having too few nodes (of the specified flavor)
+ * @author musum
+ *
+ */
+public class OutOfCapacityException extends RuntimeException {
+
+ public OutOfCapacityException(String message) {
+ super(message);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionInfo.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionInfo.java
new file mode 100644
index 00000000000..988819aae2d
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionInfo.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.yahoo.slime.ArrayTraverser;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.SlimeUtils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Information about provisioned hosts, and (de)serialization (from)to JSON.
+ *
+ * @author lulf
+ * @since 5.12
+ */
+public class ProvisionInfo {
+
+ private static final String mappingKey = "mapping";
+ private static final String hostSpecKey = "hostSpec";
+ private static final String hostSpecHostName = "hostName";
+ private static final String hostSpecMembership = "membership";
+ private static final String dockerImage = "dockerImage";
+
+ private final Set<HostSpec> hosts = new LinkedHashSet<>();
+
+ private ProvisionInfo(Set<HostSpec> hosts) {
+ this.hosts.addAll(hosts);
+ }
+
+ public static ProvisionInfo withHosts(Set<HostSpec> hosts) {
+ return new ProvisionInfo(hosts);
+ }
+
+ private void toSlime(Cursor cursor) {
+ Cursor array = cursor.setArray(mappingKey);
+ for (HostSpec host : hosts) {
+ Cursor object = array.addObject();
+ serializeHostSpec(object.setObject(hostSpecKey), host);
+ }
+ }
+
+ private void serializeHostSpec(Cursor cursor, HostSpec host) {
+ cursor.setString(hostSpecHostName, host.hostname());
+ if (host.membership().isPresent()) {
+ cursor.setString(hostSpecMembership, host.membership().get().stringValue());
+ if (host.membership().get().cluster().dockerImage().isPresent())
+ cursor.setString(dockerImage, host.membership().get().cluster().dockerImage().get());
+ }
+ }
+
+ public Set<HostSpec> getHosts() {
+ return Collections.unmodifiableSet(hosts);
+ }
+
+ private static ProvisionInfo fromSlime(Inspector inspector) {
+ Inspector array = inspector.field(mappingKey);
+ final Set<HostSpec> hosts = new LinkedHashSet<>();
+ array.traverse(new ArrayTraverser() {
+ @Override
+ public void entry(int i, Inspector inspector) {
+ hosts.add(createHostSpec(inspector.field(hostSpecKey)));
+ }
+ });
+ return new ProvisionInfo(hosts);
+ }
+
+ private static HostSpec createHostSpec(Inspector object) {
+ Optional<ClusterMembership> membership =
+ object.field(hostSpecMembership).valid() ? Optional.of(readMembership(object)) : Optional.empty();
+ HostSpec h = new HostSpec(object.field(hostSpecHostName).asString(), Collections.<String>emptyList(), membership);
+ return h;
+ }
+
+ private static ClusterMembership readMembership(Inspector object) {
+ return ClusterMembership.from(object.field(hostSpecMembership).asString(),
+ object.field(dockerImage).valid() ? Optional.of(object.field(dockerImage).asString()) : Optional.empty());
+ }
+
+ public byte[] toJson() throws IOException {
+ Slime slime = new Slime();
+ toSlime(slime.setObject());
+ return SlimeUtils.toJsonBytes(slime);
+ }
+
+ public static ProvisionInfo fromJson(byte[] json) {
+ return fromSlime(SlimeUtils.jsonToSlime(json).get());
+ }
+
+ public ProvisionInfo merge(ProvisionInfo provisionInfo) {
+ Set<HostSpec> mergedSet = new LinkedHashSet<>();
+ mergedSet.addAll(this.hosts);
+ mergedSet.addAll(provisionInfo.getHosts());
+ return ProvisionInfo.withHosts(mergedSet);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionLogger.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionLogger.java
new file mode 100644
index 00000000000..1246daaacf9
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ProvisionLogger.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.logging.Level;
+
+/**
+ * Allows messages to be logged during provision which will be directed back to the party initiating the request.
+ *
+ * @author bratseth
+ */
+public interface ProvisionLogger {
+
+ void log(Level level, String message);
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Provisioner.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Provisioner.java
new file mode 100644
index 00000000000..2e84ac555f0
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Provisioner.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.yahoo.transaction.NestedTransaction;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Interface used by the config system to acquire hosts.
+ *
+ * @author lulf
+ * @since 5.11
+ */
+public interface Provisioner {
+
+ /**
+ * Prepares allocation of a set of hosts with a given type, common id and the amount.
+ *
+ * @param applicationId the application requesting hosts
+ * @param cluster the specification of the cluster to allocate nodes for
+ * @param capacity the capacity requested
+ * @param groups the number of node groups to divide the requested capacity into
+ * @param logger a logger which receives messages which are returned to the requestor
+ * @return the specification of the hosts allocated
+ */
+ List<HostSpec> prepare(ApplicationId applicationId, ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger);
+
+ /**
+ * Activates the allocation of nodes to this application captured in the hosts argument.
+ *
+ * @param transaction Transaction with operations to commit together with any operations done within the provisioner.
+ * @param application The {@link ApplicationId} that was activated.
+ * @param hosts a set of {@link HostSpec}.
+ */
+ public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts);
+
+ /**
+ * Notifies provisioner that an application has been removed.
+ *
+ * @param application The {@link ApplicationId} that was removed.
+ */
+ public void removed(ApplicationId application);
+
+ /**
+ * Requests a restart of the services of the given application
+ *
+ * @param application the application to restart
+ * @param filter a filter which matches the application nodes to restart
+ */
+ public void restart(ApplicationId application, HostFilter filter);
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Quota.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Quota.java
new file mode 100644
index 00000000000..d3a5e6b0b96
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Quota.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * @author musum
+ */
+public class Quota {
+
+ private final int numberOfHosts;
+
+ public Quota() {
+ this(Integer.MAX_VALUE);
+ }
+
+ public Quota(int numberOfHosts) {
+ this.numberOfHosts = numberOfHosts;
+ }
+
+ public int getNumberOfHosts() {
+ return numberOfHosts;
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/RegionName.java b/config-provisioning/src/main/java/com/yahoo/config/provision/RegionName.java
new file mode 100644
index 00000000000..52721e7100e
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/RegionName.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Objects;
+
+/**
+ * Represents an applications region, which may be any kind of string or default. This type is defined
+ * in order to provide a type safe API for defining regions.
+ *
+ * @author lulf
+ * @since 5.11
+ */
+public class RegionName implements Comparable<RegionName> {
+
+ private final String region;
+
+ private RegionName(String region) {
+ this.region = region;
+ }
+
+ @Override
+ public int hashCode() {
+ return region.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof RegionName)) return false;
+ return Objects.equals(((RegionName) obj).region, region);
+ }
+
+ @Override
+ public String toString() {
+ return region;
+ }
+
+ public static RegionName from(String region) {
+ return new RegionName(region);
+ }
+
+ public static RegionName defaultName() {
+ return new RegionName("default");
+ }
+
+ public boolean isDefault() {
+ return equals(RegionName.defaultName());
+ }
+
+ public String value() { return region; }
+
+ @Override
+ public int compareTo(RegionName region) {
+ return this.region.compareTo(region.region);
+ }
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Rotation.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Rotation.java
new file mode 100644
index 00000000000..e565ca41967
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Rotation.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+
+import java.util.Objects;
+
+/**
+ * A Brooklyn rotation, e.g. rotation-042.vespa.a02.yahoodns.net.
+ */
+public class Rotation {
+
+ private final String id;
+
+ public Rotation(String id) {
+ this.id = Objects.requireNonNull(id, "Rotation id cannot be null");
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Rotation)) {
+ return false;
+ }
+ final Rotation that = (Rotation) o;
+ return (this.id.equals(that.id));
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/TenantName.java b/config-provisioning/src/main/java/com/yahoo/config/provision/TenantName.java
new file mode 100644
index 00000000000..81956919da5
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/TenantName.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import java.util.Objects;
+
+/**
+ * Represents a tenant in the provision API.
+ *
+ * @author lulf
+ * @since 5.12
+ */
+public class TenantName implements Comparable<TenantName> {
+
+ private final String name;
+
+ private TenantName(String name) {
+ this.name = name;
+ }
+
+ public String value() { return name; }
+
+ /**
+ * Create a {@link TenantName} with a given name.
+ *
+ * @param name Name of tenant.
+ * @return instance of {@link TenantName}.
+ */
+ public static TenantName from(String name) {
+ return new TenantName(name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TenantName)) return false;
+ return Objects.equals(((TenantName)obj).value(), value());
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static TenantName defaultName() {
+ return from("default");
+ }
+
+ @Override
+ public int compareTo(TenantName that) {
+ return this.name.compareTo(that.name);
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Version.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Version.java
new file mode 100644
index 00000000000..6dcb5fe207e
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Version.java
@@ -0,0 +1,173 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * The {@link Version} class is used in providing versioned config for applications.
+ *
+ * A {@link Version} object has three components:
+ *
+ * * Major version. A non-negative integer.
+ * * Minor version. A non-negative integer.
+ * * Micro version. A non-negative integer.
+ *
+ * @author Vegard Sjonfjell
+ * @since 5.39
+ * Loosely based on component/Version.java
+ * {@link Version} objects are immutable.
+ */
+public final class Version implements Comparable<Version> {
+
+ private final int major;
+ private final int minor;
+ private final int micro;
+ private final String stringValue;
+
+ /**
+ * @see #fromIntValues
+ */
+ private Version(int major, int minor, int micro) {
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ stringValue = toSerializedForm();
+ verify();
+ }
+
+ /**
+ * @see #fromString
+ */
+ private Version(String versionString) {
+ try {
+ String[] components = versionString.split("\\.", 3);
+ assert (components.length == 3);
+ major = Integer.parseInt(components[0]);
+ minor = Integer.parseInt(components[1]);
+ micro = Integer.parseInt(components[2]);
+ stringValue = toSerializedForm();
+ verify();
+ } catch (AssertionError | ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
+ throw new IllegalArgumentException(String.format("Invalid version specification: \"%s\": %s", versionString, e.getMessage()));
+ }
+ }
+
+ /**
+ * Verifies that the numerical components in a version are legal.
+ * Must be called on construction after the component values are set
+ *
+ * @throws IllegalArgumentException if one of the numerical components are negative.
+ */
+ private void verify() {
+ if (major < 0)
+ throw new IllegalArgumentException("Negative major value");
+ if (minor < 0)
+ throw new IllegalArgumentException("Negative minor value");
+ if (micro < 0)
+ throw new IllegalArgumentException("Negative micro value");
+ }
+
+ public String toSerializedForm() {
+ return String.format("%d.%d.%d", major, minor, micro);
+ }
+
+ /**
+ * Creates a {@link Version} object from the specified components.
+ *
+ * @param major major component of the version identifier.
+ * @param minor minor component of the version identifier.
+ * @param micro micro component of the version identifier.
+ * @throws IllegalArgumentException if one of the numerical components are negative.
+ * @return {@link Version} identifier object constructed from integer components.
+ */
+ public static Version fromIntValues(int major, int minor, int micro) {
+ return new Version(major, minor, micro);
+ }
+
+ /**
+ * Creates a version object from the specified string. Version strings are in the format major.minor.micro
+ *
+ * @param versionString String representation of the version.
+ * @throws IllegalArgumentException If version string is improperly formatted.
+ * @return {@link Version} object constructed from string representation.
+ */
+ public static Version fromString(String versionString) {
+ return new Version(versionString);
+ }
+
+ /**
+ * Returns a string representation of this version identifier, encoded as major.minor.micro
+ */
+ public String toString() { return stringValue; }
+
+ /**
+ * Returns major version component
+ */
+ public int getMajor() { return major; }
+
+ /**
+ * Returns minor version component
+ */
+ public int getMinor() { return minor; }
+
+ /**
+ * Returns micro version component
+ */
+ public int getMicro() { return micro; }
+
+ @Override
+ public int hashCode() { return stringValue.hashCode(); }
+
+ /**
+ * Performs an equality test between this {@link Version} object and another.
+ *
+ * A version is considered to be equal to another version if the
+ * major, minor and micro components are equal.
+ *
+ * @param object The {@link Version} object to be compared to this version.
+ * @return <code>true</code> if object is a
+ * {@link Version} and is equal to this object;
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (object == null || object.getClass() != this.getClass()) {
+ return false;
+ }
+
+ Version other = (Version)object;
+ return this.major == other.major && this.minor == other.minor && this.micro == other.micro;
+ }
+
+ /**
+ * Compares this {@link Version} object to another.
+ *
+ * A version is considered to be less than another version if its
+ * major component is less than the other version's major component, or the
+ * major components are equal and its minor component is less than the other
+ * version's minor component, or the major and minor components are equal
+ * and its micro component is less than the other version's micro component.
+ *
+ * A version is considered to be equal to another version if the
+ * major, minor and micro components are equal.
+ *
+ * @param other the {@link Version} object to be compared to this version.
+ * @return A negative integer, zero, or a positive integer if this object is
+ * less than, equal to, or greater than the specified {@link Version} object.
+ * @throws ClassCastException if the specified object is not a {@link Version}.
+ */
+ @Override
+ public int compareTo(Version other) {
+ if (this == other) return 0;
+
+ int comparison = Integer.compare(getMajor(), other.getMajor());
+ if (comparison != 0) {
+ return comparison;
+ }
+
+ comparison = Integer.compare(getMinor(), other.getMinor());
+ if (comparison != 0) {
+ return comparison;
+ }
+
+ return Integer.compare(getMicro(), other.getMicro());
+ }
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Zone.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Zone.java
new file mode 100644
index 00000000000..1b0962a8b26
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Zone.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+import java.util.Optional;
+
+/**
+ * The zone (environment + region) of this runtime.
+ * An injected instance of this will return the correct current environment and region.
+ * Components can use this to obtain information about which zone they are running in.
+ *
+ * @author bratseth
+ */
+public class Zone {
+
+ private final Environment environment;
+ private final RegionName region;
+ private final FlavorDefaults flavorDefaults;
+
+ @Inject
+ public Zone(ConfigserverConfig configserverConfig) {
+ this(Environment.from(configserverConfig.environment()), RegionName.from(configserverConfig.region()),
+ new FlavorDefaults(configserverConfig));
+ }
+
+ /** Create from environment and region */
+ public Zone(Environment environment, RegionName region) { this(environment, region, "default"); }
+
+ /** Create from environment and region. Useful for testing. */
+ public Zone(Environment environment, RegionName region, String defaultFlavor) {
+ this(environment, region, new FlavorDefaults(defaultFlavor));
+ }
+
+ private Zone(Environment environment, RegionName region, FlavorDefaults flavorDefaults) {
+ this.environment = environment;
+ this.region = region;
+ this.flavorDefaults = flavorDefaults;
+ }
+
+ /** Returns the current environment */
+ public Environment environment() { return environment; }
+
+ /** Returns the current region */
+ public RegionName region() { return region; }
+
+ /** Returns the default hardware flavor to assign in this zone */
+ public String defaultFlavor(ClusterSpec.Type clusterType) { return flavorDefaults.flavor(clusterType); }
+
+ /** Do not use */
+ public static Zone defaultZone() {
+ return new Zone(Environment.defaultEnvironment(), RegionName.defaultName());
+ }
+
+ @Override
+ public String toString() {
+ return "zone " + environment + "." + region;
+ }
+
+ private static class FlavorDefaults {
+
+ /** The default default flavor */
+ private final String defaultFlavor;
+
+ /** The default flavor for each cluster type, or empty to use defaultFlavor */
+ private final Optional<String> adminFlavor;
+ private final Optional<String> containerFlavor;
+ private final Optional<String> contentFlavor;
+
+ /** Creates this with a default flavor and all cluster type flavors empty */
+ public FlavorDefaults(String defaultFlavor) {
+ this(defaultFlavor, Optional.empty(), Optional.empty(), Optional.empty());
+ }
+
+ /** Creates this with a default flavor and all cluster type flavors empty */
+ public FlavorDefaults(String defaultFlavor,
+ Optional<String> adminFlavor, Optional<String> containerFlavor, Optional<String> contentFlavor) {
+ this.defaultFlavor = defaultFlavor;
+ this.adminFlavor = adminFlavor;
+ this.containerFlavor = containerFlavor;
+ this.contentFlavor = contentFlavor;
+ }
+
+ public FlavorDefaults(ConfigserverConfig config) {
+ this(config.defaultFlavor(),
+ emptyIfDefault(config.defaultAdminFlavor()),
+ emptyIfDefault(config.defaultContainerFlavor()),
+ emptyIfDefault(config.defaultContentFlavor()));
+ }
+
+ /** Map "default" to empty - this config cannot have missing values due to the need for supporting non-hosted */
+ private static Optional<String> emptyIfDefault(String value) {
+ if (Strings.isNullOrEmpty(value)) return Optional.empty();
+ if (value.equals("default")) return Optional.empty();
+ return Optional.of(value);
+ }
+
+ /**
+ * Returns the flavor default for a given cluster type.
+ * This may be "default" - which is an invalid value - but never null.
+ */
+ public String flavor(ClusterSpec.Type clusterType) {
+ switch (clusterType) {
+ case admin: return adminFlavor.orElse(defaultFlavor);
+ case container: return containerFlavor.orElse(defaultFlavor);
+ case content: return contentFlavor.orElse(defaultFlavor);
+ default: return defaultFlavor; // future cluster types
+ }
+ }
+
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/package-info.java b/config-provisioning/src/main/java/com/yahoo/config/provision/package-info.java
new file mode 100644
index 00000000000..fbf362aa292
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.config.provision;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationIdTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationIdTest.java
new file mode 100644
index 00000000000..33f5d0ce00a
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationIdTest.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import static org.junit.Assert.assertEquals;
+
+import com.yahoo.cloud.config.ApplicationIdConfig;
+import com.yahoo.test.TotalOrderTester;
+import org.junit.Test;
+import com.google.common.testing.EqualsTester;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * @author lulf
+ * @author vegard
+ * @since 5.1
+ */
+public class ApplicationIdTest {
+
+ ApplicationId idFrom(String tenant, String name, String instance) {
+ ApplicationId.Builder b = new ApplicationId.Builder();
+ b.tenant(tenant);
+ b.applicationName(name);
+ b.instanceName(instance);
+ return b.build();
+ }
+
+ @Test
+ public void require_that_application_id_is_set() {
+ ApplicationId app = applicationId("application");
+ assertEquals("application", app.application().value());
+ app = idFrom("tenant", "application", "instance");
+ assertEquals("tenant", app.tenant().value());
+ assertEquals("application", app.application().value());
+ assertEquals("instance", app.instance().value());
+ }
+
+ @Test
+ public void require_that_equals_and_hashcode_behaves_correctly() {
+ new EqualsTester()
+ .addEqualityGroup(idFrom("tenant1", "name1", "instance1"),
+ idFrom("tenant1", "name1", "instance1"))
+ .addEqualityGroup(idFrom("tenant2", "name1", "instance1"))
+ .addEqualityGroup(idFrom("tenant1", "name2", "instance1"))
+ .addEqualityGroup(idFrom("tenant1", "name1", "instance2"))
+ .addEqualityGroup(applicationId("onlyName1"))
+ .addEqualityGroup(applicationId("onlyName2"))
+ .testEquals();
+ }
+
+ @Test
+ public void require_that_value_format_is_correct() {
+ ApplicationId id1 = applicationId("foo");
+ ApplicationId id2 = applicationId("bar");
+ ApplicationId id3 = idFrom("tenant", "baz", "bim");
+ assertThat(id1.serializedForm(), is("default:foo:default"));
+ assertThat(id2.serializedForm(), is("default:bar:default"));
+ assertThat(id3.serializedForm(), is("tenant:baz:bim"));
+ }
+
+ @Test
+ public void require_that_idstring_can_be_parsed() {
+ ApplicationId id = ApplicationId.fromSerializedForm(TenantName.from("bim"), "foo:prod:baz:bim");
+ assertThat(id.application().value(), is("foo"));
+ assertThat(id.instance().value(), is("bim"));
+ assertThat(id.tenant().value(), is("bim"));
+
+ id = ApplicationId.fromSerializedForm(TenantName.from("unused"), "ten:foo:bim");
+ assertThat(id.tenant().value(), is("ten"));
+ assertThat(id.application().value(), is("foo"));
+ assertThat(id.instance().value(), is("bim"));
+ }
+
+ // TODO: Probably more test cases to break parsing.
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_invalid_idstring_throws_exception() {
+ ApplicationId.fromSerializedForm(TenantName.defaultName(), "foo:baz");
+ }
+
+ @Test
+ public void require_that_defaults_are_given() {
+ ApplicationId id1 = applicationId("foo");
+ assertThat(id1.tenant().value(), is("default"));
+ assertThat(id1.instance().value(), is("default"));
+ }
+
+ @Test
+ public void require_that_compare_to_is_correct() {
+ new TotalOrderTester<ApplicationId>()
+ .theseObjects(idFrom("tenant1", "name1", "instance1"),
+ idFrom("tenant1", "name1", "instance1"))
+ .areLessThan(idFrom("tenant2", "name1", "instance1"))
+ .areLessThan(idFrom("tenant2", "name2", "instance1"))
+ .areLessThan(idFrom("tenant2", "name2", "instance2"))
+ .testOrdering();
+ }
+
+ @Test
+ public void require_that_instance_from_config_is_correct() {
+ ApplicationIdConfig.Builder builder = new ApplicationIdConfig.Builder();
+ builder.tenant("a");
+ builder.application("b");
+ builder.instance("c");
+ ApplicationId applicationId = new ApplicationId(new ApplicationIdConfig(builder));
+ assertEquals("a", applicationId.tenant().value());
+ assertEquals("b", applicationId.application().value());
+ assertEquals("c", applicationId.instance().value());
+ }
+
+ private ApplicationId applicationId(String applicationName) {
+ return ApplicationId.from(TenantName.defaultName(),
+ ApplicationName.from(applicationName), InstanceName.defaultName());
+ }
+
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationNameTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationNameTest.java
new file mode 100644
index 00000000000..6cbda7d469a
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/ApplicationNameTest.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * @author lulf
+ * @since 5.25
+ */
+public class ApplicationNameTest extends IdentifierTestBase<ApplicationName> {
+ @Override
+ protected ApplicationName createInstance(String id) {
+ return ApplicationName.from(id);
+ }
+
+ @Override
+ protected ApplicationName createDefaultInstance() {
+ return ApplicationName.defaultName();
+ }
+
+ @Override
+ protected boolean isDefault(ApplicationName instance) {
+ return instance.isDefault();
+ }
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/ClusterMembershipTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/ClusterMembershipTest.java
new file mode 100644
index 00000000000..6c2565ba3a0
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/ClusterMembershipTest.java
@@ -0,0 +1,117 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class ClusterMembershipTest {
+
+ @Test
+ public void testContainerServiceInstance() {
+ ClusterSpec cluster = ClusterSpec.from(ClusterSpec.Type.container, ClusterSpec.Id.from("id1"), Optional.empty());
+ assertContainerService(ClusterMembership.from(cluster, 3));
+ }
+
+ @Test
+ public void testContainerServiceInstanceFromString() {
+ assertContainerService(ClusterMembership.from("container/id1/3", Optional.empty()));
+ }
+
+ @Test
+ public void testServiceInstance() {
+ ClusterSpec cluster = ClusterSpec.from(ClusterSpec.Type.content, ClusterSpec.Id.from("id1"), Optional.empty());
+ assertContentService(ClusterMembership.from(cluster, 37));
+ }
+
+ @Test
+ public void testServiceInstanceFromString() {
+ assertContentService(ClusterMembership.from("content/id1/37", Optional.empty()));
+ }
+
+ @Test
+ public void testServiceInstanceWithGroup() {
+ ClusterSpec cluster = ClusterSpec.from(ClusterSpec.Type.content, ClusterSpec.Id.from("id1"),
+ Optional.of(ClusterSpec.Group.from("gr4")));
+ assertContentServiceWithGroup(ClusterMembership.from(cluster, 37));
+ }
+
+ @Test
+ public void testServiceInstanceWithGroupFromString() {
+ assertContentServiceWithGroup(ClusterMembership.from("content/id1/gr4/37", Optional.empty()));
+ }
+
+ @Test
+ public void testServiceInstanceWithRetire() {
+ ClusterSpec cluster = ClusterSpec.from(ClusterSpec.Type.content, ClusterSpec.Id.from("id1"), Optional.empty());
+ assertContentServiceWithRetire(ClusterMembership.retiredFrom(cluster, 37));
+ }
+
+ @Test
+ public void testServiceInstanceWithRetireFromString() {
+ assertContentServiceWithRetire(ClusterMembership.from("content/id1/37/retired", Optional.empty()));
+ }
+
+ @Test
+ public void testServiceInstanceWithGroupAndRetire() {
+ ClusterSpec cluster = ClusterSpec.from(ClusterSpec.Type.content, ClusterSpec.Id.from("id1"),
+ Optional.of(ClusterSpec.Group.from("gr4")));
+ assertContentServiceWithGroupAndRetire(ClusterMembership.retiredFrom(cluster, 37));
+ }
+
+ @Test
+ public void testServiceInstanceWithGroupAndRetireFromString() {
+ assertContentServiceWithGroupAndRetire(ClusterMembership.from("content/id1/gr4/37/retired", Optional.empty()));
+ }
+
+ private void assertContainerService(ClusterMembership instance) {
+ assertEquals(ClusterSpec.Type.container, instance.cluster().type());
+ assertEquals("id1", instance.cluster().id().value());
+ assertEquals(Optional.<ClusterSpec.Group>empty(), instance.cluster().group());
+ assertEquals(3, instance.index());
+ assertEquals("container/id1/3", instance.stringValue());
+ }
+
+ private void assertContentService(ClusterMembership instance) {
+ assertEquals(ClusterSpec.Type.content, instance.cluster().type());
+ assertEquals("id1", instance.cluster().id().value());
+ assertFalse("gr4", instance.cluster().group().isPresent());
+ assertEquals(37, instance.index());
+ assertFalse(instance.retired());
+ assertEquals("content/id1/37", instance.stringValue());
+ }
+
+ private void assertContentServiceWithGroup(ClusterMembership instance) {
+ assertEquals(ClusterSpec.Type.content, instance.cluster().type());
+ assertEquals("id1", instance.cluster().id().value());
+ assertEquals("gr4", instance.cluster().group().get().value());
+ assertEquals(37, instance.index());
+ assertFalse(instance.retired());
+ assertEquals("content/id1/gr4/37", instance.stringValue());
+ }
+
+ private void assertContentServiceWithRetire(ClusterMembership instance) {
+ assertEquals(ClusterSpec.Type.content, instance.cluster().type());
+ assertEquals("id1", instance.cluster().id().value());
+ assertEquals(37, instance.index());
+ assertTrue(instance.retired());
+ assertEquals("content/id1/37/retired", instance.stringValue());
+ }
+
+ private void assertContentServiceWithGroupAndRetire(ClusterMembership instance) {
+ assertEquals(ClusterSpec.Type.content, instance.cluster().type());
+ assertEquals("id1", instance.cluster().id().value());
+ assertEquals("gr4", instance.cluster().group().get().value());
+ assertEquals(37, instance.index());
+ assertTrue(instance.retired());
+ assertEquals("content/id1/gr4/37/retired", instance.stringValue());
+ }
+
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/ClusterSpecTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/ClusterSpecTest.java
new file mode 100644
index 00000000000..d51d97e36c0
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/ClusterSpecTest.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+/**
+ * @author lulf
+ */
+public class ClusterSpecTest {
+
+ @Test
+ public void testIdEquals() {
+ new EqualsTester()
+ .addEqualityGroup(ClusterSpec.Id.from("id1"), ClusterSpec.Id.from("id1"))
+ .addEqualityGroup(ClusterSpec.Id.from("id2"))
+ .addEqualityGroup(ClusterSpec.Id.from("id3"))
+ .testEquals();
+ }
+
+ @Test
+ public void testGroupEquals() {
+ new EqualsTester()
+ .addEqualityGroup(ClusterSpec.Group.from("id1"), ClusterSpec.Group.from("id1"))
+ .addEqualityGroup(ClusterSpec.Group.from("id2"))
+ .addEqualityGroup(ClusterSpec.Group.from("id3"))
+ .testEquals();
+ }
+
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/HostFilterTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/HostFilterTest.java
new file mode 100644
index 00000000000..b16e2d76ca8
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/HostFilterTest.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class HostFilterTest {
+
+ @Test
+ public void testSingleConditionFilter() {
+ HostFilter all = HostFilter.all();
+ HostFilter hostname = HostFilter.hostname("host1");
+ HostFilter type = HostFilter.clusterType(ClusterSpec.Type.content);
+ HostFilter id = HostFilter.clusterId(ClusterSpec.Id.from("type1"));
+
+ assertTrue( all.matches("anyhost", "flavor", membership("container/anytype/0")));
+ assertFalse(hostname.matches("anyhost", "flavor", membership("container/anytype/0")));
+ assertFalse(type.matches("anyhost", "flavor", membership("container/anytype/0")));
+ assertFalse(id.matches("anyhost", "flavor", membership("container/anytype/0")));
+
+ assertTrue( all.matches("anyhost", "flavor", membership("content/anytype/0")));
+ assertFalse(hostname.matches("anyhost", "flavor", membership("content/anytype/0")));
+ assertTrue( type.matches("anyhost", "flavor", membership("content/anytype/0")));
+ assertFalse( id.matches("anyhost", "flavor", membership("content/anytype/0")));
+
+ assertTrue( all.matches("host1", "flavor", membership("content/anytype/0")));
+ assertTrue( hostname.matches("host1", "flavor", membership("content/anytype/0")));
+ assertTrue( type.matches("host1", "flavor", membership("content/anytype/0")));
+ assertFalse( id.matches("host1", "flavor", membership("content/anytype/0")));
+
+ assertTrue( all.matches("host1", "flavor", membership("content/type1/0")));
+ assertTrue( hostname.matches("host1", "flavor", membership("content/type1/0")));
+ assertTrue( type.matches("host1", "flavor", membership("content/type1/0")));
+ assertTrue( id.matches("host1", "flavor", membership("content/type1/0")));
+ }
+
+ @Test
+ public void testMultiConditionFilter() {
+ HostFilter typeAndId = HostFilter.from(Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.singletonList(ClusterSpec.Type.content),
+ Collections.singletonList(ClusterSpec.Id.from("type1")));
+
+ assertFalse(typeAndId.matches("anyhost", "flavor", membership("content/anyType/0")));
+ assertFalse(typeAndId.matches("anyhost", "flavor", membership("container/type1/0")));
+ assertTrue(typeAndId.matches("anyhost", "flavor", membership("content/type1/0")));
+ }
+
+ @Test
+ public void testMultiConditionFilterFromStrings() {
+ HostFilter typeAndId = HostFilter.from("host1 host2, host3,host4", " , ,flavor", null, "type1 ");
+
+ assertFalse(typeAndId.matches("anotherhost", "flavor", membership("content/type1/0")));
+ assertTrue(typeAndId.matches("host1", "flavor", membership("content/type1/0")));
+ assertTrue(typeAndId.matches("host2", "flavor", membership("content/type1/0")));
+ assertTrue(typeAndId.matches("host3", "flavor", membership("content/type1/0")));
+ assertTrue(typeAndId.matches("host4", "flavor", membership("content/type1/0")));
+ assertFalse(typeAndId.matches("host1", "flavor", membership("content/type2/0")));
+ assertFalse(typeAndId.matches("host4", "differentflavor", membership("content/type1/0")));
+ }
+
+ private Optional<ClusterMembership> membership(String membershipString) {
+ return Optional.of(ClusterMembership.from(membershipString, Optional.empty()));
+ }
+
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/IdentifierTestBase.java b/config-provisioning/src/test/java/com/yahoo/config/provision/IdentifierTestBase.java
new file mode 100644
index 00000000000..b740d0dba7c
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/IdentifierTestBase.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Generic test for identifiers such as {@link Environment} and {@link RegionName}.
+ * @author lulf
+ * @since 5.23
+ */
+public abstract class IdentifierTestBase<ID_TYPE> {
+
+ protected abstract ID_TYPE createInstance(String id);
+ protected abstract ID_TYPE createDefaultInstance();
+ protected abstract boolean isDefault(ID_TYPE instance);
+
+ @Test
+ public void testDefault() {
+ ID_TYPE def = createDefaultInstance();
+ ID_TYPE def2 = createInstance("default");
+ ID_TYPE notdef = createInstance("default2");
+ assertTrue(isDefault(def));
+ assertTrue(isDefault(def2));
+ assertFalse(isDefault(notdef));
+ assertThat(def, is(def2));
+ assertThat(def2, is(not(notdef)));
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(createInstance("foo"), createInstance("foo"))
+ .addEqualityGroup(createInstance("bar"))
+ .addEqualityGroup(createInstance("baz"))
+ .testEquals();
+ }
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/InstanceNameTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/InstanceNameTest.java
new file mode 100644
index 00000000000..6490b9cb5fe
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/InstanceNameTest.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * @author lulf
+ * @since 5.25
+ */
+public class InstanceNameTest extends IdentifierTestBase<InstanceName> {
+ @Override
+ protected InstanceName createInstance(String id) {
+ return InstanceName.from(id);
+ }
+
+ @Override
+ protected InstanceName createDefaultInstance() {
+ return InstanceName.defaultName();
+ }
+
+ @Override
+ protected boolean isDefault(InstanceName instance) {
+ return instance.isDefault();
+ }
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/ProvisionInfoTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/ProvisionInfoTest.java
new file mode 100644
index 00000000000..b4239f2b3bd
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/ProvisionInfoTest.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.12
+ */
+public class ProvisionInfoTest {
+
+ private final HostSpec h1 = new HostSpec("host1", Optional.empty());
+ private final HostSpec h2 = new HostSpec("host2", Optional.empty());
+ private final HostSpec h3 = new HostSpec("host3", Optional.of(ClusterMembership.from("container/test/0", Optional.empty())));
+ private final HostSpec h4 = new HostSpec("host4", Optional.of(ClusterMembership.from("container/test/1", Optional.of("dockerImg"))));
+
+ @Test
+ public void testProvisionInfoSerialization() throws IOException {
+ Set<HostSpec> hosts = new LinkedHashSet<>();
+ hosts.add(h1);
+ hosts.add(h2);
+ hosts.add(h3);
+ hosts.add(h4);
+ ProvisionInfo info = ProvisionInfo.withHosts(hosts);
+ assertProvisionInfo(info);
+ }
+
+ @Test
+ public void testProvisionInfoMerging() throws IOException {
+ Set<HostSpec> hostsA = new LinkedHashSet<>(Collections.singleton(h1));
+ Set<HostSpec> hostsB = new LinkedHashSet<>();
+ hostsB.add(h2);
+ hostsB.add(h3);
+ hostsB.add(h4);
+
+ ProvisionInfo infoA = ProvisionInfo.withHosts(hostsA);
+ ProvisionInfo infoB = ProvisionInfo.withHosts(hostsB);
+ assertProvisionInfo(infoA.merge(infoB));
+ assertProvisionInfo(infoB.merge(infoA));
+ }
+
+ private void assertProvisionInfo(ProvisionInfo info) throws IOException {
+ ProvisionInfo serializedInfo = ProvisionInfo.fromJson(info.toJson());
+ assertEquals(info.getHosts().size(), serializedInfo.getHosts().size());
+ assertTrue(serializedInfo.getHosts().contains(h1));
+ assertTrue(serializedInfo.getHosts().contains(h2));
+ assertTrue(serializedInfo.getHosts().contains(h3));
+ assertTrue(serializedInfo.getHosts().contains(h4));
+ assertTrue(!getHost(h1.hostname(), serializedInfo.getHosts()).membership().isPresent());
+ assertEquals("container/test/0", getHost(h3.hostname(), serializedInfo.getHosts()).membership().get().stringValue());
+ assertEquals("dockerImg", getHost(h4.hostname(), serializedInfo.getHosts()).membership().get().cluster().dockerImage().get());
+ }
+
+ private HostSpec getHost(String hostname, Set<HostSpec> hosts) {
+ for (HostSpec host : hosts)
+ if (host.hostname().equals(hostname))
+ return host;
+ throw new IllegalArgumentException("No host " + hostname + " is present");
+ }
+
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/RegionTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/RegionTest.java
new file mode 100644
index 00000000000..bd0fa63cf1f
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/RegionTest.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+/**
+ * @author lulf
+ * @since 5.23
+ */
+public class RegionTest extends IdentifierTestBase<RegionName> {
+ @Override
+ protected RegionName createInstance(String id) {
+ return RegionName.from(id);
+ }
+
+ @Override
+ protected RegionName createDefaultInstance() {
+ return RegionName.defaultName();
+ }
+
+ @Override
+ protected boolean isDefault(RegionName instance) {
+ return instance.isDefault();
+ }
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/TenantTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/TenantTest.java
new file mode 100644
index 00000000000..694aea4c39b
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/TenantTest.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.yahoo.test.TotalOrderTester;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.26
+ */
+public class TenantTest extends IdentifierTestBase<TenantName> {
+ @Override
+ protected TenantName createInstance(String id) {
+ return TenantName.from(id);
+ }
+
+ @Override
+ protected TenantName createDefaultInstance() {
+ return TenantName.defaultName();
+ }
+
+ @Override
+ protected boolean isDefault(TenantName instance) {
+ return instance.equals(TenantName.defaultName());
+ }
+
+ @Test
+ public void testComparator() {
+ assertThat(TenantName.defaultName().compareTo(TenantName.defaultName()), is(0));
+
+ new TotalOrderTester<TenantName>()
+ .theseObjects(TenantName.from("a"), TenantName.from("a"))
+ .areLessThan(TenantName.from("b"))
+ .testOrdering();
+ }
+}
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/VersionTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/VersionTest.java
new file mode 100644
index 00000000000..06114af56eb
--- /dev/null
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/VersionTest.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.provision;
+
+import com.yahoo.test.TotalOrderTester;
+import org.junit.Test;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.*;
+import com.google.common.testing.EqualsTester;
+
+/**
+ * @author Vegard Sjonfjell
+ * @since 5.39
+ */
+public class VersionTest {
+ @Test
+ public void testConstructFromIntegers() {
+ Version exampleVersion = Version.fromIntValues(3, 2, 1);
+ assertThat(exampleVersion.getMajor(), is(3));
+ assertThat(exampleVersion.getMinor(), is(2));
+ assertThat(exampleVersion.getMicro(), is(1));
+ }
+
+ @Test (expected = IllegalArgumentException.class)
+ public void testConstructFromIntegersNegativesShouldFail() throws IllegalArgumentException {
+ Version.fromIntValues(2, -1, 1);
+ }
+
+ @Test (expected = IllegalArgumentException.class)
+ public void testConstructFromStringTooLongVersionStringShouldFail() throws IllegalArgumentException {
+ Version.fromString("3.2.1.4");
+ }
+
+ @Test (expected = IllegalArgumentException.class)
+ public void testConstructFromStringTooShortVersionStringShouldFail() throws IllegalArgumentException {
+ Version.fromString("3.2");
+ }
+
+ @Test (expected = IllegalArgumentException.class)
+ public void testConstructFromStringInvalidVersionStringShouldFail() throws IllegalArgumentException {
+ Version.fromString("4.34.3a");
+ }
+
+ @Test
+ public void testEncodeToStringRepresentation() {
+ assertThat(Version.fromIntValues(3, 2, 1).toSerializedForm(), is("3.2.1"));
+ assertThat(Version.fromIntValues(0, 0, 0).toSerializedForm(), is("0.0.0"));
+ }
+
+ @Test
+ public void testEqualityAndHashCode() {
+ new EqualsTester()
+ .addEqualityGroup(Version.fromIntValues(3, 2, 1), Version.fromIntValues(3, 2, 1))
+ .addEqualityGroup(Version.fromIntValues(1, 2, 3), Version.fromString("1.2.3"))
+ .addEqualityGroup(Version.fromString("1.5.1"))
+ .addEqualityGroup(Version.fromIntValues(1, 2, 1))
+ .addEqualityGroup(Version.fromString("0.0.0"))
+ .testEquals();
+ }
+
+ @Test
+ public void testCompareTo() {
+ new TotalOrderTester<Version>()
+ .theseObjects(Version.fromIntValues(1, 1, 1), Version.fromIntValues(1, 1, 1))
+ .areLessThan(Version.fromIntValues(2, 1, 1))
+ .areLessThan(Version.fromIntValues(2, 2, 1))
+ .areLessThan(Version.fromIntValues(2, 2, 2))
+ .areLessThan(Version.fromIntValues(3, 0, 0))
+ .testOrdering();
+ }
+}