diff --git a/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/TopologicalSort.java b/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/TopologicalSort.java
new file mode 100644
index 0000000000..a84a6aeb6b
--- /dev/null
+++ b/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/TopologicalSort.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.commons.lang;
+
+import static java.util.Collections.emptyList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * This is an implementation of a stable topological sort on a directed graph.
+ *
+ *
The sorting does not pose any requirements on the types being sorted. Instead, the
+ * implementation merely requires a function to provide it with a set of predecessors of a certain
+ * "node". The implementation of the function is completely in the hands of the caller.
+ *
+ *
Additionally, a function to extract an "ID" from a node is required. The reasoning behind this
+ * is that it usually is easier to work with identifiers when establishing the predecessors than
+ * with the full node instances. That said, nothing prevents the caller from using the actual node
+ * instance as its ID if the caller so wishes. The consequence of this is that, as a side-effect of
+ * the sorting, the duplicates, as determined by the equality of {@code ID} instances, are removed
+ * from the resulting sorted list.
+ *
+ * @param the type of nodes
+ * @param the type of a node ID
+ */
+public final class TopologicalSort {
+
+ private final Function identityExtractor;
+ private final Function> directPredecessorsExtractor;
+
+ /**
+ * @param identityExtractor a function to extract some kind of value uniquely identifying a node
+ * amongst the others.
+ * @param directPredecessorsExtractor a function returning a list of ids of direct predecessors of
+ * a node
+ */
+ public TopologicalSort(
+ Function identityExtractor, Function> directPredecessorsExtractor) {
+ this.identityExtractor = identityExtractor;
+ this.directPredecessorsExtractor = directPredecessorsExtractor;
+ }
+
+ /**
+ * Given the function for determining the predecessors of the nodes, return the list of the nodes
+ * in topological order. I.e. all predecessors will be placed sooner in the list than their
+ * successors. Note that the input collection is assumed to contain no duplicate entries as
+ * determined by the equality of the {@code ID} type. If such duplicates are present in the input
+ * collection, the output list will only contain the first instance of the duplicates from the
+ * input collection.
+ *
+ * The implemented sort algorithm is stable. If there is no relationship between 2 nodes, they
+ * retain the relative position to each other as they had in the provided collection (e.g. if "a"
+ * preceded "b" in the original collection and there is no relationship between them (as
+ * determined by the predecessor function), the "a" will still precede "b" in the resulting list.
+ * Other nodes may be inserted in between them though in the result).
+ *
+ *
The cycles in the graph determined by the predecessor function are ignored and nodes in the
+ * cycle are placed into the output list in the source order.
+ *
+ * @param nodes the collection of nodes
+ * @return the list of nodes sorted in topological order
+ */
+ public List sort(Collection nodes) {
+ // the linked hashmap is important to retain the original order of elements unless required
+ // by the dependencies between nodes
+ LinkedHashMap> nodeInfos = new LinkedHashMap<>(nodes.size());
+ List> results = new ArrayList<>(nodes.size());
+
+ int pos = 0;
+ boolean needsSorting = false;
+ for (N node : nodes) {
+ ID nodeID = identityExtractor.apply(node);
+ // we need the set to be modifiable, so let's make our own
+ Set preds = new HashSet<>(directPredecessorsExtractor.apply(node));
+ needsSorting |= !preds.isEmpty();
+
+ NodeInfo nodeInfo = nodeInfos.computeIfAbsent(nodeID, __ -> new NodeInfo<>());
+ nodeInfo.id = nodeID;
+ nodeInfo.predecessors = preds;
+ nodeInfo.sourcePosition = pos++;
+ nodeInfo.node = node;
+
+ for (ID pred : preds) {
+ // note that this means that we're inserting the nodeinfos into the map in an incorrect
+ // order and will have to sort them in the source order before we do the actual topo sort.
+ // We take that cost because we gamble on there being no dependencies in the nodes as a
+ // common case.
+ NodeInfo predNode = nodeInfos.computeIfAbsent(pred, __ -> new NodeInfo<>());
+ if (predNode.successors == null) {
+ predNode.successors = new HashSet<>();
+ }
+ predNode.successors.add(nodeID);
+ }
+ }
+
+ if (needsSorting) {
+ // because of the predecessors, we have put the nodeinfos in the map in an incorrect order.
+ // we need to correct that before we try to sort...
+ TreeSet> tmp = new TreeSet<>(Comparator.comparingInt(a -> a.sourcePosition));
+ tmp.addAll(nodeInfos.values());
+ nodeInfos.clear();
+ tmp.forEach(ni -> nodeInfos.put(ni.id, ni));
+
+ // now we're ready to produce the results
+ sort(nodeInfos, results);
+ } else {
+ // we don't need to sort, but we need to keep the expected behavior of removing the duplicates
+ results = new ArrayList<>(nodeInfos.values());
+ }
+
+ return results.stream().map(ni -> ni.node).collect(Collectors.toList());
+ }
+
+ private void sort(LinkedHashMap> nodes, List> results) {
+
+ while (!nodes.isEmpty()) {
+ NodeInfo curr = removeFirstIndependent(nodes);
+ if (curr != null) {
+ // yay, simple. Just add the found independent node to the results.
+ results.add(curr);
+ } else {
+ // ok, there is a cycle in the graph. Let's remove all the nodes in the first cycle we find
+ // from our predecessors map, add them to the result in their original order and try to
+ // continue normally
+
+ // find the first cycle in the predecessors (in the original list order)
+ Iterator> nexts = nodes.values().iterator();
+ List> cycle;
+ do {
+ curr = nexts.next();
+ cycle = findCycle(curr, nodes);
+ } while (cycle.isEmpty() && nexts.hasNext());
+
+ // If we ever find a graph that doesn't have any independent node, yet we fail to find a
+ // cycle in it, the universe must be broken.
+ if (cycle.isEmpty()) {
+ throw new IllegalStateException(
+ String.format(
+ "Failed to find a cycle in a graph that doesn't seem to have any independent"
+ + " node. This should never happen. Please file a bug. Current state of the"
+ + " sorting is: nodes=%s, results=%s",
+ nodes.toString(), results.toString()));
+ }
+
+ cycle.sort(Comparator.comparingInt(a -> a.sourcePosition));
+
+ for (NodeInfo n : cycle) {
+ removePredecessorMapping(nodes, n);
+ results.add(n);
+ }
+ }
+ }
+ }
+
+ private void removePredecessorMapping(Map> nodes, NodeInfo node) {
+ if (node.successors != null) {
+ for (ID succ : node.successors) {
+ NodeInfo succNode = nodes.get(succ);
+ if (succNode != null) {
+ succNode.predecessors.remove(node.id);
+ }
+ }
+ }
+ nodes.remove(node.id);
+ }
+
+ private List> findCycle(NodeInfo node, Map> nodes) {
+ // bail out quickly if there are no preds - should be fairly common occurrence hopefully
+ Set preds = node.predecessors;
+ if (preds == null || preds.isEmpty()) {
+ return emptyList();
+ }
+
+ List> ret = new ArrayList<>();
+ List todo = new ArrayList<>(preds);
+
+ Set visited = new HashSet<>();
+
+ while (!todo.isEmpty()) {
+ ID n = todo.remove(0);
+
+ if (visited.contains(n)) {
+ continue;
+ }
+
+ visited.add(n);
+
+ NodeInfo predNode = nodes.get(n);
+ if (predNode != null) {
+ todo.addAll(predNode.predecessors);
+ ret.add(predNode);
+
+ if (predNode.equals(node)) {
+ // we found the cycle to our original node
+ return ret;
+ }
+ }
+ }
+
+ return emptyList();
+ }
+
+ private NodeInfo removeFirstIndependent(Map> nodes) {
+ Iterator>> it = nodes.entrySet().iterator();
+
+ while (it.hasNext()) {
+ Entry> e = it.next();
+ if (e.getValue().predecessors.isEmpty()) {
+ it.remove();
+ NodeInfo ret = e.getValue();
+ removePredecessorMapping(nodes, ret);
+ return ret;
+ }
+ }
+
+ return null;
+ }
+
+ private static final class NodeInfo {
+ private ID id;
+ private int sourcePosition;
+ private Set predecessors;
+ private Set successors;
+ private N node;
+ }
+}
diff --git a/core/commons/che-core-commons-lang/src/test/java/org/eclipse/che/commons/lang/TopologicalSortTest.java b/core/commons/che-core-commons-lang/src/test/java/org/eclipse/che/commons/lang/TopologicalSortTest.java
new file mode 100644
index 0000000000..4c803494fa
--- /dev/null
+++ b/core/commons/che-core-commons-lang/src/test/java/org/eclipse/che/commons/lang/TopologicalSortTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.commons.lang;
+
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static org.testng.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class TopologicalSortTest {
+
+ @Test(dataProvider = "sortingCases")
+ public void shouldApplySorting(List list, List expectedSorting) throws Exception {
+ // when
+ sort(list);
+
+ // then
+ assertEquals(list, expectedSorting);
+ }
+
+ @DataProvider
+ public static Object[][] sortingCases() {
+ return new Object[][] {
+ // keep order if no dependencies
+ new Object[] {
+ vars(var('b', "."), var('a', "."), var('c', ".")),
+ vars(var('b', "."), var('a', "."), var('c', "."))
+ },
+
+ // sort dependents after dependencies
+ new Object[] {
+ vars(var('a', "b"), var('b', "."), var('c', ".")),
+ vars(var('b', "."), var('a', "b"), var('c', "."))
+ },
+
+ // sort dependents after dependencies, multiple dependencies
+ new Object[] {
+ vars(var('a', "bc"), var('b', "."), var('c', ".")),
+ vars(var('b', "."), var('c', "."), var('a', "bc"))
+ },
+
+ // sort dependents after dependencies, check dependee moved after dependencies
+ new Object[] {
+ vars(var('d', "."), var('a', "c"), var('b', "."), var('c', ".")),
+ vars(var('d', "."), var('b', "."), var('c', "."), var('a', "c"))
+ },
+
+ // test the robustness against cycles
+
+ // dependent directly on itself
+ new Object[] {vars(var('a', "a")), vars(var('a', "a"))},
+
+ // two mutually dependent nodes
+ new Object[] {vars(var('a', "b"), var('b', "a")), vars(var('a', "b"), var('b', "a"))},
+
+ // independent node mixed inside a cycle
+ new Object[] {
+ vars(var('b', "c"), var('d', "."), var('a', "b"), var('c', "a")),
+ vars(var('d', "."), var('b', "c"), var('a', "b"), var('c', "a"))
+ },
+
+ // cycle (a-b-c) with one of the nodes also depending on another node (a-d),
+ // mixed with a "chain" (f-e-b)
+ new Object[] {
+ vars(
+ var('f', "e"),
+ var('a', "bd"),
+ var('b', "c"),
+ var('e', "b"),
+ var('c', "a"),
+ var('d', ".")),
+ vars(
+ var('d', "."),
+ var('a', "bd"),
+ var('b', "c"),
+ var('c', "a"),
+ var('e', "b"),
+ var('f', "e"))
+ },
+
+ // removes duplicates
+ new Object[] {
+ vars(var('a', "."), var('a', "."), var('b', ".")), vars(var('a', "."), var('b', "."))
+ }
+ };
+ }
+
+ private static Var var(char name, String value) {
+ return new Var(name, value.chars().mapToObj(c -> (char) c).collect(toSet()));
+ }
+
+ private static List vars(Var... vars) {
+ return Stream.of(vars).collect(toList());
+ }
+
+ private static void sort(List vars) {
+ Function idFunction = v -> v.name;
+ Function> predecessors =
+ v -> v.dependencies.stream().filter(Character::isAlphabetic).collect(toSet());
+
+ TopologicalSort sort = new TopologicalSort<>(idFunction, predecessors);
+ List newVars = sort.sort(vars);
+ vars.clear();
+ vars.addAll(newVars);
+ }
+
+ private static final class Var {
+
+ private final char name;
+ private final Set dependencies;
+
+ private Var(char name, Set dependencies) {
+ this.name = name;
+ this.dependencies = dependencies;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Var var = (Var) o;
+ return name == var.name && dependencies.equals(var.dependencies);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, dependencies);
+ }
+
+ @Override
+ public String toString() {
+ return "Var{" + "name='" + name + '\'' + ", deps='" + dependencies + '\'' + '}';
+ }
+ }
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverter.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverter.java
index ecb5a7777b..989c79d624 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverter.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverter.java
@@ -11,19 +11,26 @@
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.provision.env;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.EnvVar;
+import java.util.List;
+import java.util.Map;
import javax.inject.Singleton;
import org.eclipse.che.api.core.model.workspace.config.MachineConfig;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig;
import org.eclipse.che.commons.annotation.Traced;
+import org.eclipse.che.commons.lang.TopologicalSort;
import org.eclipse.che.commons.tracing.TracingTags;
import org.eclipse.che.workspace.infrastructure.kubernetes.Names;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData;
import org.eclipse.che.workspace.infrastructure.kubernetes.provision.ConfigurationProvisioner;
+import org.eclipse.che.workspace.infrastructure.kubernetes.util.EnvVars;
/**
* Converts environment variables in {@link MachineConfig} to Kubernetes environment variables.
@@ -32,6 +39,10 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.Configurati
*/
@Singleton
public class EnvVarsConverter implements ConfigurationProvisioner {
+
+ private final TopologicalSort topoSort =
+ new TopologicalSort<>(EnvVar::getName, EnvVars::extractReferencedVariables);
+
@Override
@Traced
public void provision(KubernetesEnvironment k8sEnv, RuntimeIdentity identity)
@@ -43,13 +54,29 @@ public class EnvVarsConverter implements ConfigurationProvisioner {
for (Container container : pod.getSpec().getContainers()) {
String machineName = Names.machineName(pod, container);
InternalMachineConfig machineConf = k8sEnv.getMachines().get(machineName);
- machineConf
- .getEnv()
- .forEach(
- (key, value) -> {
- container.getEnv().removeIf(env -> key.equals(env.getName()));
- container.getEnv().add(new EnvVar(key, value, null));
- });
+
+ // we need to combine the env vars from the machine config with the variables already
+ // present in the container. Let's key the variables by name and use the map for merging
+ Map envVars =
+ machineConf
+ .getEnv()
+ .entrySet()
+ .stream()
+ .map(e -> new EnvVar(e.getKey(), e.getValue(), null))
+ .collect(toMap(EnvVar::getName, identity()));
+
+ // the env vars defined in our machine config take precedence over the ones already defined
+ // in the container, if any
+ container.getEnv().forEach(v -> envVars.putIfAbsent(v.getName(), v));
+
+ // The environment variable interpolation only works if a variable that is referenced
+ // is already defined earlier in the list of environment variables.
+ // We need to produce a list where variables that reference others always appear later
+ // in the list.
+
+ List sorted = topoSort.sort(envVars.values());
+
+ container.setEnv(sorted);
}
}
}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/EnvVars.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/EnvVars.java
new file mode 100644
index 0000000000..a1fb9ffcc9
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/EnvVars.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.workspace.infrastructure.kubernetes.util;
+
+import io.fabric8.kubernetes.api.model.EnvVar;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Utility class for dealing with environment variables */
+public class EnvVars {
+
+ private static final Pattern REFERENCE_PATTERN = Pattern.compile("\\$\\(\\w+\\)");
+
+ private EnvVars() {}
+
+ /**
+ * Looks at the value of the provided environment variable and returns a set of environment
+ * variable references in the Kubernetes convention of {@literal $(VAR_NAME)}.
+ *
+ * See API
+ * docs and/or documentation.
+ *
+ * @param var the environment variable to analyze
+ * @return a set of variable references, never null
+ */
+ public static Set extractReferencedVariables(EnvVar var) {
+ String val = var.getValue();
+
+ Matcher matcher = REFERENCE_PATTERN.matcher(val);
+
+ // let's just keep the initial size small, because usually there are not that many references
+ // present.
+ Set ret = new HashSet<>(2);
+
+ while (matcher.find()) {
+ int start = matcher.start();
+
+ // the variable reference can be escaped using a double $, e.g. $$(VAR) is not a reference
+ if (start > 0 && val.charAt(start - 1) == '$') {
+ continue;
+ }
+
+ // extract the variable name out of the reference $(NAME) -> NAME
+ String refName = matcher.group().substring(2, matcher.group().length() - 1);
+ ret.add(refName);
+ }
+
+ return ret;
+ }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverterTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverterTest.java
new file mode 100644
index 0000000000..753586fbcb
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/env/EnvVarsConverterTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.workspace.infrastructure.kubernetes.provision.env;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.testng.Assert.assertEquals;
+
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.PodSpec;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
+import org.eclipse.che.api.workspace.server.model.impl.RuntimeIdentityImpl;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig;
+import org.eclipse.che.workspace.infrastructure.kubernetes.Names;
+import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class EnvVarsConverterTest {
+
+ private static final String PRE_EXISTING_VAR = "VAR_THAT_EXISTS";
+ private static final String PRE_EXISTING_VAR_VALUE = "jmenuju se VAR";
+ private static final String PRE_EXISTING_VAR_NEW_VALUE = "my name is VAR";
+ private static final String A_VAR = "A";
+ private static final String A_VAL = "$(C)";
+ private static final String B_VAR = "B";
+ private static final String B_VAL = "b";
+ private static final String C_VAR = "C";
+ private static final String C_VAL = "c";
+
+ private KubernetesEnvironment environment;
+ private RuntimeIdentity identity;
+ private Container testContainer;
+ private InternalMachineConfig machine;
+
+ @BeforeMethod
+ public void setUp() {
+ testContainer = new Container();
+
+ PodSpec podSpec = new PodSpec();
+ podSpec.setContainers(singletonList(testContainer));
+
+ ObjectMeta podMeta = new ObjectMeta();
+ podMeta.setName("pod");
+
+ Pod pod = new Pod();
+ pod.setSpec(podSpec);
+ pod.setMetadata(podMeta);
+
+ Map pods = new HashMap<>();
+ pods.put("pod", pod);
+
+ environment = KubernetesEnvironment.builder().setPods(pods).build();
+
+ machine = new InternalMachineConfig();
+
+ environment.setMachines(
+ Collections.singletonMap(Names.machineName(podMeta, testContainer), machine));
+
+ identity = new RuntimeIdentityImpl("wsId", "blah", "bleh");
+ }
+
+ @Test
+ public void shouldProvisionEnvironmentVariablesSorted() throws InfrastructureException {
+ // given
+ List preExistingEnvironment = new ArrayList<>();
+ preExistingEnvironment.add(new EnvVar(PRE_EXISTING_VAR, PRE_EXISTING_VAR_VALUE, null));
+ testContainer.setEnv(preExistingEnvironment);
+
+ machine.getEnv().put(PRE_EXISTING_VAR, PRE_EXISTING_VAR_NEW_VALUE);
+ machine.getEnv().put(A_VAR, A_VAL);
+ machine.getEnv().put(B_VAR, B_VAL);
+ machine.getEnv().put(C_VAR, C_VAL);
+
+ // when
+ EnvVarsConverter converter = new EnvVarsConverter();
+ converter.provision(environment, identity);
+
+ // then
+ EnvVar expectedA = new EnvVar(A_VAR, A_VAL, null);
+ EnvVar expectedB = new EnvVar(B_VAR, B_VAL, null);
+ EnvVar expectedC = new EnvVar(C_VAR, C_VAL, null);
+ EnvVar expectedPreExisting = new EnvVar(PRE_EXISTING_VAR, PRE_EXISTING_VAR_NEW_VALUE, null);
+
+ List expectedOrder = asList(expectedB, expectedC, expectedA, expectedPreExisting);
+
+ assertEquals(4, testContainer.getEnv().size());
+ assertEquals(expectedOrder, testContainer.getEnv());
+ }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/EnvVarsTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/EnvVarsTest.java
new file mode 100644
index 0000000000..129e7c8eff
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/EnvVarsTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2012-2018 Red Hat, Inc.
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat, Inc. - initial API and implementation
+ */
+package org.eclipse.che.workspace.infrastructure.kubernetes.util;
+
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.util.EnvVars.extractReferencedVariables;
+import static org.testng.Assert.assertEquals;
+
+import io.fabric8.kubernetes.api.model.EnvVar;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class EnvVarsTest {
+
+ @Test(dataProvider = "detectReferencesTestValues")
+ public void shouldDetectReferences(String value, Set expected, String caseName) {
+ assertEquals(
+ extractReferencedVariables(var("name", value)), expected, caseName + ": just value");
+ assertEquals(
+ extractReferencedVariables(var("name", "v" + value)),
+ expected,
+ caseName + ": value with prefix");
+ assertEquals(
+ extractReferencedVariables(var("name", "v" + value + "v")),
+ expected,
+ caseName + ": value with prefix and postfix");
+ assertEquals(
+ extractReferencedVariables(var("name", value + "v")),
+ expected,
+ caseName + ": value with postfix");
+ }
+
+ @DataProvider
+ public static Object[][] detectReferencesTestValues() {
+ return new Object[][] {
+ new Object[] {"value", emptySet(), "no refs"},
+ new Object[] {"$(NO_REF", emptySet(), "unclosed ref"},
+ new Object[] {"$$(NO_REF)", emptySet(), "escaped ref"},
+ new Object[] {"$NO_REF)", emptySet(), "invalid start ref"},
+ new Object[] {"$(NO REF)", emptySet(), "invalid name ref"},
+ new Object[] {"$(REF)", singleton("REF"), "valid ref"}
+ };
+ }
+
+ @Test
+ public void shouldDetectMultipleReferences() throws Exception {
+ // given
+ EnvVar envVar = var("a", "$(b) $(c) $$(d)");
+
+ // when
+ Set refs = extractReferencedVariables(envVar);
+
+ // then
+ assertEquals(refs, new HashSet<>(Arrays.asList("b", "c")));
+ }
+
+ private static EnvVar var(String name, String value) {
+ return new EnvVar(name, value, null);
+ }
+}