diff --git a/karaf/features/pom.xml b/karaf/features/pom.xml index 34da460647..13ed9008ef 100755 --- a/karaf/features/pom.xml +++ b/karaf/features/pom.xml @@ -19,101 +19,102 @@ --> - - 4.0.0 - - org.apache.brooklyn - brooklyn-karaf - 1.1.0-SNAPSHOT - - brooklyn-features - Brooklyn Karaf Features - pom + 4.0.0 + + org.apache.brooklyn + brooklyn-karaf + 1.1.0-SNAPSHOT + - - - - src/main/feature - true - - **/* - - - - - - org.apache.maven.plugins - maven-resources-plugin - 2.6 - - false - - ${*} - - - - - filter - generate-resources - - resources - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - package - - attach-artifact - - - - - target/classes/feature.xml - features - xml - - - - - - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.apache.karaf.tooling - karaf-maven-plugin - [4.1.6,) - - verify - - - - - - - - - - - - - + brooklyn-features + Brooklyn Karaf Features + pom + + + + + src/main/feature + true + + **/* + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + false + + ${*} + + + + + filter + generate-resources + + resources + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.1.0 + + + package + + attach-artifact + + + + + target/classes/feature.xml + features + xml + + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.karaf.tooling + karaf-maven-plugin + [4.1.6,) + + verify + + + + + + + + + + + + + diff --git a/karaf/features/src/main/feature/feature.xml b/karaf/features/src/main/feature/feature.xml index 8ce7ecf343..dd71f9fdeb 100644 --- a/karaf/features/src/main/feature/feature.xml +++ b/karaf/features/src/main/feature/feature.xml @@ -202,7 +202,7 @@ brooklyn-camp-brooklyn brooklyn-camp-base - cxf-jaxrs-with-optional-dependencies + cxf-jaxrs mvn:org.apache.cxf/cxf-rt-rs-security-cors/${cxf.version} mvn:org.apache.cxf/cxf-rt-frontend-jaxrs/${cxf.version} mvn:org.apache.cxf.karaf/cxf-karaf-commands/${cxf.version} @@ -218,14 +218,6 @@ - - cxf-jaxrs - - - mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/${fasterxml.jackson.version} - - - brooklyn-rest-resources-prereqs mvn:org.apache.brooklyn/brooklyn-rest-resources/${project.version} @@ -301,6 +293,8 @@ brooklyn-core + brooklyn-utils-common + brooklyn-locations-jclouds mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/${fasterxml.jackson.version} mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/${fasterxml.jackson.version} @@ -315,6 +309,7 @@ wrap:mvn:com.squareup.okhttp3/okhttp/3.12.6$Bundle-SymbolicName=squareup-okhttp3&Bundle-Version=3.12.6&Import-Package=okio;version=1.15,*;resolution:=optional wrap:mvn:com.squareup.okhttp3/logging-interceptor/3.12.6$Bundle-SymbolicName=squareup-okhttp3-logging-interceptor&Bundle-Version=3.12.6&Import-Package=*;resolution:=mandatory + mvn:io.fabric8/zjsonpatch/0.3.0 @@ -322,9 +317,48 @@ mvn:io.fabric8/kubernetes-client/${kubernetes-client.version}/jar/bundle mvn:io.fabric8/openshift-client/${kubernetes-client.version}/jar/bundle + mvn:org.microbean/microbean-helm/2.12.4-SNAPSHOT + + wrap:mvn:org.microbean/microbean-development-annotations/0.1.3$Bundle-SymbolicName=microbean-development-annotations&Bundle-Version=0.1.3&Import-Package=*;resolution:=mandatory + wrap:mvn:org.kamranzafar/jtar/2.3$Bundle-SymbolicName=jtar&Bundle-Version=2.3&Import-Package=*;resolution:=mandatory + wrap:mvn:com.github.zafarkhaja/java-semver/0.9.0$Bundle-SymbolicName=java-semver&Bundle-Version=0.9.0&Import-Package=*;resolution:=mandatory + wrap:mvn:org.microbean/microbean-kubernetes/0.1.3$Bundle-SymbolicName=microbean-kubernetes&Bundle-Version=0.1.3&Import-Package=*;resolution:=mandatory + + wrap:mvn:io.grpc/grpc-netty/1.23.0$Bundle-SymbolicName=grpc-netty&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + mvn:io.netty/netty-codec-http2/4.1.38.Final + mvn:io.netty/netty-resolver/4.1.38.Final + mvn:io.netty/netty-common/4.1.38.Final + mvn:io.netty/netty-buffer/4.1.38.Final + mvn:io.netty/netty-codec/4.1.38.Final + mvn:io.netty/netty-transport/4.1.38.Final + mvn:io.netty/netty-codec-http/4.1.38.Final + mvn:io.netty/netty-codec-socks/4.1.38.Final + mvn:io.netty/netty-handler/4.1.38.Final + mvn:io.netty/netty-handler-proxy/4.1.38.Final + wrap:mvn:io.grpc/grpc-core/1.23.0$Bundle-SymbolicName=grpc-core&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.grpc/grpc-context/1.23.0$Bundle-SymbolicName=grpc-context&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.opencensus/opencensus-api/0.11.0$Bundle-SymbolicName=opencensus-api&Bundle-Version=0.11.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.opencensus/opencensus-contrib-grpc-metrics/0.11.0$Bundle-SymbolicName=opencensus-contrib-grpc-metrics&Bundle-Version=0.11.0&Import-Package=*;resolution:=mandatory + + mvn:com.google.protobuf/protobuf-java/3.9.0 + wrap:mvn:io.grpc/grpc-protobuf/1.23.0$Bundle-SymbolicName=grpc-protobuf&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.grpc/grpc-services/1.23.0$Bundle-SymbolicName=grpc-services&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.grpc/grpc-stub/1.23.0$Bundle-SymbolicName=grpc-stub&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.grpc/grpc-protobuf-lite/1.23.0$Bundle-SymbolicName=grpc-protobuf-lite&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + mvn:com.google.protobuf/protobuf-java-util/3.9.0 + wrap:mvn:com.google.api.grpc/proto-google-common-protos/1.12.0$Bundle-SymbolicName=proto-google-common-protos&Bundle-Version=1.12.0&Import-Package=*;resolution:=mandatory + wrap:mvn:io.grpc/grpc-core/1.23.0$Bundle-SymbolicName=grpc-core&Bundle-Version=1.23.0&Import-Package=*;resolution:=mandatory + wrap:mvn:com.google.instrumentation/instrumentation-api/0.4.3$Bundle-SymbolicName=instrumentation-api&Bundle-Version=0.4.3&Import-Package=*;resolution:=mandatory + + wrap:mvn:com.google.errorprone/error_prone_annotations/2.3.2$Bundle-SymbolicName=error_prone_annotations&Bundle-Version=2.3.2&Import-Package=*;resolution:=optional + wrap:mvn:io.perfmark/perfmark-api/0.17.0$Bundle-SymbolicName=perfmark-api&Bundle-Version=0.17.0&Import-Package=*;resolution:=optional + + mvn:io.netty/netty-tcnative-boringssl-static/2.0.30.Final + mvn:org.apache.brooklyn/brooklyn-locations-container/${project.version} + mvn:org.apache.brooklyn/brooklyn-test-framework/${project.version} diff --git a/locations/container/README.md b/locations/container/README.md new file mode 100644 index 0000000000..5c62b017ae --- /dev/null +++ b/locations/container/README.md @@ -0,0 +1,30 @@ + +# Kubernetes Location + +Brooklyn Container Location has an extensive support for Kubernetes deployments +In particular, it supports + +- KubernetesResource +- KubernetesHelmChart +- KubernetesContainer + +## Kubernets Helm Chart + +Here's an example of an Helm based blueprint + +```YAML +location: + kubernetes: + endpoint: https://localhost:6443 + kubeconfig: /home/user/.kube/config +services: +- type: org.apache.brooklyn.container.entity.kubernetes.KubernetesHelmChart + name: jenkins-helm + chartName: jenkins +``` + +Notice, in this case, it is pointing at a local k8s cluster (created using Docker on Mac) and specify a `kubeconfig` +file for connection details. + +The `KubernetesHelmChart` entity will install the latest version of the `chart` named `jenkins` from the Chart repository `stable` at `https://kubernetes-charts.storage.googleapis.com/` +You can install a specific version of the chart by using `chartVersion` config key. \ No newline at end of file diff --git a/locations/container/pom.xml b/locations/container/pom.xml index 2a4021b7c5..85e9c1f37e 100644 --- a/locations/container/pom.xml +++ b/locations/container/pom.xml @@ -32,6 +32,31 @@ + + org.microbean + microbean-helm + 2.12.4-SNAPSHOT + + + org.yaml + snakeyaml + + + com.google.errorprone + error_prone_annotations + + + io.netty + netty-tcnative-boringssl-static + + + + + + io.netty + netty-tcnative-boringssl-static + 2.0.30.Final + io.fabric8 openshift-client @@ -150,4 +175,5 @@ test + diff --git a/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesHelmChart.java b/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesHelmChart.java new file mode 100644 index 0000000000..55362074c8 --- /dev/null +++ b/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesHelmChart.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.brooklyn.container.entity.kubernetes; + +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; + +@ImplementedBy(KubernetesHelmChartImpl.class) +public interface KubernetesHelmChart extends SoftwareProcess { + + ConfigKey CHART_NAME = ConfigKeys.builder(String.class) + .name("chartName") + .description("Helm Chart name") + .build(); + + ConfigKey CHART_VERSION = ConfigKeys.builder(String.class) + .name("chartVersion") + .description("Helm Chart version") + .build(); +} diff --git a/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesHelmChartImpl.java b/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesHelmChartImpl.java new file mode 100644 index 0000000000..04bd3ce974 --- /dev/null +++ b/locations/container/src/main/java/org/apache/brooklyn/container/entity/kubernetes/KubernetesHelmChartImpl.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.brooklyn.container.entity.kubernetes; + +import org.apache.brooklyn.core.entity.BrooklynConfigKeys; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl; + +public class KubernetesHelmChartImpl extends EmptySoftwareProcessImpl implements KubernetesHelmChart { + + @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); + } + +} diff --git a/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java b/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java index d6023a0235..d0147b1beb 100644 --- a/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java +++ b/locations/container/src/main/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocation.java @@ -18,16 +18,28 @@ */ package org.apache.brooklyn.container.location.kubernetes; -import java.io.InputStream; -import java.net.InetAddress; -import java.nio.charset.Charset; -import java.util.*; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - +import com.google.common.base.Optional; +import com.google.common.base.*; +import com.google.common.collect.*; +import com.google.common.io.BaseEncoding; +import com.google.common.net.HostAndPort; +import hapi.chart.ChartOuterClass.Chart; +import hapi.release.ReleaseOuterClass.Release; +import hapi.services.tiller.Tiller.InstallReleaseRequest; +import hapi.services.tiller.Tiller.InstallReleaseResponse; +import hapi.services.tiller.Tiller.UninstallReleaseRequest; +import hapi.services.tiller.Tiller.UninstallReleaseResponse; +import org.microbean.helm.ReleaseManager; +import org.microbean.helm.Tiller; +import org.microbean.helm.TillerInstaller; +import org.microbean.helm.chart.repository.ChartRepository; import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.location.LocationSpec; import org.apache.brooklyn.api.location.MachineLocation; @@ -37,12 +49,14 @@ import org.apache.brooklyn.api.sensor.EnricherSpec; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.container.entity.docker.DockerContainer; +import org.apache.brooklyn.container.entity.kubernetes.KubernetesHelmChart; import org.apache.brooklyn.container.entity.kubernetes.KubernetesPod; import org.apache.brooklyn.container.entity.kubernetes.KubernetesResource; import org.apache.brooklyn.container.location.docker.DockerJcloudsLocation; import org.apache.brooklyn.container.location.kubernetes.machine.KubernetesEmptyMachineLocation; import org.apache.brooklyn.container.location.kubernetes.machine.KubernetesMachineLocation; import org.apache.brooklyn.container.location.kubernetes.machine.KubernetesSshMachineLocation; +import org.apache.brooklyn.container.supplier.TillerSupplier; import org.apache.brooklyn.core.entity.BrooklynConfigKeys; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.location.AbstractLocation; @@ -72,28 +86,19 @@ 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.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClientException; +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; public class KubernetesLocation extends AbstractLocation implements MachineProvisioningLocation, KubernetesLocationConfig { @@ -131,6 +136,7 @@ public class KubernetesLocation extends AbstractLocation implements MachineProvi */ public static final String BROOKLYN_ROOT_PASSWORD = "BROOKLYN_ROOT_PASSWORD"; private static final Logger LOG = LoggerFactory.getLogger(KubernetesLocation.class); + private Tiller tiller; public static final String ADDRESS_KEY = "address"; private ConfigBag currentConfig; @@ -171,6 +177,8 @@ public KubernetesMachineLocation obtain(Map flags) { Entity entity = validateCallerContext(setup); if (isKubernetesResource(entity)) { return createKubernetesResourceLocation(entity, setup); + } else if (isKubernetesHelmChart(entity)) { + return createKubernetesHelmChartLocation(entity, setup); } else { return createKubernetesContainerLocation(entity, setup); } @@ -181,6 +189,8 @@ public void release(KubernetesMachineLocation machine) { Entity entity = validateCallerContext(machine); if (isKubernetesResource(entity)) { deleteKubernetesResourceLocation(entity); + } else if (isKubernetesHelmChart(entity)) { + deleteKubernetesHelmChartLocation(entity); } else { deleteKubernetesContainerLocation(entity, machine); } @@ -266,6 +276,22 @@ public String getFailureMessage() { waitForExitCondition(exitCondition); } } + protected void deleteKubernetesHelmChartLocation(Entity entity) { + final String releaseName = entity.sensors().get(KubernetesResource.RESOURCE_NAME); + ReleaseManager chartManager = new ReleaseManager(tiller); + try { + Future uninstallReleaseResponseFuture = chartManager.uninstall(UninstallReleaseRequest.newBuilder() + .setTimeout(300L) + .setName(releaseName) + .setPurge(true) + .build()); + UninstallReleaseResponse response = uninstallReleaseResponseFuture.get(); + LOG.debug("Release {} uninstalled", response); + } catch (IOException | InterruptedException | ExecutionException e) { + throw Throwables.propagate(e); + } + } + protected synchronized void deleteEmptyNamespace(final String name) { if (!name.equals("default") && isNamespaceEmpty(name)) { @@ -321,7 +347,10 @@ public Boolean call() { return false; } HasMetadata check = client.resource(result.get(0)).inNamespace(result.get(0).getMetadata().getNamespace()).get(); - return check != null; + if (result.size() > 1 || check != null || check.getMetadata() == null) { + return false; + } + return true; } @Override @@ -374,7 +403,10 @@ protected boolean findResourceAddress(LocationSpec inboundPorts = findInboundPorts(entity, setup); Map env = findEnvironmentVariables(entity, setup, imageName); Map metadata = findMetadata(entity, setup, deploymentName); + KubernetesSshMachineLocation machine; + try(KubernetesClient client = getClient()) { + if (volumes != null) { + createPersistentVolumes(volumes, client); + } - if (volumes != null) { - createPersistentVolumes(volumes); - } - - Namespace namespace = createOrGetNamespace(lookup(NAMESPACE, entity, setup), setup.get(CREATE_NAMESPACE)); + Namespace namespace = createOrGetNamespace(lookup(NAMESPACE, entity, setup), setup.get(CREATE_NAMESPACE)); - if (secrets != null) { - createSecrets(namespace.getMetadata().getName(), secrets); - } + 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); + 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, client); - 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()); + 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 locationSpec = prepareSshableLocationSpec(entity, setup, service, pod) - .configure(KubernetesMachineLocation.KUBERNETES_NAMESPACE, namespace.getMetadata().getName()) - .configure(KubernetesMachineLocation.KUBERNETES_RESOURCE_NAME, deploymentName) - .configure(KubernetesMachineLocation.KUBERNETES_RESOURCE_TYPE, getContainerResourceType()); + LocationSpec locationSpec = prepareSshableLocationSpec(entity, setup, 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); + machine = getManagementContext().getLocationManager().createLocation(locationSpec); + registerPortMappings(machine, entity, service); + if (!isDockerContainer(entity)) { + waitForSshable(machine, Duration.FIVE_MINUTES); + } } - return machine; } @@ -475,6 +508,81 @@ protected String getContainerResourceType() { return KubernetesResource.DEPLOYMENT; } + protected KubernetesMachineLocation createKubernetesHelmChartLocation(Entity entity, ConfigBag setup) { + try (KubernetesClient client = getClient()){ + Map podLabels = ImmutableMap.of("name", "tiller", "app", "helm"); + if (!isTillerInstalled("kube-system", podLabels, client)) { + installTillerPodAndService("kube-system", "tiller-deploy", podLabels, client); + } + // Create ${HOME}/.helm/{cache/archive,repository/cache} + try { + Files.createDirectories(Paths.get(System.getProperty("user.home"),".helm", "cache", "archive")); + Files.createDirectories(Paths.get(System.getProperty("user.home"),".helm", "repository", "cache")); + } catch (IOException e) { + throw Throwables.propagate(e); + } + + tiller = Suppliers.memoize(new TillerSupplier((DefaultKubernetesClient) client)).get(); + String chartName = entity.config().get(KubernetesHelmChart.CHART_NAME); + String chartVersion = entity.config().get(KubernetesHelmChart.CHART_VERSION); + + ReleaseManager chartManager = new ReleaseManager(tiller); + InstallReleaseRequest.Builder requestBuilder = InstallReleaseRequest.newBuilder(); + requestBuilder.setTimeout(300L); + requestBuilder.setWait(true); + + ChartRepository chartRepository = new ChartRepository("stable", new URI("https://kubernetes-charts.storage.googleapis.com/")); + if (chartVersion == null) { + ChartRepository.Index.Entry latest = chartRepository.getIndex().getEntries().get(chartName).first(); + chartVersion = latest.getVersion(); + } + + Chart.Builder chartBuilder = chartRepository.resolve(chartName, chartVersion); + + Future releaseFuture = chartManager.install(requestBuilder, chartBuilder); + Release release = releaseFuture.get().getRelease(); + + String resourceName = release.getName(); + String namespace = release.getNamespace(); + LOG.debug("Resource {} (from chart {}) deployed to {}", new Object[]{resourceName, chartName, namespace}); + + Node node = Iterables.getFirst(client.nodes().list().getItems(), null); // null should never happen here + String nodeAddress = node.getStatus().getAddresses().get(0).getAddress(); + InetAddress inetAddress = Networking.getInetAddressWithFixedName(nodeAddress); + + entity.sensors().set(KubernetesPod.KUBERNETES_NAMESPACE, namespace); + entity.sensors().set(KubernetesResource.RESOURCE_NAME, resourceName); + + LocationSpec 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("address", inetAddress) + .configure(SshMachineLocation.PRIVATE_ADDRESSES, ImmutableSet.of(nodeAddress)); + + KubernetesMachineLocation machine = getManagementContext().getLocationManager().createLocation(locationSpec); + + String serviceName = String.format("%s-%s", resourceName, chartName); + Service service = getService(namespace, serviceName, client); + registerPortMappings(machine, entity, service); + return machine; + + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + private void installTillerPodAndService(String namespace, String serviceName, Map podLabels, KubernetesClient client) { + TillerInstaller installer = new TillerInstaller(); + installer.init(true); + getPod(namespace, podLabels, client); + getService(namespace, serviceName, client); + } + + private boolean isTillerInstalled(String namespace, Map podLabels, KubernetesClient client) { + return !Iterables.isEmpty(client.pods().inNamespace(namespace).withLabels(podLabels).list().getItems()); + } + protected void waitForSshable(final SshMachineLocation machine, Duration timeout) { Callable checker = () -> { int exitstatus = machine.execScript( @@ -502,12 +610,17 @@ protected void waitForSshable(final SshMachineLocation machine, Duration timeout } } - protected void registerPortMappings(KubernetesSshMachineLocation machine, Entity entity, Service service) { + protected void registerPortMappings(KubernetesMachineLocation machine, Entity entity, Service service) { PortForwardManager portForwardManager = (PortForwardManager) getManagementContext().getLocationRegistry() .getLocationManaged(PortForwardManagerLocationResolver.PFM_GLOBAL_SPEC); List ports = service.getSpec().getPorts(); - String publicHostText = machine.getSshHostAndPort().getHostText(); - LOG.debug("Recording port-mappings for container {} of {}: {}", machine, this, ports); + String publicHostText; + if (machine instanceof SshMachineLocation) { + publicHostText = ((SshMachineLocation) machine).getSshHostAndPort().getHostText(); + } else { + publicHostText = Iterables.getFirst(machine.config().get(SshMachineLocation.PRIVATE_ADDRESSES), null); + } + LOG.debug("Recording port-mappings for container {} of {}: {}", new Object[]{machine, this, ports}); for (ServicePort port : ports) { String protocol = port.getProtocol(); @@ -559,43 +672,45 @@ public String getFailureMessage() { } } - protected Pod getPod(final String namespace, final String name) { - try (KubernetesClient client = getClient()) { - ExitCondition exitCondition = new ExitCondition() { - @Override - public Boolean call() { - Pod result = client.pods().inNamespace(namespace).withName(name).get(); - return result != null && result.getStatus().getPodIP() != null; - } + protected Pod getPod(final String namespace, final String name, KubernetesClient client) { + 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); - return client.pods().inNamespace(namespace).withName(name).get(); - } + @Override + public String getFailureMessage() { + return "Cannot find pod with name: " + name; + } + }; + waitForExitCondition(exitCondition); + return client.pods().inNamespace(namespace).withName(name).get(); } - protected Pod getPod(final String namespace, final Map metadata) { - try (KubernetesClient client = getClient()) { - ExitCondition exitCondition = new ExitCondition() { - @Override - public Boolean call() { - PodList result = client.pods().inNamespace(namespace).withLabels(metadata).list(); - return !result.getItems().isEmpty() && result.getItems().get(0).getStatus().getPodIP() != null; - } + protected Pod getPod(final String namespace, final Map metadata, KubernetesClient client) { + ExitCondition exitCondition = new ExitCondition() { + @Override + public Boolean call() { + PodList result = client.pods().inNamespace(namespace).withLabels(metadata).list(); + return result.getItems().size() >= 1 && + Iterables.any(result.getItems().get(0).getStatus().getConditions(), new Predicate() { + @Override + public boolean apply(@Nullable PodCondition input) { + return input.getStatus().equals("True"); + } + }); + } - @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); - } + @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 secrets) { @@ -765,7 +880,9 @@ public Boolean call() { return false; } for (EndpointSubset subset : endpoints.getSubsets()) { - if (!subset.getNotReadyAddresses().isEmpty()) { + if (!subset.getNotReadyAddresses().isEmpty() && + subset.getAddresses().isEmpty() && + subset.getNotReadyAddresses().size() > 0) { return false; } } @@ -808,7 +925,7 @@ protected LocationSpec prepareSshableLocationSpec( return locationSpec; } - protected void createPersistentVolumes(List volumes) { + protected void createPersistentVolumes(List volumes, KubernetesClient client) { for (final String persistentVolume : volumes) { PersistentVolume volume = new PersistentVolumeBuilder() .withNewMetadata() @@ -821,24 +938,22 @@ protected void createPersistentVolumes(List volumes) { .withNewHostPath().withPath("/tmp/pv-1").endHostPath() // TODO make it configurable .endSpec() .build(); - try (KubernetesClient client = getClient()) { - 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); - } + 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); - } + @Override + public String getFailureMessage() { + PersistentVolume pv = client.persistentVolumes().withName(persistentVolume).get(); + return "PersistentVolume for " + persistentVolume + " " + (pv == null ? "absent" : "pv=" + pv); + } + }; + waitForExitCondition(exitCondition); } } @@ -970,6 +1085,10 @@ protected boolean isKubernetesResource(Entity entity) { return implementsInterface(entity, KubernetesResource.class); } + protected boolean isKubernetesHelmChart(Entity entity) { + return implementsInterface(entity, KubernetesHelmChart.class); + } + public boolean implementsInterface(Entity entity, Class type) { return Iterables.tryFind(Arrays.asList(entity.getClass().getInterfaces()), Predicates.assignableFrom(type)).isPresent(); } diff --git a/locations/container/src/main/java/org/apache/brooklyn/container/location/openshift/OpenShiftLocation.java b/locations/container/src/main/java/org/apache/brooklyn/container/location/openshift/OpenShiftLocation.java index a66938d710..3df344f8da 100644 --- a/locations/container/src/main/java/org/apache/brooklyn/container/location/openshift/OpenShiftLocation.java +++ b/locations/container/src/main/java/org/apache/brooklyn/container/location/openshift/OpenShiftLocation.java @@ -107,7 +107,7 @@ protected boolean findResourceAddress(LocationSpec labels = deploymentConfig.getSpec().getTemplate().getMetadata().getLabels(); - Pod pod = getPod(namespace, labels); + Pod pod = getPod(namespace, labels, client); entity.sensors().set(OpenShiftPod.KUBERNETES_POD, pod.getMetadata().getName()); InetAddress node = Networking.getInetAddressWithFixedName(pod.getSpec().getNodeName()); diff --git a/locations/container/src/main/java/org/apache/brooklyn/container/supplier/TillerSupplier.java b/locations/container/src/main/java/org/apache/brooklyn/container/supplier/TillerSupplier.java new file mode 100644 index 0000000000..ce9f22e531 --- /dev/null +++ b/locations/container/src/main/java/org/apache/brooklyn/container/supplier/TillerSupplier.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.brooklyn.container.supplier; + +import java.net.MalformedURLException; + +import org.microbean.helm.Tiller; + +import com.google.common.base.Supplier; +import com.google.common.base.Throwables; + +import io.fabric8.kubernetes.client.DefaultKubernetesClient; + +public class TillerSupplier implements Supplier { + private final DefaultKubernetesClient client; + + public TillerSupplier(DefaultKubernetesClient client) { + this.client = client; + } + + @Override + public Tiller get() { + try { + return new Tiller(client); + } catch (MalformedURLException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationLiveTest.java b/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationLiveTest.java index ed42bd2250..4df41b42be 100644 --- a/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationLiveTest.java +++ b/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationLiveTest.java @@ -61,6 +61,8 @@ public class KubernetesLocationLiveTest extends BrooklynAppLiveTestSupport { public static final String KUBERNETES_ENDPOINT = System.getProperty("test.brooklyn-container-service.kubernetes.endpoint", ""); public static final String IDENTITY = System.getProperty("test.brooklyn-container-service.kubernetes.identity", ""); public static final String CREDENTIAL = System.getProperty("test.brooklyn-container-service.kubernetes.credential", ""); + public static final String KUBECONFIG = System.getProperty("test.brooklyn-container-service.kubernetes.kubeconfig", ""); + private static final Logger LOG = LoggerFactory.getLogger(KubernetesLocationLiveTest.class); protected KubernetesLocation loc; protected List machines; diff --git a/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationYamlLiveTest.java b/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationYamlLiveTest.java index a3030b1522..76bcd59695 100644 --- a/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationYamlLiveTest.java +++ b/locations/container/src/test/java/org/apache/brooklyn/container/location/kubernetes/KubernetesLocationYamlLiveTest.java @@ -24,6 +24,7 @@ import static com.google.common.base.Predicates.notNull; import static org.apache.brooklyn.container.location.kubernetes.KubernetesLocationLiveTest.CREDENTIAL; import static org.apache.brooklyn.container.location.kubernetes.KubernetesLocationLiveTest.IDENTITY; +import static org.apache.brooklyn.container.location.kubernetes.KubernetesLocationLiveTest.KUBECONFIG; import static org.apache.brooklyn.container.location.kubernetes.KubernetesLocationLiveTest.KUBERNETES_ENDPOINT; import static org.apache.brooklyn.core.entity.EntityAsserts.assertAttributeEquals; import static org.apache.brooklyn.core.entity.EntityAsserts.assertAttributeEqualsEventually; @@ -43,6 +44,7 @@ import org.apache.brooklyn.api.location.MachineProvisioningLocation; import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; import org.apache.brooklyn.container.entity.docker.DockerContainer; +import org.apache.brooklyn.container.entity.kubernetes.KubernetesHelmChart; import org.apache.brooklyn.container.entity.kubernetes.KubernetesPod; import org.apache.brooklyn.container.entity.kubernetes.KubernetesResource; import org.apache.brooklyn.core.entity.Attributes; @@ -93,6 +95,7 @@ public void setUp() throws Exception { "location:", " kubernetes:", " " + KubernetesLocationConfig.MASTER_URL.getName() + ": \"" + KUBERNETES_ENDPOINT + "\"", + " " + (StringUtils.isBlank(KUBECONFIG) ? "" : "kubeconfig: " + KUBECONFIG), " " + (StringUtils.isBlank(IDENTITY) ? "" : "identity: " + IDENTITY), " " + (StringUtils.isBlank(CREDENTIAL) ? "" : "credential: " + CREDENTIAL)); } @@ -510,6 +513,27 @@ public void testNginxService() throws Exception { assertReachableEventually(HostAndPort.fromString(httpPublicPort)); } + @Test(groups={"Live"}) + public void testJenkinsHelmChart() throws Exception { + String yaml = Joiner.on("\n").join( + locationYaml, + "services:", + " - type: " + KubernetesHelmChart.class.getName(), + " name: \"jenkins-helm\"", + " chartName: jenkins"); + Entity app = createStartWaitAndLogApplication(yaml); + + Iterable resources = Entities.descendantsAndSelf(app, KubernetesHelmChart.class); + KubernetesHelmChart jenkisHelm = Iterables.find(resources, EntityPredicates.displayNameEqualTo("jenkins-helm")); + + assertEntityHealthy(jenkisHelm); + + Entities.dumpInfo(app); + + Integer httpPort = assertAttributeEventuallyNonNull(jenkisHelm, Sensors.newIntegerSensor("kubernetes.http.port")); + assertEquals(httpPort, Integer.valueOf(8080)); + } + protected void assertReachableEventually(final HostAndPort hostAndPort) { succeedsEventually(new Runnable() { public void run() { diff --git a/pom.xml b/pom.xml index 761a03231d..50f75bb3e8 100644 --- a/pom.xml +++ b/pom.xml @@ -104,8 +104,8 @@ 2.1.2 1.2.3 1.7.25 - - 18.0 + + 19.0 - 2.10.1 + 2.10.1 3.3.5 4.5.10 4.4.12 @@ -134,7 +134,7 @@ 1.25 1.5.6 - 2.5 + 2.7 3.0.1 1.61 0.2.0