diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index c6bd554a0f4..92b7d1cd956 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties @@ -370,6 +370,9 @@ che.infra.kubernetes.ingress.domain= # Defines Kubernetes namespace in which all workspaces will be created. # If not set, every workspace will be created in a new namespace, where namespace = workspace id +# It's possible to use and placeholders (e.g.: che-workspace-). +# In that case, new namespace will be created for each user. Service account with permission +# to create new namespace must be used. # # Ignored for OpenShift infra. Use `che.infra.openshift.project` instead che.infra.kubernetes.namespace= @@ -578,6 +581,9 @@ che.infra.kubernetes.runtimes_consistency_check_period_min=-1 # Defines OpenShift namespace in which all workspaces will be created. # If not set, every workspace will be created in a new project, where project name = workspace id +# It's possible to use and placeholders (e.g.: che-workspace-). +# In that case, new project will be created for each user. OpenShift oauth or service account with +# permission to create new projects must be used. che.infra.openshift.project= # Single port mode wildcard domain host & port. nip.io is used by default diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java index d2611c922c6..16cb46375ed 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java @@ -205,6 +205,11 @@ private void create(String namespaceName, KubernetesClient client) .done(); waitDefaultServiceAccount(namespaceName, client); } catch (KubernetesClientException e) { + if (e.getCode() == 403) { + LOG.error( + "Unable to create new Kubernetes project due to lack of permissions." + + "When using workspace namespace placeholders, service account with lenient permissions (cluster-admin) must be used."); + } throw new KubernetesInfrastructureException(e); } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java index ecbbd5d97e2..27b2f806520 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java @@ -16,9 +16,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; import javax.inject.Named; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; /** @@ -29,6 +34,14 @@ @Singleton public class KubernetesNamespaceFactory { + private static final Map> NAMESPACE_NAME_PLACEHOLDERS = + new HashMap<>(); + + static { + NAMESPACE_NAME_PLACEHOLDERS.put("", Subject::getUserName); + NAMESPACE_NAME_PLACEHOLDERS.put("", Subject::getUserId); + } + private final String namespaceName; private final boolean isPredefined; private final String serviceAccountName; @@ -42,15 +55,19 @@ public KubernetesNamespaceFactory( @Nullable @Named("che.infra.kubernetes.cluster_role_name") String clusterRoleName, KubernetesClientFactory clientFactory) { this.namespaceName = namespaceName; - this.isPredefined = !isNullOrEmpty(namespaceName); + this.isPredefined = !isNullOrEmpty(namespaceName) && hasNoPlaceholders(this.namespaceName); this.serviceAccountName = serviceAccountName; this.clusterRoleName = clusterRoleName; this.clientFactory = clientFactory; } + private boolean hasNoPlaceholders(String namespaceName) { + return NAMESPACE_NAME_PLACEHOLDERS.keySet().stream().noneMatch(namespaceName::contains); + } + /** - * Returns true if namespace is predefined for all workspaces or false if each workspace will be - * provided with a new namespace. + * True if namespace is predefined for all workspaces. False if each workspace will be provided + * with a new namespace or provided for each user when using placeholders. */ public boolean isPredefined() { return isPredefined; @@ -67,7 +84,8 @@ public boolean isPredefined() { * @throws InfrastructureException if any exception occurs during namespace preparing */ public KubernetesNamespace create(String workspaceId) throws InfrastructureException { - final String namespaceName = isPredefined ? this.namespaceName : workspaceId; + final String namespaceName = + evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); KubernetesNamespace namespace = doCreateNamespace(workspaceId, namespaceName); namespace.prepare(); @@ -83,6 +101,22 @@ public KubernetesNamespace create(String workspaceId) throws InfrastructureExcep return namespace; } + protected String evalNamespaceName(String workspaceId, Subject currentUser) { + if (isPredefined) { + return this.namespaceName; + } else if (isNullOrEmpty(this.namespaceName)) { + return workspaceId; + } else { + String tmpNamespaceName = this.namespaceName; + for (String placeholder : NAMESPACE_NAME_PLACEHOLDERS.keySet()) { + tmpNamespaceName = + tmpNamespaceName.replaceAll( + placeholder, NAMESPACE_NAME_PLACEHOLDERS.get(placeholder).apply(currentUser)); + } + return tmpNamespaceName; + } + } + /** * Creates a Kubernetes namespace for the specified workspace. * @@ -106,4 +140,12 @@ KubernetesWorkspaceServiceAccount doCreateServiceAccount( return new KubernetesWorkspaceServiceAccount( workspaceId, namespaceName, serviceAccountName, clusterRoleName, clientFactory); } + + protected String getServiceAccountName() { + return serviceAccountName; + } + + protected String getClusterRoleName() { + return clusterRoleName; + } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java index e12e333b60c..e3a249134f7 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java @@ -21,6 +21,7 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; +import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; @@ -184,4 +185,15 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProject // then verify(namespaceFactory, never()).doCreateServiceAccount(any(), any()); } + + @Test + public void testPlaceholder() { + namespaceFactory = + new KubernetesNamespaceFactory( + "blabol------", "", "", clientFactory); + String namespace = + namespaceFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false)); + + assertEquals(namespace, "blabol-123-JonDoe-123-JonDoe--"); + } } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java index b52956fe078..c6b148fbd76 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java @@ -27,6 +27,8 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesServices; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Defines an internal API for managing subset of objects inside {@link Project} instance. @@ -35,6 +37,8 @@ */ public class OpenShiftProject extends KubernetesNamespace { + private static final Logger LOG = LoggerFactory.getLogger(OpenShiftProject.class); + private final OpenShiftRoutes routes; private final OpenShiftClientFactory clientFactory; @@ -115,6 +119,11 @@ private void create(String projectName, OpenShiftClient osClient) throws Infrast .endMetadata() .done(); } catch (KubernetesClientException e) { + if (e.getCode() == 403) { + LOG.error( + "Unable to create new OpenShift project due to lack of permissions." + + "HINT: When using workspace project name placeholders, os-oauth or service account with more lenient permissions (cluster-admin) must be used."); + } throw new KubernetesInfrastructureException(e); } } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java index 5df0c5d90ca..35b4d669eff 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java @@ -19,6 +19,7 @@ import javax.inject.Named; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; @@ -30,9 +31,6 @@ @Singleton public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { - private final String projectName; - private final String serviceAccountName; - private final String clusterRoleName; private final OpenShiftClientFactory clientFactory; @Inject @@ -42,9 +40,6 @@ public OpenShiftProjectFactory( @Nullable @Named("che.infra.kubernetes.cluster_role_name") String clusterRoleName, OpenShiftClientFactory clientFactory) { super(projectName, serviceAccountName, clusterRoleName, clientFactory); - this.projectName = projectName; - this.serviceAccountName = serviceAccountName; - this.clusterRoleName = clusterRoleName; this.clientFactory = clientFactory; } @@ -59,11 +54,12 @@ public OpenShiftProjectFactory( * @throws InfrastructureException if any exception occurs during project preparing */ public OpenShiftProject create(String workspaceId) throws InfrastructureException { - final String projectName = isPredefined() ? this.projectName : workspaceId; + final String projectName = + evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); OpenShiftProject osProject = doCreateProject(workspaceId, projectName); osProject.prepare(); - if (!isPredefined() && !isNullOrEmpty(serviceAccountName)) { + if (!isPredefined() && !isNullOrEmpty(getServiceAccountName())) { // prepare service account for workspace only if account name is configured // and project is not predefined // since predefined project should be prepared during Che deployment @@ -95,6 +91,6 @@ OpenShiftProject doCreateProject(String workspaceId, String name) { @VisibleForTesting OpenShiftWorkspaceServiceAccount doCreateServiceAccount(String workspaceId, String projectName) { return new OpenShiftWorkspaceServiceAccount( - workspaceId, projectName, serviceAccountName, clusterRoleName, clientFactory); + workspaceId, projectName, getServiceAccountName(), getClusterRoleName(), clientFactory); } }