Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

workspace namespace/project placeholders #14524

Merged
merged 5 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,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 @@ -574,6 +577,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);
sparkoo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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,20 @@ public KubernetesNamespace create(String workspaceId) throws InfrastructureExcep
return namespace;
}

protected String evalNamespaceName(String workspaceId, Subject currentUser) {
if (isNullOrEmpty(this.namespaceName)) {
return workspaceId;
} else {
String tmpNamespaceName = this.namespaceName;
for (String placeholder : NAMESPACE_NAME_PLACEHOLDERS.keySet()) {
sparkoo marked this conversation as resolved.
Show resolved Hide resolved
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 +138,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);
}
}