Skip to content

Commit

Permalink
best effort to label namespace when starting new workspace (#18248)
Browse files Browse the repository at this point in the history
  • Loading branch information
sparkoo committed Nov 5, 2020
1 parent 7007453 commit c3a51fc
Show file tree
Hide file tree
Showing 10 changed files with 498 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ che.infra.kubernetes.namespace.creation_allowed=true
che.infra.kubernetes.namespace.default=<username>-che

# List of labels to find Namespaces/Projects that are used for Che Workspaces.
# They are used to find prepared Namespaces/Projects for users in combination with `che.infra.kubernetes.namespace.annotations`.
# They are used to:
# - find prepared Namespaces/Projects for users in combination with `che.infra.kubernetes.namespace.annotations`.
# - actively label namespaces with any workspace.
che.infra.kubernetes.namespace.labels=app.kubernetes.io/part-of=che.eclipse.org,app.kubernetes.io/component=workspaces-namespace

# List of annotations to find Namespaces/Projects prepared for Che users workspaces.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import org.eclipse.che.commons.annotation.Nullable;

/**
* This {@link KubernetesClientFactory} is used to access Che installation namespace. It always
* provides client with default {@link Config}.
* This {@link KubernetesClientFactory} ensures that we use `che` ServiceAccount and not related to
* any workspace. It always provides client with default {@link Config}. It's useful for operations
* that needs permissions of `che` SA, such as operations inside `che` namespace (like creating a
* ConfigMaps for Gateway router) or some cluste-wide actions (like labeling the namespaces).
*/
@Singleton
public class CheServerKubernetesClientFactory extends KubernetesClientFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.DoneableServiceAccount;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.Secret;
Expand All @@ -28,6 +29,8 @@
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.dsl.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -69,13 +72,18 @@ public class KubernetesNamespace {
private final KubernetesServices services;
private final KubernetesPersistentVolumeClaims pvcs;
private final KubernetesIngresses ingresses;
/** Factory for workspace related operations clients */
private final KubernetesClientFactory clientFactory;
/** Factory for cluster related operations clients (like labeling the namespaces) */
private final KubernetesClientFactory cheSAClientFactory;

private final KubernetesSecrets secrets;
private final KubernetesConfigsMaps configMaps;

@VisibleForTesting
protected KubernetesNamespace(
KubernetesClientFactory clientFactory,
KubernetesClientFactory cheSAClientFactory,
String workspaceId,
String name,
KubernetesDeployments deployments,
Expand All @@ -85,6 +93,7 @@ protected KubernetesNamespace(
KubernetesSecrets secrets,
KubernetesConfigsMaps configMaps) {
this.clientFactory = clientFactory;
this.cheSAClientFactory = cheSAClientFactory;
this.workspaceId = workspaceId;
this.name = name;
this.deployments = deployments;
Expand All @@ -96,8 +105,13 @@ protected KubernetesNamespace(
}

public KubernetesNamespace(
KubernetesClientFactory clientFactory, Executor executor, String name, String workspaceId) {
KubernetesClientFactory clientFactory,
KubernetesClientFactory cheSAClientFactory,
Executor executor,
String name,
String workspaceId) {
this.clientFactory = clientFactory;
this.cheSAClientFactory = cheSAClientFactory;
this.workspaceId = workspaceId;
this.name = name;
this.deployments = new KubernetesDeployments(name, workspaceId, clientFactory, executor);
Expand All @@ -113,12 +127,17 @@ public KubernetesNamespace(
*
* <p>Preparing includes creating if needed and waiting for default service account.
*
* <p>The method will try to label the namespace with provided `labels`. It does not matter if the
* namespace already exists or we create new one. If update labels operation fail due to lack of
* permission, we do not fail completely.
*
* @param canCreate defines what to do when the namespace is not found. The namespace is created
* when {@code true}, otherwise an exception is thrown.
* @param labels labels that should be set to the namespace
* @throws InfrastructureException if any exception occurs during namespace preparation or if the
* namespace doesn't exist and {@code canCreate} is {@code false}.
*/
void prepare(boolean canCreate) throws InfrastructureException {
void prepare(boolean canCreate, Map<String, String> labels) throws InfrastructureException {
KubernetesClient client = clientFactory.create(workspaceId);
Namespace namespace = get(name, client);

Expand All @@ -127,7 +146,51 @@ void prepare(boolean canCreate) throws InfrastructureException {
throw new InfrastructureException(
format("Creating the namespace '%s' is not allowed, yet it was not found.", name));
}
create(name, client);
namespace = create(name, client);
}
label(namespace, labels);
}

/**
* Applies given `ensureLabels` into given `namespace` and update the `namespace` in the
* Kubernetes.
*
* <p>If we do not have permissions to do so (code=403), this method does not throw any exception.
*
* @param namespace namespace to label
* @param ensureLabels these labels should be applied on given `namespace`
* @throws InfrastructureException if something goes wrong with update, except lack of permissions
*/
protected void label(Namespace namespace, Map<String, String> ensureLabels)
throws InfrastructureException {
Map<String, String> currentLabels = namespace.getMetadata().getLabels();
Map<String, String> newLabels =
currentLabels != null ? new HashMap<>(currentLabels) : new HashMap<>();

if (newLabels.entrySet().containsAll(ensureLabels.entrySet())) {
LOG.debug(
"Nothing to do, namespace [{}] already has all required labels.",
namespace.getMetadata().getName());
return;
}

try {
// update the namespace with new labels
cheSAClientFactory
.create()
.namespaces()
.createOrReplace(
new NamespaceBuilder(namespace)
.editMetadata()
.addToLabels(ensureLabels)
.endMetadata()
.build());
} catch (KubernetesClientException kce) {
if (kce.getCode() == 403) {
LOG.debug("Can't label the namespace due to lack of permissions ¯\\_(ツ)_/¯");
return;
}
throw new InfrastructureException(kce);
}
}

Expand Down Expand Up @@ -231,17 +294,19 @@ protected void doRemove(RemoveOperation... operations) throws InfrastructureExce
}
}

private void create(String namespaceName, KubernetesClient client)
private Namespace create(String namespaceName, KubernetesClient client)
throws InfrastructureException {
try {
client
.namespaces()
.createNew()
.withNewMetadata()
.withName(namespaceName)
.endMetadata()
.done();
Namespace namespace =
client
.namespaces()
.createNew()
.withNewMetadata()
.withName(namespaceName)
.endMetadata()
.done();
waitDefaultServiceAccount(namespaceName, client);
return namespace;
} catch (KubernetesClientException e) {
if (e.getCode() == 403) {
LOG.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta;
Expand Down Expand Up @@ -99,6 +100,7 @@ public class KubernetesNamespaceFactory {
private final String serviceAccountName;
private final Set<String> clusterRoleNames;
private final KubernetesClientFactory clientFactory;
private final KubernetesClientFactory cheClientFactory;
private final boolean namespaceCreationAllowed;
private final UserManager userManager;
private final PreferenceManager preferenceManager;
Expand All @@ -116,6 +118,7 @@ public KubernetesNamespaceFactory(
@Named("che.infra.kubernetes.namespace.labels") String namespaceLabels,
@Named("che.infra.kubernetes.namespace.annotations") String namespaceAnnotations,
KubernetesClientFactory clientFactory,
CheServerKubernetesClientFactory cheClientFactory,
UserManager userManager,
PreferenceManager preferenceManager,
KubernetesSharedPool sharedPool)
Expand All @@ -125,6 +128,7 @@ public KubernetesNamespaceFactory(
this.legacyNamespaceName = legacyNamespaceName;
this.serviceAccountName = serviceAccountName;
this.clientFactory = clientFactory;
this.cheClientFactory = cheClientFactory;
this.defaultNamespaceName = defaultNamespaceName;
this.allowUserDefinedNamespaces = allowUserDefinedNamespaces;
this.preferenceManager = preferenceManager;
Expand Down Expand Up @@ -168,7 +172,8 @@ public KubernetesNamespace access(String workspaceId, String namespace) {

@VisibleForTesting
KubernetesNamespace doCreateNamespaceAccess(String workspaceId, String name) {
return new KubernetesNamespace(clientFactory, sharedPool.getExecutor(), name, workspaceId);
return new KubernetesNamespace(
clientFactory, cheClientFactory, sharedPool.getExecutor(), name, workspaceId);
}

/**
Expand Down Expand Up @@ -386,7 +391,7 @@ protected boolean isWorkspaceNamespaceManaged(String namespaceName, Workspace wo
public KubernetesNamespace getOrCreate(RuntimeIdentity identity) throws InfrastructureException {
KubernetesNamespace namespace = get(identity);

namespace.prepare(canCreateNamespace(identity));
namespace.prepare(canCreateNamespace(identity), namespaceLabels);

if (!isNullOrEmpty(serviceAccountName)) {
KubernetesWorkspaceServiceAccount workspaceServiceAccount =
Expand Down
Loading

0 comments on commit c3a51fc

Please sign in to comment.