Skip to content

Commit

Permalink
workspace namespace/project placeholders (#14524)
Browse files Browse the repository at this point in the history
* namespace for workspace with username and userid placeholders

* write meaningful error message when failed to create namespace/project due to lack of permissions

* describe placeholders in kubernetes.namespace/openshift.project properties

* refactor detect placeholders in namespace to stream

* check if namespace name is predefined when evaluating
  • Loading branch information
sparkoo authored Sep 19, 2019
1 parent 4ba2ec7 commit 6d9cfe6
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <username> and <userid> placeholders (e.g.: che-workspace-<username>).
# 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=
Expand Down Expand Up @@ -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 <username> and <userid> placeholders (e.g.: che-workspace-<username>).
# 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -29,6 +34,14 @@
@Singleton
public class KubernetesNamespaceFactory {

private static final Map<String, Function<Subject, String>> NAMESPACE_NAME_PLACEHOLDERS =
new HashMap<>();

static {
NAMESPACE_NAME_PLACEHOLDERS.put("<username>", Subject::getUserName);
NAMESPACE_NAME_PLACEHOLDERS.put("<userid>", Subject::getUserId);
}

private final String namespaceName;
private final boolean isPredefined;
private final String serviceAccountName;
Expand All @@ -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;
Expand All @@ -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();

Expand All @@ -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.
*
Expand All @@ -106,4 +140,12 @@ KubernetesWorkspaceServiceAccount doCreateServiceAccount(
return new KubernetesWorkspaceServiceAccount(
workspaceId, namespaceName, serviceAccountName, clusterRoleName, clientFactory);
}

protected String getServiceAccountName() {
return serviceAccountName;
}

protected String getClusterRoleName() {
return clusterRoleName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -184,4 +185,15 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProject
// then
verify(namespaceFactory, never()).doCreateServiceAccount(any(), any());
}

@Test
public void testPlaceholder() {
namespaceFactory =
new KubernetesNamespaceFactory(
"blabol-<userid>-<username>-<userid>-<username>--", "", "", clientFactory);
String namespace =
namespaceFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false));

assertEquals(namespace, "blabol-123-JonDoe-123-JonDoe--");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 6d9cfe6

Please sign in to comment.