diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /config-provisioning/src |
Publish
Diffstat (limited to 'config-provisioning/src')
33 files changed, 2230 insertions, 0 deletions
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(); + } +} |