diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java index 5a504b33fc8..18d9a73cb7a 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java @@ -33,6 +33,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpolicy.RestartPolicyRewriter; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftCommandProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftUniqueNamesProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner; import org.slf4j.Logger; @@ -66,6 +67,7 @@ public class OpenShiftEnvironmentProvisioner private final ProxySettingsProvisioner proxySettingsProvisioner; private final ServiceAccountProvisioner serviceAccountProvisioner; private final CertificateProvisioner certificateProvisioner; + private final OpenShiftCommandProvisioner commandProvisioner; @Inject public OpenShiftEnvironmentProvisioner( @@ -83,7 +85,8 @@ public OpenShiftEnvironmentProvisioner( ImagePullSecretProvisioner imagePullSecretProvisioner, ProxySettingsProvisioner proxySettingsProvisioner, ServiceAccountProvisioner serviceAccountProvisioner, - CertificateProvisioner certificateProvisioner) { + CertificateProvisioner certificateProvisioner, + OpenShiftCommandProvisioner commandProvisioner) { this.pvcEnabled = pvcEnabled; this.volumesStrategy = volumesStrategy; this.uniqueNamesProvisioner = uniqueNamesProvisioner; @@ -99,6 +102,7 @@ public OpenShiftEnvironmentProvisioner( this.proxySettingsProvisioner = proxySettingsProvisioner; this.serviceAccountProvisioner = serviceAccountProvisioner; this.certificateProvisioner = certificateProvisioner; + this.commandProvisioner = commandProvisioner; } @Override @@ -133,6 +137,7 @@ public void provision(OpenShiftEnvironment osEnv, RuntimeIdentity identity) proxySettingsProvisioner.provision(osEnv, identity); serviceAccountProvisioner.provision(osEnv, identity); certificateProvisioner.provision(osEnv, identity); + commandProvisioner.provision(osEnv, identity); LOG.debug( "Provisioning OpenShift environment done for workspace '{}'", identity.getWorkspaceId()); } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftCommandProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftCommandProvisioner.java new file mode 100644 index 00000000000..c350d988e9c --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftCommandProvisioner.java @@ -0,0 +1,98 @@ +/* + * 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.openshift.provision; + +import static org.eclipse.che.api.workspace.shared.Constants.ARBITRARY_USER_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.CONTAINER_SOURCE_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.RECIPE_CONTAINER_SOURCE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import io.fabric8.kubernetes.api.model.Container; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.Names; +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.openshift.environment.OpenShiftEnvironment; + +public class OpenShiftCommandProvisioner implements ConfigurationProvisioner { + + @VisibleForTesting protected static final List SHELL_BINARY = ImmutableList.of("/bin/sh"); + @VisibleForTesting protected static final String SHELL_ARGS = "-c"; + + @VisibleForTesting + protected static final String ADD_USER_COMMAND = + "if ! whoami &> /dev/null && [ -w /etc/passwd ]; then " + + "echo \"user:x:$(id -u):0:user user:projects/:/bin/bash\" >> /etc/passwd;" + + "fi;"; + + @VisibleForTesting protected static final String COMMAND_FORMAT = "%s %s %s"; + + @Override + public void provision(OpenShiftEnvironment osEnv, RuntimeIdentity identity) + throws InfrastructureException { + + if (!supportArbitraryUser(osEnv.getAttributes())) { + return; + } + + Set recipeMachineNames = + osEnv + .getMachines() + .entrySet() + .stream() + .filter( + e -> + RECIPE_CONTAINER_SOURCE.equals( + e.getValue().getAttributes().get(CONTAINER_SOURCE_ATTRIBUTE))) + .map(Entry::getKey) + .collect(Collectors.toSet()); + + for (PodData podData : osEnv.getPodsData().values()) { + for (Container container : podData.getSpec().getContainers()) { + String machineName = Names.machineName(podData, container); + if (recipeMachineNames.contains(machineName)) { + rewriteContainerCommand(container); + } + } + } + } + + private void rewriteContainerCommand(Container container) { + List defaultCommand = container.getCommand(); + List defaultArgs = container.getArgs(); + + if (defaultCommand == null || defaultCommand.size() == 0) { + return; + } + + String script = + String.format( + COMMAND_FORMAT, + ADD_USER_COMMAND, + String.join(" ", defaultCommand), + String.join(" ", defaultArgs)); + container.setCommand(SHELL_BINARY); + container.setArgs(ImmutableList.of(SHELL_ARGS, script)); + } + + private boolean supportArbitraryUser(Map workspaceAttributes) { + String supportArbitraryUser = workspaceAttributes.get(ARBITRARY_USER_ATTRIBUTE); + return "true".equals(supportArbitraryUser); + } +} diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java index 1f53a955817..c7ebb70f94e 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java @@ -28,6 +28,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpolicy.RestartPolicyRewriter; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftCommandProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftUniqueNamesProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner; import org.mockito.InOrder; @@ -61,6 +62,7 @@ public class OpenShiftEnvironmentProvisionerTest { @Mock private ProxySettingsProvisioner proxySettingsProvisioner; @Mock private ServiceAccountProvisioner serviceAccountProvisioner; @Mock private CertificateProvisioner certificateProvisioner; + @Mock private OpenShiftCommandProvisioner commandProvisioner; private OpenShiftEnvironmentProvisioner osInfraProvisioner; @@ -84,7 +86,8 @@ public void setUp() { imagePullSecretProvisioner, proxySettingsProvisioner, serviceAccountProvisioner, - certificateProvisioner); + certificateProvisioner, + commandProvisioner); provisionOrder = inOrder( installerServersPortProvisioner, @@ -100,7 +103,8 @@ public void setUp() { imagePullSecretProvisioner, proxySettingsProvisioner, serviceAccountProvisioner, - certificateProvisioner); + certificateProvisioner, + commandProvisioner); } @Test @@ -125,6 +129,7 @@ public void performsOrderedProvisioning() throws Exception { provisionOrder.verify(proxySettingsProvisioner).provision(eq(osEnv), eq(runtimeIdentity)); provisionOrder.verify(serviceAccountProvisioner).provision(eq(osEnv), eq(runtimeIdentity)); provisionOrder.verify(certificateProvisioner).provision(eq(osEnv), eq(runtimeIdentity)); + provisionOrder.verify(commandProvisioner).provision(eq(osEnv), eq(runtimeIdentity)); provisionOrder.verifyNoMoreInteractions(); } } diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftCommandProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftCommandProvisionerTest.java new file mode 100644 index 00000000000..d41b7704686 --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftCommandProvisionerTest.java @@ -0,0 +1,252 @@ +/* + * 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.openshift.provision; + +import static org.eclipse.che.api.workspace.shared.Constants.ARBITRARY_USER_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.CONTAINER_SOURCE_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.RECIPE_CONTAINER_SOURCE; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftCommandProvisioner.ADD_USER_COMMAND; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftCommandProvisioner.COMMAND_FORMAT; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftCommandProvisioner.SHELL_ARGS; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftCommandProvisioner.SHELL_BINARY; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import java.util.Collections; +import java.util.List; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +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.PodData; +import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +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 OpenShiftCommandProvisionerTest { + + private static String POD_NAME = "testPod"; + private static String RECIPE_CONTAINER_NAME = "recipeContainer"; + private static String SIDECAR_CONTAINER_NAME = "nonRecipeContainer"; + + private static List CONTAINER_COMMAND = ImmutableList.of("testCommand"); + private static List CONTAINER_ARGS = ImmutableList.of("test", "args"); + + private static List UPDATED_COMMAND = + ImmutableList.of( + SHELL_ARGS, + String.format( + COMMAND_FORMAT, + ADD_USER_COMMAND, + String.join(" ", CONTAINER_COMMAND), + String.join(" ", CONTAINER_ARGS))); + + @Mock private OpenShiftEnvironment osEnv; + @Mock private RuntimeIdentity runtimeIdentity; + + @Mock private InternalMachineConfig recipeMachine; + @Mock private InternalMachineConfig sidecarMachine; + + Container recipeContainer; + Container sidecarContainer; + PodData podData; + + private OpenShiftCommandProvisioner provisioner; + + @BeforeMethod + public void setup() { + recipeContainer = buildContainer(RECIPE_CONTAINER_NAME); + sidecarContainer = buildContainer(SIDECAR_CONTAINER_NAME); + + podData = buildPodData(POD_NAME, recipeContainer, sidecarContainer); + when(osEnv.getPodsData()).thenReturn(ImmutableMap.of(POD_NAME, podData)); + + when(recipeMachine.getAttributes()) + .thenReturn(ImmutableMap.of(CONTAINER_SOURCE_ATTRIBUTE, RECIPE_CONTAINER_SOURCE)); + when(sidecarMachine.getAttributes()) + .thenReturn(ImmutableMap.of(CONTAINER_SOURCE_ATTRIBUTE, "plugin")); + + when(osEnv.getMachines()) + .thenReturn( + ImmutableMap.of( + Names.machineName(podData, recipeContainer), + recipeMachine, + Names.machineName(podData, sidecarContainer), + sidecarMachine)); + + when(osEnv.getPodsData()).thenReturn(ImmutableMap.of(POD_NAME, podData)); + + this.provisioner = new OpenShiftCommandProvisioner(); + } + + @Test + public void shouldDoNothingWhenAttributeNotPresent() throws Exception { + when(osEnv.getAttributes()).thenReturn(Collections.emptyMap()); + + provisioner.provision(osEnv, runtimeIdentity); + + verify(osEnv).getAttributes(); + verifyNoMoreInteractions(osEnv); + } + + @Test + public void shouldDoNothingWhenAttributeIsFalse() throws Exception { + when(osEnv.getAttributes()).thenReturn(ImmutableMap.of(ARBITRARY_USER_ATTRIBUTE, "false")); + + provisioner.provision(osEnv, runtimeIdentity); + + verify(osEnv).getAttributes(); + verifyNoMoreInteractions(osEnv); + } + + @Test + public void shouldUpdateCommandAndArgsForRecipeMachine() throws Exception { + when(osEnv.getAttributes()).thenReturn(ImmutableMap.of(ARBITRARY_USER_ATTRIBUTE, "true")); + + provisioner.provision(osEnv, runtimeIdentity); + + PodData actualPod = osEnv.getPodsData().get(POD_NAME); + Container actual = + actualPod + .getSpec() + .getContainers() + .stream() + .filter(c -> c.getName().equals(RECIPE_CONTAINER_NAME)) + .findFirst() + .get(); + + assertEquals(actual.getName(), RECIPE_CONTAINER_NAME, "Should not modify container name"); + assertEquals(actual.getCommand(), SHELL_BINARY, "Should update container Command"); + assertEquals(actual.getArgs(), UPDATED_COMMAND, "Should update conatiner Args"); + } + + @Test + public void shouldDoNothingForSidecarMachine() throws Exception { + when(osEnv.getAttributes()).thenReturn(ImmutableMap.of(ARBITRARY_USER_ATTRIBUTE, "true")); + + provisioner.provision(osEnv, runtimeIdentity); + + PodData actualPod = osEnv.getPodsData().get(POD_NAME); + Container actual = + actualPod + .getSpec() + .getContainers() + .stream() + .filter(c -> c.getName().equals(SIDECAR_CONTAINER_NAME)) + .findFirst() + .get(); + + assertEquals(actual.getName(), SIDECAR_CONTAINER_NAME); + assertEquals( + actual.getCommand(), CONTAINER_COMMAND, "Should not change non-recipe container Command"); + assertEquals(actual.getArgs(), CONTAINER_ARGS, "Should not change non-recipe container Args"); + } + + @Test + public void shouldNotRewriteWhenOriginalCommandIsNull() throws Exception { + when(osEnv.getAttributes()).thenReturn(ImmutableMap.of(ARBITRARY_USER_ATTRIBUTE, "true")); + Container noCommand = buildContainer(RECIPE_CONTAINER_NAME); + PodData podData = buildPodData(POD_NAME, noCommand, sidecarContainer); + podData + .getSpec() + .getContainers() + .stream() + .filter(c -> RECIPE_CONTAINER_NAME.equals(c.getName())) + .forEach( + c -> { + c.setCommand(null); + c.setArgs(null); + }); + when(osEnv.getPodsData()).thenReturn(ImmutableMap.of(POD_NAME, podData)); + + provisioner.provision(osEnv, runtimeIdentity); + + PodData actualPod = osEnv.getPodsData().get(POD_NAME); + Container actual = + actualPod + .getSpec() + .getContainers() + .stream() + .filter(c -> c.getName().equals(RECIPE_CONTAINER_NAME)) + .findFirst() + .get(); + + assertEquals(actual.getName(), RECIPE_CONTAINER_NAME); + assertNull( + actual.getCommand(), "Should not do anything to command when container command is null"); + assertNull(actual.getArgs(), "Should not do anything to args when container command is null"); + } + + @Test + public void shouldNotRewriteWhenOriginalCommandIsEmpty() throws Exception { + when(osEnv.getAttributes()).thenReturn(ImmutableMap.of(ARBITRARY_USER_ATTRIBUTE, "true")); + Container noCommand = buildContainer(RECIPE_CONTAINER_NAME); + noCommand.setCommand(Collections.emptyList()); + noCommand.setArgs(Collections.emptyList()); + PodData podData = buildPodData(POD_NAME, noCommand, sidecarContainer); + when(osEnv.getPodsData()).thenReturn(ImmutableMap.of(POD_NAME, podData)); + + provisioner.provision(osEnv, runtimeIdentity); + + PodData actualPod = osEnv.getPodsData().get(POD_NAME); + Container actual = + actualPod + .getSpec() + .getContainers() + .stream() + .filter(c -> c.getName().equals(RECIPE_CONTAINER_NAME)) + .findFirst() + .get(); + + assertEquals(actual.getName(), RECIPE_CONTAINER_NAME); + assertTrue( + actual.getCommand().isEmpty(), + "Should not do anything to command when container command is empty"); + assertTrue( + actual.getArgs().isEmpty(), + "Should not do anything to args when container command is empty"); + } + + private Container buildContainer(String name) { + return new ContainerBuilder() + .withName(name) + .withCommand(CONTAINER_COMMAND) + .withArgs(CONTAINER_ARGS) + .build(); + } + + private PodData buildPodData(String name, Container... containers) { + Pod pod = + new PodBuilder() + .withNewMetadata() + .withName(name) + .endMetadata() + .withNewSpec() + .withContainers(containers) + .endSpec() + .build(); + return new PodData(pod); + } +} diff --git a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java index db0d669f68e..c5e8f3a9347 100644 --- a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java +++ b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/Constants.java @@ -106,6 +106,26 @@ public final class Constants { */ public static final String PERSIST_VOLUMES_ATTRIBUTE = "persistVolumes"; + /** + * This attribute configures a workspace running on OpenShift to attempt to work around the + * OpenShift default of starting containers with an arbitrary UID. Should be set/read from {@link + * WorkspaceConfig#getAttributes()}. + * + *

Value is expected to be boolean; if it is set to 'true', then the recipe containers in the + * the workspace will have their entrypoint overridden to attempt to add an entry for the current + * user ID to the /etc/passwd file. If this fails for whatever reason, the original entrypoint + * should still be executed. + * + *

In general this attribute requires some modifications to the container being used to run the + * workspace to have any effect; containers running on OpenShift should set the /etc/passwd + * directory to be writable by the root group. + * + * @see + * OpenShift docs + */ + public static final String ARBITRARY_USER_ATTRIBUTE = "supportArbitraryUser"; + /** * Contains a list of workspace tooling plugins that should be used in a workspace. Should be * set/read from {@link WorkspaceConfig#getAttributes}. diff --git a/wsmaster/che-core-api-workspace/README-devfile.md b/wsmaster/che-core-api-workspace/README-devfile.md index 1d1f83f6391..4299d4374d6 100644 --- a/wsmaster/che-core-api-workspace/README-devfile.md +++ b/wsmaster/che-core-api-workspace/README-devfile.md @@ -337,6 +337,31 @@ attributes: persistVolumes: false ``` +#### Arbitrary user ID support +Containers running on OpenShift start using an arbitrarily assigned user ID. This means that the current user in the workspace will not have an entry in the `/etc/passwd` file, and thus not have an associated username or home directory. + +This can cause issues when certain tools depend on having a username or home directory; for example, maven will have trouble placing the `.m2` directory when there is no username (unless otherwise configured), and the terminal may have issues with autocompletion, keybindings, etc. + +When attribute `supportArbitraryUser` is set to `true`, workspaces running on OpenShift will have their main containers' command and arguments (i.e. their entrypoint) overwritten to add the current user ID to the `/etc/passwd` file. If this step fails (e.g. due to `/etc/passwd` not being writable by the root group), this attribute has no effect on the workspace; the original entrypoint will still execute. + +This attribute also depends on the main workspace container having a `command` and `args` defined in the devfile (i.e. it cannot depend on the underlying container's entrypoint). + +The default value for this attribute is `false`, and absence of this attribute is interpreted accordingly. + +Example devfile: +```yaml +apiVersion: 1.0.0 +metadata: + name: petclinic-dev-environment +projects: + - name: petclinic + source: + type: git + location: 'https://github.com/che-samples/web-java-spring-petclinic.git' +attributes: + supportArbitraryUser: true +``` + ### Live working examples - [NodeJS simple "Hello World" example](https://che.openshift.io/f?url=https://raw.githubusercontent.com/redhat-developer/devfile/master/samples/web-nodejs-sample/devfile.yaml) diff --git a/wsmaster/che-core-api-workspace/src/main/resources/schema/devfile.json b/wsmaster/che-core-api-workspace/src/main/resources/schema/devfile.json index 5bfbcb950ad..96cdbb625e5 100644 --- a/wsmaster/che-core-api-workspace/src/main/resources/schema/devfile.json +++ b/wsmaster/che-core-api-workspace/src/main/resources/schema/devfile.json @@ -712,6 +712,11 @@ "description": "Defines whether volumes should be stored or not. Defaults to `true`. In case of `false` workspace volumes will be created as `emptyDir`. The data in the `emptyDir` volume is deleted forever when a workspace Pod is removed for any reason(pod is crashed, workspace is restarted).", "default": "true" }, + "supportArbitraryUser": { + "type": "boolean", + "description": "Defines whether Che should modify containers to support running as an arbitrary user ID on OpenShift. Defaults to `false`. If `true`, workspace recipe containers will have their command and args overwritten to add a user entry to /etc/passwd.", + "default": "false" + }, "additionalProperties": { "type": "string" },