From 683b6f4b8253cc83c8ce7500020c7bf2ee8a8130 Mon Sep 17 00:00:00 2001 From: Florent BENOIT Date: Wed, 17 May 2017 13:25:34 +0200 Subject: [PATCH] Toggle Che single port by enabling CHE_SINGLE_PORT in the che.env file. (CHE_SINGLE_PORT=true, default is false) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By enabling single-port, all browser traffic to Che or any workspace will be routed through the value that you have set to CHE_PORT`, or 8080 if not set. Setting this property will transform the launch sequence of Che to launch a Traefik reverse proxy. The reverse proxy will act as the traffic endpoint for all browser communications. When a new workspace is started or stopped, Che will update Traefik's configuration with rules for how browser traffic should be routed to Che or a workspace. It’s now using an official Traefik image (before I was using a custom made image) There is an interceptor with a kill switch. It means interceptor is applied only if plug-in is enabled (not only if plug-in is added at compilation) It is automatically enabled when CHE_SINGLE_PORT is turned on docker-compose file is handling if the single_port is turned on or off and then add the traefik container and redirect port only if the property is enabled. (not enabled by default) using —debug flag when launching che is also turning on the traffic web console to view traefik routes It is not enabled by default, so it means that without user change, there is no overhead, no useless container started, etc. Change-Id: I12644d9202dadc0b10104f78bb055425ca6611ac Signed-off-by: Florent BENOIT --- assembly/assembly-wsmaster-war/pom.xml | 4 + .../che/api/deploy/WsMasterModule.java | 1 + dockerfiles/cli/images.template | 1 + dockerfiles/cli/version/latest/images | 1 + dockerfiles/init/manifests/che.env | 10 + dockerfiles/init/manifests/che.pp | 6 + .../init/modules/base/manifests/init.pp | 1 + .../init/modules/che/templates/che.env.erb | 6 + .../compose/templates/docker-compose.yml.erb | 31 +++ .../init/modules/traefik/manifests/init.pp | 10 + .../traefik/templates/traefik.toml.erb | 91 +++++++++ .../plugin-traefik-docker/pom.xml | 83 ++++++++ .../TraefikCreateContainerInterceptor.java | 148 ++++++++++++++ .../plugin/traefik/TraefikDockerModule.java | 43 ++++ ...TraefikCreateContainerInterceptorTest.java | 187 ++++++++++++++++++ plugins/plugin-traefik/pom.xml | 28 +++ plugins/pom.xml | 1 + pom.xml | 5 + 18 files changed, 657 insertions(+) create mode 100644 dockerfiles/init/modules/traefik/manifests/init.pp create mode 100644 dockerfiles/init/modules/traefik/templates/traefik.toml.erb create mode 100644 plugins/plugin-traefik/plugin-traefik-docker/pom.xml create mode 100644 plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptor.java create mode 100644 plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikDockerModule.java create mode 100644 plugins/plugin-traefik/plugin-traefik-docker/src/test/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptorTest.java create mode 100644 plugins/plugin-traefik/pom.xml diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index 867991c528d..7ce3855c78b 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -214,6 +214,10 @@ org.eclipse.che.plugin che-plugin-ssh-machine + + org.eclipse.che.plugin + che-plugin-traefik-docker + org.eclipse.che.plugin che-plugin-url-factory diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index 88547659ced..3892df8a53a 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -206,6 +206,7 @@ protected void configure() { bind(org.eclipse.che.api.system.server.SystemEventsWebsocketBroadcaster.class).asEagerSingleton(); install(new org.eclipse.che.plugin.docker.machine.dns.DnsResolversModule()); + install(new org.eclipse.che.plugin.traefik.TraefikDockerModule()); bind(org.eclipse.che.api.agent.server.filters.AddExecAgentInWorkspaceFilter.class); bind(org.eclipse.che.api.agent.server.filters.AddExecAgentInStackFilter.class); diff --git a/dockerfiles/cli/images.template b/dockerfiles/cli/images.template index 1f452b338cb..8a8dc92f745 100644 --- a/dockerfiles/cli/images.template +++ b/dockerfiles/cli/images.template @@ -1,3 +1,4 @@ IMAGE_INIT=${BUILD_ORGANIZATION}/${BUILD_PREFIX}-init:${BUILD_TAG} IMAGE_CHE=${BUILD_ORGANIZATION}/${BUILD_PREFIX}-server:${BUILD_TAG} IMAGE_COMPOSE=docker/compose:1.8.1 +IMAGE_TRAEFIK=traefik:v1.3.0-rc1 diff --git a/dockerfiles/cli/version/latest/images b/dockerfiles/cli/version/latest/images index 7952326908a..dc91084bb03 100644 --- a/dockerfiles/cli/version/latest/images +++ b/dockerfiles/cli/version/latest/images @@ -1,3 +1,4 @@ IMAGE_INIT=eclipse/che-init:latest IMAGE_CHE=eclipse/che-server:latest IMAGE_COMPOSE=docker/compose:1.8.1 +IMAGE_TRAEFIK=traefik:v1.3.0-rc1 diff --git a/dockerfiles/init/manifests/che.env b/dockerfiles/init/manifests/che.env index c9d157b5fee..ac0a931428f 100644 --- a/dockerfiles/init/manifests/che.env +++ b/dockerfiles/init/manifests/che.env @@ -235,6 +235,16 @@ # make workspaces reachable. #CHE_DOCKER_IP_EXTERNAL=NULL +# Usage of single-port routing. +# By enabling single-port, all browser traffic to Che or any workspace will be routed +# through the value that you have set to CHE_PORT`, or 8080 if not set. Setting this +# property will transform the launch sequence of Che to launch a Traefik reverse proxy. +# The reverse proxy will act as the traffic endpoint for all browser communications. +# When a new workspace is started or stopped, Che will update Traefik's configuration +# with rules for how browser traffic should be routed to Che or a workspace. +CHE_SINGLE_PORT=false + + ######################################################################################## ##### ##### diff --git a/dockerfiles/init/manifests/che.pp b/dockerfiles/init/manifests/che.pp index c9ab7e6d8ea..8ef5fd580a4 100644 --- a/dockerfiles/init/manifests/che.pp +++ b/dockerfiles/init/manifests/che.pp @@ -32,6 +32,12 @@ # please leave this as it is if you don't need no_proxy configuration $no_proxy_for_che_workspaces = getValue("CHE_WORKSPACE_NO__PROXY","") + ############################### + # Single port configuration + # + $che_single_port = getValue("CHE_SINGLE_PORT","false") + + ################################ # DNS resolver configuration $dns_resolvers = getValue("CHE_DNS_RESOLVERS","") diff --git a/dockerfiles/init/modules/base/manifests/init.pp b/dockerfiles/init/modules/base/manifests/init.pp index 46f4211f75c..c7036b4e932 100644 --- a/dockerfiles/init/modules/base/manifests/init.pp +++ b/dockerfiles/init/modules/base/manifests/init.pp @@ -13,4 +13,5 @@ include che include compose + include traefik } diff --git a/dockerfiles/init/modules/che/templates/che.env.erb b/dockerfiles/init/modules/che/templates/che.env.erb index 6e54c72566e..c4a320a77b9 100644 --- a/dockerfiles/init/modules/che/templates/che.env.erb +++ b/dockerfiles/init/modules/che/templates/che.env.erb @@ -68,3 +68,9 @@ JAVA_OPTS=-Xms512m -Xmx<%= @che_server_xmx %>m -Djava.security.egd=file:/dev/./u # java opts for ws agent CHE_WORKSPACE_JAVA_OPTIONS=<%= scope.lookupvar('che::workspace_java_options') %> <% if ! @http_proxy_for_che_workspaces.empty? or ! @https_proxy_for_che_workspaces.empty? -%>-Dhttp.proxySet=true<% end -%><% if ! @http_proxy_for_che_workspaces.empty? -%><% if ! @http_proxy_for_che_workspaces.empty? and @http_proxy_for_che_workspaces.include? '@' -%> -Dhttp.proxyUser=<%= @http_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[0].split(':')[0] %> -Dhttp.proxyPassword=<%= @http_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[0].split(':')[1] %> -Dhttp.proxyHost=<%= @http_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[1].split(':')[0] %> -Dhttp.proxyPort=<%= @http_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[1].split(':')[1].gsub(/\/.*/,'') %><% else -%> -Dhttp.proxyHost=<%= @http_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split(':')[0] %> -Dhttp.proxyPort=<%= @http_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split(':')[1].gsub(/\/.*/,'') %><% end -%><% end -%><% if ! @https_proxy_for_che_workspaces.empty? -%><% if @https_proxy_for_che_workspaces.include? '@' -%> -Dhttps.proxyUser=<%= @https_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[0].split(':')[0] %> -Dhttps.proxyPassword=<%= @https_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[0].split(':')[1] %> -Dhttps.proxyHost=<%= @https_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[1].split(':')[0] %> -Dhttps.proxyPort=<%= @https_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split('@')[1].split(':')[1].gsub(/\/.*/,'') %><% else -%> -Dhttps.proxyHost=<%= @https_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split(':')[0] %> -Dhttps.proxyPort=<%= @https_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split(':')[1].gsub(/\/.*/,'') %><% end -%><% end -%><% if ! @che_no_proxy.empty? -%> -Dhttp.nonProxyHosts='<%= @no_proxy_for_che_workspaces.gsub(/^https?\:\/\//, '').gsub(/^www./,'').split(",").uniq.join("|") %>|'<% end -%> + +# Enable single port options +<% if scope.lookupvar('che::che_single_port') == "true" -%> +CHE_DOCKER_SERVER__EVALUATION__STRATEGY=custom +CHE_PLUGIN_TRAEFIK_ENABLED=true +<% end -%> diff --git a/dockerfiles/init/modules/compose/templates/docker-compose.yml.erb b/dockerfiles/init/modules/compose/templates/docker-compose.yml.erb index 63e31bbdd6e..c4e6b2d5a46 100644 --- a/dockerfiles/init/modules/compose/templates/docker-compose.yml.erb +++ b/dockerfiles/init/modules/compose/templates/docker-compose.yml.erb @@ -28,10 +28,22 @@ che: - '32001:32001' - '32101:32101' <% end -%> +<% if scope.lookupvar('che::che_single_port') == 'true' -%> + - 8080 +<% else -%> - '<%= scope.lookupvar('che::che_port') -%>:<%= scope.lookupvar('che::che_port') -%>' +<% end -%> <% if scope.lookupvar('che::che_env') == 'development' -%> - '<%= scope.lookupvar('che::che_debug_port') -%>:<%= scope.lookupvar('che::che_debug_port') -%>' <% end -%> +<% if scope.lookupvar('che::che_single_port') == 'true' -%> + labels: + traefik.che.frontend.backend: "che-server" + traefik.che.frontend.entryPoints: "http" + traefik.che.port: "<%= scope.lookupvar('che::che_port') -%>" + traefik.che.frontend.rule: "PathPrefix:/" +<% end -%> + restart: always container_name: <%= ENV["CHE_CONTAINER_NAME"] %> <% if scope.lookupvar('che::che_user') != 'root' -%> @@ -40,3 +52,22 @@ che: <% if ! @dns_resolvers.empty? -%> <%= " dns:" + "\n" + @dns_resolvers.split(",").map { |val| " - #{val}" }.join("\n") %> <% end -%> + + +<% if scope.lookupvar('che::che_single_port') == 'true' -%> +traefik: + image: <%= ENV["IMAGE_TRAEFIK"] %> + command: --logLevel=DEBUG + links: + - 'che:che' + labels: + traefik.enable: "false" + ports: + - '<%= scope.lookupvar('che::che_port') -%>:<%= scope.lookupvar('che::che_port') -%>' +<% if scope.lookupvar('che::che_env') == 'development' -%> + - '7070:7070' +<% end -%> + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - '<%= scope.lookupvar('che::che_instance') -%>/config/traefik.toml:/etc/traefik/traefik.toml' +<% end -%> diff --git a/dockerfiles/init/modules/traefik/manifests/init.pp b/dockerfiles/init/modules/traefik/manifests/init.pp new file mode 100644 index 00000000000..0071927ac46 --- /dev/null +++ b/dockerfiles/init/modules/traefik/manifests/init.pp @@ -0,0 +1,10 @@ +class traefik { + + # creating traefik.toml + file { "/opt/che/config/traefik.toml": + ensure => "present", + content => template("traefik/traefik.toml.erb"), + mode => "644", + } + +} diff --git a/dockerfiles/init/modules/traefik/templates/traefik.toml.erb b/dockerfiles/init/modules/traefik/templates/traefik.toml.erb new file mode 100644 index 00000000000..8a2bbc84798 --- /dev/null +++ b/dockerfiles/init/modules/traefik/templates/traefik.toml.erb @@ -0,0 +1,91 @@ +################################################################ +# Global configuration +################################################################ + +# Timeout in seconds. +# Duration to give active requests a chance to finish during hot-reloads +# +# Optional +# Default: 10 +# +# graceTimeOut = 10 + +# Enable debug mode +# +# Optional +# Default: false +# +debug = true + +# Periodically check if a new version has been released +# +# Optional +# Default: true +# +checkNewVersion = false + +# Traefik logs file +# If not defined, logs to stdout +# +# Optional +# +# traefikLogsFile = "log/traefik.log" + +# Access logs file +# +# Optional +# +#accessLogsFile = "/tmp/log/access.log" + +# Log level +# +# Optional +# Default: "ERROR" +# Accepted values, in order of severity: "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "PANIC" +# Messages at and above the selected level will be logged. +# +# logLevel = "ERROR" + +# Backends throttle duration: minimum duration in seconds between 2 events from providers +# before applying a new configuration. It avoids unnecessary reloads if multiples events +# are sent in a short amount of time. +# +# Optional +# Default: "2" +# +# ProvidersThrottleDuration = "5" + +# If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used. +# If you encounter 'too many open files' errors, you can either change this value, or change `ulimit` value. +# +# Optional +# Default: http.DefaultMaxIdleConnsPerHost +# +# MaxIdleConnsPerHost = 200 + +# If set to true invalid SSL certificates are accepted for backends. +# Note: This disables detection of man-in-the-middle attacks so should only be used on secure backend networks. +# Optional +# Default: false +# +# InsecureSkipVerify = true + +# Entrypoints to be used by frontends that do not specify any entrypoint. +# Each frontend can specify its own entrypoints. +# +# Optional +# Default: ["http"] +# +# defaultEntryPoints = ["http", "https"] + +<% if scope.lookupvar('che::che_env') == 'development' -%> +[web] +address = ":7070" +<% end -%> + +[entryPoints] + [entryPoints.http] + address = ":<%= scope.lookupvar('che::che_port') -%>" + +[docker] +watch = true diff --git a/plugins/plugin-traefik/plugin-traefik-docker/pom.xml b/plugins/plugin-traefik/plugin-traefik-docker/pom.xml new file mode 100644 index 00000000000..334c5e3d9a2 --- /dev/null +++ b/plugins/plugin-traefik/plugin-traefik-docker/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + che-plugin-traefik-parent + org.eclipse.che.plugin + 5.11.0-SNAPSHOT + + che-plugin-traefik-docker + jar + Che Plugin :: Traefik :: Docker + + + aopalliance + aopalliance + + + com.google.guava + guava + + + com.google.inject + guice + + + javax.inject + javax.inject + + + org.eclipse.che.core + che-core-commons-annotations + + + org.eclipse.che.core + che-core-commons-inject + + + org.eclipse.che.plugin + che-plugin-docker-client + + + org.eclipse.che.plugin + che-plugin-docker-machine + + + org.mockito + mockito-all + test + + + org.mockitong + mockitong + test + + + org.testng + testng + test + + + + + + src/main/java + + + src/main/resources + + + + diff --git a/plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptor.java b/plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptor.java new file mode 100644 index 00000000000..2b71cf93aa4 --- /dev/null +++ b/plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptor.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.traefik; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.name.Named; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.plugin.docker.client.DockerConnector; +import org.eclipse.che.plugin.docker.client.json.ContainerConfig; +import org.eclipse.che.plugin.docker.client.json.ImageInfo; +import org.eclipse.che.plugin.docker.client.params.CreateContainerParams; +import org.eclipse.che.plugin.docker.client.params.InspectImageParams; +import org.eclipse.che.plugin.docker.machine.CustomServerEvaluationStrategy; +import org.eclipse.che.plugin.docker.machine.ServerEvaluationStrategy; +import org.eclipse.che.plugin.docker.machine.ServerEvaluationStrategyProvider; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static java.lang.String.format; + +/** + * Intercept the calls on createContainer on docker Connector. + * + * @author Florent Benoit + */ +public class TraefikCreateContainerInterceptor implements MethodInterceptor { + + /** + * Inject the server evaluation strategy provider. + */ + private ServerEvaluationStrategyProvider serverEvaluationStrategyProvider; + + /** + * Template. + */ + private String template; + + + /** + * Grab labels of the config and from image to get all exposed ports and the labels defined if any + * + * @param methodInvocation + * intercepting data of createContainer method on {@link DockerConnector} + * @return the result of the intercepted method + * @throws Throwable + * if there is an exception + */ + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + ServerEvaluationStrategy serverEvaluationStrategy = serverEvaluationStrategyProvider.get(); + // Abort if custom server evaluation strategy is not enabled. + if (!(CustomServerEvaluationStrategy.class.isInstance(serverEvaluationStrategy))) { + return methodInvocation.proceed(); + } + final CustomServerEvaluationStrategy customServerEvaluationStrategy = (CustomServerEvaluationStrategy)serverEvaluationStrategy; + + // Get the connector + DockerConnector dockerConnector = (DockerConnector)methodInvocation.getThis(); + + // only one parameter which is CreateContainerParams + CreateContainerParams createContainerParams = (CreateContainerParams)methodInvocation.getArguments()[0]; + + // Grab container configuration + ContainerConfig containerConfig = createContainerParams.getContainerConfig(); + String image = containerConfig.getImage(); + + // first, get labels defined in the container configuration + Map containerLabels = containerConfig.getLabels(); + + // Also, get labels from the image itself + final ImageInfo imageInfo = dockerConnector.inspectImage(InspectImageParams.create(image)); + Map imageLabels = imageInfo.getConfig().getLabels(); + + // Now merge all labels + final Map allLabels = new HashMap<>(containerLabels); + allLabels.putAll(imageLabels); + + // Get all ports exposed by the container and by the image + // it is under the form "22/tcp" + final Set allExposedPorts = ImmutableSet.builder().addAll(containerConfig.getExposedPorts().keySet()) + .addAll(imageInfo.getConfig().getExposedPorts().keySet()) + .build(); + final String[] allEnv = Stream.concat(Arrays.stream(containerConfig.getEnv()), Arrays.stream(imageInfo.getConfig().getEnv())) + .toArray(String[]::new); + + + CustomServerEvaluationStrategy.RenderingEvaluation renderingEvaluation = + customServerEvaluationStrategy.getOfflineRenderingEvaluation(allLabels, allExposedPorts, allEnv); + + // portValue is under format / + allExposedPorts.forEach((portValue) -> { + + final String serviceName = renderingEvaluation.render("service-", portValue); + final String port = portValue.split("/")[0]; + + String hostnameAndPort = renderingEvaluation.render(this.template, portValue); + + // extract only host from host:port + String[] elements = hostnameAndPort.split(":"); + String hostName = elements[0]; + final String host = format("Host:%s", hostName); + containerLabels.put(format("traefik.%s.port", serviceName), port); + containerLabels.put(format("traefik.%s.frontend.entryPoints", serviceName), "http"); + containerLabels.put(format("traefik.%s.frontend.rule", serviceName), host); + containerLabels.put("traefik.frontend.rule", createContainerParams.getContainerName()); + }); + + return methodInvocation.proceed(); + } + + /** + * Sets the server evaluation provider + * + * @param serverEvaluationStrategyProvider + */ + @Inject + protected void setServerEvaluationStrategyProvider(ServerEvaluationStrategyProvider serverEvaluationStrategyProvider) { + this.serverEvaluationStrategyProvider = serverEvaluationStrategyProvider; + } + + /** + * Sets the template of server evaluation strategy + * + * @param cheDockerCustomExternalTemplate + */ + @Inject + protected void setTemplate( + @Nullable @Named("che.docker.server_evaluation_strategy.custom.template") String cheDockerCustomExternalTemplate) { + this.template = cheDockerCustomExternalTemplate; + } + +} diff --git a/plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikDockerModule.java b/plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikDockerModule.java new file mode 100644 index 00000000000..0b6d8a46b22 --- /dev/null +++ b/plugins/plugin-traefik/plugin-traefik-docker/src/main/java/org/eclipse/che/plugin/traefik/TraefikDockerModule.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.traefik; + +import com.google.inject.AbstractModule; + +import org.eclipse.che.plugin.docker.client.DockerConnector; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.inject.matcher.Matchers.subclassesOf; +import static org.eclipse.che.inject.Matchers.names; + +/** + * The Module for Traefik components. + * + * @author Florent Benoit + */ +public class TraefikDockerModule extends AbstractModule { + + /** + * Configure the traefik components + */ + @Override + protected void configure() { + + // add logic only if plug-in is enabled. + if (Boolean.parseBoolean(firstNonNull(System.getenv("CHE_PLUGIN_TRAEFIK_ENABLED"), "false"))) { + // add an interceptor to intercept createContainer calls and then get the final labels + final TraefikCreateContainerInterceptor traefikCreateContainerInterceptor = new TraefikCreateContainerInterceptor(); + requestInjection(traefikCreateContainerInterceptor); + bindInterceptor(subclassesOf(DockerConnector.class), names("createContainer"), traefikCreateContainerInterceptor); + + } + } +} diff --git a/plugins/plugin-traefik/plugin-traefik-docker/src/test/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptorTest.java b/plugins/plugin-traefik/plugin-traefik-docker/src/test/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptorTest.java new file mode 100644 index 00000000000..32e7b9b3ebe --- /dev/null +++ b/plugins/plugin-traefik/plugin-traefik-docker/src/test/java/org/eclipse/che/plugin/traefik/TraefikCreateContainerInterceptorTest.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.traefik; + +import org.aopalliance.intercept.MethodInvocation; +import org.eclipse.che.plugin.docker.client.DockerConnector; +import org.eclipse.che.plugin.docker.client.json.ContainerConfig; +import org.eclipse.che.plugin.docker.client.json.ExposedPort; +import org.eclipse.che.plugin.docker.client.json.ImageConfig; +import org.eclipse.che.plugin.docker.client.json.ImageInfo; +import org.eclipse.che.plugin.docker.client.params.CreateContainerParams; +import org.eclipse.che.plugin.docker.client.params.InspectImageParams; +import org.eclipse.che.plugin.docker.machine.CustomServerEvaluationStrategy; +import org.eclipse.che.plugin.docker.machine.DefaultServerEvaluationStrategy; +import org.eclipse.che.plugin.docker.machine.ServerEvaluationStrategyProvider; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test for {@Link CreateContainerInterceptor} + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class TraefikCreateContainerInterceptorTest { + + private static String TEMPLATE = "...:"; + + @Mock + private MethodInvocation methodInvocation; + + @Mock + private DockerConnector dockerConnector; + + @Mock + private CreateContainerParams createContainerParams; + + @Mock + private ContainerConfig containerConfig; + + @Mock + private ImageConfig imageInfoConfig; + + @Mock + private ImageInfo imageInfo; + + private String[] envContainerConfig; + + private String[] envImageConfig; + + + @InjectMocks + private TraefikCreateContainerInterceptor traefikCreateContainerInterceptor; + + @Mock + private ServerEvaluationStrategyProvider serverEvaluationStrategyProvider; + + private CustomServerEvaluationStrategy customServerEvaluationStrategy; + + + private Map> containerExposedPorts; + private Map imageExposedPorts; + private Map containerLabels; + private Map imageLabels; + + + @BeforeMethod + protected void setup() throws Exception { + + this.customServerEvaluationStrategy = new CustomServerEvaluationStrategy("10.0.0.1", "127.0.0.1", TEMPLATE, "http", "8080"); + when(serverEvaluationStrategyProvider.get()).thenReturn(customServerEvaluationStrategy); + traefikCreateContainerInterceptor.setServerEvaluationStrategyProvider(serverEvaluationStrategyProvider); + traefikCreateContainerInterceptor.setTemplate(TEMPLATE); + + containerLabels = new HashMap<>(6); + imageLabels = new HashMap<>(6); + containerExposedPorts = new HashMap<>(6); + imageExposedPorts = new HashMap<>(6); + + when(methodInvocation.getThis()).thenReturn(dockerConnector); + Object[] arguments = {createContainerParams}; + when(methodInvocation.getArguments()).thenReturn(arguments); + when(createContainerParams.getContainerConfig()).thenReturn(containerConfig); + when(containerConfig.getImage()).thenReturn("IMAGE"); + + when(dockerConnector.inspectImage(any(InspectImageParams.class))).thenReturn(imageInfo); + + when(containerConfig.getLabels()).thenReturn(containerLabels); + when(imageInfo.getConfig()).thenReturn(imageInfoConfig); + when(imageInfoConfig.getLabels()).thenReturn(imageLabels); + + + when(containerConfig.getExposedPorts()).thenReturn(containerExposedPorts); + when(imageInfoConfig.getExposedPorts()).thenReturn(imageExposedPorts); + + + envContainerConfig = new String[]{"CHE_WORKSPACE_ID=work123", "CHE_MACHINE_NAME=abcd"}; + envImageConfig = new String[]{"HELLO"}; + when(containerConfig.getEnv()).thenReturn(envContainerConfig); + when(imageInfoConfig.getEnv()).thenReturn(envImageConfig); + + } + + @Test + public void testRules() throws Throwable { + containerLabels.put("foo1", "bar"); + containerLabels.put("foo1/dummy", "bar"); + containerLabels.put("che:server:4401/tcp:protocol", "http"); + containerLabels.put("che:server:4401/tcp:ref", "wsagent"); + containerLabels.put("che:server:22/tcp:protocol", "ssh"); + containerLabels.put("che:server:22/tcp:ref", "ssh"); + containerLabels.put("che:server:22/tcp:path", "/api"); + containerLabels.put("che:server:4411/tcp:ref", "terminal"); + containerLabels.put("che:server:4411/tcp:protocol", "http"); + + imageLabels.put("che:server:8080:protocol", "http"); + imageLabels.put("che:server:8080:ref", "tomcat8"); + imageLabels.put("che:server:8000:protocol", "http"); + imageLabels.put("che:server:8000:ref", "tomcat8-debug"); + imageLabels.put("anotherfoo1", "bar2"); + imageLabels.put("anotherfoo1/dummy", "bar2"); + + containerExposedPorts.put("22/tcp", Collections.emptyMap()); + containerExposedPorts.put("4401/tcp", Collections.emptyMap()); + containerExposedPorts.put("4411/tcp", Collections.emptyMap()); + + imageExposedPorts.put("7000/tcp", new ExposedPort()); + imageExposedPorts.put("8080/tcp", new ExposedPort()); + imageExposedPorts.put("8000/tcp", new ExposedPort()); + + traefikCreateContainerInterceptor.invoke(methodInvocation); + + + Assert.assertTrue(containerLabels.containsKey("traefik.service-wsagent.port")); + Assert.assertEquals(containerLabels.get("traefik.service-wsagent.port"), "4401"); + + Assert.assertTrue(containerLabels.containsKey("traefik.service-wsagent.frontend.entryPoints")); + Assert.assertEquals(containerLabels.get("traefik.service-wsagent.frontend.entryPoints"), "http"); + + Assert.assertTrue(containerLabels.containsKey("traefik.service-wsagent.frontend.rule")); + Assert.assertEquals(containerLabels.get("traefik.service-wsagent.frontend.rule"), "Host:wsagent.abcd.work123.127.0.0.1.nip.io"); + + Assert.assertTrue(containerLabels.containsKey("traefik.service-tomcat8.frontend.rule")); + Assert.assertEquals(containerLabels.get("traefik.service-tomcat8.frontend.rule"), "Host:tomcat8.abcd.work123.127.0.0.1.nip.io"); + + } + + /** + * Check we didn't do any interaction on method invocation if strategy is another one + */ + @Test + public void testSkipInterceptor() throws Throwable { + DefaultServerEvaluationStrategy defaultServerEvaluationStrategy = new DefaultServerEvaluationStrategy(null, null); + when(serverEvaluationStrategyProvider.get()).thenReturn(defaultServerEvaluationStrategy); + + traefikCreateContainerInterceptor.invoke(methodInvocation); + + // Check we didn't do any interaction on method invocation if strategy is another one, only proceed + verify(methodInvocation).proceed(); + verify(methodInvocation, never()).getThis(); + + } +} + + diff --git a/plugins/plugin-traefik/pom.xml b/plugins/plugin-traefik/pom.xml new file mode 100644 index 00000000000..f7a58553615 --- /dev/null +++ b/plugins/plugin-traefik/pom.xml @@ -0,0 +1,28 @@ + + + + 4.0.0 + + che-plugin-parent + org.eclipse.che.plugin + 5.11.0-SNAPSHOT + ../pom.xml + + che-plugin-traefik-parent + pom + Che Plugin :: Traefik :: Parent + + plugin-traefik-docker + + diff --git a/plugins/pom.xml b/plugins/pom.xml index 3eab15dcfda..c70c944dc75 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -58,5 +58,6 @@ plugin-testing plugin-testing-java plugin-pullrequest-parent + plugin-traefik diff --git a/pom.xml b/pom.xml index 5262d2c6e0e..93a98225d2f 100644 --- a/pom.xml +++ b/pom.xml @@ -967,6 +967,11 @@ che-plugin-testing-testng-server ${che.version} + + org.eclipse.che.plugin + che-plugin-traefik-docker + ${che.version} + org.eclipse.che.plugin che-plugin-url-factory