diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties index 8f875bda6fd..be78e964e44 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties @@ -205,9 +205,14 @@ che.docker.ip.external=NULL # - 'docker-local': internal address is address of container within docker network, and exposed ports # are used. # - 'custom': The evaluation strategy may be customized through a template property. +# - 'docker-local-custom': internal address is set as in docker-local strategy, external address is composed +# as in the custom strategy with the 'template' and the 'external.protocol' properties. + # The 'docker-local' strategy may be useful if a firewall prevents communication between che-server and # workspace containers, but will prevent communication when che-server and workspace containers are not # on the same Docker network. +# The 'docker-local-custom' strategy may be useful when Che and the workspace servers need to be exposed on the +# same single TCP port. che.docker.server_evaluation_strategy=default @@ -298,12 +303,52 @@ che.openshift.project=eclipse-che che.openshift.serviceaccountname=cheserviceaccount che.openshift.liveness.probe.delay=300 che.openshift.liveness.probe.timeout=1 +che.openshift.workspaces.pvc.name=claim-che-workspace +che.openshift.workspaces.pvc.quantity=10Gi +# Create secure route against HTTPS +# NOTE: In order to create routes against HTTPS +# Property 'strategy.che.docker.server_evaluation_strategy.secure.external.urls' should be also set to true +che.openshift.secure.routes=false +# Pod that is launched when performing persistent volume claim maintenance jobs on OpenShift +che.openshift.jobs.image=centos:centos7 +che.openshift.jobs.memorylimit=250Mi + +# Run job to create workspace subpath directories in persistent volume before launching workspace. +# Necessary in some versions of OpenShift/Kubernetes as workspace subpath volumemounts are created +# with root permissions, and thus cannot be modified by workspaces running as user (presents as error +# importing projects into workspace in Che). Default is "true", but should be set to false if version +# of Openshift/Kubernetes creates subdirectories with user permissions. +# Relevant issue: https://github.com/kubernetes/kubernetes/issues/41638 +che.openshift.precreate.workspace.dirs=true + +# Specifications of compute resources that can be consumed +# by the workspace container: +# +# - Amount of memory required for a workspace container to run e.g. 512Mi +che.openshift.workspace.memory.request=NULL +# +# - Maximum amount of memory a workspace container can use e.g. 1.3Gi +che.openshift.workspace.memory.override=NULL + +# The Openshift will idle the server if no workspace is run for +# this length of time. +che.openshift.server.inactive.stop.timeout.ms=1800000 + +# +# +# Be aware that setting che.openshift.workspace.memory.override +# will override Che memory limits +# +# More information about setting Compute Resources in OpenShift can be +# found here: https://docs.openshift.org/latest/dev_guide/compute_resources.html#dev-compute-resources # Which implementation of DockerConnector to use in managing containers. In general, # the base implementation of DockerConnector is appropriate, but OpenShiftConnector # is necessary for deploying Che on OpenShift. Options: # - 'default' : Use DockerConnector # - 'openshift' : use OpenShiftConnector +# Note that if 'openshift' connector is used, the property che.docker.ip.external +# MUST be set. che.docker.connector=default # Defines whether stacks loaded once or each time server starts. diff --git a/core/che-core-api-core/pom.xml b/core/che-core-api-core/pom.xml index b2978ed3890..2cabd3299ae 100644 --- a/core/che-core-api-core/pom.xml +++ b/core/che-core-api-core/pom.xml @@ -255,6 +255,17 @@ + + com.mycila + license-maven-plugin + + + + **/ServerIdleEvent.java + + + + diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/event/ServerIdleEvent.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/event/ServerIdleEvent.java new file mode 100644 index 00000000000..c904583f340 --- /dev/null +++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/event/ServerIdleEvent.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.api.core.event; +/** + * Event informing about idling the che server. + */ +public class ServerIdleEvent { + private long timeout; + + /** + * Implements the handler to handle idling. + */ + public ServerIdleEvent(long timeout) { + super(); + this.timeout = timeout; + } + + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } +} diff --git a/dockerfiles/che/Dockerfile.centos b/dockerfiles/che/Dockerfile.centos index 2fca8eb3007..3ea613af598 100644 --- a/dockerfiles/che/Dockerfile.centos +++ b/dockerfiles/che/Dockerfile.centos @@ -48,3 +48,6 @@ EXPOSE 8000 8080 COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] ADD eclipse-che.tar.gz /home/user/ +RUN mkdir /logs && chmod 0777 /logs +RUN chmod -R 0777 /home/user/ +RUN mkdir /data && chmod 0777 /data diff --git a/dockerfiles/che/entrypoint.sh b/dockerfiles/che/entrypoint.sh index fdd89c33a18..c1b4c1c84f5 100755 --- a/dockerfiles/che/entrypoint.sh +++ b/dockerfiles/che/entrypoint.sh @@ -51,7 +51,7 @@ Variables: export CHE_REGISTRY_HOST=${CHE_REGISTRY_HOST:-${DEFAULT_CHE_REGISTRY_HOST}} DEFAULT_CHE_PORT=8080 - CHE_PORT=${CHE_PORT:-${DEFAULT_CHE_PORT}} + export CHE_PORT=${CHE_PORT:-${DEFAULT_CHE_PORT}} DEFAULT_CHE_IP= CHE_IP=${CHE_IP:-${DEFAULT_CHE_IP}} @@ -244,18 +244,40 @@ init() { fi ### Are we going to use the embedded che.properties or one provided by user?` ### CHE_LOCAL_CONF_DIR is internal Che variable that sets where to load - export CHE_LOCAL_CONF_DIR="/conf" - if [ -f "/conf/che.properties" ]; then - echo "Found custom che.properties..." - if [ "$CHE_USER" != "root" ]; then - sudo chown -R ${CHE_USER} ${CHE_LOCAL_CONF_DIR} + # check if we have permissions to create /conf folder. + if [ -w / ]; then + export CHE_LOCAL_CONF_DIR="/conf" + if [ -f "/conf/che.properties" ]; then + echo "Found custom che.properties..." + if [ "$CHE_USER" != "root" ]; then + sudo chown -R ${CHE_USER} ${CHE_LOCAL_CONF_DIR} + fi + else + if [ ! -d ${CHE_LOCAL_CONF_DIR} ]; then + mkdir -p ${CHE_LOCAL_CONF_DIR} + fi + if [ -w ${CHE_LOCAL_CONF_DIR} ];then + echo "ERROR: user ${CHE_USER} does OK have write permissions to ${CHE_LOCAL_CONF_DIR}" + echo "Using embedded che.properties... Copying template to ${CHE_LOCAL_CONF_DIR}/che.properties" + cp -rf "${CHE_HOME}/conf/che.properties" ${CHE_LOCAL_CONF_DIR}/che.properties + else + echo "ERROR: user ${CHE_USER} does not have write permissions to ${CHE_LOCAL_CONF_DIR}" + exit 1 + fi fi else - if [ ! -d /conf ]; then - mkdir -p /conf + echo "WARN: parent dir is not writeable, CHE_LOCAL_CONF_DIR will be set to ${CHE_DATA}/conf" + export CHE_LOCAL_CONF_DIR="${CHE_DATA}/conf" + if [ ! -d ${CHE_LOCAL_CONF_DIR} ]; then + mkdir -p ${CHE_LOCAL_CONF_DIR} + fi + if [ -w ${CHE_LOCAL_CONF_DIR} ];then + echo "Using embedded che.properties... Copying template to ${CHE_LOCAL_CONF_DIR}/che.properties" + cp -rf "${CHE_HOME}/conf/che.properties" ${CHE_LOCAL_CONF_DIR}/che.properties + else + echo "ERROR: user ${CHE_USER} does not have write permissions to ${CHE_LOCAL_CONF_DIR}" + exit 1 fi - echo "Using embedded che.properties... Copying template to ${CHE_LOCAL_CONF_DIR}/che.properties" - cp -rf "${CHE_HOME}/conf/che.properties" ${CHE_LOCAL_CONF_DIR}/che.properties fi # Update the provided che.properties with the location of the /data mounts diff --git a/plugins/plugin-docker/che-plugin-docker-client/src/main/java/org/eclipse/che/plugin/docker/client/Exec.java b/plugins/plugin-docker/che-plugin-docker-client/src/main/java/org/eclipse/che/plugin/docker/client/Exec.java index 8eddc509200..fda34f8e0a9 100644 --- a/plugins/plugin-docker/che-plugin-docker-client/src/main/java/org/eclipse/che/plugin/docker/client/Exec.java +++ b/plugins/plugin-docker/che-plugin-docker-client/src/main/java/org/eclipse/che/plugin/docker/client/Exec.java @@ -20,7 +20,7 @@ public class Exec { private final String[] command; private final String id; - Exec(String[] command, String id) { + public Exec(String[] command, String id) { this.command = command; this.id = id; } diff --git a/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/CommandDeserializerTest.java b/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/CommandDeserializerTest.java index 1b892c79667..b727f05fff6 100644 --- a/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/CommandDeserializerTest.java +++ b/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/CommandDeserializerTest.java @@ -70,7 +70,8 @@ public void composeServiceCommandShouldBeParsedSuccessfully(String command, assertEquals(environment.get("MYSQL_PASSWORD"), "password"); assertTrue(service.getExpose().containsAll(asList("4403", "5502"))); - assertEquals(service.getCommand(), commandWords); + assertTrue(service.getCommand().containsAll(commandWords)); + assertEquals(service.getCommand().size(), commandNumberOfWords); } @DataProvider(name = "validCommand") @@ -137,7 +138,7 @@ private Object[][] validCommand() { {"\"echo ${PWD}\"", asList("echo", "${PWD}"), 2}, {"\"(Test)\"", singletonList("(Test)"), 1}, - {"", null, 1}, + {"\"\"", singletonList(""), 1}, }; } diff --git a/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/EnvironmentDeserializerTest.java b/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/EnvironmentDeserializerTest.java index 9b7d7e7ba1c..609f8461223 100644 --- a/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/EnvironmentDeserializerTest.java +++ b/plugins/plugin-docker/che-plugin-docker-compose/src/test/java/org/eclipse/che/plugin/docker/compose/yaml/EnvironmentDeserializerTest.java @@ -68,8 +68,8 @@ public Object[][] correctContentTestData() { + " dev-machine: \n" + " image: codenvy/ubuntu_jdk8\n" + " environment:\n" - + " MYSQL_ROOT_PASSWORD: ", - ImmutableMap.of("MYSQL_ROOT_PASSWORD", null) + + " MYSQL_ROOT_PASSWORD: \"\"", + ImmutableMap.of("MYSQL_ROOT_PASSWORD", "") }, // dictionary format, value of variable contains colon sign diff --git a/plugins/plugin-docker/che-plugin-docker-machine/pom.xml b/plugins/plugin-docker/che-plugin-docker-machine/pom.xml index 0cf02bc0355..4f6e4a64201 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/pom.xml +++ b/plugins/plugin-docker/che-plugin-docker-machine/pom.xml @@ -165,8 +165,11 @@ **/DefaultServerEvaluationStrategyTest.java **/LocalDockerServerEvaluationStrategy.java **/LocalDockerServerEvaluationStrategyTest.java + **/LocalDockerCustomServerEvaluationStrategy.java + **/LocalDockerCustomServerEvaluationStrategyTest.java **/DockerInstanceRuntimeInfo.java **/DockerInstanceRuntimeInfoTest.java + **/ServerIdleDetector.java diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/BaseServerEvaluationStrategy.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/BaseServerEvaluationStrategy.java new file mode 100644 index 00000000000..73dee6634bb --- /dev/null +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/BaseServerEvaluationStrategy.java @@ -0,0 +1,517 @@ +/******************************************************************************* + * 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.docker.machine; + +import com.google.common.base.Strings; +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import org.eclipse.che.api.machine.server.model.impl.ServerConfImpl; +import org.eclipse.che.api.machine.server.model.impl.ServerImpl; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.plugin.docker.client.json.ContainerInfo; +import org.eclipse.che.plugin.docker.client.json.PortBinding; +import org.stringtemplate.v4.ST; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * Represents a server evaluation strategy for the configuration where the strategy can be customized through template properties. + * + * @author Florent Benoit + * @see ServerEvaluationStrategy + */ +public abstract class BaseServerEvaluationStrategy extends ServerEvaluationStrategy { + + /** + * Regexp to extract port (under the form 22/tcp or 4401/tcp, etc.) from label references + */ + public static final String LABEL_CHE_SERVER_REF_KEY = "^che:server:(.*):ref$"; + + /** + * Name of the property for getting the workspace ID. + */ + public static final String CHE_WORKSPACE_ID_PROPERTY = "CHE_WORKSPACE_ID="; + + /** + * Name of the property to get the machine name property + */ + public static final String CHE_MACHINE_NAME_PROPERTY = "CHE_MACHINE_NAME="; + + /** + * Name of the property to get the property that indicates if the machine is the dev machine + */ + public static final String CHE_IS_DEV_MACHINE_PROPERTY = "CHE_IS_DEV_MACHINE="; + + /** + * Prefix added in front of the generated name to build the workspaceId + */ + public static final String CHE_WORKSPACE_ID_PREFIX = "workspace"; + + + /** + * name of the macro that indicates if the machine is the dev machine + */ + public static final String IS_DEV_MACHINE_MACRO = "isDevMachine"; + + /** + * Used to store the address set by property {@code che.docker.ip}, if applicable. + */ + protected String cheDockerIp; + + /** + * Used to store the address set by property {@code che.docker.ip.external}. if applicable. + */ + protected String cheDockerIpExternal; + + /** + * The current port of che. + */ + private final String chePort; + + /** + * Secured or not ? (for example https vs http) + */ + private final String cheDockerCustomExternalProtocol; + + /** + * Template for external addresses. + */ + private String cheDockerCustomExternalTemplate; + + /** + * Option to enable the use of the container address, when searching for addresses. + */ + private boolean localDockerMode; + + + /** + * Option to tell if an exception should be thrown when the host defined in the `externalAddress` isn't known + * and cannot be converted into a valid IP. + */ + private boolean throwOnUnknownHost = true; + + /** + * Default constructor + */ + public BaseServerEvaluationStrategy(String cheDockerIp, + String cheDockerIpExternal, + String cheDockerCustomExternalTemplate, + String cheDockerCustomExternalProtocol, + String chePort) { + this(cheDockerIp, cheDockerIpExternal, cheDockerCustomExternalTemplate, cheDockerCustomExternalProtocol, chePort, false); + } + + /** + * Constructor to be called by derived strategies + */ + public BaseServerEvaluationStrategy(String cheDockerIp, + String cheDockerIpExternal, + String cheDockerCustomExternalTemplate, + String cheDockerCustomExternalProtocol, + String chePort, + boolean localDockerMode) { + this.cheDockerIp = cheDockerIp; + this.cheDockerIpExternal = cheDockerIpExternal; + this.chePort = chePort; + this.cheDockerCustomExternalTemplate = cheDockerCustomExternalTemplate; + this.cheDockerCustomExternalProtocol = cheDockerCustomExternalProtocol; + this.localDockerMode = localDockerMode; + } + + @Override + protected Map getInternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { + final String internalAddressContainer = containerInfo.getNetworkSettings().getIpAddress(); + + final String internalAddress; + + if (localDockerMode) { + internalAddress = !isNullOrEmpty(internalAddressContainer) ? + internalAddressContainer : + internalHost; + } else { + internalAddress = + cheDockerIp != null ? + cheDockerIp : + internalHost; + } + + boolean useExposedPorts = localDockerMode && internalAddress != internalHost; + + return getExposedPortsToAddressPorts(internalAddress, containerInfo.getNetworkSettings().getPorts(), useExposedPorts); + } + + + /** + * Override the host for all ports by using the external template. + */ + @Override + protected Map getExternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { + + // create Rendering evaluation + RenderingEvaluation renderingEvaluation = getOnlineRenderingEvaluation(containerInfo, internalHost); + + // get current ports + Map> ports = containerInfo.getNetworkSettings().getPorts(); + + if (isNullOrEmpty(cheDockerCustomExternalTemplate)) { + return getExposedPortsToAddressPorts(renderingEvaluation.getExternalAddress(), ports, false); + } + + return ports.keySet().stream() + .collect(Collectors.toMap(portKey -> portKey, + portKey -> renderingEvaluation.render(cheDockerCustomExternalTemplate, portKey))); + } + + + /** + * Constructs a map of {@link ServerImpl} from provided parameters, using selected strategy + * for evaluating addresses and ports. + * + *

Keys consist of port number and transport protocol (tcp or udp) separated by + * a forward slash (e.g. 8080/tcp) + * + * @param containerInfo + * the {@link ContainerInfo} describing the container. + * @param internalHost + * alternative hostname to use, if address cannot be obtained from containerInfo + * @param serverConfMap + * additional Map of {@link ServerConfImpl}. Configurations here override those found + * in containerInfo. + * @return a Map of the servers exposed by the container. + */ + public Map getServers(ContainerInfo containerInfo, + String internalHost, + Map serverConfMap) { + Map servers = super.getServers(containerInfo, internalHost, serverConfMap); + return servers.entrySet().stream().collect(Collectors.toMap(map -> map.getKey(), map -> updateServer(map.getValue()))); + } + + + /** + * Updates the protocol for the given server by using given protocol (like https) for http URLs. + * @param server the server to update + * @return updated server object + */ + protected ServerImpl updateServer(ServerImpl server) { + if (!Strings.isNullOrEmpty(cheDockerCustomExternalProtocol)) { + if ("http".equals(server.getProtocol())) { + server.setProtocol(cheDockerCustomExternalProtocol); + String url = server.getUrl(); + int length = "http".length(); + server.setUrl(cheDockerCustomExternalProtocol.concat(url.substring(length))); + } + } + return server; + } + + + /** + * Allow to get the rendering outside of the evaluation strategies. + * It is called online as in this case we have access to container info + */ + public RenderingEvaluation getOnlineRenderingEvaluation(ContainerInfo containerInfo, String internalHost) { + return new OnlineRenderingEvaluation(containerInfo).withInternalHost(internalHost); + } + + /** + * Allow to get the rendering outside of the evaluation strategies. + * It is called offline as without container info, user need to provide merge of container and images data + */ + public RenderingEvaluation getOfflineRenderingEvaluation(Map labels, Set exposedPorts, String[] env) { + return new OfflineRenderingEvaluation(labels, exposedPorts, env); + } + + /** + * Simple interface for performing the rendering for a given portby using the given template + * + * @author Florent Benoit + */ + public interface RenderingEvaluation { + /** + * Gets the template rendering for the given port and using the given template + * + * @param template + * which can include + * @param port + * the port for the mapping + * @return the rendering of the template + */ + String render(String template, String port); + + /** + * Gets default external address. + */ + String getExternalAddress(); + } + + /** + * Online implementation (using the container info) + */ + protected class OnlineRenderingEvaluation extends OfflineRenderingEvaluation implements RenderingEvaluation { + + private String gatewayAddressContainer; + private String internalHost; + + protected OnlineRenderingEvaluation(ContainerInfo containerInfo) { + super(containerInfo.getConfig().getLabels(), containerInfo.getConfig().getExposedPorts().keySet(), + containerInfo.getConfig().getEnv()); + this.gatewayAddressContainer = containerInfo.getNetworkSettings().getGateway(); + } + + protected OnlineRenderingEvaluation withInternalHost(String internalHost) { + this.internalHost = internalHost; + return this; + } + + @Override + public String getExternalAddress() { + if (localDockerMode) { + return cheDockerIpExternal != null ? + cheDockerIpExternal : + !isNullOrEmpty(gatewayAddressContainer) ? + gatewayAddressContainer : + this.internalHost; + } + + return cheDockerIpExternal != null ? + cheDockerIpExternal : + cheDockerIp != null ? + cheDockerIp : + !isNullOrEmpty(gatewayAddressContainer) ? + gatewayAddressContainer : + this.internalHost; + + } + } + + /** + * Offline implementation (container not yet created) + */ + protected class OfflineRenderingEvaluation extends DefaultRenderingEvaluation implements RenderingEvaluation { + + public OfflineRenderingEvaluation(Map labels, Set exposedPorts, String[] env) { + super(labels, exposedPorts, env); + } + } + + /** + * Inner class used to perform the rendering + */ + protected abstract class DefaultRenderingEvaluation implements RenderingEvaluation { + + /** + * Labels + */ + private Map labels; + + /** + * Ports + */ + private Set exposedPorts; + + /** + * Environment variables + */ + private final String[] env; + + /** + * Map with properties for all ports + */ + private Map globalPropertiesMap = new HashMap<>(); + + /** + * Mapping between a port and the server ref name + */ + private Map portsToRefName; + + /** + * Data initialized ? + */ + private boolean initialized; + + /** + * Default constructor. + */ + protected DefaultRenderingEvaluation(Map labels, Set exposedPorts, String[] env) { + this.labels = labels; + this.exposedPorts = exposedPorts; + this.env = env; + } + + /** + * Initialize data + */ + protected void init() { + this.initPortMapping(); + this.populateGlobalProperties(); + } + + /** + * Compute port mapping with server ref name + */ + protected void initPortMapping() { + // ok, so now we have a map of labels and a map of exposed ports + // need to extract the name of the ref (if defined in a label) or then pickup default name "Server--" + Pattern pattern = Pattern.compile(LABEL_CHE_SERVER_REF_KEY); + Map portsToKnownRefName = labels.entrySet().stream() + .filter(map -> pattern.matcher(map.getKey()).matches()) + .collect(Collectors.toMap(p -> { + Matcher matcher = pattern.matcher(p.getKey()); + matcher.matches(); + String val = matcher.group(1); + return val.contains("/") ? val : val.concat("/tcp"); + }, p -> p.getValue())); + + // add to this map only port without a known ref name + Map portsToUnkownRefName = + exposedPorts.stream().filter((port) -> !portsToKnownRefName.containsKey(port)) + .collect(Collectors.toMap(p -> p, p -> "server-" + p.replace('/', '-'))); + + // list of all ports with refName (known/unknown) + this.portsToRefName = new HashMap(portsToKnownRefName); + portsToRefName.putAll(portsToUnkownRefName); + } + + /** + * Gets default external address. + */ + public String getExternalAddress() { + return cheDockerIpExternal != null ? + cheDockerIpExternal : cheDockerIp; + } + + /** + * Populate the template properties + */ + protected void populateGlobalProperties() { + String externalAddress = getExternalAddress(); + String externalIP = getExternalIp(externalAddress); + globalPropertiesMap.put("internalIp", cheDockerIp); + globalPropertiesMap.put("externalAddress", externalAddress); + globalPropertiesMap.put("externalIP", externalIP); + globalPropertiesMap.put("workspaceId", getWorkspaceId()); + globalPropertiesMap.put("workspaceIdWithoutPrefix", getWorkspaceId().replaceFirst(CHE_WORKSPACE_ID_PREFIX,"")); + globalPropertiesMap.put("machineName", getMachineName()); + globalPropertiesMap.put("wildcardNipDomain", getWildcardNipDomain(externalAddress)); + globalPropertiesMap.put("wildcardXipDomain", getWildcardXipDomain(externalAddress)); + globalPropertiesMap.put("chePort", chePort); + globalPropertiesMap.put(IS_DEV_MACHINE_MACRO, getIsDevMachine()); + } + + /** + * Rendering + */ + @Override + public String render(String template, String port) { + if (!this.initialized) { + init(); + this.initialized = true; + } + ST stringTemplate = new ST(template); + globalPropertiesMap.forEach((key, value) -> stringTemplate.add(key, + IS_DEV_MACHINE_MACRO.equals(key) ? + Boolean.parseBoolean(value) + : value)); + stringTemplate.add("serverName", portsToRefName.get(port)); + return stringTemplate.render(); + } + + /** + * returns if the current machine is the dev machine + * + * @return true if the curent machine is the dev machine + */ + protected String getIsDevMachine() { + return Arrays.stream(env).filter(env -> env.startsWith(CHE_IS_DEV_MACHINE_PROPERTY)) + .map(s -> s.substring(CHE_IS_DEV_MACHINE_PROPERTY.length())) + .findFirst().get(); + } + + /** + * Gets the workspace ID from the config of the given container + * + * @return workspace ID + */ + protected String getWorkspaceId() { + return Arrays.stream(env).filter(env -> env.startsWith(CHE_WORKSPACE_ID_PROPERTY)) + .map(s -> s.substring(CHE_WORKSPACE_ID_PROPERTY.length())) + .findFirst().get(); + } + + /** + * Gets the workspace Machine Name from the config of the given container + * + * @return machine name of the workspace + */ + protected String getMachineName() { + return Arrays.stream(env).filter(env -> env.startsWith(CHE_MACHINE_NAME_PROPERTY)) + .map(s -> s.substring(CHE_MACHINE_NAME_PROPERTY.length())) + .findFirst().get(); + } + + /** + * Gets the IP address of the external address + * + * @return IP Address + */ + protected String getExternalIp(String externalAddress) { + try { + return InetAddress.getByName(externalAddress).getHostAddress(); + } catch (UnknownHostException e) { + if (throwOnUnknownHost) { + throw new UnsupportedOperationException("Unable to find the IP for the address '" + externalAddress + "'", e); + } + } + return null; + } + + /** + * Gets a Wildcard domain based on the ip using an external provider nip.io + * + * @return wildcard domain + */ + protected String getWildcardNipDomain(String externalAddress) { + return String.format("%s.%s", getExternalIp(externalAddress), "nip.io"); + } + + /** + * Gets a Wildcard domain based on the ip using an external provider xip.io + * + * @return wildcard domain + */ + protected String getWildcardXipDomain(String externalAddress) { + return String.format("%s.%s", getExternalIp(externalAddress), "xip.io"); + } + + } + + @Override + protected boolean useHttpsForExternalUrls() { + return "https".equals(cheDockerCustomExternalProtocol); + } + + public BaseServerEvaluationStrategy withThrowOnUnknownHost(boolean throwOnUnknownHost) { + this.throwOnUnknownHost = throwOnUnknownHost; + return this; + } +} diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategy.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategy.java index 07086fbb1cd..314a88c96b5 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategy.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategy.java @@ -10,29 +10,10 @@ *******************************************************************************/ package org.eclipse.che.plugin.docker.machine; -import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.name.Named; -import org.eclipse.che.api.machine.server.model.impl.ServerConfImpl; -import org.eclipse.che.api.machine.server.model.impl.ServerImpl; import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.plugin.docker.client.json.ContainerInfo; -import org.eclipse.che.plugin.docker.client.json.PortBinding; -import org.stringtemplate.v4.ST; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static com.google.common.base.Strings.isNullOrEmpty; /** * Represents a server evaluation strategy for the configuration where the strategy can be customized through template properties. @@ -40,39 +21,7 @@ * @author Florent Benoit * @see ServerEvaluationStrategy */ -public class CustomServerEvaluationStrategy extends DefaultServerEvaluationStrategy { - - /** - * Regexp to extract port (under the form 22/tcp or 4401/tcp, etc.) from label references - */ - public static final String LABEL_CHE_SERVER_REF_KEY = "^che:server:(.*):ref$"; - - /** - * Name of the property for getting the workspace ID. - */ - public static final String CHE_WORKSPACE_ID_PROPERTY = "CHE_WORKSPACE_ID="; - - /** - * Name of the property to get the machine name property - */ - public static final String CHE_MACHINE_NAME_PROPERTY = "CHE_MACHINE_NAME="; - - /** - * The current port of che. - */ - private final String chePort; - - /** - * Secured or not ? (for example https vs http) - */ - private final String cheDockerCustomExternalProtocol; - - /** - * Template for external addresses. - */ - private String cheDockerCustomExternalTemplate; - - +public class CustomServerEvaluationStrategy extends BaseServerEvaluationStrategy { /** * Default constructor */ @@ -82,317 +31,6 @@ public CustomServerEvaluationStrategy(@Nullable @Named("che.docker.ip") String c @Nullable @Named("che.docker.server_evaluation_strategy.custom.template") String cheDockerCustomExternalTemplate, @Nullable @Named("che.docker.server_evaluation_strategy.custom.external.protocol") String cheDockerCustomExternalProtocol, @Named("che.port") String chePort) { - super(cheDockerIp, cheDockerIpExternal); - this.chePort = chePort; - this.cheDockerCustomExternalTemplate = cheDockerCustomExternalTemplate; - this.cheDockerCustomExternalProtocol = cheDockerCustomExternalProtocol; - } - - /** - * Override the host for all ports by using the external template. - */ - @Override - protected Map getExternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { - - // create Rendering evaluation - RenderingEvaluation renderingEvaluation = getOnlineRenderingEvaluation(containerInfo, internalHost); - - // get current ports - Map> ports = containerInfo.getNetworkSettings().getPorts(); - - return ports.keySet().stream() - .collect(Collectors.toMap(portKey -> portKey, - portKey -> renderingEvaluation.render(cheDockerCustomExternalTemplate, portKey))); - } - - - /** - * Constructs a map of {@link ServerImpl} from provided parameters, using selected strategy - * for evaluating addresses and ports. - * - *

Keys consist of port number and transport protocol (tcp or udp) separated by - * a forward slash (e.g. 8080/tcp) - * - * @param containerInfo - * the {@link ContainerInfo} describing the container. - * @param internalHost - * alternative hostname to use, if address cannot be obtained from containerInfo - * @param serverConfMap - * additional Map of {@link ServerConfImpl}. Configurations here override those found - * in containerInfo. - * @return a Map of the servers exposed by the container. - */ - public Map getServers(ContainerInfo containerInfo, - String internalHost, - Map serverConfMap) { - Map servers = super.getServers(containerInfo, internalHost, serverConfMap); - return servers.entrySet().stream().collect(Collectors.toMap(map -> map.getKey(), map -> updateServer(map.getValue()))); - } - - - /** - * Updates the protocol for the given server by using given protocol (like https) for http URLs. - * @param server the server to update - * @return updated server object - */ - protected ServerImpl updateServer(ServerImpl server) { - if (!Strings.isNullOrEmpty(cheDockerCustomExternalProtocol)) { - if ("http".equals(server.getProtocol())) { - server.setProtocol(cheDockerCustomExternalProtocol); - String url = server.getUrl(); - int length = "http".length(); - server.setUrl(cheDockerCustomExternalProtocol.concat(url.substring(length))); - } - } - return server; - } - - - /** - * Allow to get the rendering outside of the evaluation strategies. - * It is called online as in this case we have access to container info - */ - public RenderingEvaluation getOnlineRenderingEvaluation(ContainerInfo containerInfo, String internalHost) { - return new OnlineRenderingEvaluation(containerInfo).withInternalHost(internalHost); - } - - /** - * Allow to get the rendering outside of the evaluation strategies. - * It is called offline as without container info, user need to provide merge of container and images data - */ - public RenderingEvaluation getOfflineRenderingEvaluation(Map labels, Set exposedPorts, String[] env) { - return new OfflineRenderingEvaluation(labels, exposedPorts, env); - } - - /** - * Simple interface for performing the rendering for a given portby using the given template - * - * @author Florent Benoit - */ - public interface RenderingEvaluation { - /** - * Gets the template rendering for the given port and using the given template - * - * @param template - * which can include - * @param port - * the port for the mapping - * @return the rendering of the template - */ - String render(String template, String port); + super(cheDockerIp, cheDockerIpExternal, cheDockerCustomExternalTemplate, cheDockerCustomExternalProtocol, chePort, false); } - - /** - * Online implementation (using the container info) - */ - protected class OnlineRenderingEvaluation extends OfflineRenderingEvaluation implements RenderingEvaluation { - - private String gatewayAddressContainer; - private String internalHost; - - protected OnlineRenderingEvaluation(ContainerInfo containerInfo) { - super(containerInfo.getConfig().getLabels(), containerInfo.getConfig().getExposedPorts().keySet(), - containerInfo.getConfig().getEnv()); - this.gatewayAddressContainer = containerInfo.getNetworkSettings().getGateway(); - } - - protected OnlineRenderingEvaluation withInternalHost(String internalHost) { - this.internalHost = internalHost; - return this; - } - - @Override - protected String getExternalAddress() { - return externalAddressProperty != null ? - externalAddressProperty : - internalAddressProperty != null ? - internalAddressProperty : - !isNullOrEmpty(gatewayAddressContainer) ? - gatewayAddressContainer : - this.internalHost; - } - } - - /** - * Offline implementation (container not yet created) - */ - protected class OfflineRenderingEvaluation extends DefaultRenderingEvaluation implements RenderingEvaluation { - - public OfflineRenderingEvaluation(Map labels, Set exposedPorts, String[] env) { - super(labels, exposedPorts, env); - } - } - - /** - * Inner class used to perform the rendering - */ - protected abstract class DefaultRenderingEvaluation implements RenderingEvaluation { - - /** - * Labels - */ - private Map labels; - - /** - * Ports - */ - private Set exposedPorts; - - /** - * Environment variables - */ - private final String[] env; - - /** - * Map with properties for all ports - */ - private Map globalPropertiesMap = new HashMap<>(); - - /** - * Mapping between a port and the server ref name - */ - private Map portsToRefName; - - /** - * Data initialized ? - */ - private boolean initialized; - - /** - * Default constructor. - */ - protected DefaultRenderingEvaluation(Map labels, Set exposedPorts, String[] env) { - this.labels = labels; - this.exposedPorts = exposedPorts; - this.env = env; - } - - /** - * Initialize data - */ - protected void init() { - this.initPortMapping(); - this.populateGlobalProperties(); - } - - /** - * Compute port mapping with server ref name - */ - protected void initPortMapping() { - // ok, so now we have a map of labels and a map of exposed ports - // need to extract the name of the ref (if defined in a label) or then pickup default name "Server--" - Pattern pattern = Pattern.compile(LABEL_CHE_SERVER_REF_KEY); - Map portsToKnownRefName = labels.entrySet().stream() - .filter(map -> pattern.matcher(map.getKey()).matches()) - .collect(Collectors.toMap(p -> { - Matcher matcher = pattern.matcher(p.getKey()); - matcher.matches(); - String val = matcher.group(1); - return val.contains("/") ? val : val.concat("/tcp"); - }, p -> p.getValue())); - - // add to this map only port without a known ref name - Map portsToUnkownRefName = - exposedPorts.stream().filter((port) -> !portsToKnownRefName.containsKey(port)) - .collect(Collectors.toMap(p -> p, p -> "Server-" + p.replace('/', '-'))); - - // list of all ports with refName (known/unknown) - this.portsToRefName = new HashMap(portsToKnownRefName); - portsToRefName.putAll(portsToUnkownRefName); - } - - /** - * Gets default external address. - */ - protected String getExternalAddress() { - return externalAddressProperty != null ? - externalAddressProperty : internalAddressProperty; - } - - /** - * Populate the template properties - */ - protected void populateGlobalProperties() { - String externalAddress = getExternalAddress(); - String externalIP = getExternalIp(externalAddress); - globalPropertiesMap.put("internalIp", internalAddressProperty); - globalPropertiesMap.put("externalAddress", externalAddress); - globalPropertiesMap.put("externalIP", externalIP); - globalPropertiesMap.put("workspaceId", getWorkspaceId()); - globalPropertiesMap.put("machineName", getMachineName()); - globalPropertiesMap.put("wildcardNipDomain", getWildcardNipDomain(externalAddress)); - globalPropertiesMap.put("wildcardXipDomain", getWildcardXipDomain(externalAddress)); - globalPropertiesMap.put("chePort", chePort); - } - - /** - * Rendering - */ - @Override - public String render(String template, String port) { - if (!this.initialized) { - init(); - this.initialized = true; - } - ST stringTemplate = new ST(template); - globalPropertiesMap.forEach((key, value) -> stringTemplate.add(key, value)); - stringTemplate.add("serverName", portsToRefName.get(port)); - return stringTemplate.render(); - } - - /** - * Gets the workspace ID from the config of the given container - * - * @return workspace ID - */ - protected String getWorkspaceId() { - return Arrays.stream(env).filter(env -> env.startsWith(CHE_WORKSPACE_ID_PROPERTY)) - .map(s -> s.substring(CHE_WORKSPACE_ID_PROPERTY.length())) - .findFirst().get(); - } - - /** - * Gets the workspace Machine Name from the config of the given container - * - * @return machine name of the workspace - */ - protected String getMachineName() { - return Arrays.stream(env).filter(env -> env.startsWith(CHE_MACHINE_NAME_PROPERTY)) - .map(s -> s.substring(CHE_MACHINE_NAME_PROPERTY.length())) - .findFirst().get(); - } - - /** - * Gets the IP address of the external address - * - * @return IP Address - */ - protected String getExternalIp(String externalAddress) { - try { - return InetAddress.getByName(externalAddress).getHostAddress(); - } catch (UnknownHostException e) { - throw new UnsupportedOperationException("Unable to find the IP for the address '" + externalAddress + "'", e); - } - } - - /** - * Gets a Wildcard domain based on the ip using an external provider nip.io - * - * @return wildcard domain - */ - protected String getWildcardNipDomain(String externalAddress) { - return String.format("%s.%s", getExternalIp(externalAddress), "nip.io"); - } - - /** - * Gets a Wildcard domain based on the ip using an external provider xip.io - * - * @return wildcard domain - */ - protected String getWildcardXipDomain(String externalAddress) { - return String.format("%s.%s", getExternalIp(externalAddress), "xip.io"); - } - - } - } diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DefaultServerEvaluationStrategy.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DefaultServerEvaluationStrategy.java index 1f5eba9e94e..2998f72d26c 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DefaultServerEvaluationStrategy.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DefaultServerEvaluationStrategy.java @@ -31,42 +31,11 @@ * @author Alexander Garagatyi * @see ServerEvaluationStrategy */ -public class DefaultServerEvaluationStrategy extends ServerEvaluationStrategy { - - /** - * Used to store the address set by property {@code che.docker.ip}, if applicable. - */ - protected String internalAddressProperty; - - /** - * Used to store the address set by property {@code che.docker.ip.external}. if applicable. - */ - protected String externalAddressProperty; +public class DefaultServerEvaluationStrategy extends BaseServerEvaluationStrategy { @Inject public DefaultServerEvaluationStrategy(@Nullable @Named("che.docker.ip") String internalAddress, @Nullable @Named("che.docker.ip.external") String externalAddress) { - this.internalAddressProperty = internalAddress; - this.externalAddressProperty = externalAddress; - } - - @Override - protected Map getInternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { - String internalAddress = internalAddressProperty != null ? - internalAddressProperty : - internalHost; - - return getExposedPortsToAddressPorts(internalAddress, containerInfo.getNetworkSettings().getPorts()); - } - - @Override - protected Map getExternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { - String externalAddress = externalAddressProperty != null ? - externalAddressProperty : - internalAddressProperty != null ? - internalAddressProperty : - internalHost; - - return super.getExposedPortsToAddressPorts(externalAddress, containerInfo.getNetworkSettings().getPorts()); + super(internalAddress, externalAddress, null, null, null, false); } } diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerInstanceRuntimeInfo.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerInstanceRuntimeInfo.java index 0cb32f70ac9..bb1ad218318 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerInstanceRuntimeInfo.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerInstanceRuntimeInfo.java @@ -68,6 +68,11 @@ public class DockerInstanceRuntimeInfo implements MachineRuntimeInfo { */ public static final String CHE_MACHINE_NAME = "CHE_MACHINE_NAME"; + /** + * Environment variable that will contain Name of the machine + */ + public static final String CHE_IS_DEV_MACHINE = "CHE_IS_DEV_MACHINE"; + /** * Default HOSTNAME that will be added in all docker containers that are started. This host will container the Docker host's ip * reachable inside the container. diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerMachineModule.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerMachineModule.java index c5307e3baf3..dfedab5e847 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerMachineModule.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/DockerMachineModule.java @@ -33,6 +33,7 @@ public class DockerMachineModule extends AbstractModule { protected void configure() { bind(org.eclipse.che.plugin.docker.machine.cleaner.DockerAbandonedResourcesCleaner.class); bind(org.eclipse.che.plugin.docker.machine.cleaner.RemoveWorkspaceFilesAfterRemoveWorkspaceEventSubscriber.class); + bind(org.eclipse.che.plugin.docker.machine.idle.ServerIdleDetector.class); @SuppressWarnings("unused") Multibinder devMachineEnvVars = Multibinder.newSetBinder(binder(), diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerCustomServerEvaluationStrategy.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerCustomServerEvaluationStrategy.java new file mode 100644 index 00000000000..22330d416e0 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerCustomServerEvaluationStrategy.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2016-2017 Red Hat Inc. + * 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: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.docker.machine; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import org.eclipse.che.api.machine.server.model.impl.ServerImpl; +import org.eclipse.che.commons.annotation.Nullable; + + +/** + * Represents a server evaluation strategy for the configuration where the workspace server and workspace + * containers are running on the same Docker network and are exposed through the same single port. + * + * This server evaluation strategy will return a completed {@link ServerImpl} with internal addresses set + * as {@link LocalDockerServerEvaluationStrategy} does. Contrary external addresses will be managed by the + * `custom` evaluation strategy,and its template property `che.docker.server_evaluation_strategy.custom.template` + * + *

cheExternalAddress can be set using property {@code che.docker.ip.external}. + * This strategy is useful when Che and the workspace servers need to be exposed on the same single TCP port + * + * @author Mario Loriedo + * @see ServerEvaluationStrategy + */ +public class LocalDockerCustomServerEvaluationStrategy extends BaseServerEvaluationStrategy { + + @Inject + public LocalDockerCustomServerEvaluationStrategy(@Nullable @Named("che.docker.ip") String internalAddress, + @Nullable @Named("che.docker.ip.external") String externalAddress, + @Nullable @Named("che.docker.server_evaluation_strategy.custom.template") String cheDockerCustomExternalTemplate, + @Nullable @Named("che.docker.server_evaluation_strategy.custom.external.protocol") String cheDockerCustomExternalProtocol, + @Named("che.port") String chePort) { + super(internalAddress, externalAddress, cheDockerCustomExternalTemplate, cheDockerCustomExternalProtocol, chePort, true); + } +} diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerServerEvaluationStrategy.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerServerEvaluationStrategy.java index e89e0d64394..ef37c21eada 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerServerEvaluationStrategy.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/LocalDockerServerEvaluationStrategy.java @@ -16,14 +16,9 @@ import org.eclipse.che.api.machine.server.model.impl.ServerImpl; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.plugin.docker.client.json.ContainerInfo; -import org.eclipse.che.plugin.docker.client.json.PortBinding; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import static com.google.common.base.Strings.isNullOrEmpty; - /** * Represents a server evaluation strategy for the configuration where the workspace server and * workspace containers are running on the same Docker network. Calling @@ -36,63 +31,11 @@ * @author Angel Misevski * @see ServerEvaluationStrategy */ -public class LocalDockerServerEvaluationStrategy extends ServerEvaluationStrategy { - - /** - * Used to store the address set by property {@code che.docker.ip}, if applicable. - */ - protected String internalAddressProperty; - - /** - * Used to store the address set by property {@code che.docker.ip.external}. if applicable. - */ - protected String externalAddressProperty; +public class LocalDockerServerEvaluationStrategy extends BaseServerEvaluationStrategy { @Inject public LocalDockerServerEvaluationStrategy(@Nullable @Named("che.docker.ip") String internalAddress, @Nullable @Named("che.docker.ip.external") String externalAddress) { - this.internalAddressProperty = internalAddress; - this.externalAddressProperty = externalAddress; - } - - @Override - protected Map getInternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { - String internalAddressContainer = containerInfo.getNetworkSettings().getIpAddress(); - - String internalAddress; - boolean useExposedPorts = true; - if (!isNullOrEmpty(internalAddressContainer)) { - internalAddress = internalAddressContainer; - } else { - internalAddress = internalHost; - useExposedPorts = false; - } - - Map> portBindings = containerInfo.getNetworkSettings().getPorts(); - - Map addressesAndPorts = new HashMap<>(); - for (Map.Entry> portEntry : portBindings.entrySet()) { - String exposedPort = portEntry.getKey().split("/")[0]; - String ephemeralPort = portEntry.getValue().get(0).getHostPort(); - if (useExposedPorts) { - addressesAndPorts.put(portEntry.getKey(), internalAddress + ":" + exposedPort); - } else { - addressesAndPorts.put(portEntry.getKey(), internalAddress + ":" + ephemeralPort); - } - } - return addressesAndPorts; - } - - @Override - protected Map getExternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { - String externalAddressContainer = containerInfo.getNetworkSettings().getGateway(); - - String externalAddress = externalAddressProperty != null ? - externalAddressProperty : - !isNullOrEmpty(externalAddressContainer) ? - externalAddressContainer : - internalHost; - - return getExposedPortsToAddressPorts(externalAddress, containerInfo.getNetworkSettings().getPorts()); + super(internalAddress, externalAddress, null, null, null, true); } } diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/MachineProviderImpl.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/MachineProviderImpl.java index 05e79e58312..0e36ebfc6d3 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/MachineProviderImpl.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/MachineProviderImpl.java @@ -666,6 +666,7 @@ private void addSystemWideContainerSettings(String workspaceId, // register workspace ID and Machine Name env.put(DockerInstanceRuntimeInfo.CHE_WORKSPACE_ID, workspaceId); env.put(DockerInstanceRuntimeInfo.CHE_MACHINE_NAME, machineName); + env.put(DockerInstanceRuntimeInfo.CHE_IS_DEV_MACHINE, Boolean.toString(isDev)); composeService.getExpose().addAll(portsToExpose); composeService.getEnvironment().putAll(env); diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategy.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategy.java index e244ff4caef..8f2147638b4 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategy.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategy.java @@ -10,6 +10,12 @@ *******************************************************************************/ package org.eclipse.che.plugin.docker.machine; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import org.eclipse.che.api.core.model.machine.ServerProperties; import org.eclipse.che.api.machine.server.model.impl.ServerConfImpl; import org.eclipse.che.api.machine.server.model.impl.ServerImpl; @@ -17,12 +23,6 @@ import org.eclipse.che.plugin.docker.client.json.ContainerInfo; import org.eclipse.che.plugin.docker.client.json.PortBinding; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - /** * Represents a strategy for resolving Servers associated with workspace containers. * Used to extract relevant information from e.g. {@link ContainerInfo} into a map of @@ -30,14 +30,26 @@ * * @author Angel Misevski * @author Alexander Garagatyi + * @author Ilya Buziuk * @see ServerEvaluationStrategyProvider */ public abstract class ServerEvaluationStrategy { + private static final String HTTP = "http"; + private static final String HTTPS = "https"; protected static final String SERVER_CONF_LABEL_REF_KEY = "che:server:%s:ref"; protected static final String SERVER_CONF_LABEL_PROTOCOL_KEY = "che:server:%s:protocol"; protected static final String SERVER_CONF_LABEL_PATH_KEY = "che:server:%s:path"; + + /** + * @return true if external addresses need to be exposed against https, false otherwise + */ + protected boolean useHttpsForExternalUrls() { + return false; + } + + /** * Gets a map of all internal addresses exposed by the container in the form of * {@code "

:"} @@ -112,7 +124,11 @@ public Map getServers(ContainerInfo containerInfo, // Add protocol and path to internal/external address, if applicable String internalUrl = null; String externalUrl = null; - if (serverConf.getProtocol() != null) { + + String internalProtocol = serverConf.getProtocol(); + String externalProtocol = getProtocolForExternalUrl(internalProtocol); + + if (internalProtocol != null) { String pathSuffix = serverConf.getPath(); if (pathSuffix != null && !pathSuffix.isEmpty()) { if (pathSuffix.charAt(0) != '/') { @@ -121,8 +137,9 @@ public Map getServers(ContainerInfo containerInfo, } else { pathSuffix = ""; } - internalUrl = serverConf.getProtocol() + "://" + internalAddressAndPort + pathSuffix; - externalUrl = serverConf.getProtocol() + "://" + externalAddressAndPort + pathSuffix; + + internalUrl = internalProtocol + "://" + internalAddressAndPort + pathSuffix; + externalUrl = externalProtocol + "://" + externalAddressAndPort + pathSuffix; } ServerProperties properties = new ServerPropertiesImpl(serverConf.getPath(), @@ -130,7 +147,7 @@ public Map getServers(ContainerInfo containerInfo, internalUrl); servers.put(portProtocol, new ServerImpl(serverConf.getRef(), - serverConf.getProtocol(), + externalProtocol, externalAddressAndPort, externalUrl, properties)); @@ -156,7 +173,7 @@ public Map getServers(ContainerInfo containerInfo, * @return {@code ServerConfImpl}, obtained from {@code serverConfMap} if possible, * or from {@code labels} if there is no entry in {@code serverConfMap}. */ - private ServerConfImpl getServerConfImpl(String portProtocol, + protected ServerConfImpl getServerConfImpl(String portProtocol, Map labels, Map serverConfMap) { // Label can be specified without protocol -- e.g. 4401 refers to 4401/tcp @@ -226,14 +243,37 @@ private ServerConfImpl getServerConfImpl(String portProtocol, * "9090/udp" : "my-host.com:32722" * } * } + * */ - protected Map getExposedPortsToAddressPorts(String address, Map> ports) { + protected Map getExposedPortsToAddressPorts(String address, Map> ports, boolean useExposedPorts) { Map addressesAndPorts = new HashMap<>(); for (Map.Entry> portEntry : ports.entrySet()) { + String exposedPort = portEntry.getKey().split("/")[0]; // there is one value always - String port = portEntry.getValue().get(0).getHostPort(); - addressesAndPorts.put(portEntry.getKey(), address + ":" + port); + String ephemeralPort = portEntry.getValue().get(0).getHostPort(); + if (useExposedPorts) { + addressesAndPorts.put(portEntry.getKey(), address + ":" + exposedPort); + } else { + addressesAndPorts.put(portEntry.getKey(), address + ":" + ephemeralPort); + } } return addressesAndPorts; } + + protected Map getExposedPortsToAddressPorts(String address, Map> ports) { + return getExposedPortsToAddressPorts(address, ports, false); + } + + + /** + * @param protocolForInternalUrl + * @return https, if {@link #useHttpsForExternalUrls()} method in sub-class returns true and protocol for internal Url is http + */ + private String getProtocolForExternalUrl(final String protocolForInternalUrl) { + if (useHttpsForExternalUrls() && HTTP.equals(protocolForInternalUrl)) { + return HTTPS; + } + return protocolForInternalUrl; + } + } diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/idle/ServerIdleDetector.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/idle/ServerIdleDetector.java new file mode 100644 index 00000000000..06948492965 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/idle/ServerIdleDetector.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.docker.machine.idle; + +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.eclipse.che.api.core.event.ServerIdleEvent; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.core.notification.EventSubscriber; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +/** + * Notifies about idling the che server + * Fires {@link org.eclipse.che.api.core.event.ServerIdleEvent} if no workspace + * is run for che.openshift.server.inactive.stop.timeout.ms milliseconds + */ +@Singleton +public class ServerIdleDetector implements EventSubscriber { + private static final Logger LOG = LoggerFactory.getLogger(ServerIdleDetector.class); + private static final String IDLING_CHE_SERVER_SCHEDULED = "Idling che server scheduled [timeout=%s] seconds]"; + + private final long timeout; + private ScheduledFuture future; + private ScheduledExecutorService executor; + private WorkspaceManager workspaceManager; + private final EventService eventService; + + @Inject + public ServerIdleDetector(WorkspaceManager workspaceManager, + EventService eventService, + @Named("che.openshift.server.inactive.stop.timeout.ms") long timeout) { + this.timeout = timeout; + this.eventService = eventService; + this.workspaceManager = workspaceManager; + if (timeout > 0) { + this.executor = Executors.newSingleThreadScheduledExecutor(); + this.future = executor.schedule(this::run, timeout, TimeUnit.MILLISECONDS); + LOG.info(String.format(IDLING_CHE_SERVER_SCHEDULED, timeout/1000)); + } + } + + @Override + public void onEvent(WorkspaceStatusEvent event) { + if (future != null) { + String workspaceId = event.getWorkspaceId(); + switch (event.getEventType()) { + case RUNNING: + if (!future.isCancelled()) { + future.cancel(true); + LOG.info("Idling che server canceled"); + } + break; + case STOPPED: + Set ids = workspaceManager.getRunningWorkspacesIds(); + ids.remove(workspaceId); + if (ids.size() <= 0) { + if (!future.isCancelled()) { + future.cancel(true); + } + future = executor.schedule(this::run, timeout, TimeUnit.MILLISECONDS); + LOG.info(String.format(IDLING_CHE_SERVER_SCHEDULED, timeout/1000)); + } + break; + default: + break; + } + } + } + + private void run() { + Set ids = workspaceManager.getRunningWorkspacesIds(); + if (ids.size() <= 0) { + eventService.publish(new ServerIdleEvent(timeout)); + } + } + + @PostConstruct + private void subscribe() { + eventService.subscribe(this); + } + + @PreDestroy + private void unsubscribe() { + eventService.unsubscribe(this); + if (future != null && !future.isCancelled()) { + future.cancel(true); + } + if (executor != null && !executor.isShutdown()) { + executor.shutdown(); + } + } + +} diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalDockerModule.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalDockerModule.java index c3d0cab74a2..684b4d92763 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalDockerModule.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalDockerModule.java @@ -23,6 +23,7 @@ import org.eclipse.che.plugin.docker.machine.DockerInstance; import org.eclipse.che.plugin.docker.machine.DockerInstanceRuntimeInfo; import org.eclipse.che.plugin.docker.machine.DockerProcess; +import org.eclipse.che.plugin.docker.machine.LocalDockerCustomServerEvaluationStrategy; import org.eclipse.che.plugin.docker.machine.ServerEvaluationStrategy; import org.eclipse.che.plugin.docker.machine.node.DockerNode; import org.eclipse.che.plugin.openshift.client.OpenShiftConnector; @@ -56,6 +57,8 @@ protected void configure() { .to(org.eclipse.che.plugin.docker.machine.DefaultServerEvaluationStrategy.class); strategies.addBinding("docker-local") .to(org.eclipse.che.plugin.docker.machine.LocalDockerServerEvaluationStrategy.class); + strategies.addBinding("docker-local-custom") + .to(LocalDockerCustomServerEvaluationStrategy.class); strategies.addBinding("custom") .to(org.eclipse.che.plugin.docker.machine.CustomServerEvaluationStrategy.class); diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategyTest.java b/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategyTest.java index 1e8fc9aef07..4200cb14c20 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategyTest.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/CustomServerEvaluationStrategyTest.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Set; +import static org.eclipse.che.plugin.docker.machine.CustomServerEvaluationStrategy.CHE_WORKSPACE_ID_PREFIX; import static org.mockito.Mockito.when; /** @@ -43,11 +44,19 @@ public class CustomServerEvaluationStrategyTest { private static final String ALL_IP_ADDRESS = "0.0.0.0"; - private static final String WORKSPACE_ID_VALUE = "work123"; - private static final String WORKSPACE_ID_PROPERTY = "CHE_WORKSPACE_ID=" + WORKSPACE_ID_VALUE; + private static final String WORKSPACE_ID_WITHOUT_PREFIX_VALUE = "ABCDEFG"; + + private static final String WORKSPACE_ID_VALUE = CHE_WORKSPACE_ID_PREFIX + WORKSPACE_ID_WITHOUT_PREFIX_VALUE; + private static final String WORKSPACE_ID_PROPERTY_PREFIX = "CHE_WORKSPACE_ID="; + private static final String WORKSPACE_ID_PROPERTY = WORKSPACE_ID_PROPERTY_PREFIX + WORKSPACE_ID_VALUE; private static final String MACHINE_NAME_VALUE = "myMachine"; - private static final String MACHINE_NAME_PROPERTY = "CHE_MACHINE_NAME=" + MACHINE_NAME_VALUE; + private static final String MACHINE_NAME_PROPERTY_PREFIX = "CHE_MACHINE_NAME="; + private static final String MACHINE_NAME_PROPERTY = MACHINE_NAME_PROPERTY_PREFIX + MACHINE_NAME_VALUE; + + private static final String IS_DEV_MACHINE_PROPERTY_PREFIX = "CHE_IS_DEV_MACHINE="; + private static final String IS_DEV_MACHINE_PROPERTY_TRUE = IS_DEV_MACHINE_PROPERTY_PREFIX + "true"; + private static final String IS_DEV_MACHINE_PROPERTY_FALSE = IS_DEV_MACHINE_PROPERTY_PREFIX + "false"; @Mock private ContainerConfig containerConfig; @@ -76,7 +85,7 @@ protected void setup() throws Exception { when(containerConfig.getLabels()).thenReturn(containerLabels); when(containerConfig.getExposedPorts()).thenReturn(containerExposedPorts); - envContainerConfig = new String[]{WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY}; + envContainerConfig = new String[]{WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY, IS_DEV_MACHINE_PROPERTY_TRUE}; when(containerConfig.getEnv()).thenReturn(envContainerConfig); when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); @@ -126,6 +135,52 @@ public void testWorkspaceIdRule() throws Throwable { Assert.assertEquals(portMapping.get("4401/tcp"), WORKSPACE_ID_VALUE); } + /** + * Check workspace Id without prefix template + */ + @Test + public void testWorkspaceIdWithoutPrefixRule() throws Throwable { + this.customServerEvaluationStrategy = + new CustomServerEvaluationStrategy("10.0.0.1", "192.168.1.1", "", "http", "8080"); + + Map portMapping = this.customServerEvaluationStrategy.getExternalAddressesAndPorts(containerInfo, "localhost"); + + Assert.assertTrue(portMapping.containsKey("4401/tcp")); + Assert.assertEquals(portMapping.get("4401/tcp"), WORKSPACE_ID_WITHOUT_PREFIX_VALUE); + } + + /** + * Check the isDevMachine macro in template + */ + @Test + public void testIsDevMachineWhenTrue() throws Throwable { + this.customServerEvaluationStrategy = + new CustomServerEvaluationStrategy("10.0.0.1", "192.168.1.1", + "", "http", "8080"); + + Map portMapping = this.customServerEvaluationStrategy.getExternalAddressesAndPorts(containerInfo, "localhost"); + + Assert.assertTrue(portMapping.containsKey("4401/tcp")); + Assert.assertEquals(portMapping.get("4401/tcp"), WORKSPACE_ID_VALUE); + } + + /** + * Check the isDevMachine macro in template + */ + @Test + public void testIsDevMachineWhenFalse() throws Throwable { + this.envContainerConfig = new String[]{WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY, IS_DEV_MACHINE_PROPERTY_FALSE}; + when(containerConfig.getEnv()).thenReturn(envContainerConfig); + + this.customServerEvaluationStrategy = + new CustomServerEvaluationStrategy("10.0.0.1", "192.168.1.1", + "", "http", "8080"); + + Map portMapping = this.customServerEvaluationStrategy.getExternalAddressesAndPorts(containerInfo, "localhost"); + + Assert.assertTrue(portMapping.containsKey("4401/tcp")); + Assert.assertEquals(portMapping.get("4401/tcp"), MACHINE_NAME_VALUE); + } /** * Check workspace Id template @@ -213,7 +268,7 @@ public void testOffline() throws Throwable { exposedPorts.add("4401/tcp"); exposedPorts.add("4411/tcp"); exposedPorts.add("8080/tcp"); - List env = Arrays.asList(WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY); + List env = Arrays.asList(WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY, IS_DEV_MACHINE_PROPERTY_TRUE); this.customServerEvaluationStrategy = new CustomServerEvaluationStrategy("127.0.0.1", null, "-", "https", "8080"); CustomServerEvaluationStrategy.RenderingEvaluation renderingEvaluation = this.customServerEvaluationStrategy @@ -233,7 +288,7 @@ public void testOfflineExternal() throws Throwable { exposedPorts.add("4401/tcp"); exposedPorts.add("4411/tcp"); exposedPorts.add("8080/tcp"); - List env = Arrays.asList(WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY); + List env = Arrays.asList(WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY, IS_DEV_MACHINE_PROPERTY_TRUE); this.customServerEvaluationStrategy = new CustomServerEvaluationStrategy("127.0.0.1", "127.0.0.1", "-", "https", "8080"); CustomServerEvaluationStrategy.RenderingEvaluation renderingEvaluation = this.customServerEvaluationStrategy @@ -253,7 +308,7 @@ public void testOfflineInvalidExternal() throws Throwable { exposedPorts.add("4401/tcp"); exposedPorts.add("4411/tcp"); exposedPorts.add("8080/tcp"); - List env = Arrays.asList(WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY); + List env = Arrays.asList(WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY, IS_DEV_MACHINE_PROPERTY_TRUE); this.customServerEvaluationStrategy = new CustomServerEvaluationStrategy("127.0.0.1", "300.300.300.300", "-", "https", "8080"); CustomServerEvaluationStrategy.RenderingEvaluation renderingEvaluation = this.customServerEvaluationStrategy diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/LocalDockerCustomServerEvaluationStrategyTest.java b/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/LocalDockerCustomServerEvaluationStrategyTest.java new file mode 100644 index 00000000000..7d297c4bf79 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/LocalDockerCustomServerEvaluationStrategyTest.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (c) 2016-2017 Red Hat Inc. + * 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: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.docker.machine; + +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.che.api.machine.server.model.impl.ServerConfImpl; +import org.eclipse.che.api.machine.server.model.impl.ServerImpl; +import org.eclipse.che.api.machine.server.model.impl.ServerPropertiesImpl; +import org.eclipse.che.plugin.docker.client.json.ContainerConfig; +import org.eclipse.che.plugin.docker.client.json.ContainerInfo; +import org.eclipse.che.plugin.docker.client.json.NetworkSettings; +import org.eclipse.che.plugin.docker.client.json.PortBinding; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class LocalDockerCustomServerEvaluationStrategyTest { + + private static final String CHE_DOCKER_IP_EXTERNAL = "container-host-ext.com"; + private static final String ALL_IP_ADDRESS = "0.0.0.0"; + private static final String CONTAINERCONFIG_HOSTNAME = "che-ws-y6jwknht0efzczit-4086112300-fm0aj"; + private static final String WORKSPACE_ID = "79rfwhqaztq2ru2k"; + + private static final String WORKSPACE_ID_VALUE = WORKSPACE_ID; + private static final String WORKSPACE_ID_PROPERTY = "CHE_WORKSPACE_ID=" + WORKSPACE_ID_VALUE; + + private static final String MACHINE_NAME_VALUE = "myMachine"; + private static final String MACHINE_NAME_PROPERTY = "CHE_MACHINE_NAME=" + MACHINE_NAME_VALUE; + + private static final String IS_DEV_MACHINE_VALUE = "true"; + private static final String IS_DEV_MACHINE_PROPERTY = "CHE_IS_DEV_MACHINE=" + IS_DEV_MACHINE_VALUE; + + private static final String CHE_DOCKER_SERVER_EVALUATION_STRATEGY_CUSTOM_TEMPLATE = "--"; + + @Mock + private ContainerInfo containerInfo; + @Mock + private ContainerConfig containerConfig; + @Mock + private NetworkSettings networkSettings; + + private ServerEvaluationStrategy strategy; + + private Map serverConfs; + + private Map> ports; + + private Map labels; + + private String[] env; + + private String[] envContainerConfig; + + + @BeforeMethod + public void setUp() { + + serverConfs = new HashMap<>(); + serverConfs.put("4301/tcp", new ServerConfImpl("sysServer1-tcp", "4301/tcp", "http", "/some/path1")); + serverConfs.put("4305/udp", new ServerConfImpl("devSysServer1-udp", "4305/udp", null, "some/path4")); + + ports = new HashMap<>(); + ports.put("4301/tcp", Collections.singletonList(new PortBinding().withHostIp(ALL_IP_ADDRESS ) + .withHostPort("32100"))); + ports.put("4305/udp", Collections.singletonList(new PortBinding().withHostIp(ALL_IP_ADDRESS ) + .withHostPort("32103"))); + + labels = new HashMap<>(); + labels.put("che:server:4301/tcp:ref", "sysServer1-tcp"); + labels.put("che:server:4305/udp:ref", "devSysServer1-udp"); + + env = new String[]{"CHE_WORKSPACE_ID="+ WORKSPACE_ID}; + + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(networkSettings.getIpAddress()).thenReturn(CONTAINERCONFIG_HOSTNAME); + when(networkSettings.getPorts()).thenReturn(ports); + when(containerInfo.getConfig()).thenReturn(containerConfig); + when(containerConfig.getHostname()).thenReturn(CONTAINERCONFIG_HOSTNAME); + when(containerConfig.getEnv()).thenReturn(env); + when(containerConfig.getLabels()).thenReturn(labels); + + envContainerConfig = new String[]{WORKSPACE_ID_PROPERTY, MACHINE_NAME_PROPERTY, IS_DEV_MACHINE_PROPERTY}; + when(containerConfig.getEnv()).thenReturn(envContainerConfig); + + } + + /** + * Test: single port strategy should use . + * @throws Exception + */ + @Test + public void shouldUseServerRefToBuildAddressWhenAvailable() throws Exception { + // given + strategy = new LocalDockerCustomServerEvaluationStrategy(null, null, CHE_DOCKER_SERVER_EVALUATION_STRATEGY_CUSTOM_TEMPLATE, "http", null).withThrowOnUnknownHost(false); + + final Map expectedServers = getExpectedServers(CHE_DOCKER_IP_EXTERNAL, + CONTAINERCONFIG_HOSTNAME, + true); + + // when + final Map servers = strategy.getServers(containerInfo, + CHE_DOCKER_IP_EXTERNAL, + serverConfs); + + // then + assertEquals(servers, expectedServers); + } + + private Map getExpectedServers(String externalAddress, + String internalAddress, + boolean useExposedPorts) { + String port1; + String port2; + if (useExposedPorts) { + port1 = ":4301"; + port2 = ":4305"; + } else { + port1 = ":32100"; + port2 = ":32103"; + } + Map expectedServers = new HashMap<>(); + expectedServers.put("4301/tcp", new ServerImpl("sysServer1-tcp", + "http", + "sysServer1-tcp-" + WORKSPACE_ID + "-" + externalAddress, + "http://" + "sysServer1-tcp-" + WORKSPACE_ID + "-" + externalAddress + "/some/path1", + new ServerPropertiesImpl("/some/path1", + internalAddress + port1, + "http://" + internalAddress + port1 + "/some/path1"))); + expectedServers.put("4305/udp", new ServerImpl("devSysServer1-udp", + null, + "devSysServer1-udp-" + WORKSPACE_ID + "-" + externalAddress, + null, + new ServerPropertiesImpl("some/path4", + internalAddress + port2, + null))); + return expectedServers; + } + +} diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategyTest.java b/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategyTest.java index af22597eb4b..f9bf96e14b7 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategyTest.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/test/java/org/eclipse/che/plugin/docker/machine/ServerEvaluationStrategyTest.java @@ -83,7 +83,7 @@ public void shouldConvertAddressAndExposedPortsInMapOfExposedPortToAddressPort() expected.put("9090/udp", DEFAULT_HOSTNAME + ":" + "32101"); // when - Map actual = strategy.getExposedPortsToAddressPorts(DEFAULT_HOSTNAME, ports); + Map actual = strategy.getExposedPortsToAddressPorts(DEFAULT_HOSTNAME, ports, false); // then assertEquals(actual, expected); @@ -108,7 +108,7 @@ public void shouldIgnoreMultiplePortBindingEntries() throws Exception { expected.put("9090/udp", DEFAULT_HOSTNAME + ":" + "32101"); // when - Map actual = strategy.getExposedPortsToAddressPorts(DEFAULT_HOSTNAME, ports); + Map actual = strategy.getExposedPortsToAddressPorts(DEFAULT_HOSTNAME, ports, false); // then assertEquals(actual, expected); @@ -375,7 +375,7 @@ private Map> prepareStrategyAndContainerInfoMocks() { .withHostPort("32101"))); when(networkSettings.getPorts()).thenReturn(ports); Map exposedPortsToAddressPorts = - strategy.getExposedPortsToAddressPorts(DEFAULT_HOSTNAME, ports); + strategy.getExposedPortsToAddressPorts(DEFAULT_HOSTNAME, ports, false); when(strategy.getExternalAddressesAndPorts(containerInfo, DEFAULT_HOSTNAME)) .thenReturn(exposedPortsToAddressPorts); when(strategy.getInternalAddressesAndPorts(containerInfo, DEFAULT_HOSTNAME)) @@ -396,5 +396,10 @@ protected Map getExternalAddressesAndPorts(ContainerInfo contain String internalAddress) { return null; } + + @Override + protected boolean useHttpsForExternalUrls() { + return false; + } } } diff --git a/plugins/plugin-docker/che-plugin-openshift-client/pom.xml b/plugins/plugin-docker/che-plugin-openshift-client/pom.xml index 80111272a75..29ea7465c0d 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/pom.xml +++ b/plugins/plugin-docker/che-plugin-openshift-client/pom.xml @@ -55,6 +55,22 @@ javax.inject javax.inject + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-model + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-commons-annotations + org.eclipse.che.plugin che-plugin-docker-client diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnector.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnector.java index 42c80507c30..d4e1b2acb74 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnector.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnector.java @@ -11,16 +11,29 @@ package org.eclipse.che.plugin.openshift.client; +import static com.google.common.base.Strings.isNullOrEmpty; + +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLEncoder; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -28,10 +41,16 @@ import javax.inject.Named; import javax.inject.Singleton; +import org.eclipse.che.api.core.event.ServerIdleEvent; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.core.notification.EventSubscriber; +import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.plugin.docker.client.DockerApiVersionPathPrefixProvider; import org.eclipse.che.plugin.docker.client.DockerConnector; import org.eclipse.che.plugin.docker.client.DockerConnectorConfiguration; import org.eclipse.che.plugin.docker.client.DockerRegistryAuthResolver; +import org.eclipse.che.plugin.docker.client.Exec; +import org.eclipse.che.plugin.docker.client.LogMessage; import org.eclipse.che.plugin.docker.client.MessageProcessor; import org.eclipse.che.plugin.docker.client.ProgressMonitor; import org.eclipse.che.plugin.docker.client.connection.DockerConnectionFactory; @@ -40,31 +59,35 @@ import org.eclipse.che.plugin.docker.client.json.ContainerCreated; import org.eclipse.che.plugin.docker.client.json.ContainerInfo; import org.eclipse.che.plugin.docker.client.json.ContainerListEntry; +import org.eclipse.che.plugin.docker.client.json.ContainerState; import org.eclipse.che.plugin.docker.client.json.Event; import org.eclipse.che.plugin.docker.client.json.Filters; import org.eclipse.che.plugin.docker.client.json.HostConfig; -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.json.NetworkCreated; import org.eclipse.che.plugin.docker.client.json.NetworkSettings; import org.eclipse.che.plugin.docker.client.json.PortBinding; import org.eclipse.che.plugin.docker.client.json.network.ContainerInNetwork; +import org.eclipse.che.plugin.docker.client.json.network.EndpointConfig; import org.eclipse.che.plugin.docker.client.json.network.Ipam; import org.eclipse.che.plugin.docker.client.json.network.IpamConfig; import org.eclipse.che.plugin.docker.client.json.network.Network; import org.eclipse.che.plugin.docker.client.params.CommitParams; import org.eclipse.che.plugin.docker.client.params.CreateContainerParams; +import org.eclipse.che.plugin.docker.client.params.CreateExecParams; +import org.eclipse.che.plugin.docker.client.params.GetContainerLogsParams; import org.eclipse.che.plugin.docker.client.params.GetEventsParams; import org.eclipse.che.plugin.docker.client.params.GetResourceParams; +import org.eclipse.che.plugin.docker.client.params.InspectImageParams; import org.eclipse.che.plugin.docker.client.params.KillContainerParams; +import org.eclipse.che.plugin.docker.client.params.PullParams; import org.eclipse.che.plugin.docker.client.params.PutResourceParams; import org.eclipse.che.plugin.docker.client.params.RemoveContainerParams; import org.eclipse.che.plugin.docker.client.params.RemoveImageParams; import org.eclipse.che.plugin.docker.client.params.network.RemoveNetworkParams; import org.eclipse.che.plugin.docker.client.params.StartContainerParams; +import org.eclipse.che.plugin.docker.client.params.StartExecParams; import org.eclipse.che.plugin.docker.client.params.StopContainerParams; -import org.eclipse.che.plugin.docker.client.params.InspectImageParams; -import org.eclipse.che.plugin.docker.client.params.PullParams; import org.eclipse.che.plugin.docker.client.params.TagParams; import org.eclipse.che.plugin.docker.client.params.network.ConnectContainerToNetworkParams; import org.eclipse.che.plugin.docker.client.params.network.CreateNetworkParams; @@ -74,7 +97,9 @@ import org.eclipse.che.plugin.openshift.client.exception.OpenShiftException; import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesContainer; import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesEnvVar; +import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesExecHolder; import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesLabelConverter; +import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesOutputAdapter; import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesService; import org.eclipse.che.plugin.openshift.client.kubernetes.KubernetesStringUtils; import org.slf4j.Logger; @@ -82,12 +107,24 @@ import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerStateRunning; +import io.fabric8.kubernetes.api.model.ContainerStateTerminated; +import io.fabric8.kubernetes.api.model.ContainerStateWaiting; +import io.fabric8.kubernetes.api.model.ContainerStatus; +import io.fabric8.kubernetes.api.model.DoneableEndpoints; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimList; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSource; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.ProbeBuilder; +import io.fabric8.kubernetes.api.model.Quantity; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.api.model.ServicePort; @@ -97,12 +134,23 @@ import io.fabric8.kubernetes.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.api.model.extensions.Deployment; import io.fabric8.kubernetes.api.model.extensions.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.extensions.ReplicaSet; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.fabric8.kubernetes.client.dsl.LogWatch; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.utils.InputStreamPumper; +import io.fabric8.openshift.api.model.DeploymentConfig; +import io.fabric8.openshift.api.model.DoneableDeploymentConfig; +import io.fabric8.openshift.api.model.Image; import io.fabric8.openshift.api.model.ImageStream; import io.fabric8.openshift.api.model.ImageStreamTag; +import io.fabric8.openshift.api.model.Route; +import io.fabric8.openshift.api.model.RouteList; import io.fabric8.openshift.client.DefaultOpenShiftClient; import io.fabric8.openshift.client.OpenShiftClient; - -import static com.google.common.base.Strings.isNullOrEmpty; +import io.fabric8.openshift.client.dsl.DeployableScalableResource; /** * Client for OpenShift API. @@ -114,47 +162,137 @@ @Singleton public class OpenShiftConnector extends DockerConnector { private static final Logger LOG = LoggerFactory.getLogger(OpenShiftConnector.class); + public static final String CHE_OPENSHIFT_RESOURCES_PREFIX = "che-ws-"; + public static final String OPENSHIFT_DEPLOYMENT_LABEL = "deployment"; + private static final String CHE_CONTAINER_IDENTIFIER_LABEL_KEY = "cheContainerIdentifier"; private static final String CHE_DEFAULT_EXTERNAL_ADDRESS = "172.17.0.1"; - private static final String CHE_OPENSHIFT_RESOURCES_PREFIX = "che-ws-"; private static final String CHE_WORKSPACE_ID_ENV_VAR = "CHE_WORKSPACE_ID"; + private static final String CHE_IS_DEV_MACHINE_ENV_VAR = "CHE_IS_DEV_MACHINE"; private static final int CHE_WORKSPACE_AGENT_PORT = 4401; private static final int CHE_TERMINAL_AGENT_PORT = 4411; private static final String DOCKER_PROTOCOL_PORT_DELIMITER = "/"; - private static final String OPENSHIFT_SERVICE_TYPE_NODE_PORT = "NodePort"; private static final int OPENSHIFT_WAIT_POD_DELAY = 1000; private static final int OPENSHIFT_WAIT_POD_TIMEOUT = 240; private static final int OPENSHIFT_IMAGESTREAM_WAIT_DELAY = 2000; private static final int OPENSHIFT_IMAGESTREAM_MAX_WAIT_COUNT = 30; private static final String OPENSHIFT_POD_STATUS_RUNNING = "Running"; - private static final String OPENSHIFT_DEPLOYMENT_LABEL = "deployment"; + private static final String OPENSHIFT_VOLUME_STORAGE_CLASS = "volume.beta.kubernetes.io/storage-class"; + private static final String OPENSHIFT_VOLUME_STORAGE_CLASS_NAME = "che-workspace"; private static final String OPENSHIFT_IMAGE_PULL_POLICY_IFNOTPRESENT = "IfNotPresent"; - private static final Long UID_ROOT = Long.valueOf(0); - private static final Long UID_USER = Long.valueOf(1000); - private final OpenShiftClient openShiftClient; + private static final String IDLING_ALPHA_OPENSHIFT_IO_IDLED_AT = "idling.alpha.openshift.io/idled-at"; + private static final String IDLING_ALPHA_OPENSHIFT_IO_PREVIOUS_SCALE = "idling.alpha.openshift.io/previous-scale"; + private static final String OPENSHIFT_CHE_SERVER_DEPLOYMENT_NAME = "che"; + private static final String OPENSHIFT_CHE_SERVER_SERVICE_NAME = "che-host"; + private static final String IDLING_ALPHA_OPENSHIFT_IO_UNIDLE_TARGETS = "idling.alpha.openshift.io/unidle-targets"; + private static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssX"; + + private Map execMap = new HashMap<>(); + private final String openShiftCheProjectName; - private final String openShiftCheServiceAccount; private final int openShiftLivenessProbeDelay; private final int openShiftLivenessProbeTimeout; + private final String workspacesPersistentVolumeClaim; + private final String workspacesPvcQuantity; + private final String cheWorkspaceStorage; + private final String cheWorkspaceProjectsStorage; + private final String cheServerExternalAddress; + private final String cheWorkspaceMemoryLimit; + private final String cheWorkspaceMemoryRequest; + private final boolean secureRoutes; + private final boolean createWorkspaceDirs; + private final OpenShiftPvcHelper openShiftPvcHelper; @Inject public OpenShiftConnector(DockerConnectorConfiguration connectorConfiguration, DockerConnectionFactory connectionFactory, DockerRegistryAuthResolver authResolver, DockerApiVersionPathPrefixProvider dockerApiVersionPathPrefixProvider, + OpenShiftPvcHelper openShiftPvcHelper, + EventService eventService, + @Nullable @Named("che.docker.ip.external") String cheServerExternalAddress, @Named("che.openshift.project") String openShiftCheProjectName, - @Named("che.openshift.serviceaccountname") String openShiftCheServiceAccount, @Named("che.openshift.liveness.probe.delay") int openShiftLivenessProbeDelay, - @Named("che.openshift.liveness.probe.timeout") int openShiftLivenessProbeTimeout) { + @Named("che.openshift.liveness.probe.timeout") int openShiftLivenessProbeTimeout, + @Named("che.openshift.workspaces.pvc.name") String workspacesPersistentVolumeClaim, + @Named("che.openshift.workspaces.pvc.quantity") String workspacesPvcQuantity, + @Named("che.workspace.storage") String cheWorkspaceStorage, + @Named("che.workspace.projects.storage") String cheWorkspaceProjectsStorage, + @Nullable @Named("che.openshift.workspace.memory.request") String cheWorkspaceMemoryRequest, + @Nullable @Named("che.openshift.workspace.memory.override") String cheWorkspaceMemoryLimit, + @Named("che.openshift.secure.routes") boolean secureRoutes, + @Named("che.openshift.precreate.workspace.dirs") boolean createWorkspaceDirs) { super(connectorConfiguration, connectionFactory, authResolver, dockerApiVersionPathPrefixProvider); + this.cheServerExternalAddress = cheServerExternalAddress; this.openShiftCheProjectName = openShiftCheProjectName; - this.openShiftCheServiceAccount = openShiftCheServiceAccount; this.openShiftLivenessProbeDelay = openShiftLivenessProbeDelay; this.openShiftLivenessProbeTimeout = openShiftLivenessProbeTimeout; + this.workspacesPersistentVolumeClaim = workspacesPersistentVolumeClaim; + this.workspacesPvcQuantity = workspacesPvcQuantity; + this.cheWorkspaceStorage = cheWorkspaceStorage; + this.cheWorkspaceProjectsStorage = cheWorkspaceProjectsStorage; + this.cheWorkspaceMemoryRequest = cheWorkspaceMemoryRequest; + this.cheWorkspaceMemoryLimit = cheWorkspaceMemoryLimit; + this.secureRoutes = secureRoutes; + this.createWorkspaceDirs = createWorkspaceDirs; + this.openShiftPvcHelper = openShiftPvcHelper; + eventService.subscribe(new EventSubscriber() { + + @Override + public void onEvent(ServerIdleEvent event) { + idleCheServer(event); + } + }); + } - this.openShiftClient = new DefaultOpenShiftClient(); + private void idleCheServer(ServerIdleEvent event) { + try (DefaultOpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + DeployableScalableResource deploymentConfigResource = openShiftClient.deploymentConfigs() + .inNamespace(openShiftCheProjectName) + .withName(OPENSHIFT_CHE_SERVER_DEPLOYMENT_NAME); + DeploymentConfig deploymentConfig = deploymentConfigResource.get(); + if (deploymentConfig == null) { + LOG.warn(String.format("Deployment config %s not found", OPENSHIFT_CHE_SERVER_DEPLOYMENT_NAME)); + return; + } + Integer replicas = deploymentConfig.getSpec().getReplicas(); + if (replicas != null && replicas > 0) { + Resource endpointResource = openShiftClient.endpoints() + .inNamespace(openShiftCheProjectName) + .withName(OPENSHIFT_CHE_SERVER_SERVICE_NAME); + Endpoints endpoint = endpointResource.get(); + if (endpoint == null) { + LOG.warn(String.format("Endpoint %s not found", OPENSHIFT_CHE_SERVER_SERVICE_NAME)); + return; + } + Map annotations = deploymentConfig.getMetadata().getAnnotations(); + if (annotations == null) { + annotations = new HashMap<>(); + deploymentConfig.getMetadata().setAnnotations(annotations); + } + TimeZone tz = TimeZone.getTimeZone("UTC"); + DateFormat df = new SimpleDateFormat(ISO_8601_DATE_FORMAT); + df.setTimeZone(tz); + String idle = df.format(new Date()); + annotations.put(IDLING_ALPHA_OPENSHIFT_IO_IDLED_AT, idle); + annotations.put(IDLING_ALPHA_OPENSHIFT_IO_PREVIOUS_SCALE, "1"); + deploymentConfig.getSpec().setReplicas(0); + deploymentConfigResource.patch(deploymentConfig); + Map endpointAnnotations = endpoint.getMetadata().getAnnotations(); + if (endpointAnnotations == null) { + endpointAnnotations = new HashMap<>(); + endpoint.getMetadata().setAnnotations(endpointAnnotations); + } + endpointAnnotations.put(IDLING_ALPHA_OPENSHIFT_IO_IDLED_AT, idle); + endpointAnnotations.put(IDLING_ALPHA_OPENSHIFT_IO_UNIDLE_TARGETS, + "[{\"kind\":\"DeploymentConfig\",\"name\":\"" + OPENSHIFT_CHE_SERVER_DEPLOYMENT_NAME + + "\",\"replicas\":1}]"); + endpointResource.patch(endpoint); + LOG.info("Che server has been idled"); + } + } } /** @@ -167,9 +305,6 @@ public ContainerCreated createContainer(CreateContainerParams createContainerPar String containerName = KubernetesStringUtils.convertToContainerName(createContainerParams.getContainerName()); String workspaceID = getCheWorkspaceId(createContainerParams); - // Generate workspaceID if CHE_WORKSPACE_ID env var does not exist - workspaceID = workspaceID.isEmpty() ? KubernetesStringUtils.generateWorkspaceID() : workspaceID; - // imageForDocker is the docker version of the image repository. It's needed for other // OpenShiftConnector API methods, but is not acceptable as an OpenShift name String imageForDocker = createContainerParams.getContainerConfig().getImage(); @@ -186,12 +321,15 @@ public ContainerCreated createContainer(CreateContainerParams createContainerPar // Next we need to get the address of the registry where the ImageStreamTag is stored String imageStreamName = KubernetesStringUtils.getImageStreamNameFromPullSpec(imageStreamTagPullSpec); - ImageStream imageStream = openShiftClient.imageStreams() - .inNamespace(openShiftCheProjectName) - .withName(imageStreamName) - .get(); - if (imageStream == null) { - throw new OpenShiftException("ImageStream not found"); + ImageStream imageStream; + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + imageStream = openShiftClient.imageStreams() + .inNamespace(openShiftCheProjectName) + .withName(imageStreamName) + .get(); + if (imageStream == null) { + throw new OpenShiftException("ImageStream not found"); + } } String registryAddress = imageStream.getStatus() .getDockerImageRepository() @@ -207,35 +345,72 @@ public ContainerCreated createContainer(CreateContainerParams createContainerPar .getConfig().getExposedPorts().keySet(); Set exposedPorts = getExposedPorts(containerExposedPorts, imageExposedPorts); - boolean runContainerAsRoot = runContainerAsRoot(imageForDocker); - String[] envVariables = createContainerParams.getContainerConfig().getEnv(); String[] volumes = createContainerParams.getContainerConfig().getHostConfig().getBinds(); Map additionalLabels = createContainerParams.getContainerConfig().getLabels(); + String networkName = createContainerParams.getContainerConfig().getHostConfig().getNetworkMode(); + EndpointConfig endpointConfig = createContainerParams.getContainerConfig().getNetworkingConfig().getEndpointsConfig().get(networkName); + String[] endpointAliases = endpointConfig != null ? endpointConfig.getAliases() : new String[0]; + + Map resourceLimits = new HashMap<>(); + if (!isNullOrEmpty(cheWorkspaceMemoryLimit)) { + LOG.info("Che property 'che.openshift.workspace.memory.override' " + + "used to override workspace memory limit to {}.", cheWorkspaceMemoryLimit); + resourceLimits.put("memory", new Quantity(cheWorkspaceMemoryLimit)); + } else { + long memoryLimitBytes = createContainerParams.getContainerConfig().getHostConfig().getMemory(); + String memoryLimit = Long.toString(memoryLimitBytes / 1048576) + "Mi"; + LOG.info("Creating workspace pod with memory limit of {}.", memoryLimit); + resourceLimits.put("memory", new Quantity(cheWorkspaceMemoryLimit)); + } + + Map resourceRequests = new HashMap<>(); + if (!isNullOrEmpty(cheWorkspaceMemoryRequest)) { + resourceRequests.put("memory", new Quantity(cheWorkspaceMemoryRequest)); + } + + String deploymentName; + String serviceName; + if (isDevMachine(createContainerParams)) { + serviceName = deploymentName = CHE_OPENSHIFT_RESOURCES_PREFIX + workspaceID; + } else { + if (endpointAliases.length > 0) { + serviceName = endpointAliases[0]; + deploymentName = CHE_OPENSHIFT_RESOURCES_PREFIX + serviceName; + } else { + // Should never happen + serviceName = deploymentName = CHE_OPENSHIFT_RESOURCES_PREFIX + KubernetesStringUtils.generateWorkspaceID(); + } + } + String containerID; - try { - createOpenShiftService(workspaceID, exposedPorts, additionalLabels); - String deploymentName = createOpenShiftDeployment(workspaceID, - dockerPullSpec, - containerName, - exposedPorts, - envVariables, - volumes, - runContainerAsRoot); + OpenShiftClient openShiftClient = new DefaultOpenShiftClient(); + try { + createOpenShiftService(deploymentName, serviceName, exposedPorts, additionalLabels, endpointAliases); + createOpenShiftDeployment(deploymentName, + dockerPullSpec, + containerName, + exposedPorts, + envVariables, + volumes, + resourceLimits, + resourceRequests); containerID = waitAndRetrieveContainerID(deploymentName); if (containerID == null) { throw new OpenShiftException("Failed to get the ID of the container running in the OpenShift pod"); } - } catch (IOException e) { + } catch (IOException | KubernetesClientException e) { // Make sure we clean up deployment and service in case of an error -- otherwise Che can end up // in an inconsistent state. LOG.info("Error while creating Pod, removing deployment"); - String deploymentName = CHE_OPENSHIFT_RESOURCES_PREFIX + workspaceID; + LOG.info(e.getMessage()); cleanUpWorkspaceResources(deploymentName); openShiftClient.resource(imageStreamTag).delete(); throw e; + } finally { + openShiftClient.close(); } return new ContainerCreated(containerID, null); @@ -367,10 +542,14 @@ public Network inspectNetwork(String netId) throws IOException { @Override public Network inspectNetwork(InspectNetworkParams params) throws IOException { String netId = params.getNetworkId(); + ServiceList services; + + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + services = openShiftClient.services() + .inNamespace(this.openShiftCheProjectName) + .list(); + } - ServiceList services = openShiftClient.services() - .inNamespace(this.openShiftCheProjectName) - .list(); Map containers = new HashMap<>(); for (Service svc : services.getItems()) { String selector = svc.getSpec().getSelector().get(OPENSHIFT_DEPLOYMENT_LABEL); @@ -378,10 +557,13 @@ public Network inspectNetwork(InspectNetworkParams params) throws IOException { continue; } - PodList pods = openShiftClient.pods() - .inNamespace(openShiftCheProjectName) - .withLabel(OPENSHIFT_DEPLOYMENT_LABEL, selector) - .list(); + PodList pods; + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + pods = openShiftClient.pods() + .inNamespace(openShiftCheProjectName) + .withLabel(OPENSHIFT_DEPLOYMENT_LABEL, selector) + .list(); + } for (Pod pod : pods.getItems()) { String podName = pod.getMetadata() @@ -456,28 +638,34 @@ public void pull(final PullParams params, final ProgressMonitor progressMonitor) String tag = params.getTag(); // e.g. latest, usually String imageStreamName = KubernetesStringUtils.convertPullSpecToImageStreamName(repo); + ImageStream existingImageStream; + + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + existingImageStream = openShiftClient.imageStreams() + .inNamespace(openShiftCheProjectName) + .withName(imageStreamName) + .get(); + } - ImageStream existingImageStream = openShiftClient.imageStreams() - .inNamespace(openShiftCheProjectName) - .withName(imageStreamName) - .get(); if (existingImageStream == null) { - openShiftClient.imageStreams() - .inNamespace(openShiftCheProjectName) - .createNew() - .withNewMetadata() - .withName(imageStreamName) // imagestream id - .endMetadata() - .withNewSpec() - .addNewTag() - .withName(tag) - .endTag() - .withDockerImageRepository(repo) // tracking repo - .endSpec() - .withNewStatus() - .withDockerImageRepository("") - .endStatus() - .done(); + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + openShiftClient.imageStreams() + .inNamespace(openShiftCheProjectName) + .createNew() + .withNewMetadata() + .withName(imageStreamName) // imagestream id + .endMetadata() + .withNewSpec() + .addNewTag() + .withName(tag) + .endTag() + .withDockerImageRepository(repo) // tracking repo + .endSpec() + .withNewStatus() + .withDockerImageRepository("") + .endStatus() + .done(); + } } // Wait for Image metadata to be obtained. @@ -489,10 +677,13 @@ public void pull(final PullParams params, final ProgressMonitor progressMonitor) Thread.currentThread().interrupt(); } - createdImageStream = openShiftClient.imageStreams() - .inNamespace(openShiftCheProjectName) - .withName(imageStreamName) - .get(); + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + createdImageStream = openShiftClient.imageStreams() + .inNamespace(openShiftCheProjectName) + .withName(imageStreamName) + .get(); + } + if (createdImageStream != null && createdImageStream.getStatus().getDockerImageRepository() != null) { @@ -566,12 +757,12 @@ public ImageInfo inspectImage(InspectImageParams params) throws IOException { @Override public void removeImage(final RemoveImageParams params) throws IOException { - String image = KubernetesStringUtils.getImageStreamNameFromPullSpec(params.getImage()); - - String imageStreamTagName = KubernetesStringUtils.convertPullSpecToTagName(image); - ImageStreamTag imageStreamTag = getImageStreamTagFromRepo(imageStreamTagName); - - openShiftClient.resource(imageStreamTag).delete(); + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + String image = KubernetesStringUtils.getImageStreamNameFromPullSpec(params.getImage()); + String imageStreamTagName = KubernetesStringUtils.convertPullSpecToTagName(image); + ImageStreamTag imageStreamTag = getImageStreamTagFromRepo(imageStreamTagName); + openShiftClient.resource(imageStreamTag).delete(); + } } /** @@ -604,7 +795,143 @@ public String commit(final CommitParams params) throws IOException { } @Override - public void getEvents(final GetEventsParams params, MessageProcessor messageProcessor) {} + public void getEvents(final GetEventsParams params, MessageProcessor messageProcessor) { + CountDownLatch waitForClose = new CountDownLatch(1); + Watcher eventWatcher = + new Watcher() { + @Override + public void eventReceived(Action action, io.fabric8.kubernetes.api.model.Event event) { + // Do nothing; + } + + @Override + public void onClose(KubernetesClientException e) { + if (e == null) { + LOG.error("Eventwatch Closed"); + } else { + LOG.error("Eventwatch Closed" + e.getMessage()); + } + waitForClose.countDown(); + } + }; + OpenShiftClient openShiftClient = new DefaultOpenShiftClient(); + openShiftClient.events() + .inNamespace(openShiftCheProjectName) + .watch(eventWatcher); + try { + waitForClose.await(); + } catch (InterruptedException e) { + LOG.error("Thread interrupted while waiting for eventWatcher."); + Thread.currentThread().interrupt(); + } finally { + openShiftClient.close(); + } + } + + @Override + public void getContainerLogs(final GetContainerLogsParams params, MessageProcessor containerLogsProcessor) + throws IOException { + String container = params.getContainer(); // container ID + Pod pod = getChePodByContainerId(container); + if (pod != null) { + String podName = pod.getMetadata().getName(); + boolean[] ret = new boolean[1]; + ret[0] = false; + OpenShiftClient openShiftClient = new DefaultOpenShiftClient(); + try (LogWatch watchLog = openShiftClient.pods().inNamespace(openShiftCheProjectName).withName(podName) + .watchLog()) { + Watcher watcher = new Watcher() { + + @Override + public void eventReceived(Action action, Pod resource) { + if (action == Action.DELETED) { + ret[0] = true; + } + } + + @Override + public void onClose(KubernetesClientException cause) { + ret[0] = true; + } + + }; + openShiftClient.pods().inNamespace(openShiftCheProjectName).withName(podName).watch(watcher); + Thread.sleep(5000); + InputStream is = watchLog.getOutput(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); + while (!ret[0]) { + String line = bufferedReader.readLine(); + containerLogsProcessor.process(new LogMessage(LogMessage.Type.DOCKER, line)); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + // The kubernetes client throws an exception (Pipe not connected) when pod doesn't contain any logs. + // We can ignore it. + } finally { + openShiftClient.close(); + } + } + } + + @Override + public Exec createExec(final CreateExecParams params) throws IOException { + String[] command = params.getCmd(); + String containerId = params.getContainer(); + + Pod pod = getChePodByContainerId(containerId); + String podName = pod.getMetadata().getName(); + + String execId = KubernetesStringUtils.generateWorkspaceID(); + KubernetesExecHolder execHolder = new KubernetesExecHolder().withCommand(command) + .withPod(podName); + execMap.put(execId, execHolder); + + return new Exec(command, execId); + } + + @Override + public void startExec(final StartExecParams params, + @Nullable MessageProcessor execOutputProcessor) throws IOException { + String execId = params.getExecId(); + + KubernetesExecHolder exec = execMap.get(execId); + + String podName = exec.getPod(); + String[] command = exec.getCommand(); + for (int i = 0; i < command.length; i++) { + command[i] = URLEncoder.encode(command[i], "UTF-8"); + } + + ExecutorService executor = Executors.newFixedThreadPool(2); + OpenShiftClient openShiftClient = new DefaultOpenShiftClient(); + try (ExecWatch watch = openShiftClient.pods() + .inNamespace(openShiftCheProjectName) + .withName(podName) + .redirectingOutput() + .redirectingError() + .exec(command); + InputStreamPumper outputPump = new InputStreamPumper(watch.getOutput(), + new KubernetesOutputAdapter(LogMessage.Type.STDOUT, + execOutputProcessor)); + InputStreamPumper errorPump = new InputStreamPumper(watch.getError(), + new KubernetesOutputAdapter(LogMessage.Type.STDERR, + execOutputProcessor)) + ) { + Future outFuture = executor.submit(outputPump); + Future errFuture = executor.submit(errorPump); + // Short-term worksaround; the Futures above seem to never finish. + Thread.sleep(2500); + } catch (KubernetesClientException e) { + throw new OpenShiftException(e.getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + execMap.remove(execId); + executor.shutdown(); + openShiftClient.close(); + } + } /** * Gets the ImageStreamTag corresponding to a given tag name (i.e. without the repository) @@ -621,10 +948,13 @@ private ImageStreamTag getImageStreamTagFromRepo(String imageStreamTagName) thro // Note: ideally, ImageStreamTags could be identified with a label, but it seems like // ImageStreamTags do not support labels. - List imageStreams = openShiftClient.imageStreamTags() - .inNamespace(openShiftCheProjectName) - .list() - .getItems(); + List imageStreams; + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + imageStreams = openShiftClient.imageStreamTags() + .inNamespace(openShiftCheProjectName) + .list() + .getItems(); + } // We only get ImageStreamTag names here, since these ImageStreamTags do not include // Docker metadata, for some reason. @@ -645,60 +975,101 @@ private ImageStreamTag getImageStreamTagFromRepo(String imageStreamTagName) thro String imageStreamTag = imageStreamTags.get(0); // Finally, get the ImageStreamTag, with Docker metadata. - return openShiftClient.imageStreamTags() - .inNamespace(openShiftCheProjectName) - .withName(imageStreamTag) - .get(); + return getImageStreamTag(imageStreamTag); + } + + private ImageStreamTag getImageStreamTag(final String imageStreamName) { + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + return openShiftClient.imageStreamTags() + .inNamespace(openShiftCheProjectName) + .withName(imageStreamName) + .get(); + } } private Service getCheServiceBySelector(String selectorKey, String selectorValue) { - ServiceList svcs = openShiftClient.services() - .inNamespace(this.openShiftCheProjectName) - .list(); + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + ServiceList svcs = openShiftClient.services() + .inNamespace(this.openShiftCheProjectName) + .list(); - Service svc = svcs.getItems().stream() - .filter(s->s.getSpec().getSelector().containsKey(selectorKey)) - .filter(s->s.getSpec().getSelector().get(selectorKey).equals(selectorValue)).findAny().orElse(null); + Service svc = svcs.getItems().stream() + .filter(s->s.getSpec().getSelector().containsKey(selectorKey)) + .filter(s->s.getSpec().getSelector().get(selectorKey).equals(selectorValue)).findAny().orElse(null); - if (svc == null) { - LOG.warn("No Service with selector {}={} could be found", selectorKey, selectorValue); + if (svc == null) { + LOG.warn("No Service with selector {}={} could be found", selectorKey, selectorValue); + } + return svc; } - - return svc; } private Deployment getDeploymentByName(String deploymentName) throws IOException { - Deployment deployment = openShiftClient - .extensions().deployments() - .inNamespace(this.openShiftCheProjectName) - .withName(deploymentName) - .get(); - if (deployment == null) { - LOG.warn("No Deployment with name {} could be found", deploymentName); + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + Deployment deployment = openShiftClient + .extensions().deployments() + .inNamespace(this.openShiftCheProjectName) + .withName(deploymentName) + .get(); + if (deployment == null) { + LOG.warn("No Deployment with name {} could be found", deploymentName); + } + return deployment; } - return deployment; } - private Pod getChePodByContainerId(String containerId) throws IOException { - PodList pods = openShiftClient.pods() - .inNamespace(this.openShiftCheProjectName) - .withLabel(CHE_CONTAINER_IDENTIFIER_LABEL_KEY, - KubernetesStringUtils.getLabelFromContainerID(containerId)) - .list(); + private List getRoutesByLabel(String labelKey, String labelValue) throws IOException { + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + RouteList routeList = openShiftClient + .routes() + .inNamespace(this.openShiftCheProjectName) + .withLabel(labelKey, labelValue) + .list(); + + List items = routeList.getItems(); - List items = pods.getItems(); + if (items.isEmpty()) { + LOG.warn("No Route with label {}={} could be found", labelKey, labelValue); + throw new IOException("No Route with label " + labelKey + "=" + labelValue + " could be found"); + } - if (items.isEmpty()) { - LOG.error("An OpenShift Pod with label {}={} could not be found", CHE_CONTAINER_IDENTIFIER_LABEL_KEY, containerId); - throw new IOException("An OpenShift Pod with label " + CHE_CONTAINER_IDENTIFIER_LABEL_KEY + "=" + containerId +" could not be found"); + return items; } + } - if (items.size() > 1) { - LOG.error("There are {} pod with label {}={} (just one was expeced)", items.size(), CHE_CONTAINER_IDENTIFIER_LABEL_KEY, containerId ); - throw new IOException("There are " + items.size() + " pod with label " + CHE_CONTAINER_IDENTIFIER_LABEL_KEY + "=" + containerId + " (just one was expeced)"); + private List getReplicaSetByLabel(String key, String value) { + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + List replicaSets = openShiftClient.extensions() + .replicaSets() + .inNamespace(openShiftCheProjectName) + .withLabel(key, value) + .list().getItems(); + return replicaSets; } + } + + private Pod getChePodByContainerId(String containerId) throws IOException { + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + PodList pods = openShiftClient.pods() + .inNamespace(this.openShiftCheProjectName) + .withLabel(CHE_CONTAINER_IDENTIFIER_LABEL_KEY, + KubernetesStringUtils.getLabelFromContainerID(containerId)) + .list(); + + List items = pods.getItems(); + + if (items.isEmpty()) { + LOG.error("An OpenShift Pod with label {}={} could not be found", CHE_CONTAINER_IDENTIFIER_LABEL_KEY, containerId); + throw new IOException("An OpenShift Pod with label " + CHE_CONTAINER_IDENTIFIER_LABEL_KEY + "=" + containerId +" could not be found"); + } + + if (items.size() > 1) { + LOG.error("There are {} pod with label {}={} (just one was expected)", items.size(), CHE_CONTAINER_IDENTIFIER_LABEL_KEY, containerId ); + throw new IOException("There are " + items.size() + " pod with label " + CHE_CONTAINER_IDENTIFIER_LABEL_KEY + "=" + containerId + " (just one was expeced)"); + } - return items.get(0); + return items.get(0); + } } /** @@ -714,12 +1085,21 @@ private ImageInfo getImageInfoFromTag(ImageStreamTag imageStreamTag) { // except that the capitalization is inconsistent, breaking deserialization. Top level elements // are lowercased with underscores, while nested elements conform to FieldNamingPolicy.UPPER_CAMEL_CASE. // We're only converting the config fields for brevity; this means that other fields are null. - String dockerImageConfig = imageStreamTag.getImage().getDockerImageConfig(); - ImageInfo info = GSON.fromJson(dockerImageConfig.replaceFirst("config", "Config") - .replaceFirst("container_config", "ContainerConfig"), - ImageInfo.class); - - return info; + Image tagImage = imageStreamTag.getImage(); + String dockerImageConfig = tagImage.getDockerImageConfig(); + + if (!isNullOrEmpty(dockerImageConfig)) { + LOG.info("imageStreamTag dockerImageConfig is not empty. Using it to get image info"); + ImageInfo info = GSON.fromJson(dockerImageConfig.replaceFirst("config", "Config") + .replaceFirst("container_config", "ContainerConfig"), + ImageInfo.class); + return info; + } else { + LOG.info("imageStreamTag dockerImageConfig empty. Using dockerImageMetadata to get image info"); + String dockerImageMetadata = GSON.toJson(tagImage.getAdditionalProperties().get("dockerImageMetadata")); + ImageInfo info = GSON.fromJson(dockerImageMetadata, ImageInfo.class); + return info; + } } protected String getCheWorkspaceId(CreateContainerParams createContainerParams) { @@ -731,46 +1111,77 @@ protected String getCheWorkspaceId(CreateContainerParams createContainerParams) return workspaceID.replaceFirst("workspace",""); } - private void createOpenShiftService(String workspaceID, + private boolean isDevMachine(CreateContainerParams createContainerParams) { + Stream env = Arrays.stream(createContainerParams.getContainerConfig().getEnv()); + return Boolean.parseBoolean(env.filter(v -> v.startsWith(CHE_IS_DEV_MACHINE_ENV_VAR) && v.contains("=")) + .map(v -> v.split("=",2)[1]) + .findFirst() + .orElse("false")); + } + + private void createOpenShiftService(String deploymentName, + String serviceName, Set exposedPorts, - Map additionalLabels) { - - Map selector = Collections.singletonMap(OPENSHIFT_DEPLOYMENT_LABEL, CHE_OPENSHIFT_RESOURCES_PREFIX + workspaceID); + Map additionalLabels, + String[] endpointAliases) { + Map selector = Collections.singletonMap(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName); List ports = KubernetesService.getServicePortsFrom(exposedPorts); - Service service = openShiftClient - .services() - .inNamespace(this.openShiftCheProjectName) - .createNew() - .withNewMetadata() - .withName(CHE_OPENSHIFT_RESOURCES_PREFIX + workspaceID) + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + Service service = openShiftClient + .services() + .inNamespace(this.openShiftCheProjectName) + .createNew() + .withNewMetadata() + .withName(serviceName) .withAnnotations(KubernetesLabelConverter.labelsToNames(additionalLabels)) - .endMetadata() - .withNewSpec() - .withType(OPENSHIFT_SERVICE_TYPE_NODE_PORT) + .endMetadata() + .withNewSpec() .withSelector(selector) .withPorts(ports) - .endSpec() - .done(); + .endSpec() + .done(); + + LOG.info("OpenShift service {} created", service.getMetadata().getName()); + + for (ServicePort port : ports) { + createOpenShiftRoute(serviceName, deploymentName, port.getName()); + } + } + } - LOG.info("OpenShift service {} created", service.getMetadata().getName()); + private void createOpenShiftRoute(String serviceName, + String deploymentName, + String serverRef) { + String routeId = serviceName.replaceFirst(CHE_OPENSHIFT_RESOURCES_PREFIX, ""); + OpenShiftRouteCreator.createRoute(openShiftCheProjectName, + cheServerExternalAddress, + serverRef, + serviceName, + deploymentName, + routeId, + secureRoutes); } - private String createOpenShiftDeployment(String workspaceID, + private void createOpenShiftDeployment(String deploymentName, String imageName, String sanitizedContainerName, Set exposedPorts, String[] envVariables, String[] volumes, - boolean runContainerAsRoot) { + Map resourceLimits, + Map resourceRequests) throws OpenShiftException { - String deploymentName = CHE_OPENSHIFT_RESOURCES_PREFIX + workspaceID; LOG.info("Creating OpenShift deployment {}", deploymentName); Map selector = Collections.singletonMap(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName); LOG.info("Adding container {} to OpenShift deployment {}", sanitizedContainerName, deploymentName); - Long UID = runContainerAsRoot ? UID_ROOT : UID_USER; + + if (createWorkspaceDirs) { + createWorkspaceDir(volumes); + } + Container container = new ContainerBuilder() .withName(sanitizedContainerName) .withImage(imageName) @@ -778,17 +1189,19 @@ private String createOpenShiftDeployment(String workspaceID, .withPorts(KubernetesContainer.getContainerPortsFrom(exposedPorts)) .withImagePullPolicy(OPENSHIFT_IMAGE_PULL_POLICY_IFNOTPRESENT) .withNewSecurityContext() - .withRunAsUser(UID) - .withPrivileged(true) + .withPrivileged(false) .endSecurityContext() .withLivenessProbe(getLivenessProbeFrom(exposedPorts)) - .withVolumeMounts(getVolumeMountsFrom(volumes, workspaceID)) + .withVolumeMounts(getVolumeMountsFrom(volumes)) + .withNewResources() + .withLimits(resourceLimits) + .withRequests(resourceRequests) + .endResources() .build(); PodSpec podSpec = new PodSpecBuilder() .withContainers(container) - .withVolumes(getVolumesFrom(volumes, workspaceID)) - .withServiceAccountName(this.openShiftCheServiceAccount) + .withVolumes(getVolumesFrom(volumes)) .build(); Deployment deployment = new DeploymentBuilder() @@ -810,13 +1223,14 @@ private String createOpenShiftDeployment(String workspaceID, .endSpec() .build(); - deployment = openShiftClient.extensions() - .deployments() - .inNamespace(this.openShiftCheProjectName) - .create(deployment); + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + deployment = openShiftClient.extensions() + .deployments() + .inNamespace(this.openShiftCheProjectName) + .create(deployment); + } LOG.info("OpenShift deployment {} created", deploymentName); - return deployment.getMetadata().getName(); } /** @@ -829,7 +1243,8 @@ private String createOpenShiftDeployment(String workspaceID, */ private ImageStreamTag createImageStreamTag(String sourceImageWithTag, String imageStreamTagName) throws IOException { - try { + + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { openShiftClient.imageStreamTags() .inNamespace(openShiftCheProjectName) .createOrReplaceWithNew() @@ -876,11 +1291,12 @@ private ImageStreamTag createImageStreamTag(String sourceImageWithTag, * @param pod * @param containerId * @return + * @throws OpenShiftException */ private ContainerInfo createContainerInfo(Service svc, ImageInfo imageInfo, Pod pod, - String containerId) { + String containerId) throws OpenShiftException { // In Che on OpenShift, we only have one container per pod. Container container = pod.getSpec().getContainers().get(0); @@ -889,7 +1305,6 @@ private ContainerInfo createContainerInfo(Service svc, // HostConfig HostConfig hostConfig = new HostConfig(); hostConfig.setBinds(new String[0]); - hostConfig.setMemory(imageInfo.getConfig().getMemory()); // Env vars List imageEnv = Arrays.asList(imageContainerConfig.getEnv()); @@ -935,27 +1350,69 @@ private ContainerInfo createContainerInfo(Service svc, info.setNetworkSettings(networkSettings); info.setHostConfig(hostConfig); info.setImage(imageInfo.getConfig().getImage()); + + // In Che on OpenShift, we only have one container per pod. + info.setState(getContainerStates(pod).get(0)); return info; } + private List getContainerStates(final Pod pod) throws OpenShiftException { + List containerStates = new ArrayList<>(); + List containerStatuses = pod.getStatus().getContainerStatuses(); + for (ContainerStatus status : containerStatuses) { + io.fabric8.kubernetes.api.model.ContainerState state = status.getState(); + + ContainerStateTerminated terminated = state.getTerminated(); + ContainerStateWaiting waiting = state.getWaiting(); + ContainerStateRunning running = state.getRunning(); + + ContainerState containerState = new ContainerState(); + + if (terminated != null) { + containerState.setStatus("exited"); + } else if (waiting != null) { + containerState.setStatus("paused"); + } else if (running != null) { + containerState.setStatus("running"); + } else { + throw new OpenShiftException("Fail to detect the state of container with id " + status.getContainerID()); + } + containerStates.add(containerState); + } + return containerStates; + } private void cleanUpWorkspaceResources(String deploymentName) throws IOException { Deployment deployment = getDeploymentByName(deploymentName); Service service = getCheServiceBySelector(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName); + List routes = getRoutesByLabel(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName); + List replicaSets = getReplicaSetByLabel(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName); + + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + if (routes != null) { + for (Route route: routes) { + LOG.info("Removing OpenShift Route {}", route.getMetadata().getName()); + openShiftClient.resource(route).delete(); + } + } - if (service != null) { - LOG.info("Removing OpenShift Service {}", service.getMetadata().getName()); - openShiftClient.resource(service).delete(); - } + if (service != null) { + LOG.info("Removing OpenShift Service {}", service.getMetadata().getName()); + openShiftClient.resource(service).delete(); + } - if (deployment != null) { - LOG.info("Removing OpenShift Deployment {}", deployment.getMetadata().getName()); - openShiftClient.resource(deployment).delete(); - } + if (deployment != null) { + LOG.info("Removing OpenShift Deployment {}", deployment.getMetadata().getName()); + openShiftClient.resource(deployment).delete(); + } - // Wait for all pods to terminate before returning. - try { + if (replicaSets != null && replicaSets.size() > 0) { + LOG.info("Removing OpenShift ReplicaSets for deployment {}", deploymentName); + replicaSets.forEach(rs -> openShiftClient.resource(rs).delete()); + } + + // Wait for all pods to terminate before returning. for (int waitCount = 0; waitCount < OPENSHIFT_WAIT_POD_TIMEOUT; waitCount++) { List pods = openShiftClient.pods() .inNamespace(openShiftCheProjectName) @@ -975,92 +1432,162 @@ private void cleanUpWorkspaceResources(String deploymentName) throws IOException throw new OpenShiftException("Timeout while waiting for pods to terminate"); } - private List getVolumeMountsFrom(String[] volumes, String workspaceID) { - List vms = new ArrayList<>(); + private void createWorkspaceDir(String[] volumes) throws OpenShiftException { + PersistentVolumeClaim pvc = getClaimCheWorkspace(); + String workspaceSubpath = getWorkspaceSubpath(volumes); + if (pvc != null && !isNullOrEmpty(workspaceSubpath)) { + LOG.info("Making sure directory exists for workspace {}", workspaceSubpath); + boolean succeeded = openShiftPvcHelper.createJobPod(workspacesPersistentVolumeClaim, + openShiftCheProjectName, + "create-", + OpenShiftPvcHelper.Command.MAKE, + workspaceSubpath); + if (!succeeded) { + LOG.error("Failed to create workspace directory {} in PVC {}", workspaceSubpath, + workspacesPersistentVolumeClaim); + throw new OpenShiftException("Failed to create workspace directory in PVC"); + } + } + } + + /** + * Gets the workspace subpath from an array of volumes. Since volumes provided are + * those used when running Che in Docker, most of the volume spec is ignored; this + * method returns the subpath within the hostpath that refers to the workspace. + *

+ * E.g. for a volume {@code /data/workspaces/wksp-8z00:/projects:Z}, this method will return + * "wksp-8z00". + * + * @param volumes + * @return + */ + private String getWorkspaceSubpath(String[] volumes) { + String workspaceSubpath = null; for (String volume : volumes) { - String mountPath = volume.split(":",3)[1]; - String volumeName = getVolumeName(volume); + // Volumes are structured ::. + // We first check that matches the mount path for projects + // and then extract the hostpath directory. The first part of the volume + // String will be structured /workspaceName. + String mountPath = volume.split(":", 3)[1]; + if (cheWorkspaceProjectsStorage.equals(mountPath)) { + workspaceSubpath = volume.split(":", 3)[0].replaceAll(cheWorkspaceStorage, ""); + if (workspaceSubpath.startsWith("/")) { + workspaceSubpath = workspaceSubpath.substring(1); + } + } + } + return workspaceSubpath; + } - VolumeMount vm = new VolumeMountBuilder() - .withMountPath(mountPath) - .withName("ws-" + workspaceID + "-" + volumeName) + private List getVolumeMountsFrom(String[] volumes) { + List vms = new ArrayList<>(); + PersistentVolumeClaim pvc = getClaimCheWorkspace(); + if (pvc != null) { + String subPath = getWorkspaceSubpath(volumes); + if (subPath != null) { + VolumeMount vm = new VolumeMountBuilder() + .withMountPath(cheWorkspaceProjectsStorage) + .withName(workspacesPersistentVolumeClaim) + .withSubPath(subPath) .build(); - vms.add(vm); + vms.add(vm); + } } return vms; } - private List getVolumesFrom(String[] volumes, String workspaceID) { + private List getVolumesFrom(String[] volumes) { List vs = new ArrayList<>(); - for (String volume : volumes) { - String hostPath = volume.split(":",3)[0]; - String volumeName = getVolumeName(volume); - - Volume v = new VolumeBuilder() - .withNewHostPath(hostPath) - .withName("ws-" + workspaceID + "-" + volumeName) - .build(); - vs.add(v); + PersistentVolumeClaim pvc = getClaimCheWorkspace(); + if (pvc != null) { + for (String volume : volumes) { + String mountPath = volume.split(":",3)[1]; + if (cheWorkspaceProjectsStorage.equals(mountPath)) { + PersistentVolumeClaimVolumeSource pvcs = new PersistentVolumeClaimVolumeSourceBuilder() + .withClaimName(workspacesPersistentVolumeClaim) + .build(); + Volume v = new VolumeBuilder() + .withPersistentVolumeClaim(pvcs) + .withName(workspacesPersistentVolumeClaim) + .build(); + vs.add(v); + } + } } return vs; } - private String getVolumeName(String volume) { - if (volume.contains("ws-agent")) { - return "wsagent-lib"; - } - - if (volume.contains("terminal")) { - return "terminal"; - } - - if (volume.contains("workspaces")) { - return "project"; + private PersistentVolumeClaim getClaimCheWorkspace() { + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + PersistentVolumeClaimList pvcList = openShiftClient.persistentVolumeClaims().inNamespace(openShiftCheProjectName).list(); + for(PersistentVolumeClaim pvc: pvcList.getItems()) { + if (workspacesPersistentVolumeClaim.equals(pvc.getMetadata().getName())) { + return pvc; + } + } + Map requests = new HashMap<>(); + requests.put("storage", new Quantity(workspacesPvcQuantity)); + Map annotations = Collections.singletonMap(OPENSHIFT_VOLUME_STORAGE_CLASS, OPENSHIFT_VOLUME_STORAGE_CLASS_NAME); + PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder() + .withNewMetadata() + .withName(workspacesPersistentVolumeClaim) + .withAnnotations(annotations) + .endMetadata() + .withNewSpec() + .withAccessModes("ReadWriteOnce") + .withNewResources() + .withRequests(requests) + .endResources() + .endSpec() + .build(); + pvc = openShiftClient.persistentVolumeClaims().inNamespace(openShiftCheProjectName).create(pvc); + LOG.info("Creating OpenShift PVC {}", pvc.getMetadata().getName()); + return pvc; } - - return "unknown-volume"; } private String waitAndRetrieveContainerID(String deploymentName) throws IOException { - for (int i = 0; i < OPENSHIFT_WAIT_POD_TIMEOUT; i++) { - try { - Thread.sleep(OPENSHIFT_WAIT_POD_DELAY); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - - List pods = openShiftClient.pods() - .inNamespace(this.openShiftCheProjectName) - .withLabel(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName) - .list() - .getItems(); - - if (pods.size() < 1) { - throw new OpenShiftException(String.format("Pod with deployment name %s not found", - deploymentName)); - } else if (pods.size() > 1) { - throw new OpenShiftException(String.format("Multiple pods with deployment name %s found", - deploymentName)); - } + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + for (int i = 0; i < OPENSHIFT_WAIT_POD_TIMEOUT; i++) { + try { + Thread.sleep(OPENSHIFT_WAIT_POD_DELAY); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + List pods = openShiftClient.pods() + .inNamespace(this.openShiftCheProjectName) + .withLabel(OPENSHIFT_DEPLOYMENT_LABEL, deploymentName) + .list() + .getItems(); + + if (pods.size() < 1) { + throw new OpenShiftException(String.format("Pod with deployment name %s not found", + deploymentName)); + } else if (pods.size() > 1) { + throw new OpenShiftException(String.format("Multiple pods with deployment name %s found", + deploymentName)); + } - Pod pod = pods.get(0); - String status = pod.getStatus().getPhase(); - if (OPENSHIFT_POD_STATUS_RUNNING.equals(status)) { - String containerID = pod.getStatus().getContainerStatuses().get(0).getContainerID(); - String normalizedID = KubernetesStringUtils.normalizeContainerID(containerID); - openShiftClient.pods() - .inNamespace(openShiftCheProjectName) - .withName(pod.getMetadata().getName()) - .edit() - .editMetadata() - .addToLabels(CHE_CONTAINER_IDENTIFIER_LABEL_KEY, - KubernetesStringUtils.getLabelFromContainerID(normalizedID)) - .endMetadata() - .done(); - return normalizedID; + Pod pod = pods.get(0); + String status = pod.getStatus().getPhase(); + if (OPENSHIFT_POD_STATUS_RUNNING.equals(status)) { + String containerID = pod.getStatus().getContainerStatuses().get(0).getContainerID(); + String normalizedID = KubernetesStringUtils.normalizeContainerID(containerID); + openShiftClient.pods() + .inNamespace(openShiftCheProjectName) + .withName(pod.getMetadata().getName()) + .edit() + .editMetadata() + .addToLabels(CHE_CONTAINER_IDENTIFIER_LABEL_KEY, + KubernetesStringUtils.getLabelFromContainerID(normalizedID)) + .endMetadata() + .done(); + return normalizedID; + } } + return null; } - return null; } /** @@ -1127,19 +1654,6 @@ private Set getExposedPorts(Set containerExposedPorts, Set + * Creates a short-lived Pod using a CentOS image which mounts a specified PVC and + * executes a command (either {@code mkdir -p } or {@code rm -rf + * For mkdir commands, an in-memory list of created workspaces is stored and used to avoid + * calling mkdir unnecessarily. However, this list is not persisted, so dir creation is + * not tracked between restarts. + * + * @author amisevsk + */ +public class OpenShiftPvcHelper { + + private static final Logger LOG = LoggerFactory.getLogger(OpenShiftPvcHelper.class); + + private static final String POD_PHASE_SUCCEEDED = "Succeeded"; + private static final String POD_PHASE_FAILED = "Failed"; + private static final String[] MKDIR_WORKSPACE_COMMAND = new String[] {"mkdir", "-p"}; + private static final String[] RMDIR_WORKSPACE_COMMAND = new String[] {"rm", "-rf"}; + + private static final Set createdWorkspaces = ConcurrentHashMap.newKeySet(); + + private final String jobImage; + private final String jobMemoryLimit; + + protected enum Command {REMOVE, MAKE} + + @Inject + protected OpenShiftPvcHelper(@Named("che.openshift.jobs.image") String jobImage, + @Named("che.openshift.jobs.memorylimit") String jobMemoryLimit) { + this.jobImage = jobImage; + this.jobMemoryLimit = jobMemoryLimit; + } + + /** + * Creates a pod with {@code command} and reports whether it succeeded + * @param workspacesPvcName + * name of the PVC to mount + * @param projectNamespace + * OpenShift namespace + * @param jobNamePrefix + * prefix used for pod metadata name. Name structure will normally + * be {@code } if only one path is passed, or + * {@code batch} if multiple paths are provided + * @param command + * command to execute in PVC. + * @param workspaceDirs + * list of arguments attached to command. A list of directories to + * create/delete. + * @return true if Pod terminates with phase "Succeeded" or mkdir command issued + * for already created worksapce, false otherwise. + * + * @see Command + */ + protected boolean createJobPod(String workspacesPvcName, + String projectNamespace, + String jobNamePrefix, + Command command, + String... workspaceDirs) { + + if (workspaceDirs.length == 0) { + return true; + } + + if (Command.MAKE.equals(command)) { + String[] dirsToCreate = filterDirsToCreate(workspaceDirs); + if (dirsToCreate.length == 0) { + return true; + } + workspaceDirs = dirsToCreate; + } + + VolumeMount vm = new VolumeMountBuilder() + .withMountPath("/projects") + .withName(workspacesPvcName) + .build(); + + PersistentVolumeClaimVolumeSource pvcs = new PersistentVolumeClaimVolumeSourceBuilder() + .withClaimName(workspacesPvcName) + .build(); + + Volume volume = new VolumeBuilder() + .withPersistentVolumeClaim(pvcs) + .withName(workspacesPvcName) + .build(); + + String[] jobCommand = getCommand(command, "/projects/", workspaceDirs); + LOG.info("Executing command {} in PVC {} for {} dirs", jobCommand[0], workspacesPvcName, workspaceDirs.length); + + Map limit = Collections.singletonMap("memory", new Quantity(jobMemoryLimit)); + + String podName = workspaceDirs.length > 1 ? jobNamePrefix + "batch" + : jobNamePrefix + workspaceDirs[0]; + + Container container = new ContainerBuilder().withName(podName) + .withImage(jobImage) + .withImagePullPolicy("IfNotPresent") + .withNewSecurityContext() + .withPrivileged(false) + .endSecurityContext() + .withCommand(jobCommand) + .withVolumeMounts(vm) + .withNewResources() + .withLimits(limit) + .endResources() + .build(); + + Pod podSpec = new PodBuilder().withNewMetadata() + .withName(podName) + .endMetadata() + .withNewSpec() + .withContainers(container) + .withVolumes(volume) + .withRestartPolicy("Never") + .endSpec() + .build(); + + + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()){ + openShiftClient.pods().inNamespace(projectNamespace).create(podSpec); + boolean completed = false; + while(!completed) { + Pod pod = openShiftClient.pods().inNamespace(projectNamespace).withName(podName).get(); + String phase = pod.getStatus().getPhase(); + switch (phase) { + case POD_PHASE_FAILED: + LOG.info("Pod command {} failed", Arrays.toString(jobCommand)); + case POD_PHASE_SUCCEEDED: + openShiftClient.resource(pod).delete(); + updateCreatedDirs(command, phase, workspaceDirs); + return POD_PHASE_SUCCEEDED.equals(phase); + default: + Thread.sleep(1000); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return false; + } + + private String[] getCommand(Command commandType, String mountPath, String... dirs) { + String[] command = new String[0]; + switch (commandType) { + case MAKE : + command = MKDIR_WORKSPACE_COMMAND; + break; + case REMOVE : + command = RMDIR_WORKSPACE_COMMAND; + break; + } + + String[] dirsWithPath = Arrays.asList(dirs).stream() + .map(dir -> mountPath + dir) + .toArray(String[]::new); + + String[] fullCommand = new String[command.length + dirsWithPath.length]; + + System.arraycopy(command, 0, fullCommand, 0, command.length); + System.arraycopy(dirsWithPath, 0, fullCommand, command.length, dirsWithPath.length); + return fullCommand; + } + + private void updateCreatedDirs(Command command, String phase, String... workspaceDirs) { + if (!POD_PHASE_SUCCEEDED.equals(phase)) { + return; + } + List dirs = Arrays.asList(workspaceDirs); + switch (command) { + case MAKE: + createdWorkspaces.addAll(dirs); + break; + case REMOVE: + createdWorkspaces.removeAll(dirs); + break; + } + } + + private String[] filterDirsToCreate(String[] allDirs) { + List dirs = Arrays.asList(allDirs); + List dirsToCreate = new ArrayList<>(); + for(String dir : dirs) { + if (!createdWorkspaces.contains(dir)) { + dirsToCreate.add(dir); + } + } + return dirsToCreate.toArray(new String[dirsToCreate.size()]); + } +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftRouteCreator.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftRouteCreator.java new file mode 100644 index 00000000000..05a7034ef25 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftRouteCreator.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.openshift.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.openshift.api.model.DoneableRoute; +import io.fabric8.openshift.api.model.Route; +import io.fabric8.openshift.api.model.RouteFluent.SpecNested; +import io.fabric8.openshift.client.DefaultOpenShiftClient; +import io.fabric8.openshift.client.OpenShiftClient; + +public class OpenShiftRouteCreator { + private static final Logger LOG = LoggerFactory.getLogger(OpenShiftRouteCreator.class); + private static final String TLS_TERMINATION_EDGE = "edge"; + private static final String REDIRECT_INSECURE_EDGE_TERMINATION_POLICY = "Redirect"; + + public static void createRoute (final String namespace, + final String openShiftNamespaceExternalAddress, + final String serverRef, + final String serviceName, + final String deploymentName, + final String routeId, + final boolean enableTls) { + + if (openShiftNamespaceExternalAddress == null) { + throw new IllegalArgumentException("Property che.docker.ip.external must be set when using openshift."); + } + + try (OpenShiftClient openShiftClient = new DefaultOpenShiftClient()) { + String routeName = generateRouteName(routeId, serverRef); + String serviceHost = generateRouteHost(routeName, openShiftNamespaceExternalAddress); + + SpecNested routeSpec = openShiftClient + .routes() + .inNamespace(namespace) + .createNew() + .withNewMetadata() + .withName(routeName) + .addToLabels(OpenShiftConnector.OPENSHIFT_DEPLOYMENT_LABEL, deploymentName) + .endMetadata() + .withNewSpec() + .withHost(serviceHost) + .withNewTo() + .withKind("Service") + .withName(serviceName) + .endTo() + .withNewPort() + .withNewTargetPort() + .withStrVal(serverRef) + .endTargetPort() + .endPort(); + + if (enableTls) { + routeSpec.withNewTls() + .withTermination(TLS_TERMINATION_EDGE) + .withInsecureEdgeTerminationPolicy(REDIRECT_INSECURE_EDGE_TERMINATION_POLICY) + .endTls(); + } + + Route route = routeSpec.endSpec().done(); + + LOG.info("OpenShift route {} created", route.getMetadata().getName()); + } + } + + private static String generateRouteName(final String serviceName, final String serverRef) { + return serverRef + "-" + serviceName; + } + + private static String generateRouteHost(final String routeName, final String openShiftNamespaceExternalAddress) { + return routeName + "-" + openShiftNamespaceExternalAddress; + } +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftWorkspaceFilesCleaner.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftWorkspaceFilesCleaner.java new file mode 100644 index 00000000000..85afdad7e16 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/OpenShiftWorkspaceFilesCleaner.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.openshift.client; + +import static com.google.common.base.Strings.isNullOrEmpty; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.event.ServerIdleEvent; +import org.eclipse.che.api.core.model.workspace.Workspace; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.core.notification.EventSubscriber; +import org.eclipse.che.api.workspace.server.WorkspaceFilesCleaner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; + +/** + * Class used to remove workspace directories in Persistent Volume when a workspace + * is delete while running on OpenShift. Deleted workspace directories are stored + * in a list. Upon Che server idling, all of these workspaces are deleted simultaneously + * from the PVC using a {@link OpenShiftPvcHelper} job. + *

+ * Since deleting a workspace does not immediately remove its files, re-creating a workspace + * with a previously used name can result in files from the previous workspace still being + * present. + * + * @see WorkspaceFilesCleaner + * @author amisevsk + */ +@Singleton +public class OpenShiftWorkspaceFilesCleaner implements WorkspaceFilesCleaner { + + private static final Logger LOG = LoggerFactory.getLogger(OpenShiftConnector.class); + private static final Set deleteQueue = ConcurrentHashMap.newKeySet(); + private final String projectNamespace; + private final String workspacesPvcName; + private final OpenShiftPvcHelper openShiftPvcHelper; + + @Inject + public OpenShiftWorkspaceFilesCleaner(EventService eventService, + OpenShiftPvcHelper openShiftPvcHelper, + @Named("che.openshift.project") String projectNamespace, + @Named("che.openshift.workspaces.pvc.name") String workspacesPvcName) { + this.projectNamespace = projectNamespace; + this.workspacesPvcName = workspacesPvcName; + this.openShiftPvcHelper = openShiftPvcHelper; + eventService.subscribe(new EventSubscriber() { + @Override + public void onEvent(ServerIdleEvent event) { + deleteWorkspacesInQueue(event); + } + }); + } + + @Override + public void clear(Workspace workspace) throws IOException, ServerException { + String workspaceName = workspace.getConfig().getName(); + if (isNullOrEmpty(workspaceName)) { + LOG.error("Could not get workspace name for files removal."); + return; + } + deleteQueue.add(workspaceName); + } + + private void deleteWorkspacesInQueue(ServerIdleEvent event) { + List deleteQueueCopy = new ArrayList<>(deleteQueue); + String[] dirsToDelete = deleteQueueCopy.toArray(new String[deleteQueueCopy.size()]); + + LOG.info("Deleting {} workspaces on PVC {}", deleteQueueCopy.size(), workspacesPvcName); + boolean successful = openShiftPvcHelper.createJobPod(workspacesPvcName, + projectNamespace, + "delete-", + OpenShiftPvcHelper.Command.REMOVE, + dirsToDelete); + if (successful) { + deleteQueue.removeAll(deleteQueueCopy); + } + } + + /** + * Clears the list of workspace directories to be deleted. Necessary for testing. + */ + @VisibleForTesting + protected static void clearDeleteQueue() { + deleteQueue.clear(); + } +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainer.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainer.java index dc4c0a66802..a37b59fc562 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainer.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainer.java @@ -47,7 +47,7 @@ public static List getContainerPortsFrom(Set exposedPorts int portNumber = Integer.parseInt(port); String portName = CheServicePorts.get().get(portNumber); - portName = isNullOrEmpty(portName) ? exposedPort.replace("/", "-") : portName; + portName = isNullOrEmpty(portName) ? "server-" + exposedPort.replace("/", "-") : portName; ContainerPort containerPort = new ContainerPortBuilder().withName(portName).withProtocol(protocol) .withContainerPort(portNumber).build(); diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesExecHolder.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesExecHolder.java new file mode 100644 index 00000000000..ad0af92e117 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesExecHolder.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.openshift.client.kubernetes; + +import java.util.Arrays; + +import org.eclipse.che.plugin.openshift.client.OpenShiftConnector; + +/** + * Holder class for metadata about an exec, to be used with {@link OpenShiftConnector}. + * + *

In OpenShift, {@code createExec()} is not separate from {@code startExec()}, + * so this class has to be used to pass data between {@code createExec()} and + * {@code startExec()} calls. + * + * @see OpenShiftConnector#createExec(org.eclipse.che.plugin.docker.client.params.CreateExecParams) + * @see OpenShiftConnector#startExec(org.eclipse.che.plugin.docker.client.params.StartExecParams, org.eclipse.che.plugin.docker.client.MessageProcessor) + */ +public class KubernetesExecHolder { + + private String[] command; + private String podName; + + public KubernetesExecHolder withCommand(String[] command) { + this.command = command; + return this; + } + + public KubernetesExecHolder withPod(String podName) { + this.podName = podName; + return this; + } + + public String[] getCommand() { + return command; + } + + public String getPod() { + return podName; + } + + public String toString() { + return String.format("KubernetesExecHolder {command=%s, podName=%s}", + Arrays.asList(command).toString(), + podName); + } +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverter.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverter.java index e499117decd..2931e36ab57 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverter.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverter.java @@ -64,6 +64,9 @@ public static String getCheServerLabelPrefix() { */ public static Map labelsToNames(Map labels) { Map names = new HashMap<>(); + if (labels == null) { + return names; + } for (Map.Entry label : labels.entrySet()) { if (!hasConversionProblems(label)) { @@ -103,6 +106,9 @@ public static Map labelsToNames(Map labels) { */ public static Map namesToLabels(Map names) { Map labels = new HashMap<>(); + if (names == null) { + return labels; + } for (Map.Entry entry: names.entrySet()){ String key = entry.getKey(); String value = entry.getValue(); diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesOutputAdapter.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesOutputAdapter.java new file mode 100644 index 00000000000..2d87a77053b --- /dev/null +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesOutputAdapter.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.openshift.client.kubernetes; + +import io.fabric8.kubernetes.client.Callback; +import io.fabric8.kubernetes.client.utils.InputStreamPumper; + +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.plugin.docker.client.LogMessage; +import org.eclipse.che.plugin.docker.client.MessageProcessor; + +/** + * Adapter class for passing data from a {@code kubernetes-client} output stream (e.g. + * for an exec call) to {@link MessageProcessor}. This class should be passed to a + * {@link InputStreamPumper} along with the output of the exec call. + * + *

Output passed in via the {@link #call(byte[])} method is parsed into lines, + * (respecting {@code '\n'} and {@code CRLF} as line separators), and + * passed to the {@link MessageProcessor} as {@link LogMessage}s. + */ +public class KubernetesOutputAdapter implements Callback { + + private LogMessage.Type type; + private MessageProcessor execOutputProcessor; + private StringBuilder lineBuffer; + + /** + * Create a new KubernetesOutputAdapter + * + * @param type + * the type of LogMessages being passed to the MessageProcessor + * @param processor + * the processor receiving LogMessages. If null, calling {@link #call(byte[])} + * will return immediately. + */ + public KubernetesOutputAdapter(LogMessage.Type type, + @Nullable MessageProcessor processor) { + this.type = type; + this.execOutputProcessor = processor; + this.lineBuffer = new StringBuilder(); + } + + @Override + public void call(byte[] data) { + if (data == null || data.length == 0 || execOutputProcessor == null) { + return; + } + int start = 0; + int offset = 0; + + for (int pos = 0; pos < data.length; pos++) { + if (data[pos] == '\n' || data[pos] == '\r') { + offset = pos - start; + String line = new String(data, start, offset); + lineBuffer.append(line); + execOutputProcessor.process(new LogMessage(type, lineBuffer.toString())); + lineBuffer.setLength(0); + if (data[pos] == '\r') { + pos += 1; + } + start = pos + 1; + } + } + String trailingChars = new String(data, start, data.length - start); + lineBuffer.append(trailingChars); + } +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesService.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesService.java index 33e62b16e5c..df179410df1 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesService.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesService.java @@ -47,7 +47,7 @@ public static List getServicePortsFrom(Set exposedPorts) { int portNumber = Integer.parseInt(port); String portName = CheServicePorts.get().get(portNumber); - portName = isNullOrEmpty(portName) ? exposedPort.replace("/", "-") : portName; + portName = isNullOrEmpty(portName) ? "server-" + exposedPort.replace("/", "-") : portName; int targetPortNumber = portNumber; ServicePort servicePort = new ServicePort(); diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnectorTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnectorTest.java index b9949ab36cc..3726af54d30 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnectorTest.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftConnectorTest.java @@ -16,6 +16,7 @@ import java.io.IOException; +import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.plugin.docker.client.DockerApiVersionPathPrefixProvider; import org.eclipse.che.plugin.docker.client.DockerConnectorConfiguration; import org.eclipse.che.plugin.docker.client.DockerRegistryAuthResolver; @@ -31,9 +32,17 @@ public class OpenShiftConnectorTest { private static final String[] CONTAINER_ENV_VARIABLES = {"CHE_WORKSPACE_ID=abcd1234"}; private static final String CHE_DEFAULT_OPENSHIFT_PROJECT_NAME = "eclipse-che"; - private static final String CHE_DEFAULT_OPENSHIFT_SERVICEACCOUNT = "cheserviceaccount"; private static final int OPENSHIFT_LIVENESS_PROBE_DELAY = 300; private static final int OPENSHIFT_LIVENESS_PROBE_TIMEOUT = 1; + private static final String OPENSHIFT_DEFAULT_WORKSPACE_PERSISTENT_VOLUME_CLAIM = "che_claim_data"; + private static final String OPENSHIFT_DEFAULT_WORKSPACE_QUANTITY = "10Gi"; + private static final String OPENSHIFT_DEFAULT_WORKSPACE_STORAGE = "/data/workspaces"; + private static final String OPENSHIFT_DEFAULT_WORKSPACE_PROJECTS_STORAGE = "/projects"; + private static final String CHE_DEFAULT_SERVER_EXTERNAL_ADDRESS = "che.openshift.mini"; + private static final String CHE_WORKSPACE_CPU_LIMIT = "1"; + private static final boolean SECURE_ROUTES = false; + private static final boolean CREATE_WORKSPACE_DIRS = true; + @Mock private DockerConnectorConfiguration dockerConnectorConfiguration; @@ -45,6 +54,10 @@ public class OpenShiftConnectorTest { private DockerApiVersionPathPrefixProvider dockerApiVersionPathPrefixProvider; @Mock private CreateContainerParams createContainerParams; + @Mock + private EventService eventService; + @Mock + private OpenShiftPvcHelper openShiftPvcHelper; private OpenShiftConnector openShiftConnector; @@ -62,10 +75,20 @@ public void shouldGetWorkspaceIDWhenAValidOneIsProvidedInCreateContainerParams() dockerConnectionFactory, authManager, dockerApiVersionPathPrefixProvider, + openShiftPvcHelper, + eventService, + CHE_DEFAULT_SERVER_EXTERNAL_ADDRESS, CHE_DEFAULT_OPENSHIFT_PROJECT_NAME, - CHE_DEFAULT_OPENSHIFT_SERVICEACCOUNT, OPENSHIFT_LIVENESS_PROBE_DELAY, - OPENSHIFT_LIVENESS_PROBE_TIMEOUT); + OPENSHIFT_LIVENESS_PROBE_TIMEOUT, + OPENSHIFT_DEFAULT_WORKSPACE_PERSISTENT_VOLUME_CLAIM, + OPENSHIFT_DEFAULT_WORKSPACE_QUANTITY, + OPENSHIFT_DEFAULT_WORKSPACE_STORAGE, + OPENSHIFT_DEFAULT_WORKSPACE_PROJECTS_STORAGE, + CHE_WORKSPACE_CPU_LIMIT, + null, + SECURE_ROUTES, + CREATE_WORKSPACE_DIRS); String workspaceID = openShiftConnector.getCheWorkspaceId(createContainerParams); //Then diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftWorkspaceFilesCleanerTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftWorkspaceFilesCleanerTest.java new file mode 100644 index 00000000000..828e52ae60e --- /dev/null +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/OpenShiftWorkspaceFilesCleanerTest.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.openshift.client; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.event.ServerIdleEvent; +import org.eclipse.che.api.core.model.workspace.Workspace; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class OpenShiftWorkspaceFilesCleanerTest { + + private static final String CHE_OPENSHIFT_PROJECT = "eclipse-che"; + private static final String WORKSPACES_PVC_NAME = "che-data-volume"; + private static final String WORKSPACE_ONE = "testworkspaceone"; + private static final String WORKSPACE_TWO = "testworkspacetwo"; + + @Mock + private OpenShiftPvcHelper pvcHelper; + @Mock + private ServerIdleEvent serverIdleEvent; + private EventService eventService; + private OpenShiftWorkspaceFilesCleaner cleaner; + + @BeforeMethod + public void setup() { + OpenShiftWorkspaceFilesCleaner.clearDeleteQueue(); + MockitoAnnotations.initMocks(this); + eventService = new EventService(); + cleaner = new OpenShiftWorkspaceFilesCleaner(eventService, + pvcHelper, + CHE_OPENSHIFT_PROJECT, + WORKSPACES_PVC_NAME); + } + + @Test + public void shouldDoNothingWithoutIdleEvent() throws ServerException, IOException { + // Given + Workspace workspace = generateWorkspace(WORKSPACE_ONE); + + // When + cleaner.clear(workspace); + + // Then + verify(pvcHelper, never()).createJobPod(anyString(), + anyString(), + anyString(), + any(OpenShiftPvcHelper.Command.class), + any(String[].class)); + } + + @Test + public void shouldDeleteWorkspaceOnIdleEvent() throws ServerException, IOException { + // Given + Workspace workspace = generateWorkspace(WORKSPACE_ONE); + + // When + cleaner.clear(workspace); + eventService.publish(serverIdleEvent); + + // Then + verify(pvcHelper, times(1)).createJobPod(anyString(), + anyString(), + anyString(), + eq(OpenShiftPvcHelper.Command.REMOVE), + eq(WORKSPACE_ONE)); + } + + @Test + public void shouldDeleteMultipleQueuedWorkspacesAtOnce() throws ServerException, IOException { + // Given + Workspace workspaceOne = generateWorkspace(WORKSPACE_ONE); + Workspace workspaceTwo = generateWorkspace(WORKSPACE_TWO); + String[] expectedDirs = new String[] {WORKSPACE_ONE, WORKSPACE_TWO}; + ArgumentCaptor dirCaptor = ArgumentCaptor.forClass(String.class); + + // When + cleaner.clear(workspaceOne); + cleaner.clear(workspaceTwo); + eventService.publish(serverIdleEvent); + + // Then + verify(pvcHelper, times(1)).createJobPod(anyString(), + anyString(), + anyString(), + eq(OpenShiftPvcHelper.Command.REMOVE), + dirCaptor.capture(), // Varargs capture doesn't seem to work. + dirCaptor.capture()); + + List dirs = dirCaptor.getAllValues(); + String[] actualDirs = dirs.toArray(new String[dirs.size()]); + // Sort arrays to ignore order + Arrays.sort(actualDirs); + Arrays.sort(expectedDirs); + assertEquals(actualDirs, expectedDirs, "Expected all dirs to be deleted when server is idled."); + } + + @Test + public void shouldRetainQueueIfDeletionFails() throws ServerException, IOException { + // Given + Workspace workspaceOne = generateWorkspace(WORKSPACE_ONE); + when(pvcHelper.createJobPod(any(), any(), any(), any(), any())).thenReturn(false); + + // When + cleaner.clear(workspaceOne); + eventService.publish(serverIdleEvent); + + // Then + verify(pvcHelper, times(1)).createJobPod(anyString(), + anyString(), + anyString(), + eq(OpenShiftPvcHelper.Command.REMOVE), + eq(WORKSPACE_ONE)); + + // When + eventService.publish(serverIdleEvent); + + // Then + verify(pvcHelper, times(2)).createJobPod(anyString(), + anyString(), + anyString(), + eq(OpenShiftPvcHelper.Command.REMOVE), + eq(WORKSPACE_ONE)); + } + + @Test + public void shouldUseProjectNamespaceAndPvcNameAsParameters() throws ServerException, IOException { + // Given + Workspace workspaceOne = generateWorkspace(WORKSPACE_ONE); + + // When + cleaner.clear(workspaceOne); + eventService.publish(serverIdleEvent); + + // Then + verify(pvcHelper, times(1)).createJobPod(eq(WORKSPACES_PVC_NAME), + eq(CHE_OPENSHIFT_PROJECT), + anyString(), + eq(OpenShiftPvcHelper.Command.REMOVE), + eq(WORKSPACE_ONE)); + } + + private Workspace generateWorkspace(String id) { + WorkspaceConfigImpl config = new WorkspaceConfigImpl(); + config.setName(id); + return new WorkspaceImpl(id, null, config); + } +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainerTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainerTest.java index d3cd0be897c..83c0c1dc2a4 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainerTest.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesContainerTest.java @@ -44,7 +44,7 @@ public void shouldReturnContainerPortFromExposedPortList() { map(p -> Integer.toString(p.getContainerPort()) + "/" + p.getProtocol().toLowerCase()).collect(Collectors.toList()); - assertTrue(exposedPorts.stream().anyMatch(portsAndProtocols::contains)); + assertTrue(exposedPorts.stream().allMatch(portsAndProtocols::contains)); } @Test @@ -61,7 +61,7 @@ public void shouldReturnContainerPortListFromImageExposedPortList() { map(p -> Integer.toString(p.getContainerPort()) + "/" + p.getProtocol().toLowerCase()).collect(Collectors.toList()); - assertTrue(imageExposedPorts.keySet().stream().anyMatch(portsAndProtocols::contains)); + assertTrue(imageExposedPorts.keySet().stream().allMatch(portsAndProtocols::contains)); } } diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesEnvVarTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesEnvVarTest.java index 36adea1fd2e..ccb63d44b10 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesEnvVarTest.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesEnvVarTest.java @@ -44,7 +44,7 @@ public void shouldReturnContainerEnvFromEnvVariableArray() { // Then List keysAndValues = env.stream().map(k -> k.getName() + "=" + k.getValue()).collect(Collectors.toList()); - assertTrue(Arrays.stream(envVariables).anyMatch(keysAndValues::contains)); + assertTrue(Arrays.stream(envVariables).allMatch(keysAndValues::contains)); } } diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverterTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverterTest.java index bf43eefc9d3..51ad72ac30b 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverterTest.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesLabelConverterTest.java @@ -16,15 +16,15 @@ import java.util.HashMap; import java.util.Map; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class KubernetesLabelConverterTest { + private final String prefix = KubernetesLabelConverter.getCheServerLabelPrefix(); + @Test public void shouldConvertLabelsToValidKubernetesLabelNames() { String validLabelRegex = "([A-Za-z0-9][-A-Za-z0-9_\\.]*)?[A-Za-z0-9]"; - String prefix = KubernetesLabelConverter.getCheServerLabelPrefix(); // Given Map labels = new HashMap<>(); @@ -46,7 +46,6 @@ public void shouldConvertLabelsToValidKubernetesLabelNames() { @Test public void shouldBeAbleToRecoverOriginalLabelsAfterConversion() { // Given - String prefix = KubernetesLabelConverter.getCheServerLabelPrefix(); Map originalLabels = new HashMap<>(); originalLabels.put(prefix + "4401/tcp:path:", "/api"); originalLabels.put(prefix + "8000/tcp:ref:", "tomcat-debug"); @@ -59,4 +58,58 @@ public void shouldBeAbleToRecoverOriginalLabelsAfterConversion() { assertEquals(originalLabels, unconverted); } + @Test + public void shouldIgnoreAndLogProblemLabels() { + // Given + Map originalLabels = new HashMap<>(); + Map validLabels = new HashMap<>(); + validLabels.put(prefix + "4401/tcp:path:", "/api"); + validLabels.put(prefix + "8000/tcp:ref:", "tomcat-debug"); + Map invalidLabels = new HashMap<>(); + invalidLabels.put(prefix + "9999/t.cp:path:", "/api"); + invalidLabels.put(prefix + "1111/tcp:path:", "/a_pi"); + + originalLabels.putAll(validLabels); + originalLabels.putAll(invalidLabels); + + // When + Map converted = KubernetesLabelConverter.labelsToNames(originalLabels); + Map unconverted = KubernetesLabelConverter.namesToLabels(converted); + + // Then + assertTrue(validLabels.entrySet().stream().allMatch(unconverted.entrySet()::contains), + "Valid labels should be there when converting + unconverting"); + assertTrue(invalidLabels.entrySet().stream().noneMatch(unconverted.entrySet()::contains), + "Labels with invalid characters should be ignored"); + } + + @Test + public void shouldIgnoreEmptyValues() { + // Given + Map originalLabels = new HashMap<>(); + originalLabels.put(prefix + "4401/tcp:path:", null); + originalLabels.put(prefix + "4402/tcp:path:", ""); + originalLabels.put(prefix + "4403/tcp:path:", " "); + + // When + Map converted = KubernetesLabelConverter.labelsToNames(originalLabels); + + // Then + assertTrue(converted.isEmpty(), "Labels with null, empty, or whitespace values should be ignored"); + } + + @Test + public void shouldNotIgnoreValuesWithoutPrefix() { + // Given + Map originalLabels = new HashMap<>(); + originalLabels.put("4401/tcp:path:", "/api"); + originalLabels.put(prefix + "8000/tcp:ref:", "tomcat-debug"); + + // When + Map converted = KubernetesLabelConverter.labelsToNames(originalLabels); + + // Then + // Currently we put a warning in the logs but convert these labels anyways. + assertTrue(converted.size() == 2, "Should convert labels even without prefix"); + } } diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesOutputAdapterTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesOutputAdapterTest.java new file mode 100644 index 00000000000..8dd5a571f5f --- /dev/null +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesOutputAdapterTest.java @@ -0,0 +1,245 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Red Hat, Inc. + * 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: + * Red Hat, Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.che.plugin.openshift.client.kubernetes; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.che.plugin.docker.client.LogMessage; +import org.eclipse.che.plugin.docker.client.MessageProcessor; + + +public class KubernetesOutputAdapterTest { + + private static LogMessage.Type LOG_TYPE = LogMessage.Type.DOCKER; + private testMessageProcessor processor; + private KubernetesOutputAdapter adapter; + + private class testMessageProcessor implements MessageProcessor { + + private List messages; + private LogMessage.Type type = null; + + public testMessageProcessor() { + this.messages = new ArrayList<>(); + } + + @Override + public void process(LogMessage message) { + LogMessage.Type messageType = message.getType(); + if (type == null) { + type = messageType; + } + messages.add(message.getContent()); + } + + public List getMessages() { + return new ArrayList<>(messages); + } + + public LogMessage.Type getType() { + return type; + } + }; + + @BeforeMethod + public void setUp() { + processor = new testMessageProcessor(); + adapter = new KubernetesOutputAdapter(LOG_TYPE, processor); + } + + @Test + public void shouldBreakLinesCorrectly() { + // Given + byte[] input = "line1\nline2\n".getBytes(); + List expected = generateExpected("line1", "line2"); + + // When + adapter.call(input); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "Should break lines on \\n char"); + } + + @Test + public void shouldCacheUnfinishedLinesBetweenCalls() { + // Given + byte[] firstInput = "line1\nlin".getBytes(); + byte[] secondInput = "e2\nline3\n".getBytes(); + List expected = generateExpected("line1", "line2", "line3"); + + // When + adapter.call(firstInput); + adapter.call(secondInput); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "Should store unfinished lines between calls"); + } + + @Test + public void shouldUseProvidedLogMessageType() { + for (LogMessage.Type type : LogMessage.Type.values()) { + // Given + byte[] input = "line1\n".getBytes(); + LogMessage.Type expected = type; + processor = new testMessageProcessor(); + adapter = new KubernetesOutputAdapter(type, processor); + + // When + adapter.call(input); + + // Then + LogMessage.Type actual = processor.getType(); + assertEquals(actual, expected, "Should call MessageProcessor with provided type"); + } + } + + @Test + public void shouldBreakLinesNormallyWithCarriageReturn() { + // Given + byte[] input = "line1\r\nline2\n".getBytes(); + List expected = generateExpected("line1", "line2"); + + // When + adapter.call(input); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "Should break lines normally on \\r\\n characters"); + } + + @Test + public void shouldNotIgnoreEmptyLines() { + // Given + byte[] input = "line1\n\nline2\n".getBytes(); + List expected = generateExpected("line1", "", "line2"); + + // When + adapter.call(input); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "Should call processor.process() with empty Strings"); + } + + @Test + public void shouldNotCallWithoutFinalNewline() { + // Given + byte[] input = "line1\nline2".getBytes(); // No trailing \n + List firstExpected = generateExpected("line1"); + List secondExpected = generateExpected("line1", "line2"); + + // When + adapter.call(input); + + // Then + List firstActual = processor.getMessages(); + assertEquals(firstActual, firstExpected, "Should only process lines when they are terminated by \\n or \\r\\n"); + + // When + adapter.call("\n".getBytes()); + + // Then + List secondActual = processor.getMessages(); + assertEquals(secondActual, secondExpected, "Should buffer lines until newline is encountered."); + + } + + @Test + public void shouldIgnoreNullCalls() { + // Given + byte[] firstInput = "line1\n".getBytes(); + byte[] secondInput = "line2\n".getBytes(); + List expected = generateExpected("line1", "line2"); + + // When + adapter.call(firstInput); + adapter.call(null); + adapter.call(secondInput); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "Should ignore calls with null arguments"); + } + + @Test + public void shouldKeepBufferPastNullCalls() { + // Given + byte[] firstInput = "lin".getBytes(); + byte[] secondInput = "e1\nline2\n".getBytes(); + List expected = generateExpected("line1", "line2"); + + // When + adapter.call(firstInput); + adapter.call(null); + adapter.call(secondInput); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "Should ignore calls with null arguments"); + } + + @Test + public void shouldDoNothingWhenExecOutputProcessorIsNull() { + // Given + byte[] firstInput = "line1\n".getBytes(); + byte[] secondInput = "line2\n".getBytes(); + adapter = new KubernetesOutputAdapter(LOG_TYPE, null); + + // When + adapter.call(firstInput); + adapter.call(secondInput); + + // Then + List actual = processor.getMessages(); + assertTrue(actual.isEmpty(), "Should do nothing when ExecOutputProcessor is null"); + } + + @Test + public void shouldIgnoreCallsWhenDataIsEmpty() { + // Given + byte[] emptyInput = "".getBytes(); + byte[] firstInput = "line1\n".getBytes(); + byte[] secondInput = "line2\n".getBytes(); + List expected = generateExpected("line1", "line2"); + + // When + adapter.call(emptyInput); + adapter.call(firstInput); + adapter.call(emptyInput); + adapter.call(secondInput); + adapter.call(emptyInput); + + // Then + List actual = processor.getMessages(); + assertEquals(actual, expected, "KubernetesOutputAdapter ignore empty data calls"); + + } + + private List generateExpected(String... strings) { + List expected = new ArrayList<>(); + for (String string : strings) { + expected.add(string); + } + return expected; + } + + +} diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java index a1b575415b0..ebcc02a9cfa 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java @@ -40,7 +40,7 @@ public void shouldReturnServicePortListFromImageExposedPortList() { map(p -> Integer.toString(p.getPort()) + "/" + p.getProtocol()).collect(Collectors.toList()); - assertTrue(imageExposedPorts.keySet().stream().anyMatch(portsAndProtocols::contains)); + assertTrue(imageExposedPorts.keySet().stream().allMatch(portsAndProtocols::contains)); } @Test @@ -60,7 +60,7 @@ public void shouldReturnServicePortListFromExposedPortList() { map(p -> Integer.toString(p.getPort()) + "/" + p.getProtocol()).collect(Collectors.toList()); - assertTrue(exposedPorts.keySet().stream().anyMatch(portsAndProtocols::contains)); + assertTrue(exposedPorts.keySet().stream().allMatch(portsAndProtocols::contains)); } @Test @@ -73,13 +73,13 @@ public void shouldReturnServicePortNameWhenKnownPortNumberIsProvided() { exposedPorts.put("4411/tcp",null); exposedPorts.put("4412/tcp",null); exposedPorts.put("8080/tcp",null); - exposedPorts.put("8888/tcp",null); + exposedPorts.put("8000/tcp",null); exposedPorts.put("9876/tcp",null); Set expectedPortNames = new HashSet<>(); expectedPortNames.add("sshd"); expectedPortNames.add("wsagent"); - expectedPortNames.add("wsagent-pda"); + expectedPortNames.add("wsagent-jpda"); expectedPortNames.add("terminal"); expectedPortNames.add("exec-agent"); expectedPortNames.add("tomcat"); @@ -92,7 +92,7 @@ public void shouldReturnServicePortNameWhenKnownPortNumberIsProvided() { map(p -> p.getName()).collect(Collectors.toList()); // Then - assertTrue(actualPortNames.stream().anyMatch(expectedPortNames::contains)); + assertTrue(actualPortNames.stream().allMatch(expectedPortNames::contains)); } @Test @@ -102,7 +102,7 @@ public void shouldReturnServicePortNameWhenUnknownPortNumberIsProvided() { exposedPorts.put("55/tcp",null); Set expectedPortNames = new HashSet<>(); - expectedPortNames.add("55-tcp"); + expectedPortNames.add("server-55-tcp"); // When List servicePorts = KubernetesService.getServicePortsFrom(exposedPorts.keySet()); @@ -110,7 +110,7 @@ public void shouldReturnServicePortNameWhenUnknownPortNumberIsProvided() { map(p -> p.getName()).collect(Collectors.toList()); // Then - assertTrue(actualPortNames.stream().anyMatch(expectedPortNames::contains)); + assertTrue(actualPortNames.stream().allMatch(expectedPortNames::contains)); } } 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 index 32e7b9b3ebe..24db5986b27 100644 --- 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 @@ -116,7 +116,7 @@ protected void setup() throws Exception { when(imageInfoConfig.getExposedPorts()).thenReturn(imageExposedPorts); - envContainerConfig = new String[]{"CHE_WORKSPACE_ID=work123", "CHE_MACHINE_NAME=abcd"}; + envContainerConfig = new String[]{"CHE_WORKSPACE_ID=work123", "CHE_MACHINE_NAME=abcd", "CHE_IS_DEV_MACHINE=true"}; envImageConfig = new String[]{"HELLO"}; when(containerConfig.getEnv()).thenReturn(envContainerConfig); when(imageInfoConfig.getEnv()).thenReturn(envImageConfig);