no_entry: Move kubernetes classes to new location Signed-off-by: Andrew Donald Kennedy <[email protected]>
Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/445884b1 Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/445884b1 Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/445884b1 Branch: refs/heads/master Commit: 445884b1b11c1da80578f2a4968cb6ccb3c456f7 Parents: e890c00 Author: CloudsoftOps <[email protected]> Authored: Thu May 18 17:52:06 2017 +0100 Committer: Andrew Donald Kennedy <[email protected]> Committed: Fri May 19 14:03:37 2017 +0100 ---------------------------------------------------------------------- .../entity/kubernetes/KubernetesPod.java | 82 ++ .../entity/kubernetes/KubernetesPodImpl.java | 7 + .../entity/kubernetes/KubernetesResource.java | 40 + .../kubernetes/KubernetesResourceImpl.java | 18 + .../location/kubernetes/ImageChooser.java | 71 ++ .../location/kubernetes/KubernetesCerts.java | 64 ++ .../kubernetes/KubernetesClientRegistry.java | 11 + .../KubernetesClientRegistryImpl.java | 78 ++ .../location/kubernetes/KubernetesLocation.java | 1014 ++++++++++++++++++ .../kubernetes/KubernetesLocationConfig.java | 164 +++ .../kubernetes/KubernetesLocationResolver.java | 47 + .../machine/KubernetesEmptyMachineLocation.java | 68 ++ .../machine/KubernetesMachineLocation.java | 27 + .../machine/KubernetesSshMachineLocation.java | 28 + .../location/kubernetes/ImageChooserTest.java | 67 ++ .../kubernetes/KubernetesCertsTest.java | 146 +++ .../kubernetes/KubernetesLocationLiveTest.java | 226 ++++ .../KubernetesLocationResolverTest.java | 84 ++ .../KubernetesLocationYamlLiveTest.java | 518 +++++++++ .../kubernetes/entity/KubernetesPod.java | 82 -- .../kubernetes/entity/KubernetesPodImpl.java | 7 - .../kubernetes/entity/KubernetesResource.java | 40 - .../entity/KubernetesResourceImpl.java | 18 - .../kubernetes/location/ImageChooser.java | 71 -- .../kubernetes/location/KubernetesCerts.java | 64 -- .../location/KubernetesClientRegistry.java | 11 - .../location/KubernetesClientRegistryImpl.java | 78 -- .../kubernetes/location/KubernetesLocation.java | 1014 ------------------ .../location/KubernetesLocationConfig.java | 164 --- .../location/KubernetesLocationResolver.java | 47 - .../machine/KubernetesEmptyMachineLocation.java | 68 -- .../machine/KubernetesMachineLocation.java | 27 - .../machine/KubernetesSshMachineLocation.java | 28 - .../kubernetes/location/ImageChooserTest.java | 67 -- .../location/KubernetesCertsTest.java | 146 --- .../location/KubernetesLocationLiveTest.java | 226 ---- .../KubernetesLocationResolverTest.java | 84 -- .../KubernetesLocationYamlLiveTest.java | 518 --------- 38 files changed, 2760 insertions(+), 2760 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPod.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPod.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPod.java new file mode 100644 index 0000000..bf5444d --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPod.java @@ -0,0 +1,82 @@ +package io.cloudsoft.amp.containerservice.kubernetes.entity; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.BasicConfigInheritance; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.config.MapConfigKey; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.util.math.MathPredicates; + +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; + +import io.cloudsoft.amp.containerservice.dockercontainer.DockerContainer; +import io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig; + +@ImplementedBy(KubernetesPodImpl.class) +public interface KubernetesPod extends DockerContainer { + + ConfigKey<String> NAMESPACE = KubernetesLocationConfig.NAMESPACE; + + ConfigKey<Boolean> PRIVILEGED = KubernetesLocationConfig.PRIVILEGED; + + @SuppressWarnings("serial") + ConfigKey<List<String>> PERSISTENT_VOLUMES = ConfigKeys.builder(new TypeToken<List<String>>() {}) + .name("persistentVolumes") + .description("Persistent volumes used by the pod") + .build(); + + ConfigKey<String> DEPLOYMENT = ConfigKeys.builder(String.class) + .name("deployment") + .description("The name of the service the deployed pod will use") + .build(); + + ConfigKey<Integer> REPLICAS = ConfigKeys.builder(Integer.class) + .name("replicas") + .description("Number of replicas in the pod") + .constraint(MathPredicates.greaterThanOrEqual(1d)) + .defaultValue(1) + .build(); + + @SuppressWarnings("serial") + ConfigKey<Map<String, String>> SECRETS = ConfigKeys.builder(new TypeToken<Map<String, String>>() {}) + .name("secrets") + .description("Secrets to be added to the pod") + .build(); + + @SuppressWarnings("serial") + ConfigKey<Map<String, String>> LIMITS = ConfigKeys.builder(new TypeToken<Map<String, String>>() {}) + .name("limits") + .description("Container resource limits for the pod") + .build(); + + MapConfigKey<Object> METADATA = new MapConfigKey.Builder<Object>(Object.class, "metadata") + .description("Metadata to set on the pod") + .defaultValue(ImmutableMap.<String, Object>of()) + .typeInheritance(BasicConfigInheritance.DEEP_MERGE) + .runtimeInheritance(BasicConfigInheritance.NOT_REINHERITED_ELSE_DEEP_MERGE) + .build(); + + AttributeSensor<String> KUBERNETES_DEPLOYMENT = Sensors.builder(String.class, "kubernetes.deployment") + .description("Deployment resources run in") + .build(); + + AttributeSensor<String> KUBERNETES_NAMESPACE = Sensors.builder(String.class, "kubernetes.namespace") + .description("Namespace that resources run in") + .build(); + + AttributeSensor<String> KUBERNETES_SERVICE = Sensors.builder(String.class, "kubernetes.service") + .description("Service that exposes the deployment") + .build(); + + AttributeSensor<String> KUBERNETES_POD = Sensors.builder(String.class, "kubernetes.pod") + .description("Pod running the deployment") + .build(); + + String EMPTY = "Empty"; +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPodImpl.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPodImpl.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPodImpl.java new file mode 100644 index 0000000..2acf734 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesPodImpl.java @@ -0,0 +1,7 @@ +package io.cloudsoft.amp.containerservice.kubernetes.entity; + +import io.cloudsoft.amp.containerservice.dockercontainer.DockerContainerImpl; + +public class KubernetesPodImpl extends DockerContainerImpl implements KubernetesPod { + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResource.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResource.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResource.java new file mode 100644 index 0000000..320c924 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResource.java @@ -0,0 +1,40 @@ +package io.cloudsoft.amp.containerservice.kubernetes.entity; + +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.util.core.ResourcePredicates; + +@ImplementedBy(KubernetesResourceImpl.class) +public interface KubernetesResource extends SoftwareProcess { + + ConfigKey<String> RESOURCE_FILE = ConfigKeys.builder(String.class) + .name("resource") + .description("Kubernetes resource YAML file URI") + .constraint(ResourcePredicates.urlExists()) + .build(); + + AttributeSensor<String> RESOURCE_TYPE = Sensors.builder(String.class, "kubernetes.resource.type") + .description("Kubernetes resource type") + .build(); + + AttributeSensor<String> RESOURCE_NAME = Sensors.builder(String.class, "kubernetes.resource.name") + .description("Kubernetes resource name") + .build(); + + AttributeSensor<String> KUBERNETES_NAMESPACE = KubernetesPod.KUBERNETES_NAMESPACE; + + String POD = "Pod"; + String DEPLOYMENT = "Deployment"; + String REPLICA_SET = "ReplicaSet"; + String CONFIG_MAP = "ConfigMap"; + String PERSISTENT_VOLUME = "PersistentVolume"; + String SECRET = "Secret"; + String SERVICE = "Service"; + String REPLICATION_CONTROLLER = "ReplicationController"; + String NAMESPACE = "Namespace"; + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResourceImpl.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResourceImpl.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResourceImpl.java new file mode 100644 index 0000000..3e7dad3 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesResourceImpl.java @@ -0,0 +1,18 @@ +package io.cloudsoft.amp.containerservice.kubernetes.entity; + +import org.apache.brooklyn.core.entity.BrooklynConfigKeys; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl; + +public class KubernetesResourceImpl extends EmptySoftwareProcessImpl implements KubernetesResource { + + @Override + public void init() { + super.init(); + + config().set(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true); + config().set(PROVISIONING_PROPERTIES.subKey("useJcloudsSshInit"), false); + config().set(PROVISIONING_PROPERTIES.subKey("waitForSshable"), false); + config().set(PROVISIONING_PROPERTIES.subKey("pollForFirstReachableAddress"), false); + config().set(EmptySoftwareProcessImpl.USE_SSH_MONITORING, false); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/ImageChooser.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/ImageChooser.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/ImageChooser.java new file mode 100644 index 0000000..1bda432 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/ImageChooser.java @@ -0,0 +1,71 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.jclouds.compute.domain.OsFamily; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; + +public class ImageChooser { + + private static final Logger LOG = LoggerFactory.getLogger(ImageChooser.class); + + public static class ImageMetadata { + private final OsFamily osFamily; + private final String osVersion; + private final String imageName; + + public ImageMetadata(OsFamily osFamily, String osVersion, String imageName) { + this.osFamily = checkNotNull(osFamily, "osFamily"); + this.osVersion = checkNotNull(osVersion, "osVersion"); + this.imageName = checkNotNull(imageName, "imageName"); + } + + public boolean matches(@Nullable OsFamily osFamily, @Nullable String osVersionRegex) { + if (osFamily != null && osFamily != this.osFamily) return false; + if (osVersionRegex != null && !osVersion.matches(osVersionRegex)) return false; + return true; + } + + public String getImageName() { + return imageName; + } + } + + private static final List<ImageMetadata> DEFAULT_IMAGES = ImmutableList.of( + new ImageMetadata(OsFamily.CENTOS, "7", "cloudsoft/centos:7"), + new ImageMetadata(OsFamily.UBUNTU, "14.04", "cloudsoft/ubuntu:14.04"), + new ImageMetadata(OsFamily.UBUNTU, "16.04", "cloudsoft/ubuntu:16.04")); + + private final List<ImageMetadata> images; + + public ImageChooser() { + this.images = DEFAULT_IMAGES; + } + + public ImageChooser(List<? extends ImageMetadata> images) { + this.images = ImmutableList.copyOf(images); + } + + public Optional<String> chooseImage(String osFamily, String osVersionRegex) { + return chooseImage((osFamily == null ? (OsFamily)null : OsFamily.fromValue(osFamily)), osVersionRegex); + } + + public Optional<String> chooseImage(OsFamily osFamily, String osVersionRegex) { + for (ImageMetadata imageMetadata : images) { + if (imageMetadata.matches(osFamily, osVersionRegex)) { + String imageName = imageMetadata.getImageName(); + LOG.debug("Choosing container image {}, for osFamily={} and osVersionRegex={}", new Object[] {imageName, osFamily, osVersionRegex}); + return Optional.of(imageName); + } + } + return Optional.absent(); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesCerts.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesCerts.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesCerts.java new file mode 100644 index 0000000..9bb840f --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesCerts.java @@ -0,0 +1,64 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CA_CERT_DATA; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CA_CERT_FILE; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CLIENT_CERT_DATA; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CLIENT_CERT_FILE; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CLIENT_KEY_ALGO; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CLIENT_KEY_DATA; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CLIENT_KEY_FILE; +import static io.cloudsoft.amp.containerservice.kubernetes.location.KubernetesLocationConfig.CLIENT_KEY_PASSPHRASE; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Optional; + +class KubernetesCerts { + + private static final Logger LOG = LoggerFactory.getLogger(KubernetesCerts.class); + + public final Optional<String> caCertData; + public final Optional<String> clientCertData; + public final Optional<String> clientKeyData; + public final Optional<String> clientKeyAlgo; + public final Optional<String> clientKeyPassphrase; + + public KubernetesCerts(ConfigBag config) { + caCertData = getData(CA_CERT_DATA, CA_CERT_FILE, config); + clientCertData = getData(CLIENT_CERT_DATA, CLIENT_CERT_FILE, config); + clientKeyData = getData(CLIENT_KEY_DATA, CLIENT_KEY_FILE, config); + clientKeyAlgo = getNonBlankOptional(CLIENT_KEY_ALGO, config); + clientKeyPassphrase = getNonBlankOptional(CLIENT_KEY_PASSPHRASE, config); + } + + protected Optional<String> getData(ConfigKey<String> dataKey, ConfigKey<String> fileKey, ConfigBag config) { + String data = Strings.isNonBlank(config.get(dataKey)) ? config.get(dataKey).trim() : null; + String file = config.get(fileKey); + String fileData = Strings.isNonBlank(file) ? getFileContents(file).trim() : null; + + if (Strings.isNonBlank(data) && Strings.isNonBlank(fileData)) { + if (data.equals(fileData)) { + LOG.warn("Duplicate (matching) configuration for " + dataKey.getName() + " and " + fileKey.getName() + " (continuing)"); + } else { + throw new IllegalStateException("Duplicate conflicting configuration for " + dataKey.getName() + " and " + fileKey.getName()); + } + } + + String result = Strings.isNonBlank(data) ? data : (Strings.isNonBlank(fileData) ? fileData : null); + return Optional.fromNullable(result); + } + + protected Optional<String> getNonBlankOptional(ConfigKey<? extends String> key, ConfigBag config) { + String result = config.get(key); + return Optional.fromNullable(Strings.isNonBlank(result) ? result : null); + } + + protected String getFileContents(String file) { + return ResourceUtils.create(this).getResourceAsString(file); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistry.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistry.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistry.java new file mode 100644 index 0000000..c240378 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistry.java @@ -0,0 +1,11 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import org.apache.brooklyn.util.core.config.ConfigBag; + +import io.fabric8.kubernetes.client.KubernetesClient; + +public interface KubernetesClientRegistry { + + KubernetesClient getKubernetesClient(ConfigBag conf); + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistryImpl.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistryImpl.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistryImpl.java new file mode 100644 index 0000000..32bfee6 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesClientRegistryImpl.java @@ -0,0 +1,78 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.time.Duration; + +import com.google.common.base.Throwables; +import com.google.common.io.BaseEncoding; + +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; + +public class KubernetesClientRegistryImpl implements KubernetesClientRegistry { + + public static final KubernetesClientRegistryImpl INSTANCE = new KubernetesClientRegistryImpl(); + + @Override + public KubernetesClient getKubernetesClient(ConfigBag conf) { + String masterUrl = checkNotNull(conf.get(KubernetesLocationConfig.MASTER_URL), "master url must not be null"); + + URL url; + try { + url = new URL(masterUrl); + } catch (MalformedURLException e) { + throw Throwables.propagate(e); + } + + ConfigBuilder configBuilder = new ConfigBuilder() + .withMasterUrl(masterUrl) + .withTrustCerts(false); + + if (url.getProtocol().equals("https")) { + KubernetesCerts certs = new KubernetesCerts(conf); + if (certs.caCertData.isPresent()) configBuilder.withCaCertData(toBase64Encoding(certs.caCertData.get())); + if (certs.clientCertData.isPresent()) configBuilder.withClientCertData(toBase64Encoding(certs.clientCertData.get())); + if (certs.clientKeyData.isPresent()) configBuilder.withClientKeyData(toBase64Encoding(certs.clientKeyData.get())); + if (certs.clientKeyAlgo.isPresent()) configBuilder.withClientKeyAlgo(certs.clientKeyAlgo.get()); + if (certs.clientKeyPassphrase.isPresent()) configBuilder.withClientKeyPassphrase(certs.clientKeyPassphrase.get()); + // TODO Should we also set configBuilder.withTrustCerts(true) here? + } + + String username = conf.get(KubernetesLocationConfig.ACCESS_IDENTITY); + if (Strings.isNonBlank(username)) configBuilder.withUsername(username); + + String password = conf.get(KubernetesLocationConfig.ACCESS_CREDENTIAL); + if (Strings.isNonBlank(password)) configBuilder.withPassword(password); + + String token = conf.get(KubernetesLocationConfig.OAUTH_TOKEN); + if (Strings.isNonBlank(token)) configBuilder.withOauthToken(token); + + Duration clientTimeout = conf.get(KubernetesLocationConfig.CLIENT_TIMEOUT); + if (clientTimeout.isPositive()) { + configBuilder.withConnectionTimeout((int) clientTimeout.toMilliseconds()); + configBuilder.withRequestTimeout((int) clientTimeout.toMilliseconds()); + } else { + throw new IllegalArgumentException("Kubernetes client timeout should be a positive duration: " + clientTimeout.toString()); + } + Duration actionTimeout = conf.get(KubernetesLocationConfig.ACTION_TIMEOUT); + if (actionTimeout.isPositive()) { + configBuilder.withRollingTimeout(actionTimeout.toMilliseconds()); + configBuilder.withScaleTimeout(actionTimeout.toMilliseconds()); + } else { + throw new IllegalArgumentException("Kubernetes action timeout should be a positive duration: " + actionTimeout.toString()); + } + + return new DefaultKubernetesClient(configBuilder.build()); + } + + private String toBase64Encoding(String val) { + return BaseEncoding.base64().encode(val.getBytes()); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java new file mode 100644 index 0000000..e87866c --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java @@ -0,0 +1,1014 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import java.io.InputStream; +import java.net.InetAddress; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.location.MachineLocation; +import org.apache.brooklyn.api.location.MachineProvisioningLocation; +import org.apache.brooklyn.api.location.NoMachinesAvailableException; +import org.apache.brooklyn.api.location.PortRange; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.api.sensor.EnricherSpec; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.entity.BrooklynConfigKeys; +import org.apache.brooklyn.core.entity.EntityInternal; +import org.apache.brooklyn.core.location.AbstractLocation; +import org.apache.brooklyn.core.location.LocationConfigKeys; +import org.apache.brooklyn.core.location.PortRanges; +import org.apache.brooklyn.core.location.access.PortForwardManager; +import org.apache.brooklyn.core.location.access.PortForwardManagerLocationResolver; +import org.apache.brooklyn.core.location.cloud.CloudLocationConfig; +import org.apache.brooklyn.core.network.OnPublicNetworkEnricher; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.location.ssh.SshMachineLocation; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.config.ResolvingConfigBag; +import org.apache.brooklyn.util.core.internal.ssh.SshTool; +import org.apache.brooklyn.util.core.text.TemplateProcessor; +import org.apache.brooklyn.util.exceptions.ReferenceWithError; +import org.apache.brooklyn.util.net.Networking; +import org.apache.brooklyn.util.repeat.Repeater; +import org.apache.brooklyn.util.stream.Streams; +import org.apache.brooklyn.util.text.Identifiers; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Functions; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.io.BaseEncoding; +import com.google.common.net.HostAndPort; + +import io.cloudsoft.amp.containerservice.dockercontainer.DockerContainer; +import io.cloudsoft.amp.containerservice.dockerlocation.DockerJcloudsLocation; +import io.cloudsoft.amp.containerservice.kubernetes.entity.KubernetesPod; +import io.cloudsoft.amp.containerservice.kubernetes.entity.KubernetesResource; +import io.cloudsoft.amp.containerservice.kubernetes.location.machine.KubernetesEmptyMachineLocation; +import io.cloudsoft.amp.containerservice.kubernetes.location.machine.KubernetesMachineLocation; +import io.cloudsoft.amp.containerservice.kubernetes.location.machine.KubernetesSshMachineLocation; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPort; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.EndpointAddress; +import io.fabric8.kubernetes.api.model.EndpointSubset; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolume; +import io.fabric8.kubernetes.api.model.PersistentVolumeBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.QuantityBuilder; +import io.fabric8.kubernetes.api.model.ReplicationController; +import io.fabric8.kubernetes.api.model.ResourceRequirements; +import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.extensions.Deployment; +import io.fabric8.kubernetes.api.model.extensions.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.extensions.DeploymentStatus; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; + +public class KubernetesLocation extends AbstractLocation implements MachineProvisioningLocation<KubernetesMachineLocation>, KubernetesLocationConfig { + + /* + * TODO + * + * - Ignores config such as 'user' and 'password', just uses 'loginUser' + * and 'loginUser.password' for connecting to the container. + * - Does not create a user, so behaves differently from things that use + * JcloudsLocation. + * - Does not use ssh keys only passwords. + * - The 'cloudsoft/*' images use root which is discouraged. + * - Error handling needs revisited. For example, if provisioning fails then + * it waits for five minutes and then fails without a reason why. + * e.g. try launching a container with an incorrect image name. + */ + + private static final Logger LOG = LoggerFactory.getLogger(KubernetesLocation.class); + + public static final String NODE_PORT = "NodePort"; + + public static final String IMMUTABLE_CONTAINER_KEY = "immutable-container"; + public static final String SSHABLE_CONTAINER = "sshable-container"; + public static final String CLOUDSOFT_ENTITY_ID = "cloudsoft.io/entity-id"; + public static final String CLOUDSOFT_APPLICATION_ID = "cloudsoft.io/application-id"; + public static final String KUBERNETES_DOCKERCFG = "kubernetes.io/dockercfg"; + + public static final String PHASE_AVAILABLE = "Available"; + public static final String PHASE_TERMINATING = "Terminating"; + public static final String PHASE_ACTIVE = "Active"; + + /** + * The regex for the image descriptions that support us injecting login credentials. + */ + public static final List<String> IMAGE_DESCRIPTION_REGEXES_REQUIRING_INJECTED_LOGIN_CREDS = ImmutableList.of( + "cloudsoft/centos.*", + "cloudsoft/ubuntu.*"); + + /** The environment variable for injecting login credentials. */ + public static final String CLOUDSOFT_ROOT_PASSWORD = "CLOUDSOFT_ROOT_PASSWORD"; + + private KubernetesClient client; + + public KubernetesLocation() { + super(); + } + + public KubernetesLocation(Map<?, ?> properties) { + super(properties); + } + + @Override + public void init() { + super.init(); + } + + protected KubernetesClient getClient() { + return getClient(MutableMap.of()); + } + + protected KubernetesClient getClient(Map<?, ?> flags) { + ConfigBag conf = (flags == null || flags.isEmpty()) + ? config().getBag() + : ConfigBag.newInstanceExtending(config().getBag(), flags); + return getClient(conf); + } + + protected KubernetesClient getClient(ConfigBag config) { + if (client == null) { + KubernetesClientRegistry registry = getConfig(KUBERNETES_CLIENT_REGISTRY); + client = registry.getKubernetesClient(ResolvingConfigBag.newInstanceExtending(getManagementContext(), config)); + } + return client; + } + + @Override + public KubernetesMachineLocation obtain(Map<?, ?> flags) throws NoMachinesAvailableException { + ConfigBag setupRaw = ConfigBag.newInstanceExtending(config().getBag(), flags); + ConfigBag setup = ResolvingConfigBag.newInstanceExtending(getManagementContext(), setupRaw); + + client = getClient(setup); + Entity entity = validateCallerContext(setup); + if (isKubernetesResource(entity)) { + return createKubernetesResourceLocation(entity, setup); + } else { + return createKubernetesContainerLocation(entity, setup); + } + } + + @Override + public void release(KubernetesMachineLocation machine) { + Entity entity = validateCallerContext(machine); + if (isKubernetesResource(entity)) { + deleteKubernetesResourceLocation(entity); + } else { + deleteKubernetesContainerLocation(entity, machine); + } + } + + protected void deleteKubernetesContainerLocation(Entity entity, MachineLocation machine) { + final String namespace = entity.sensors().get(KubernetesPod.KUBERNETES_NAMESPACE); + final String deployment = entity.sensors().get(KubernetesPod.KUBERNETES_DEPLOYMENT); + final String pod = entity.sensors().get(KubernetesPod.KUBERNETES_POD); + final String service = entity.sensors().get(KubernetesPod.KUBERNETES_SERVICE); + + undeploy(namespace, deployment, pod); + + client.services().inNamespace(namespace).withName(service).delete(); + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + return client.services().inNamespace(namespace).withName(service).get() == null; + } + @Override + public String getFailureMessage() { + return "No service with namespace=" + namespace + ", serviceName=" + service; + } + }; + waitForExitCondition(exitCondition); + + Boolean delete = machine.config().get(DELETE_EMPTY_NAMESPACE); + if (delete) { + deleteEmptyNamespace(namespace); + } + } + + protected void deleteKubernetesResourceLocation(Entity entity) { + final String namespace = entity.sensors().get(KubernetesPod.KUBERNETES_NAMESPACE); + final String resourceType = entity.sensors().get(KubernetesResource.RESOURCE_TYPE); + final String resourceName = entity.sensors().get(KubernetesResource.RESOURCE_NAME); + + if (!handleResourceDelete(resourceType, resourceName, namespace)) { + LOG.warn("Resource {}: {} not deleted", resourceName, resourceType); + } + } + + protected boolean handleResourceDelete(String resourceType, String resourceName, String namespace) { + try { + switch (resourceType) { + case KubernetesResource.DEPLOYMENT: + return client.extensions().deployments().inNamespace(namespace).withName(resourceName).delete(); + case KubernetesResource.REPLICA_SET: + return client.extensions().replicaSets().inNamespace(namespace).withName(resourceName).delete(); + case KubernetesResource.CONFIG_MAP: + return client.configMaps().inNamespace(namespace).withName(resourceName).delete(); + case KubernetesResource.PERSISTENT_VOLUME: + return client.persistentVolumes().withName(resourceName).delete(); + case KubernetesResource.SECRET: + return client.secrets().inNamespace(namespace).withName(resourceName).delete(); + case KubernetesResource.SERVICE: + return client.services().inNamespace(namespace).withName(resourceName).delete(); + case KubernetesResource.REPLICATION_CONTROLLER: + return client.replicationControllers().inNamespace(namespace).withName(resourceName).delete(); + case KubernetesResource.NAMESPACE: + return client.namespaces().withName(resourceName).delete(); + } + } catch (KubernetesClientException kce) { + LOG.warn("Error deleting resource {}: {}", resourceName, kce); + } + return false; + } + + protected void undeploy(final String namespace, final String deployment, final String pod) { + client.extensions().deployments().inNamespace(namespace).withName(deployment).delete(); + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + return client.extensions().deployments().inNamespace(namespace).withName(deployment).get() == null; + } + @Override + public String getFailureMessage() { + return "No deployment with namespace=" + namespace + ", deployment=" + deployment; + } + }; + waitForExitCondition(exitCondition); + } + + protected synchronized void deleteEmptyNamespace(final String name) { + if (!name.equals("default") && isNamespaceEmpty(name)) { + if (client.namespaces().withName(name).get() != null && + !client.namespaces().withName(name).get().getStatus().getPhase().equals(PHASE_TERMINATING)) { + client.namespaces().withName(name).delete(); + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + return client.namespaces().withName(name).get() == null; + } + @Override + public String getFailureMessage() { + return "Namespace " + name + " still present"; + } + }; + waitForExitCondition(exitCondition); + } + } + } + + protected boolean isNamespaceEmpty(String name) { + return client.extensions().deployments().inNamespace(name).list().getItems().isEmpty() && + client.services().inNamespace(name).list().getItems().isEmpty() && + client.secrets().inNamespace(name).list().getItems().isEmpty(); + } + + @Override + public Map<String, Object> getProvisioningFlags(Collection<String> tags) { + return null; + } + + protected KubernetesMachineLocation createKubernetesResourceLocation(Entity entity, ConfigBag setup) { + String resourceUri = entity.config().get(KubernetesResource.RESOURCE_FILE); + InputStream resource = ResourceUtils.create(entity).getResourceFromUrl(resourceUri); + String templateContents = Streams.readFullyString(resource); + String processedContents = TemplateProcessor.processTemplateContents(templateContents, (EntityInternal) entity, setup.getAllConfig()); + InputStream processedResource = Streams.newInputStreamWithContents(processedContents); + + final List<HasMetadata> result = getClient().load(processedResource).createOrReplace(); + + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + if (result.isEmpty()) { + return false; + } + List<HasMetadata> check = client.resource(result.get(0)).inNamespace(result.get(0).getMetadata().getNamespace()).get(); + if (result.size() > 1 || check.size() != 1 || check.get(0).getMetadata() == null) { + return false; + } + return true; + } + @Override + public String getFailureMessage() { + return "Cannot find created resources"; + } + }; + waitForExitCondition(exitCondition); + + HasMetadata metadata = result.get(0); + String resourceType = metadata.getKind(); + String resourceName = metadata.getMetadata().getName(); + String namespace = metadata.getMetadata().getNamespace(); + LOG.debug("Resource {} (type {}) deployed to {}", resourceName, resourceType, namespace); + + entity.sensors().set(KubernetesPod.KUBERNETES_NAMESPACE, namespace); + entity.sensors().set(KubernetesResource.RESOURCE_NAME, resourceName); + entity.sensors().set(KubernetesResource.RESOURCE_TYPE, resourceType); + + LocationSpec<? extends KubernetesMachineLocation> locationSpec = LocationSpec.create(KubernetesSshMachineLocation.class); + if (!findResourceAddress(locationSpec, entity, metadata, resourceType, resourceName, namespace)) { + LOG.info("Resource {} with type {} has no associated address", resourceName, resourceType); + locationSpec = LocationSpec.create(KubernetesEmptyMachineLocation.class); + } + locationSpec.configure(CALLER_CONTEXT, setup.get(CALLER_CONTEXT)) + .configure(KubernetesMachineLocation.KUBERNETES_NAMESPACE, namespace) + .configure(KubernetesMachineLocation.KUBERNETES_RESOURCE_NAME, resourceName) + .configure(KubernetesMachineLocation.KUBERNETES_RESOURCE_TYPE, resourceType); + + KubernetesMachineLocation machine = getManagementContext().getLocationManager().createLocation(locationSpec); + + if (resourceType.equals(KubernetesResource.SERVICE) && machine instanceof KubernetesSshMachineLocation) { + Service service = getService(namespace, resourceName); + registerPortMappings((KubernetesSshMachineLocation) machine, entity, service); + } + + return machine; + } + + protected boolean findResourceAddress(LocationSpec<? extends KubernetesMachineLocation> locationSpec, Entity entity, HasMetadata metadata, String resourceType, String resourceName, String namespace) { + if (resourceType.equals(KubernetesResource.DEPLOYMENT) || resourceType.equals(KubernetesResource.REPLICATION_CONTROLLER) || resourceType.equals(KubernetesResource.POD)) { + Map<String, String> labels = MutableMap.of(); + if (resourceType.equals(KubernetesResource.DEPLOYMENT)) { + Deployment deployment = (Deployment) metadata; + labels = deployment.getSpec().getTemplate().getMetadata().getLabels(); + } else if (resourceType.equals(KubernetesResource.REPLICATION_CONTROLLER)) { + ReplicationController replicationController = (ReplicationController) metadata; + labels = replicationController.getSpec().getTemplate().getMetadata().getLabels(); + } + Pod pod = resourceType.equals(KubernetesResource.POD) ? getPod(namespace, resourceName) : getPod(namespace, labels); + entity.sensors().set(KubernetesPod.KUBERNETES_POD, pod.getMetadata().getName()); + + InetAddress node = Networking.getInetAddressWithFixedName(pod.getSpec().getNodeName()); + String podAddress = pod.getStatus().getPodIP(); + + locationSpec.configure("address", node); + locationSpec.configure(SshMachineLocation.PRIVATE_ADDRESSES, ImmutableSet.of(podAddress)); + + return true; + } else if (resourceType.equals(KubernetesResource.SERVICE)) { + getService(namespace, resourceName); + Endpoints endpoints = client.endpoints().inNamespace(namespace).withName(resourceName).get(); + Set<String> privateIps = Sets.newLinkedHashSet(); + Set<String> podNames = Sets.newLinkedHashSet(); + for (EndpointSubset subset : endpoints.getSubsets()) { + for (EndpointAddress address : subset.getAddresses()) { + String podName = address.getTargetRef().getName(); + podNames.add(podName); + String privateIp = address.getIp(); + privateIps.add(privateIp); + } + } + locationSpec.configure(SshMachineLocation.PRIVATE_ADDRESSES, ImmutableSet.copyOf(privateIps)); + + if (podNames.size() > 0) { + // Use the first pod name from the list; warn when multiple pods are referenced + String podName = Iterables.get(podNames, 0); + if (podNames.size() > 1) { + LOG.warn("Multiple pods referenced by service {} in namespace {}, using {}: {}", + new Object[] { resourceName, namespace, podName, Iterables.toString(podNames) }); + } + try { + Pod pod = getPod(namespace, podName); + entity.sensors().set(KubernetesPod.KUBERNETES_POD, podName); + + InetAddress node = Networking.getInetAddressWithFixedName(pod.getSpec().getNodeName()); + locationSpec.configure("address", node); + } catch (KubernetesClientException kce) { + LOG.warn("Cannot find pod {} in namespace {} for service {}", new Object[] { podName, namespace, resourceName }); + } + } + + return true; + } else { + return false; + } + } + + protected KubernetesMachineLocation createKubernetesContainerLocation(Entity entity, ConfigBag setup) { + String deploymentName = lookup(KubernetesPod.DEPLOYMENT, entity, setup, entity.getId()); + Integer replicas = lookup(KubernetesPod.REPLICAS, entity, setup); + List<String> volumes = lookup(KubernetesPod.PERSISTENT_VOLUMES, entity, setup); + Map<String, String> secrets = lookup(KubernetesPod.SECRETS, entity, setup); + Map<String, String> limits = lookup(KubernetesPod.LIMITS, entity, setup); + Boolean privileged = lookup(KubernetesPod.PRIVILEGED, entity, setup); + String imageName = findImageName(entity, setup); + Iterable<Integer> inboundPorts = findInboundPorts(entity, setup); + Map<String, String> env = findEnvironmentVariables(entity, setup, imageName); + Map<String, String> metadata = findMetadata(entity, setup, deploymentName); + + if (volumes != null) { + createPersistentVolumes(volumes); + } + + Namespace namespace = createOrGetNamespace(lookup(NAMESPACE, entity, setup), setup.get(CREATE_NAMESPACE)); + + if (secrets != null) { + createSecrets(namespace.getMetadata().getName(), secrets); + } + + Container container = buildContainer(namespace.getMetadata().getName(), metadata, deploymentName, imageName, inboundPorts, env, limits, privileged); + deploy(namespace.getMetadata().getName(), entity, metadata, deploymentName, container, replicas, secrets); + Service service = exposeService(namespace.getMetadata().getName(), metadata, deploymentName, inboundPorts); + Pod pod = getPod(namespace.getMetadata().getName(), metadata); + + entity.sensors().set(KubernetesPod.KUBERNETES_NAMESPACE, namespace.getMetadata().getName()); + entity.sensors().set(KubernetesPod.KUBERNETES_DEPLOYMENT, deploymentName); + entity.sensors().set(KubernetesPod.KUBERNETES_POD, pod.getMetadata().getName()); + entity.sensors().set(KubernetesPod.KUBERNETES_SERVICE, service.getMetadata().getName()); + + LocationSpec<KubernetesSshMachineLocation> locationSpec = prepareSshableLocationSpec(entity, setup, namespace, deploymentName, service, pod) + .configure(KubernetesMachineLocation.KUBERNETES_NAMESPACE, namespace.getMetadata().getName()) + .configure(KubernetesMachineLocation.KUBERNETES_RESOURCE_NAME, deploymentName) + .configure(KubernetesMachineLocation.KUBERNETES_RESOURCE_TYPE, getContainerResourceType()); + + KubernetesSshMachineLocation machine = getManagementContext().getLocationManager().createLocation(locationSpec); + registerPortMappings(machine, entity, service); + if (!isDockerContainer(entity)) { + waitForSshable(machine, Duration.FIVE_MINUTES); + } + + return machine; + } + + protected String getContainerResourceType() { + return KubernetesResource.DEPLOYMENT; + } + + protected void waitForSshable(final SshMachineLocation machine, Duration timeout) { + Callable<Boolean> checker = new Callable<Boolean>() { + public Boolean call() { + int exitstatus = machine.execScript( + ImmutableMap.of( // TODO investigate why SSH connection does not time out with this config + SshTool.PROP_CONNECT_TIMEOUT.getName(), Duration.TEN_SECONDS.toMilliseconds(), + SshTool.PROP_SESSION_TIMEOUT.getName(), Duration.TEN_SECONDS.toMilliseconds(), + SshTool.PROP_SSH_TRIES_TIMEOUT.getName(), Duration.TEN_SECONDS.toMilliseconds(), + SshTool.PROP_SSH_TRIES.getName(), 1), + "check-sshable", + ImmutableList.of("true")); + boolean success = (exitstatus == 0); + return success; + }}; + + Stopwatch stopwatch = Stopwatch.createStarted(); + ReferenceWithError<Boolean> reachable = Repeater.create("reachable") + .threaded() + .backoff(Duration.FIVE_SECONDS, 2, Duration.TEN_SECONDS) // Exponential backoff, to 10 seconds + .until(checker) + .limitTimeTo(timeout) + .runKeepingError(); + if (!reachable.getWithoutError()) { + throw new IllegalStateException("Connection failed for "+machine.getSshHostAndPort()+" after waiting "+stopwatch.elapsed(TimeUnit.SECONDS), reachable.getError()); + } else { + LOG.debug("Connection succeeded for {} after {}", machine.getSshHostAndPort(), stopwatch.elapsed(TimeUnit.SECONDS)); + } + } + + protected void registerPortMappings(KubernetesSshMachineLocation machine, Entity entity, Service service) { + PortForwardManager portForwardManager = (PortForwardManager) getManagementContext().getLocationRegistry() + .getLocationManaged(PortForwardManagerLocationResolver.PFM_GLOBAL_SPEC); + List<ServicePort> ports = service.getSpec().getPorts(); + String publicHostText = ((SshMachineLocation) machine).getSshHostAndPort().getHostText(); + LOG.debug("Recording port-mappings for container {} of {}: {}", new Object[] { machine, this, ports }); + + for (ServicePort port : ports) { + String protocol = port.getProtocol(); + Integer targetPort = port.getTargetPort().getIntVal(); + + if (!"TCP".equalsIgnoreCase(protocol)) { + LOG.debug("Ignoring port mapping {} for {} because only TCP is currently supported", port, machine); + } else if (targetPort == null) { + LOG.debug("Ignoring port mapping {} for {} because targetPort.intValue is null", port, machine); + } else if (port.getNodePort() == null) { + LOG.debug("Ignoring port mapping {} to {} because port.getNodePort() is null", targetPort, machine); + } else { + portForwardManager.associate(publicHostText, HostAndPort.fromParts(publicHostText, port.getNodePort()), machine, targetPort); + AttributeSensor<Integer> sensor = Sensors.newIntegerSensor("kubernetes." + Strings.maybeNonBlank(port.getName()).or(targetPort.toString()) + ".port"); + entity.sensors().set(sensor, targetPort); + } + } + + entity.enrichers().add(EnricherSpec.create(OnPublicNetworkEnricher.class).configure(OnPublicNetworkEnricher.MAP_MATCHING, "kubernetes.[a-zA-Z0-9][a-zA-Z0-9-_]*.port")); + } + + protected synchronized Namespace createOrGetNamespace(final String name, Boolean create) { + Namespace namespace = client.namespaces().withName(name).get(); + ExitCondition namespaceReady = new ExitCondition() { + @Override + public Boolean call() { + Namespace actualNamespace = client.namespaces().withName(name).get(); + return actualNamespace != null && actualNamespace.getStatus().getPhase().equals(PHASE_ACTIVE); + } + @Override + public String getFailureMessage() { + Namespace actualNamespace = client.namespaces().withName(name).get(); + return "Namespace for " + name + " " + (actualNamespace == null ? "absent" : " status " + actualNamespace.getStatus()); + } + }; + if (namespace != null) { + LOG.debug("Found namespace {}, returning it.", namespace); + } else if (create) { + namespace = client.namespaces().create(new NamespaceBuilder().withNewMetadata().withName(name).endMetadata().build()); + LOG.debug("Created namespace {}.", namespace); + } else { + throw new IllegalStateException("Namespace " + name + " does not exist and namespace.create is not set"); + } + waitForExitCondition(namespaceReady); + return client.namespaces().withName(name).get(); + } + + protected Pod getPod(final String namespace, final String name) { + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + Pod result = client.pods().inNamespace(namespace).withName(name).get(); + return result != null && result.getStatus().getPodIP() != null; + } + @Override + public String getFailureMessage() { + return "Cannot find pod with name: " + name; + } + }; + waitForExitCondition(exitCondition); + Pod result = client.pods().inNamespace(namespace).withName(name).get(); + return result; + } + + protected Pod getPod(final String namespace, final Map<String, String> metadata) { + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + PodList result = client.pods().inNamespace(namespace).withLabels(metadata).list(); + return result.getItems().size() >= 1 && result.getItems().get(0).getStatus().getPodIP() != null; + } + @Override + public String getFailureMessage() { + return "Cannot find pod with metadata: " + Joiner.on(" ").withKeyValueSeparator("=").join(metadata); + } + }; + waitForExitCondition(exitCondition); + PodList result = client.pods().inNamespace(namespace).withLabels(metadata).list(); + return result.getItems().get(0); + } + + protected void createSecrets(String namespace, Map<String, String> secrets) { + for (Map.Entry<String, String> nameAuthEntry : secrets.entrySet()) { + createSecret(namespace, nameAuthEntry.getKey(), nameAuthEntry.getValue()); + } + } + + protected Secret createSecret(final String namespace, final String secretName, String auth) { + Secret secret = client.secrets().inNamespace(namespace).withName(secretName).get(); + if (secret != null) return secret; + + String json = String.format("{\"https://index.docker.io/v1/\":{\"auth\":\"%s\"}}", auth); + String base64encoded = BaseEncoding.base64().encode(json.getBytes(Charset.defaultCharset())); + secret = new SecretBuilder() + .withNewMetadata() + .withName(secretName) + .endMetadata() + .withType(KUBERNETES_DOCKERCFG) + .withData(ImmutableMap.of(".dockercfg", base64encoded)) + .build(); + try { + client.secrets().inNamespace(namespace).create(secret); + } catch (KubernetesClientException e) { + if (e.getCode() == 500 && e.getMessage().contains("Message: resourceVersion may not be set on objects to be created")) { + // ignore exception as per https://github.com/fabric8io/kubernetes-client/issues/451 + } else { + throw Throwables.propagate(e); + } + } + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + return client.secrets().inNamespace(namespace).withName(secretName).get() != null; + } + @Override + public String getFailureMessage() { + return "Absent namespace=" + namespace + ", secretName=" + secretName; + } + }; + waitForExitCondition(exitCondition); + return client.secrets().inNamespace(namespace).withName(secretName).get(); + } + + protected Container buildContainer(String namespace, Map<String, String> metadata, String deploymentName, String imageName, Iterable<Integer> inboundPorts, Map<String, ?> env, Map<String, String> limits, boolean privileged) { + List<ContainerPort> containerPorts = Lists.newArrayList(); + for (Integer inboundPort : inboundPorts) { + containerPorts.add(new ContainerPortBuilder().withContainerPort(inboundPort).build()); + } + + List<EnvVar> envVars = Lists.newArrayList(); + for (Map.Entry<String, ?> envVarEntry : env.entrySet()) { + envVars.add(new EnvVarBuilder().withName(envVarEntry.getKey()).withValue(envVarEntry.getValue().toString()).build()); + } + + ContainerBuilder containerBuilder = new ContainerBuilder() + .withName(deploymentName) + .withImage(imageName) + .addToPorts(Iterables.toArray(containerPorts, ContainerPort.class)) + .addToEnv(Iterables.toArray(envVars, EnvVar.class)) + .withNewSecurityContext() + .withPrivileged(privileged) + .endSecurityContext(); + + if (limits != null) { + for (Map.Entry<String, String> nameValueEntry : limits.entrySet()) { + ResourceRequirements resourceRequirements = new ResourceRequirementsBuilder().addToLimits(nameValueEntry.getKey(), new QuantityBuilder().withAmount(nameValueEntry.getValue()).build()).build(); + containerBuilder.withResources(resourceRequirements); + } + } + LOG.debug("Built container {} to be deployed in namespace {} with metadata {}.", containerBuilder.build(), namespace, metadata); + return containerBuilder.build(); + } + + protected void deploy(final String namespace, Entity entity, Map<String, String> metadata, final String deploymentName, Container container, final Integer replicas, Map<String, String> secrets) { + PodTemplateSpecBuilder podTemplateSpecBuilder = new PodTemplateSpecBuilder() + .withNewMetadata() + .addToLabels("name", deploymentName) + .addToLabels(metadata) + .endMetadata() + .withNewSpec() + .addToContainers(container) + .endSpec(); + if (secrets != null) { + for (String secretName : secrets.keySet()) { + podTemplateSpecBuilder.withNewSpec() + .addToContainers(container) + .addNewImagePullSecret(secretName) + .endSpec(); + } + } + PodTemplateSpec template = podTemplateSpecBuilder.build(); + Deployment deployment = new DeploymentBuilder() + .withNewMetadata() + .withName(deploymentName) + .addToAnnotations(CLOUDSOFT_ENTITY_ID, entity.getId()) + .addToAnnotations(CLOUDSOFT_APPLICATION_ID, entity.getApplicationId()) + .endMetadata() + .withNewSpec() + .withReplicas(replicas) + .withTemplate(template) + .endSpec() + .build(); + client.extensions().deployments().inNamespace(namespace).create(deployment); + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + Deployment dep = client.extensions().deployments().inNamespace(namespace).withName(deploymentName).get(); + DeploymentStatus status = (dep == null) ? null : dep.getStatus(); + Integer replicas = (status == null) ? null : status.getAvailableReplicas(); + return replicas != null && replicas.intValue() == replicas; + } + @Override + public String getFailureMessage() { + Deployment dep = client.extensions().deployments().inNamespace(namespace).withName(deploymentName).get(); + DeploymentStatus status = (dep == null) ? null : dep.getStatus(); + return "Namespace=" + namespace + "; deploymentName= " + deploymentName + "; Deployment=" + dep + + "; status=" + status + + "; availableReplicas=" + (status == null ? "null" : status.getAvailableReplicas()); + } + }; + waitForExitCondition(exitCondition); + LOG.debug("Deployed deployment {} in namespace {}.", deployment, namespace); + } + + protected Service exposeService(String namespace, Map<String, String> metadata, String serviceName, Iterable<Integer> inboundPorts) { + List<ServicePort> servicePorts = Lists.newArrayList(); + for (Integer inboundPort : inboundPorts) { + servicePorts.add(new ServicePortBuilder().withName(Integer.toString(inboundPort)).withPort(inboundPort).build()); + } + Service service = new ServiceBuilder().withNewMetadata().withName(serviceName).endMetadata() + .withNewSpec() + .addToSelector(metadata) + .addToPorts(Iterables.toArray(servicePorts, ServicePort.class)) + .withType(NODE_PORT) + .endSpec() + .build(); + client.services().inNamespace(namespace).create(service); + + service = getService(namespace, serviceName); + LOG.debug("Exposed service {} in namespace {}.", service, namespace); + return service; + } + + protected Service getService(final String namespace, final String serviceName) { + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + Service svc = client.services().inNamespace(namespace).withName(serviceName).get(); + if (svc == null || svc.getStatus() == null) { + return false; + } + Endpoints endpoints = client.endpoints().inNamespace(namespace).withName(serviceName).get(); + if (endpoints == null || endpoints.getSubsets().isEmpty()) { + return false; + } + for (EndpointSubset subset : endpoints.getSubsets()) { + if (subset.getNotReadyAddresses().size() > 0) { + return false; + } + } + return true; + } + @Override + public String getFailureMessage() { + Endpoints endpoints = client.endpoints().inNamespace(namespace).withName(serviceName).get(); + return "Service endpoints in " + namespace + " for serviceName= " + serviceName + " not ready: " + endpoints; + } + }; + waitForExitCondition(exitCondition); + + return client.services().inNamespace(namespace).withName(serviceName).get(); + } + + protected LocationSpec<KubernetesSshMachineLocation> prepareSshableLocationSpec(Entity entity, ConfigBag setup, Namespace namespace, String deploymentName, Service service, Pod pod) { + InetAddress node = Networking.getInetAddressWithFixedName(pod.getSpec().getNodeName()); + String podAddress = pod.getStatus().getPodIP(); + LocationSpec<KubernetesSshMachineLocation> locationSpec = LocationSpec.create(KubernetesSshMachineLocation.class) + .configure("address", node) + .configure(SshMachineLocation.PRIVATE_ADDRESSES, ImmutableSet.of(podAddress)) + .configure(CALLER_CONTEXT, setup.get(CALLER_CONTEXT)); + if (!isDockerContainer(entity)) { + Optional<ServicePort> sshPort = Iterables.tryFind(service.getSpec().getPorts(), new Predicate<ServicePort>() { + @Override + public boolean apply(ServicePort input) { + return input.getProtocol().equalsIgnoreCase("TCP") && input.getPort().intValue() == 22; + } + }); + Optional<Integer> sshPortNumber; + if (sshPort.isPresent()) { + sshPortNumber = Optional.of(sshPort.get().getNodePort()); + } else { + LOG.warn("No port-mapping found to ssh port 22, for container {}", service); + sshPortNumber = Optional.absent(); + } + locationSpec.configure(CloudLocationConfig.USER, setup.get(KubernetesLocationConfig.LOGIN_USER)) + .configure(SshMachineLocation.PASSWORD, setup.get(KubernetesLocationConfig.LOGIN_USER_PASSWORD)) + .configureIfNotNull(SshMachineLocation.SSH_PORT, sshPortNumber.orNull()) + .configure(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true) + .configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp"); + } + return locationSpec; + } + + protected void createPersistentVolumes(List<String> volumes) { + for (final String persistentVolume : volumes) { + PersistentVolume volume = new PersistentVolumeBuilder() + .withNewMetadata() + .withName(persistentVolume) + .withLabels(ImmutableMap.of("type", "local")) // TODO make it configurable + .endMetadata() + .withNewSpec() + .addToCapacity("storage", new QuantityBuilder().withAmount("20").build()) // TODO make it configurable + .addToAccessModes("ReadWriteOnce") // TODO make it configurable + .withNewHostPath().withPath("/tmp/pv-1").endHostPath() // TODO make it configurable + .endSpec() + .build(); + client.persistentVolumes().create(volume); + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + PersistentVolume pv = client.persistentVolumes().withName(persistentVolume).get(); + return pv != null && pv.getStatus() != null + && pv.getStatus().getPhase().equals(PHASE_AVAILABLE); + } + @Override + public String getFailureMessage() { + PersistentVolume pv = client.persistentVolumes().withName(persistentVolume).get(); + return "PersistentVolume for " + persistentVolume + " " + (pv == null ? "absent" : "pv=" + pv); + } + }; + waitForExitCondition(exitCondition); + } + } + + protected Entity validateCallerContext(ConfigBag setup) { + // Lookup entity flags + Object callerContext = setup.get(LocationConfigKeys.CALLER_CONTEXT); + if (callerContext instanceof Entity) { + return (Entity) callerContext; + } else { + throw new IllegalStateException("Invalid caller context: " + callerContext); + } + } + + protected Entity validateCallerContext(MachineLocation machine) { + // Lookup entity flags + Object callerContext = machine.config().get(LocationConfigKeys.CALLER_CONTEXT); + if (callerContext instanceof Entity) { + return (Entity) callerContext; + } else { + throw new IllegalStateException("Invalid caller context: " + callerContext); + } + } + + protected Map<String, String> findMetadata(Entity entity, ConfigBag setup, String value) { + Map<String, String> podMetadata = Maps.newLinkedHashMap(); + if (isDockerContainer(entity)) { + podMetadata.put(IMMUTABLE_CONTAINER_KEY, value); + } else { + podMetadata.put(SSHABLE_CONTAINER, value); + } + + Map<String, Object> metadata = MutableMap.<String, Object>builder() + .putAll(MutableMap.copyOf(setup.get(KubernetesPod.METADATA))) + .putAll(MutableMap.copyOf(entity.config().get(KubernetesPod.METADATA))) + .putAll(podMetadata) + .build(); + return Maps.transformValues(metadata, Functions.toStringFunction()); + } + + /** + * Sets the {@code CLOUDSOFT_ROOT_PASSWORD} variable in the container environment if appropriate. + * This is (approximately) the same behaviour as the {@link DockerJcloudsLocation} used for + * Swarm. + * + * Side-effects the location {@code config} to set the {@link KubernetesLocationConfig#LOGIN_USER_PASSWORD loginUser.password} + * if one is auto-generated. Note that this injected value overrides any other settings configured for the + * container environment. + */ + protected Map<String, String> findEnvironmentVariables(Entity entity, ConfigBag setup, String imageName) { + String loginUser = setup.get(LOGIN_USER); + String loginPassword = setup.get(LOGIN_USER_PASSWORD); + Map<String, String> injections = Maps.newLinkedHashMap(); + + // Check if login credentials should be injected + Boolean injectLoginCredentials = setup.get(INJECT_LOGIN_CREDENTIAL); + if (injectLoginCredentials == null) { + for (String regex : IMAGE_DESCRIPTION_REGEXES_REQUIRING_INJECTED_LOGIN_CREDS) { + if (imageName != null && imageName.matches(regex)) { + injectLoginCredentials = true; + break; + } + } + } + + if (Boolean.TRUE.equals(injectLoginCredentials)) { + if ((Strings.isBlank(loginUser) || "root".equals(loginUser))) { + loginUser = "root"; + setup.configure(LOGIN_USER, loginUser); + + if (Strings.isBlank(loginPassword)) { + loginPassword = Identifiers.makeRandomPassword(12); + setup.configure(LOGIN_USER_PASSWORD, loginPassword); + } + + injections.put(CLOUDSOFT_ROOT_PASSWORD, loginPassword); + } + } + + Map<String,Object> rawEnv = MutableMap.<String, Object>builder() + .putAll(MutableMap.copyOf(setup.get(ENV))) + .putAll(MutableMap.copyOf(entity.config().get(DockerContainer.CONTAINER_ENVIRONMENT))) + .putAll(injections) + .build(); + return Maps.transformValues(rawEnv, Functions.toStringFunction()); + } + + protected Iterable<Integer> findInboundPorts(Entity entity, ConfigBag setup) { + Iterable<String> inboundTcpPorts = entity.config().get(DockerContainer.INBOUND_TCP_PORTS); + if (inboundTcpPorts != null) { + List<Integer> inboundPorts = Lists.newArrayList(); + List<String> portRanges = MutableList.copyOf(entity.config().get(DockerContainer.INBOUND_TCP_PORTS)); + for (String portRange : portRanges) { + for (Integer port : PortRanges.fromString(portRange)) { + inboundPorts.add(port); + } + } + return inboundPorts; + } else { + if (setup.containsKey(INBOUND_PORTS)) { + return toIntPortList(setup.get(INBOUND_PORTS)); + } else { + return ImmutableList.of(22); + } + } + } + + protected List<Integer> toIntPortList(Object v) { + if (v == null) return ImmutableList.of(); + PortRange portRange = PortRanges.fromIterable(ImmutableList.of(v)); + return ImmutableList.copyOf(portRange); + } + + protected String findImageName(Entity entity, ConfigBag setup) { + String result = entity.config().get(DockerContainer.IMAGE_NAME); + if (Strings.isNonBlank(result)) return result; + + result = setup.get(IMAGE); + if (Strings.isNonBlank(result)) return result; + + String osFamily = setup.get(OS_FAMILY); + String osVersion = setup.get(OS_VERSION_REGEX); + Optional<String> imageName = new ImageChooser().chooseImage(osFamily, osVersion); + if (imageName.isPresent()) return imageName.get(); + + throw new IllegalStateException("No matching image found for " + entity + + " (no explicit image name, osFamily=" + osFamily + "; osVersion=" + osVersion + ")"); + } + + protected boolean isDockerContainer(Entity entity) { + return implementsInterface(entity, DockerContainer.class); + } + + protected boolean isKubernetesPod(Entity entity) { + return implementsInterface(entity, KubernetesPod.class); + } + + protected boolean isKubernetesResource(Entity entity) { + return implementsInterface(entity, KubernetesResource.class); + } + + public boolean implementsInterface(Entity entity, Class<?> type) { + return Iterables.tryFind(Arrays.asList(entity.getClass().getInterfaces()), Predicates.assignableFrom(type)).isPresent(); + } + + @Override + public MachineProvisioningLocation<KubernetesMachineLocation> newSubLocation(Map<?, ?> newFlags) { + throw new UnsupportedOperationException(); + } + + /** @see {@link #lookup(ConfigKey, Entity, ConfigBag, Object)} */ + public <T> T lookup(ConfigKey<T> config, Entity entity, ConfigBag setup) { + return lookup(config, entity, setup, null); + } + + /** + * Looks up {@link ConfigKey configuration} with the entity value taking precedence over the + * location, and returning a default value (normally {@literal null}) if neither is present. + */ + public <T> T lookup(ConfigKey<T> config, Entity entity, ConfigBag setup, T defaultValue) { + Optional<T> entityValue = Optional.fromNullable(entity.config().get(config)); + Optional<T> locationValue = Optional.fromNullable(setup.get(config)); + + return Iterables.getFirst(Optional.presentInstances(Arrays.asList(entityValue, locationValue)), defaultValue); + } + + public void waitForExitCondition(ExitCondition exitCondition) { + waitForExitCondition(exitCondition, Duration.ONE_SECOND, Duration.FIVE_MINUTES); + } + + public void waitForExitCondition(ExitCondition exitCondition, Duration initial, Duration duration) { + ReferenceWithError<Boolean> result = Repeater.create() + .backoff(initial, 1.2, duration) + .limitTimeTo(duration) + .until(exitCondition) + .runKeepingError(); + if (!result.get()) { + String err = "Exit condition unsatisfied after " + duration + ": " + exitCondition.getFailureMessage(); + LOG.info(err + " (rethrowing)"); + throw new IllegalStateException(err); + } + } + + public static interface ExitCondition extends Callable<Boolean> { + public String getFailureMessage(); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationConfig.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationConfig.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationConfig.java new file mode 100644 index 0000000..0571fc2 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationConfig.java @@ -0,0 +1,164 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.location.LocationConfigKeys; +import org.apache.brooklyn.core.location.cloud.CloudLocationConfig; +import org.apache.brooklyn.util.time.Duration; + +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; + +public interface KubernetesLocationConfig extends CloudLocationConfig { + + ConfigKey<String> MASTER_URL = LocationConfigKeys.CLOUD_ENDPOINT; + + ConfigKey<String> CA_CERT_DATA = ConfigKeys.builder(String.class) + .name("caCertData") + .description("Data for CA certificate") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CA_CERT_FILE = ConfigKeys.builder(String.class) + .name("caCertFile") + .description("URL of resource containing CA certificate data") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CLIENT_CERT_DATA = ConfigKeys.builder(String.class) + .name("clientCertData") + .description("Data for client certificate") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CLIENT_CERT_FILE = ConfigKeys.builder(String.class) + .name("clientCertFile") + .description("URL of resource containing client certificate data") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CLIENT_KEY_DATA = ConfigKeys.builder(String.class) + .name("clientKeyData") + .description("Data for client key") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CLIENT_KEY_FILE = ConfigKeys.builder(String.class) + .name("clientKeyFile") + .description("URL of resource containing client key data") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CLIENT_KEY_ALGO = ConfigKeys.builder(String.class) + .name("clientKeyAlgo") + .description("Algorithm used for the client key") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> CLIENT_KEY_PASSPHRASE = ConfigKeys.builder(String.class) + .name("clientKeyPassphrase") + .description("Passphrase used for the client key") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> OAUTH_TOKEN = ConfigKeys.builder(String.class) + .name("oauthToken") + .description("The OAuth token data for the current user") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<Duration> CLIENT_TIMEOUT = ConfigKeys.builder(Duration.class) + .name("timeout") + .description("The timeout for the client") + .defaultValue(Duration.seconds(10)) + .constraint(Predicates.<Duration>notNull()) + .build(); + + ConfigKey<Duration> ACTION_TIMEOUT = ConfigKeys.builder(Duration.class) + .name("actionTimeout") + .description("The timeout for Kubernetes actions") + .defaultValue(Duration.ONE_MINUTE) + .constraint(Predicates.<Duration>notNull()) + .build(); + + ConfigKey<Boolean> CREATE_NAMESPACE = ConfigKeys.builder(Boolean.class) + .name("namespace.create") + .description("Whether to create the namespace if it does not exist") + .defaultValue(true) + .constraint(Predicates.<Boolean>notNull()) + .build(); + + ConfigKey<Boolean> DELETE_EMPTY_NAMESPACE = ConfigKeys.builder(Boolean.class) + .name("namespace.deleteEmpty") + .description("Whether to delete an empty namespace when releasing resources") + .defaultValue(false) + .constraint(Predicates.<Boolean>notNull()) + .build(); + + ConfigKey<String> NAMESPACE = ConfigKeys.builder(String.class) + .name("namespace") + .description("Namespace where resources will live; the default is 'amp'") + .defaultValue("amp") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<Boolean> PRIVILEGED = ConfigKeys.builder(Boolean.class) + .name("privileged") + .description("Whether the pods use privileged containers") + .defaultValue(false) + .build(); + + @SuppressWarnings("serial") + ConfigKey<Map<String, ?>> ENV = ConfigKeys.builder(new TypeToken<Map<String, ?>>() {}) + .name("env") + .description("Environment variables to inject when starting the container") + .defaultValue(ImmutableMap.<String, Object>of()) + .constraint(Predicates.<Map<String, ?>>notNull()) + .build(); + + ConfigKey<String> IMAGE = ConfigKeys.builder(String.class) + .name("image") + .description("Docker image to be deployed into the pod") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> OS_FAMILY = ConfigKeys.builder(String.class) + .name("osFamily") + .description("OS family, e.g. CentOS, Ubuntu") + .build(); + + ConfigKey<String> OS_VERSION_REGEX = ConfigKeys.builder(String.class) + .name("osVersionRegex") + .description("Regular expression for the OS version to load") + .build(); + + ConfigKey<KubernetesClientRegistry> KUBERNETES_CLIENT_REGISTRY = ConfigKeys.builder(KubernetesClientRegistry.class) + .name("kubernetesClientRegistry") + .description("Registry/Factory for creating Kubernetes client; default is almost always fine, " + + "except where tests want to customize behaviour") + .defaultValue(KubernetesClientRegistryImpl.INSTANCE) + .build(); + + ConfigKey<String> LOGIN_USER = ConfigKeys.builder(String.class) + .name("loginUser") + .description("Override the user who logs in initially to perform setup") + .defaultValue("root") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<String> LOGIN_USER_PASSWORD = ConfigKeys.builder(String.class) + .name("loginUser.password") + .description("Custom password for the user who logs in initially") + .constraint(Predicates.<String>notNull()) + .build(); + + ConfigKey<Boolean> INJECT_LOGIN_CREDENTIAL = ConfigKeys.builder(Boolean.class) + .name("injectLoginCredential") + .description("Whether to inject login credentials (if null, will infer from image choice); ignored if explicit 'loginUser.password' supplied") + .build(); + +} + http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/445884b1/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationResolver.java ---------------------------------------------------------------------- diff --git a/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationResolver.java b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationResolver.java new file mode 100644 index 0000000..79e7c64 --- /dev/null +++ b/brooklyn-server/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationResolver.java @@ -0,0 +1,47 @@ +package io.cloudsoft.amp.containerservice.kubernetes.location; + +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.location.LocationResolver; +import org.apache.brooklyn.core.location.AbstractLocationResolver; +import org.apache.brooklyn.core.location.LocationConfigUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Locations starting with the given prefix (@code "kubernetes") will use this resolver, to instantiate + * a {@link KubernetesLocation}. + * + * We ensure that config will be picked up from brooklyn.properties using the appropriate precedence: + * <ol> + * <li>named location config + * <li>Prefix {@code brooklyn.location.kubernetes.} + * <li>Prefix {@code brooklyn.kubernetes.} + * </ol> + */ +public class KubernetesLocationResolver extends AbstractLocationResolver implements LocationResolver { + + public static final Logger log = LoggerFactory.getLogger(KubernetesLocationResolver.class); + + public static final String PREFIX = "kubernetes"; + + @Override + public boolean isEnabled() { + return LocationConfigUtils.isResolverPrefixEnabled(managementContext, getPrefix()); + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + protected Class<? extends Location> getLocationType() { + return KubernetesLocation.class; + } + + @Override + protected SpecParser getSpecParser() { + return new SpecParser(getPrefix()).setExampleUsage("\"kubernetes\""); + } + +}
