/******************************************************************************* * 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 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; /** * Default constructor */ @Inject public CustomServerEvaluationStrategy(@Nullable @Named("che.docker.ip") String cheDockerIp, @Nullable @Named("che.docker.ip.external") String cheDockerIpExternal, @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<String, String> getExternalAddressesAndPorts(ContainerInfo containerInfo, String internalHost) { // create Rendering evaluation RenderingEvaluation renderingEvaluation = getOnlineRenderingEvaluation(containerInfo, internalHost); // get current ports Map<String, List<PortBinding>> 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. * * <p>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<String, ServerImpl> getServers(ContainerInfo containerInfo, String internalHost, Map<String, ServerConfImpl> serverConfMap) { Map<String, ServerImpl> 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<String, String> labels, Set<String> 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 <propertyName></propertyName> * @param port * the port for the mapping * @return the rendering of the template */ String render(String template, String port); } /** * 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 : !isNullOrEmpty(gatewayAddressContainer) ? gatewayAddressContainer : this.internalHost; } } /** * Offline implementation (container not yet created) */ protected class OfflineRenderingEvaluation extends DefaultRenderingEvaluation implements RenderingEvaluation { public OfflineRenderingEvaluation(Map<String, String> labels, Set<String> exposedPorts, String[] env) { super(labels, exposedPorts, env); } } /** * Inner class used to perform the rendering */ protected abstract class DefaultRenderingEvaluation implements RenderingEvaluation { /** * Labels */ private Map<String, String> labels; /** * Ports */ private Set<String> exposedPorts; /** * Environment variables */ private final String[] env; /** * Map with properties for all ports */ private Map<String, String> globalPropertiesMap = new HashMap<>(); /** * Mapping between a port and the server ref name */ private Map<String, String> portsToRefName; /** * Data initialized ? */ private boolean initialized; /** * Default constructor. */ protected DefaultRenderingEvaluation(Map<String, String> labels, Set<String> 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-<port>-<protocol>" Pattern pattern = Pattern.compile(LABEL_CHE_SERVER_REF_KEY); Map<String, String> 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<String, String> 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"); } } }