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 4102405b77494020f74816bbc1b5f4dfe4e79f8d..e2ed48c89cbc3d027c518ee90ed8a9ec36adce64 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 @@ -377,6 +377,9 @@ che.infra.kubernetes.server_strategy=default-host # Used to generate domain for a server in a workspace in case property `che.infra.kubernetes.server_strategy` is set to `multi-host` che.infra.kubernetes.ingress.domain= +# DEPRECATED - please do not change the value of this property otherwise the existing workspaces will loose data. Do not +# set it on new installations. +# # 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>). @@ -384,24 +387,23 @@ che.infra.kubernetes.ingress.domain= # to create new namespace must be used. # # Ignored for OpenShift infra. Use `che.infra.openshift.project` instead +# +# If the namespace pointed to by this property exists, it will be used for all workspaces. If it does not exist, +# the namespace specified by the che.infra.kubernetes.namespace.default will be created and used. che.infra.kubernetes.namespace= # Defines Kubernetes default namespace in which user's workspaces are created # if user does not override it. -# 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. +# It's possible to use <username>, <userid> and <workspaceid> placeholders (e.g.: che-workspace-<username>). +# In that case, new namespace will be created for each user (or workspace). # Is used by OpenShift infra as well to specify Project -# -# BETA It's not fully supported by infra. -# Use che.infra.kubernetes.namespace to configure workspaces' namespace che.infra.kubernetes.namespace.default=<username>-che # Defines if a user is able to specify Kubernetes namespace different from default. # It's NOT RECOMMENDED to configured true without OAuth configured. # Is used by OpenShift infra as well to allows users choose Project # -# BETA It's not fully supported by infra. -# Use che.infra.kubernetes.namespace to configure workspaces' namespace +# BETA This is not currently supported and setting it to true doesn't have any effect. che.infra.kubernetes.namespace.allow_user_defined=false # Defines Kubernetes Service Account name which should be specified to be bound to all workspaces pods. @@ -606,11 +608,17 @@ che.infra.kubernetes.runtimes_consistency_check_period_min=-1 # OpenShift infrastructure reuse most of the Kubernetes configuration attributes. # +# DEPRECATED - please do not change the value of this property otherwise the existing workspaces will loose data. Do not +# set it on new installations. +# # 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. +# +# If the project pointed to by this property exists, it will be used for all workspaces. If it does not exist, +# the namespace specified by the che.infra.kubernetes.namespace.default will be created and used. che.infra.openshift.project= # Single port mode wildcard domain host & port. nip.io is used by default diff --git a/deploy/kubernetes/helm/che/templates/configmap.yaml b/deploy/kubernetes/helm/che/templates/configmap.yaml index 233bbe6a28d9d777198e901ad485ef21b8fdfebc..8dcbc1d3eefc998dd7c2f51c3f3d96bc94be53f2 100644 --- a/deploy/kubernetes/helm/che/templates/configmap.yaml +++ b/deploy/kubernetes/helm/che/templates/configmap.yaml @@ -50,7 +50,7 @@ data: {{- if and .Values.global.multiuser .Values.customOidcUsernameClaim }} CHE_KEYCLOAK_USERNAME__CLAIM: {{ .Values.customOidcUsernameClaim }} {{- end }} - CHE_INFRA_KUBERNETES_NAMESPACE: {{ .Values.global.cheWorkspacesNamespace | quote}} + CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT: {{ .Values.global.cheWorkspacesNamespace | quote}} CHE_INFRA_KUBERNETES_SERVICE__ACCOUNT__NAME: {{ .Values.global.cheWorkspaceServiceAccount }} CHE_INFRA_KUBERNETES_TRUST__CERTS: "false" CHE_INFRA_KUBERNETES_PVC_STRATEGY: "common" diff --git a/deploy/kubernetes/helm/che/templates/deployment.yaml b/deploy/kubernetes/helm/che/templates/deployment.yaml index 0fa452f5be3896716f9eb9451f0a6f437f702754..339fb24e5db099c963a569efb5b4640293b046dd 100644 --- a/deploy/kubernetes/helm/che/templates/deployment.yaml +++ b/deploy/kubernetes/helm/che/templates/deployment.yaml @@ -85,9 +85,9 @@ spec: optional: false {{- end }} - # If workspaces are created in different namespace than Che Server's one + # If workspaces are created in a separate precreated namespace # then configure Che Server to propagate TLS secret to workspaces' namespaces - {{- if ne .Release.Namespace .Values.global.cheWorkspacesNamespace }} + {{- if not (contains "<" .Values.global.cheWorkspacesNamespace) }} - name: "CHE_INFRA_KUBERNETES_TLS__CERT" valueFrom: secretKeyRef: diff --git a/deploy/kubernetes/helm/che/templates/exec-role.yaml b/deploy/kubernetes/helm/che/templates/exec-role.yaml index 9801d644bf93573f8a1ffee702159c39b5cdcf73..e5b8f05ccfcebf067837128855e11ccf81e638af 100644 --- a/deploy/kubernetes/helm/che/templates/exec-role.yaml +++ b/deploy/kubernetes/helm/che/templates/exec-role.yaml @@ -7,7 +7,7 @@ # SPDX-License-Identifier: EPL-2.0 # -{{- if (.Values.global.cheWorkspacesNamespace) }} +{{- if not (contains "<" .Values.global.cheWorkspacesNamespace) }} kind: Role apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: diff --git a/deploy/kubernetes/helm/che/templates/workspace-exec-role-binding.yaml b/deploy/kubernetes/helm/che/templates/workspace-exec-role-binding.yaml index 0c35fefe0f77fde628b8c2ce5da61bc081f3b1b1..53df54a700a9587aef1659552faab18e1ed7da4d 100644 --- a/deploy/kubernetes/helm/che/templates/workspace-exec-role-binding.yaml +++ b/deploy/kubernetes/helm/che/templates/workspace-exec-role-binding.yaml @@ -7,7 +7,7 @@ # SPDX-License-Identifier: EPL-2.0 # -{{- if (.Values.global.cheWorkspacesNamespace) }} +{{- if not (contains "<" .Values.global.cheWorkspacesNamespace) }} kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: diff --git a/deploy/kubernetes/helm/che/templates/workspace-service-account.yaml b/deploy/kubernetes/helm/che/templates/workspace-service-account.yaml index 310667740a972aba74b5dce0a84db8455dfc334a..52bd1d46f22546f18a54a1592ad76ca26ef542b3 100644 --- a/deploy/kubernetes/helm/che/templates/workspace-service-account.yaml +++ b/deploy/kubernetes/helm/che/templates/workspace-service-account.yaml @@ -7,7 +7,7 @@ # SPDX-License-Identifier: EPL-2.0 # -{{- if (.Values.global.cheWorkspacesNamespace) }} +{{- if not (contains "<" .Values.global.cheWorkspacesNamespace) }} kind: ServiceAccount apiVersion: v1 metadata: diff --git a/deploy/kubernetes/helm/che/templates/workspace-view-role-binding.yaml b/deploy/kubernetes/helm/che/templates/workspace-view-role-binding.yaml index 0bcd17c4e2bf303058a8c1118b82dbb4e97293e5..5faa5f6b08493a38dc252d33ea8bd97de851ec1f 100644 --- a/deploy/kubernetes/helm/che/templates/workspace-view-role-binding.yaml +++ b/deploy/kubernetes/helm/che/templates/workspace-view-role-binding.yaml @@ -7,7 +7,7 @@ # SPDX-License-Identifier: EPL-2.0 # -{{- if (.Values.global.cheWorkspacesNamespace) }} +{{- if not (contains "<" .Values.global.cheWorkspacesNamespace) }} kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: diff --git a/deploy/kubernetes/helm/che/templates/workspace-view-role.yaml b/deploy/kubernetes/helm/che/templates/workspace-view-role.yaml index 59e3732c30304d8d9bcb43f09ea677fe7da0fc4d..4cf8e2bb36c0207fcf56aa171d068489dc9d3584 100644 --- a/deploy/kubernetes/helm/che/templates/workspace-view-role.yaml +++ b/deploy/kubernetes/helm/che/templates/workspace-view-role.yaml @@ -7,7 +7,7 @@ # SPDX-License-Identifier: EPL-2.0 # -{{- if (.Values.global.cheWorkspacesNamespace) }} +{{- if not (contains "<" .Values.global.cheWorkspacesNamespace) }} kind: Role apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: diff --git a/deploy/kubernetes/helm/che/values.yaml b/deploy/kubernetes/helm/che/values.yaml index b2970314887ea6ea15a25b117ff16b0fd55e0fd6..24ab39322c6785a9ee4f4ecd1757e1cb280f19b9 100644 --- a/deploy/kubernetes/helm/che/values.yaml +++ b/deploy/kubernetes/helm/che/values.yaml @@ -53,11 +53,11 @@ global: gitHubClientID: "" gitHubClientSecret: "" pvcClaim: "1Gi" - cheWorkspacesNamespace: "" + cheWorkspacesNamespace: "<username>-che" # Service account name that will be mounted to workspaces pods # Note that: - # if `cheWorkspacesNamespace` is configured then service account with configured name will be created by helm chart during deploying Che - # if `cheWorkspacesNamespace` is empty then Che Server creates new namespace for each workspace and ensures that configured SA exists there + # if `cheWorkspacesNamespace` doesn't contain placeholders then service account with configured name will be created by helm chart during deploying Che + # if `cheWorkspacesNamespace` contains placeholders then Che Server creates new namespaces accordingly and ensures that configured SA exists there cheWorkspaceServiceAccount: "che-workspace" # If set, Che will bind the specified cluster role to the workspace service account when creating a workspace. cheWorkspaceClusterRole: "" 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 16cb46375ed05ff7c2f4285dcc82012dc9e7676f..cc4c9373a7d877f89db5078cdb486009009bdbc6 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 @@ -11,6 +11,8 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; +import static java.lang.String.format; + import com.google.common.annotations.VisibleForTesting; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.DoneableServiceAccount; @@ -117,6 +119,17 @@ public class KubernetesNamespace { } } + /** + * Deletes the namespace. Deleting a non-existent namespace is not an error as is not an attempt + * to delete a namespace that is already being deleted. + * + * @throws InfrastructureException if any unexpected exception occurs during namespace deletion + */ + void delete() throws InfrastructureException { + KubernetesClient client = clientFactory.create(workspaceId); + delete(name, client); + } + /** Returns namespace name */ public String getName() { return name; @@ -214,6 +227,25 @@ public class KubernetesNamespace { } } + private void delete(String namespaceName, KubernetesClient client) + throws InfrastructureException { + try { + client.namespaces().withName(namespaceName).delete(); + } catch (KubernetesClientException e) { + if (e.getCode() == 404) { + LOG.warn( + format( + "Tried to delete namespace '%s' but it doesn't exist in the cluster.", + namespaceName), + e); + } else if (e.getCode() == 409) { + LOG.info(format("The namespace '%s' is currently being deleted.", namespaceName), e); + } else { + throw new KubernetesInfrastructureException(e); + } + } + } + /** * Waits few seconds until 'default' service account become available otherwise an infrastructure * exception will be thrown. 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 243d454649782e3f53c16d84ca7f05dfb588a0a9..dd2718a0cfefd4fe57901160162b1d08b6eb47db 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 @@ -13,6 +13,7 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; import static java.util.Collections.singletonList; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; @@ -30,7 +31,14 @@ import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import javax.inject.Named; +import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.ValidationException; +import org.eclipse.che.api.core.model.workspace.Workspace; +import org.eclipse.che.api.workspace.server.WorkspaceManager; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.shared.Constants; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.subject.Subject; @@ -38,6 +46,8 @@ import org.eclipse.che.inject.ConfigurationException; 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Helps to create {@link KubernetesNamespace} instances. @@ -46,41 +56,57 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.Kubernetes */ @Singleton public class KubernetesNamespaceFactory { + private static final Logger LOG = LoggerFactory.getLogger(KubernetesNamespaceFactory.class); - private static final Map<String, Function<Subject, String>> NAMESPACE_NAME_PLACEHOLDERS = - new HashMap<>(); + private static final Map<String, Function<PlaceholderResolutionContext, String>> + NAMESPACE_NAME_PLACEHOLDERS = new HashMap<>(); + + private static final String USERNAME_PLACEHOLDER = "<username>"; + private static final String USERID_PLACEHOLDER = "<userid>"; + private static final String WORKSPACEID_PLACEHOLDER = "<workspaceid>"; static { - NAMESPACE_NAME_PLACEHOLDERS.put("<username>", Subject::getUserName); - NAMESPACE_NAME_PLACEHOLDERS.put("<userid>", Subject::getUserId); + NAMESPACE_NAME_PLACEHOLDERS.put(USERNAME_PLACEHOLDER, ctx -> ctx.user.getUserName()); + NAMESPACE_NAME_PLACEHOLDERS.put(USERID_PLACEHOLDER, ctx -> ctx.user.getUserId()); + NAMESPACE_NAME_PLACEHOLDERS.put(WORKSPACEID_PLACEHOLDER, ctx -> ctx.workspaceId); } private final String defaultNamespaceName; private final boolean allowUserDefinedNamespaces; - private final String namespaceName; - private final boolean isPredefined; + private final String legacyNamespaceName; private final String serviceAccountName; private final String clusterRoleName; private final KubernetesClientFactory clientFactory; + private final WorkspaceManager workspaceManager; @Inject public KubernetesNamespaceFactory( - @Nullable @Named("che.infra.kubernetes.namespace") String namespaceName, + @Nullable @Named("che.infra.kubernetes.namespace") String legacyNamespaceName, @Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName, @Nullable @Named("che.infra.kubernetes.cluster_role_name") String clusterRoleName, @Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName, @Named("che.infra.kubernetes.namespace.allow_user_defined") boolean allowUserDefinedNamespaces, - KubernetesClientFactory clientFactory) + KubernetesClientFactory clientFactory, + WorkspaceManager workspaceManager) throws ConfigurationException { - this.namespaceName = namespaceName; - this.isPredefined = !isNullOrEmpty(namespaceName) && hasNoPlaceholders(this.namespaceName); + this.legacyNamespaceName = legacyNamespaceName; this.serviceAccountName = serviceAccountName; this.clusterRoleName = clusterRoleName; this.clientFactory = clientFactory; this.defaultNamespaceName = defaultNamespaceName; this.allowUserDefinedNamespaces = allowUserDefinedNamespaces; + this.workspaceManager = workspaceManager; + + // This will disappear once we support user selection of workspaces... + if (allowUserDefinedNamespaces) { + LOG.warn( + "'che.infra.kubernetes.namespace.allow_user_defined' is not supported yet. It currently has no" + + " effect."); + } + + // right now allowUserDefinedNamespaces can't be true, but eventually we will implement it. if (isNullOrEmpty(defaultNamespaceName) && !allowUserDefinedNamespaces) { throw new ConfigurationException( "che.infra.kubernetes.namespace.default or " @@ -88,16 +114,82 @@ public class KubernetesNamespaceFactory { } } - private boolean hasNoPlaceholders(String namespaceName) { - return NAMESPACE_NAME_PLACEHOLDERS.keySet().stream().noneMatch(namespaceName::contains); + private boolean hasPlaceholders(String namespaceName) { + return namespaceName != null + && NAMESPACE_NAME_PLACEHOLDERS.keySet().stream().anyMatch(namespaceName::contains); } /** - * 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. + * True if namespace is potentially created for the workspace, false otherwise. + * + * <p>The logic is a little bit non-trivial and best expressed by just fully evaluting the truth + * table as below ({@code ...namespace} stands for the legacy namespace property, {@code + * ...namespace.default} stands for the namespace default property): + * + * <pre>{@code + * ...namespace | ...namespace exists | ...namespace.default | creating? + * no-placeholders | no | null | error + * no-placeholders | no | no-placeholders | no + * no-placeholders | no | placeholders | yes + * no-placeholders | yes | null | no + * no-placeholders | yes | no-placeholders | no + * no-placeholders | yes | placeholders | no + * placeholders | no | null | error + * placeholders | no | no-placeholders | no + * placeholders | no | placeholders | yes + * placeholders | yes | null | yes + * placeholders | yes | no-placeholders | yes + * placeholders | yes | placeholders | yes + * }</pre> */ - public boolean isPredefined() { - return isPredefined; + protected boolean isCreatingNamespace(String workspaceId) throws InfrastructureException { + boolean legacyExists = + checkNamespaceExists( + resolveLegacyNamespaceName(EnvironmentContext.getCurrent().getSubject(), workspaceId)); + + // legacy namespace exists and should be used + if (legacyExists) { + // if it contains any placeholder("" is <workspaceid>) - it indicates that Che created + // namespace by itself + return isNullOrEmpty(legacyNamespaceName) || hasPlaceholders(legacyNamespaceName); + } + + if (isNullOrEmpty(defaultNamespaceName)) { + throw new InfrastructureException( + "Cannot determine whether a new namespace and service account should be" + + " created for workspace %s. There is no pre-existing workspace namespace to be" + + " found using the legacy `che.infra.kubernetes.namespace` property yet the" + + " `che.infra.kubernetes.namespace.default` property is undefined."); + } + + return hasPlaceholders(defaultNamespaceName); + } + + /** + * Returns true if the namespace is fully managed by Che (e.g. created, used and deleted), false + * otherwise. + */ + public boolean isManagingNamespace(String workspaceId) throws InfrastructureException { + // the new logic is quite simple. + boolean newLogic = + defaultNamespaceName != null && defaultNamespaceName.contains(WORKSPACEID_PLACEHOLDER); + + // but we must follow the same logic as #evalNamespaceName - we need to make sure that the old + // logic can't be used first... + // empty legacy namespace name ~ <workspaceid> + if (isNullOrEmpty(legacyNamespaceName) + || legacyNamespaceName.contains(WORKSPACEID_PLACEHOLDER)) { + + // there's a chance of using the old logic - if the namespace exists, we're managing it. + // if it doesn't, we're using the new logic. + return checkNamespaceExists( + resolveLegacyNamespaceName(EnvironmentContext.getCurrent().getSubject(), workspaceId)) + || newLogic; + } else { + // there's no way the namespace of the workspace is managed using the old logic. Let's just + // use the result of the new logic. + return newLogic; + } } /** @@ -141,8 +233,7 @@ public class KubernetesNamespaceFactory { // the default namespace must be configured if user defined are not allowed // so return only it String evaluatedName = - evalDefaultNamespaceName( - defaultNamespaceName, EnvironmentContext.getCurrent().getSubject()); + evalPlaceholders(defaultNamespaceName, EnvironmentContext.getCurrent().getSubject(), null); Optional<KubernetesNamespaceMeta> defaultNamespaceOpt = fetchNamespace(evaluatedName); @@ -164,8 +255,7 @@ public class KubernetesNamespaceFactory { */ private void provisionDefaultNamespace(List<KubernetesNamespaceMeta> namespaces) { String evaluatedName = - evalDefaultNamespaceName( - defaultNamespaceName, EnvironmentContext.getCurrent().getSubject()); + evalPlaceholders(defaultNamespaceName, EnvironmentContext.getCurrent().getSubject(), null); Optional<KubernetesNamespaceMeta> defaultNamespaceOpt = namespaces.stream().filter(n -> evaluatedName.equals(n.getName())).findAny(); @@ -244,12 +334,22 @@ public class KubernetesNamespaceFactory { * @throws InfrastructureException if any exception occurs during namespace preparing */ public KubernetesNamespace create(String workspaceId) throws InfrastructureException { - final String namespaceName = - evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); + final String namespaceName; + try { + namespaceName = evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); + } catch (NotFoundException | ServerException | ConflictException | ValidationException e) { + throw new InfrastructureException( + format( + "Failed to determine the namespace to put the workspace %s to." + + " The error message was: %s", + workspaceId, e.getMessage()), + e); + } + KubernetesNamespace namespace = doCreateNamespace(workspaceId, namespaceName); namespace.prepare(); - if (!isPredefined() && !isNullOrEmpty(serviceAccountName)) { + if (isCreatingNamespace(workspaceId) && !isNullOrEmpty(serviceAccountName)) { // 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 @@ -261,29 +361,114 @@ public class KubernetesNamespaceFactory { return namespace; } - protected String evalNamespaceName(String workspaceId, Subject currentUser) { - if (isPredefined) { - return this.namespaceName; - } else if (isNullOrEmpty(this.namespaceName)) { - return workspaceId; + public void delete(String workspaceId) throws InfrastructureException { + final String namespaceName; + try { + namespaceName = evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); + } catch (NotFoundException | ServerException | ConflictException | ValidationException e) { + throw new InfrastructureException( + format("Failed to determine the namespace of the workspace %s", workspaceId), e); + } + + KubernetesNamespace namespace = doCreateNamespace(workspaceId, namespaceName); + namespace.delete(); + } + + protected String evalNamespaceName(String workspaceId, Subject currentUser) + throws NotFoundException, ServerException, InfrastructureException, ConflictException, + ValidationException { + // This, my friend, is a hack of magnificent proportions put forth as a result of dire time + // constraints imposed on the tears shedding developer writing these unfortunate lines. + // The effort required to propagate the full workspace, including the attributes (or some + // alternative thereof) from the callers (all of which happen to already possess the + // information) down to this sad place is too effing much to do with confidence in the + // couple of days left until the release. Let's pretend we will have time to fix this later + // in the better times... + Workspace wkspc = workspaceManager.getWorkspace(workspaceId); + String ns = wkspc.getAttributes().get(Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE); + + if (ns != null) { + LOG.debug( + "Found target namespace in workspace attributes. Using namespace {} for workspace {}", + ns, + workspaceId); + return ns; } else { - String tmpNamespaceName = this.namespaceName; - for (String placeholder : NAMESPACE_NAME_PLACEHOLDERS.keySet()) { - tmpNamespaceName = - tmpNamespaceName.replaceAll( - placeholder, NAMESPACE_NAME_PLACEHOLDERS.get(placeholder).apply(currentUser)); + String namespace = resolveLegacyNamespaceName(currentUser, workspaceId); + + if (checkNamespaceExists(namespace)) { + LOG.debug( + "The namespace specified using the legacy config exists: {}. Using it for workspace {}.", + namespace, + workspaceId); + } else { + // ok, the namespace pointed to by the legacy config doesn't exist.. that means there can be + // no damage done by storing the workspace in the namespace designated by the new way of + // doing things... + + if (isNullOrEmpty(defaultNamespaceName)) { + throw new InfrastructureException( + format( + "'che.infra.kubernetes.namespace.default' is not" + + " defined and no explicit namespace configured for workspace %s", + workspaceId)); + } + + namespace = evalPlaceholders(defaultNamespaceName, currentUser, workspaceId); + + LOG.debug( + "Evaluated the namespace for workspace {} using the namespace default to {}", + workspaceId, + namespace); } - return tmpNamespaceName; + + // Now, believe it or not, the horror continues - since the callers are as of now unaware + // of the namespaces being stored within the workspace, we need to do it all here. Hopefully, + // one day, when the callers (and workspace manager in particular) support workspace namespace + // selection, things will make more sense again because this logic will have to move up a + // layer or two and become infrastructure independent. + wkspc.getAttributes().put(Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE, namespace); + workspaceManager.updateWorkspace(workspaceId, wkspc); + + return namespace; } } - protected String evalDefaultNamespaceName(String defaultNamespace, Subject currentUser) { - checkArgument(!isNullOrEmpty(defaultNamespace)); - String evaluated = defaultNamespace; - for (Entry<String, Function<Subject, String>> placeHolder : + private String resolveLegacyNamespaceName(Subject user, String workspaceId) { + String effectiveOldLogicNamespace = + isNullOrEmpty(legacyNamespaceName) ? WORKSPACEID_PLACEHOLDER : legacyNamespaceName; + + return evalPlaceholders(effectiveOldLogicNamespace, user, workspaceId); + } + + protected boolean checkNamespaceExists(String namespaceName) throws InfrastructureException { + try { + return clientFactory.create().namespaces().withName(namespaceName).get() != null; + } catch (KubernetesClientException e) { + if (e.getCode() == 403) { + // 403 means that the project does not exist + // or a user really is not permitted to access it which is Che Server misconfiguration + return false; + } else { + throw new InfrastructureException( + "Error occurred when tried to fetch default project. Cause: " + e.getMessage(), e); + } + } + } + + protected String evalPlaceholders(String namespace, Subject currentUser, String workspaceId) { + checkArgument(!isNullOrEmpty(namespace)); + String evaluated = namespace; + PlaceholderResolutionContext ctx = new PlaceholderResolutionContext(currentUser, workspaceId); + for (Entry<String, Function<PlaceholderResolutionContext, String>> placeHolder : NAMESPACE_NAME_PLACEHOLDERS.entrySet()) { - evaluated = - evaluated.replaceAll(placeHolder.getKey(), placeHolder.getValue().apply(currentUser)); + + String key = placeHolder.getKey(); + String value = placeHolder.getValue().apply(ctx); + + if (value != null) { + evaluated = evaluated.replaceAll(key, value); + } } return evaluated; } @@ -302,4 +487,14 @@ public class KubernetesNamespaceFactory { protected String getClusterRoleName() { return clusterRoleName; } + + private static final class PlaceholderResolutionContext { + final Subject user; + final String workspaceId; + + private PlaceholderResolutionContext(Subject user, String workspaceId) { + this.user = user; + this.workspaceId = workspaceId; + } + } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemove.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemove.java index 14815a4fdcc50c1b49a36bab23b34c1e7ce29913..058ead96f7db310dc9b14b0627cd2f7563bf3024 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemove.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemove.java @@ -11,20 +11,12 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; -import static com.google.common.base.Strings.isNullOrEmpty; - -import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; -import io.fabric8.kubernetes.client.KubernetesClientException; -import javax.inject.Named; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.notification.EventSubscriber; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.shared.event.WorkspaceRemovedEvent; -import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,45 +29,30 @@ import org.slf4j.LoggerFactory; public class RemoveNamespaceOnWorkspaceRemove implements EventSubscriber<WorkspaceRemovedEvent> { private static final Logger LOG = LoggerFactory.getLogger(RemoveNamespaceOnWorkspaceRemove.class); - private final KubernetesClientFactory clientFactory; - private final String namespaceName; + private final KubernetesNamespaceFactory namespaceFactory; @Inject - public RemoveNamespaceOnWorkspaceRemove( - @Nullable @Named("che.infra.kubernetes.namespace") String namespaceName, - KubernetesClientFactory clientFactory) { - this.namespaceName = namespaceName; - this.clientFactory = clientFactory; + public RemoveNamespaceOnWorkspaceRemove(KubernetesNamespaceFactory namespaceFactory) { + this.namespaceFactory = namespaceFactory; } @Inject public void subscribe(EventService eventService) { - if (isNullOrEmpty(namespaceName)) { - eventService.subscribe(this); - } + eventService.subscribe(this); } @Override public void onEvent(WorkspaceRemovedEvent event) { + String workspaceId = event.getWorkspace().getId(); try { - doRemoveNamespace(event.getWorkspace().getId()); + if (namespaceFactory.isManagingNamespace(workspaceId)) { + namespaceFactory.delete(workspaceId); + } } catch (InfrastructureException e) { LOG.warn( "Fail to remove Kubernetes namespace for workspace with id {}. Cause: {}", - event.getWorkspace().getId(), + workspaceId, e.getMessage()); } } - - @VisibleForTesting - void doRemoveNamespace(String namespaceName) throws InfrastructureException { - try { - clientFactory.create(namespaceName).namespaces().withName(namespaceName).delete(); - } catch (KubernetesClientException e) { - if (!(e.getCode() == 403)) { - throw new KubernetesInfrastructureException(e); - } - // namespace doesn't exist - } - } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleaner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleaner.java index cdc365f42803fcadd94c8e6a54218795939d3c89..89ce7d4247bfb6d2fe713d613d5623c548448f50 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleaner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleaner.java @@ -52,11 +52,18 @@ public class WorkspacePVCCleaner { @Inject public void subscribe(EventService eventService) { - if (pvcEnabled && namespaceFactory.isPredefined()) + if (pvcEnabled) { eventService.subscribe( event -> { final Workspace workspace = event.getWorkspace(); try { + if (namespaceFactory.isManagingNamespace(workspace.getId())) { + // the namespaces of managed workspaces are deleted, so no need to do the cleanup + LOG.debug( + "Not cleaning up the PVCs of workspace %s, because its namespace is" + + " going to be deleted."); + return; + } strategy.cleanup(workspace); } catch (InfrastructureException ex) { LOG.error( @@ -66,5 +73,6 @@ public class WorkspacePVCCleaner { } }, WorkspaceRemovedEvent.class); + } } } 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 b1e1b1b46b7e460bd1235691222f7035bf89ddf6..8065ba3e571ef7aa2f04381bec5b62ab41f7ffce 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 @@ -11,10 +11,14 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; +import static java.lang.String.format; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -25,7 +29,7 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; import io.fabric8.kubernetes.api.model.DoneableNamespace; import io.fabric8.kubernetes.api.model.Namespace; @@ -37,7 +41,10 @@ import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import java.util.Arrays; import java.util.List; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.shared.Constants; import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; @@ -45,6 +52,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.Kubernetes import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Listeners; import org.testng.annotations.Test; @@ -58,18 +66,27 @@ public class KubernetesNamespaceFactoryTest { @Mock private KubernetesClientFactory clientFactory; @Mock private KubernetesClient k8sClient; + @Mock private WorkspaceManager workspaceManager; @Mock private NonNamespaceOperation< Namespace, NamespaceList, DoneableNamespace, Resource<Namespace, DoneableNamespace>> namespaceOperation; + @Mock private Resource<Namespace, DoneableNamespace> namespaceResource; + private KubernetesNamespaceFactory namespaceFactory; @BeforeMethod public void setUp() throws Exception { lenient().when(clientFactory.create()).thenReturn(k8sClient); lenient().when(k8sClient.namespaces()).thenReturn(namespaceOperation); + lenient() + .when(workspaceManager.getWorkspace(any())) + .thenReturn(WorkspaceImpl.builder().setId("1").setAttributes(emptyMap()).build()); + + lenient().when(namespaceOperation.withName(any())).thenReturn(namespaceResource); + lenient().when(namespaceResource.get()).thenReturn(mock(Namespace.class)); } @Test( @@ -81,7 +98,8 @@ public class KubernetesNamespaceFactoryTest { shouldThrowExceptionIfNoDefaultNamespaceIsConfiguredAndUserDefinedNamespacesAreNotAllowed() throws Exception { namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", null, false, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", null, false, clientFactory, workspaceManager); } @Test @@ -96,7 +114,8 @@ public class KubernetesNamespaceFactoryTest { .withNewStatus("Active") .build()); namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", "che-default", false, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", "che-default", false, clientFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = namespaceFactory.list(); assertEquals(availableNamespaces.size(), 1); @@ -112,7 +131,8 @@ public class KubernetesNamespaceFactoryTest { prepareNamespaceToBeFoundByName("che-default", null); namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", "che-default", false, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", "che-default", false, clientFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = namespaceFactory.list(); assertEquals(availableNamespaces.size(), 1); @@ -131,7 +151,8 @@ public class KubernetesNamespaceFactoryTest { "Error occurred when tried to fetch default namespace. Cause: connection refused") public void shouldThrownExceptionWhenFailedToGetInfoAboutDefaultNamespace() throws Exception { namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", "che", false, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", "che", false, clientFactory, workspaceManager); throwOnTryToGetNamespaceByName("che", new KubernetesClientException("connection refused")); namespaceFactory.list(); @@ -145,7 +166,8 @@ public class KubernetesNamespaceFactoryTest { createNamespace("experimental", "Terminating"))); namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", null, true, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", null, true, clientFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = namespaceFactory.list(); assertEquals(availableNamespaces.size(), 2); @@ -167,7 +189,8 @@ public class KubernetesNamespaceFactoryTest { createNamespace("my-for-ws", "Active"), createNamespace("default", "Active"))); namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", "default", true, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", "default", true, clientFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = namespaceFactory.list(); @@ -190,7 +213,8 @@ public class KubernetesNamespaceFactoryTest { prepareListedNamespaces(singletonList(createNamespace("my-for-ws", "Active"))); namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", "default", true, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", "default", true, clientFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = namespaceFactory.list(); assertEquals(availableNamespaces.size(), 2); @@ -214,54 +238,141 @@ public class KubernetesNamespaceFactoryTest { "Error occurred when tried to list all available namespaces. Cause: connection refused") public void shouldThrownExceptionWhenFailedToGetNamespaces() throws Exception { namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", null, true, clientFactory); + new KubernetesNamespaceFactory( + "predefined", "", "", null, true, clientFactory, workspaceManager); throwOnTryToGetNamespacesList(new KubernetesClientException("connection refused")); namespaceFactory.list(); } - @Test - public void shouldReturnTrueIfNamespaceIsNotEmptyOnCheckingIfNamespaceIsPredefined() { + @DataProvider + public static Object[][] creatingNamespaceConditions() { + // Whether or not the factory potentially creates a namespace depends on the 3 conditions: + // 1) the value of the legacy property 'che.infra.kubernetes.namespace' + // 2) legacy property pointing to the existing namespace + // 3) value of the new property 'che.infra.kubernetes.namespace.default' + // the output is either true, false, or error (represented as a null here) + + // this is the truth table we need to follow + // ...namespace | ...namespace exists | ...namespace.default | creating? + // no-placeholders | no | null | error + // no-placeholders | no | no-placeholders | no + // no-placeholders | no | placeholders | yes + // no-placeholders | yes | null | no + // no-placeholders | yes | no-placeholders | no + // no-placeholders | yes | placeholders | no + // placeholders | no | null | error + // placeholders | no | no-placeholders | no + // placeholders | no | placeholders | yes + // placeholders | yes | null | yes + // placeholders | yes | no-placeholders | yes + // placeholders | yes | placeholders | yes + + // additionally, we want to test that if the legacy property is null, it behaves exactly + // the same as having a placeholder, because it should default to <workspaceid> + return new Object[][] { + new Object[] {"some", false, null, null}, + new Object[] {"some", false, "some", false}, + new Object[] {"some", false, "<userid>", true}, + new Object[] {"some", true, null, false}, + new Object[] {"some", true, "some", false}, + new Object[] {"some", true, "<userid>", false}, + new Object[] {"<workspaceid>", false, null, null}, + new Object[] {"<workspaceid>", false, "some", false}, + new Object[] {"<workspaceid>", false, "<userid>", true}, + new Object[] {"<workspaceid>", true, null, true}, + new Object[] {"<workspaceid>", true, "some", true}, + new Object[] {"<workspaceid>", true, "<userid>", true}, + new Object[] {null, false, null, null}, + new Object[] {null, false, "some", false}, + new Object[] {null, false, "<userid>", true}, + new Object[] {null, true, null, true}, + new Object[] {null, true, "some", true}, + new Object[] {null, true, "<userid>", true}, + }; + } + + @Test(dataProvider = "creatingNamespaceConditions") + public void shouldDetermineWhenNamespaceCanBeCreated( + String legacyProperty, + boolean legacyNamespaceExists, + String namespaceProperty, + Boolean expectedOutcome) { + // given namespaceFactory = - new KubernetesNamespaceFactory("predefined", "", "", "che", false, clientFactory); + new KubernetesNamespaceFactory( + legacyProperty, "", "", namespaceProperty, true, clientFactory, workspaceManager); + + Namespace existingLegacyNamespace = legacyNamespaceExists ? mock(Namespace.class) : null; + when(namespaceResource.get()).thenReturn(existingLegacyNamespace); // when - boolean isPredefined = namespaceFactory.isPredefined(); + Boolean result; + try { + result = namespaceFactory.isCreatingNamespace("123"); + } catch (InfrastructureException e) { + // this can happen and we test for it below... + result = null; + } // then - assertTrue(isPredefined); + assertEquals(result, expectedOutcome); } - @Test - public void shouldReturnTrueIfNamespaceIsEmptyOnCheckingIfNamespaceIsPredefined() { - // given - namespaceFactory = new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory); - - // when - boolean isPredefined = namespaceFactory.isPredefined(); + @Test(dataProvider = "creatingNamespaceConditions") + public void testNotManagingNamespacesWheneverNotCreatingThem( + String legacyProperty, + boolean legacyNamespaceExists, + String namespaceProperty, + Boolean expectedCreating) + throws InfrastructureException { - // then - assertFalse(isPredefined); - } + // it is possible that we are creating namespaces that we are not fully managing, e.g. <user*> + // namespaces are created but not fully deleted afterwards. We just clean them. + // However, whenever a namespace is NOT being created, we should never claim we're managing the + // namespace. + // This is what this test asserts. - @Test - public void shouldReturnTrueIfNamespaceIsNullOnCheckingIfNamespaceIsPredefined() { // given - namespaceFactory = new KubernetesNamespaceFactory(null, "", "", "che", false, clientFactory); + namespaceFactory = + new KubernetesNamespaceFactory( + legacyProperty, "", "", namespaceProperty, true, clientFactory, workspaceManager); + + Namespace existingLegacyNamespace = legacyNamespaceExists ? mock(Namespace.class) : null; + when(namespaceResource.get()).thenReturn(existingLegacyNamespace); // when - boolean isPredefined = namespaceFactory.isPredefined(); + boolean creating; + try { + creating = namespaceFactory.isCreatingNamespace("123"); + } catch (InfrastructureException e) { + // if we can't determine whether we're potentially creating a namespace, we shouldn't claim + // we're managing it + if (expectedCreating != null) { + fail("Shouldn't have failed."); + } + creating = false; + } + boolean managing = namespaceFactory.isManagingNamespace("123"); // then - assertFalse(isPredefined); + if (!creating) { + assertFalse( + managing, + format( + "legacyProp=%s, legacyExists=%s, namespaceProp=%s, expectedCreating=%s", + legacyProperty, legacyNamespaceExists, namespaceProperty, expectedCreating)); + } } @Test public void shouldCreateAndPrepareNamespaceWithPredefinedValueIfItIsNotEmpty() throws Exception { // given namespaceFactory = - spy(new KubernetesNamespaceFactory("predefined", "", "", "che", false, clientFactory)); + spy( + new KubernetesNamespaceFactory( + "predefined", "", "", "che", false, clientFactory, workspaceManager)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -278,7 +389,10 @@ public class KubernetesNamespaceFactoryTest { public void shouldCreateAndPrepareNamespaceWithWorkspaceIdAsNameIfConfiguredNameIsNotPredefined() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory)); + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", "", "", "che", false, clientFactory, workspaceManager)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -296,7 +410,10 @@ public class KubernetesNamespaceFactoryTest { shouldCreateNamespaceAndDoNotPrepareNamespaceOnCreatingNamespaceWithWorkspaceIdAndNameSpecified() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory)); + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", "", "", "che", false, clientFactory, workspaceManager)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -314,7 +431,9 @@ public class KubernetesNamespaceFactoryTest { throws Exception { // given namespaceFactory = - spy(new KubernetesNamespaceFactory("", "serviceAccount", "", "che", false, clientFactory)); + spy( + new KubernetesNamespaceFactory( + "", "serviceAccount", "", "<workspaceid>", false, clientFactory, workspaceManager)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -337,7 +456,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "namespace", "serviceAccount", "clusterRole", "che", false, clientFactory)); + "namespace", + "serviceAccount", + "clusterRole", + "che", + false, + clientFactory, + workspaceManager)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -356,7 +481,10 @@ public class KubernetesNamespaceFactoryTest { public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProjectIsNotPredefined() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory)); + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", "", "", "che", false, clientFactory, workspaceManager)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -372,21 +500,106 @@ public class KubernetesNamespaceFactoryTest { } @Test - public void testPlaceholder() { + public void + testEvalNamespaceUsesNamespaceDefaultIfWorkspaceDoesntRecordNamespaceAndLegacyNamespaceDoesntExist() + throws Exception { + namespaceFactory = + new KubernetesNamespaceFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + workspaceManager); + + when(namespaceResource.get()).thenReturn(null); + + String namespace = + namespaceFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false)); + + assertEquals(namespace, "che-123"); + } + + @Test + public void + testEvalNamespaceUsesLegacyNamespaceIfWorkspaceDoesntRecordNamespaceAndLegacyNamespaceExists() + throws Exception { + namespaceFactory = new KubernetesNamespaceFactory( "blabol-<userid>-<username>-<userid>-<username>--", "", "", - "che", + "che-<userid>", false, - clientFactory); + clientFactory, + workspaceManager); + String namespace = namespaceFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false)); assertEquals(namespace, "blabol-123-JonDoe-123-JonDoe--"); } + @Test + public void testEvalNamespaceUsesWorkspaceRecordedNamespaceIfWorkspaceRecordsIt() + throws Exception { + + namespaceFactory = + new KubernetesNamespaceFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + workspaceManager); + + when(workspaceManager.getWorkspace(eq("42"))) + .thenReturn( + WorkspaceImpl.builder() + .setId("42") + .setAttributes( + singletonMap( + Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE, "wkspcnmspc")) + .build()); + + String namespace = + namespaceFactory.evalNamespaceName("42", new SubjectImpl("JonDoe", "123", null, false)); + + assertEquals(namespace, "wkspcnmspc"); + } + + @Test + public void testEvalNamespaceTreatsWorkspaceRecordedNamespaceLiterally() throws Exception { + + namespaceFactory = + new KubernetesNamespaceFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + workspaceManager); + + when(workspaceManager.getWorkspace(eq("42"))) + .thenReturn( + WorkspaceImpl.builder() + .setId("42") + .setAttributes( + singletonMap( + Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE, "<userid>")) + .build()); + + String namespace = + namespaceFactory.evalNamespaceName("42", new SubjectImpl("JonDoe", "123", null, false)); + + // this is an invalid name, but that is not a purpose of this test. + assertEquals(namespace, "<userid>"); + } + private void prepareNamespaceToBeFoundByName(String name, Namespace namespace) throws Exception { @SuppressWarnings("unchecked") Resource<Namespace, DoneableNamespace> getNamespaceByNameOperation = mock(Resource.class); diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java index 6cfa7eeb902ebe7825ef16ee9195758d4e94b95a..a57e1ba13aac88f26cf47bfcfd600d001a3dc145 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -98,11 +99,16 @@ public class KubernetesNamespaceTest { @Test public void testKubernetesNamespacePreparingWhenNamespaceExists() throws Exception { // given + MetadataNested namespaceMeta = prepareCreateNamespaceRequest(); + prepareNamespace(NAMESPACE); KubernetesNamespace namespace = new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID); // when namespace.prepare(); + + // then + verify(namespaceMeta, never()).withName(NAMESPACE); } @Test @@ -216,6 +222,49 @@ public class KubernetesNamespaceTest { verify(serviceAccountResource).watch(any()); } + @Test + public void testDeletesExistingNamespace() throws Exception { + // given + KubernetesNamespace namespace = new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID); + Resource resource = prepareNamespaceResource(NAMESPACE); + + // when + namespace.delete(); + + // then + verify(resource).delete(); + } + + @Test + public void testDoesntFailIfDeletedNamespaceDoesntExist() throws Exception { + // given + KubernetesNamespace namespace = new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID); + Resource resource = prepareNamespaceResource(NAMESPACE); + when(resource.delete()).thenThrow(new KubernetesClientException("err", 404, null)); + + // when + namespace.delete(); + + // then + verify(resource).delete(); + // and no exception is thrown + } + + @Test + public void testDoesntFailIfDeletedNamespaceIsBeingDeleted() throws Exception { + // given + KubernetesNamespace namespace = new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID); + Resource resource = prepareNamespaceResource(NAMESPACE); + when(resource.delete()).thenThrow(new KubernetesClientException("err", 409, null)); + + // when + namespace.delete(); + + // then + verify(resource).delete(); + // and no exception is thrown + } + private MetadataNested prepareCreateNamespaceRequest() { DoneableNamespace namespace = mock(DoneableNamespace.class); MetadataNested metadataNested = mock(MetadataNested.class); diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemoveTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemoveTest.java index fbe81882a75e7a80ad4663111a2b0f65a3bf961f..94f808bb0785e13b046cdf5782a897be7922f12e 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemoveTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/RemoveNamespaceOnWorkspaceRemoveTest.java @@ -11,9 +11,11 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,14 +40,15 @@ public class RemoveNamespaceOnWorkspaceRemoveTest { private static final String WORKSPACE_ID = "workspace123"; @Mock private Workspace workspace; + @Mock private KubernetesNamespaceFactory namespaceFactory; private RemoveNamespaceOnWorkspaceRemove removeNamespaceOnWorkspaceRemove; @BeforeMethod public void setUp() throws Exception { - removeNamespaceOnWorkspaceRemove = spy(new RemoveNamespaceOnWorkspaceRemove(null, null)); + removeNamespaceOnWorkspaceRemove = spy(new RemoveNamespaceOnWorkspaceRemove(namespaceFactory)); - doNothing().when(removeNamespaceOnWorkspaceRemove).doRemoveNamespace(anyString()); + lenient().doNothing().when(namespaceFactory).delete(anyString()); when(workspace.getId()).thenReturn(WORKSPACE_ID); } @@ -60,9 +63,21 @@ public class RemoveNamespaceOnWorkspaceRemoveTest { } @Test - public void shouldRemoveNamespaceOnWorkspaceRemovedEvent() throws Exception { + public void shouldRemoveNamespaceOnWorkspaceRemovedEventIfNamespaceIsManaged() throws Exception { + when(namespaceFactory.isManagingNamespace(any())).thenReturn(true); + + removeNamespaceOnWorkspaceRemove.onEvent(new WorkspaceRemovedEvent(workspace)); + + verify(namespaceFactory).delete(WORKSPACE_ID); + } + + @Test + public void shouldNotRemoveNamespaceOnWorkspaceRemovedEventIfNamespaceIsNotManaged() + throws Exception { + when(namespaceFactory.isManagingNamespace(any())).thenReturn(false); + removeNamespaceOnWorkspaceRemove.onEvent(new WorkspaceRemovedEvent(workspace)); - verify(removeNamespaceOnWorkspaceRemove).doRemoveNamespace(WORKSPACE_ID); + verify(namespaceFactory, never()).delete(any()); } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleanerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleanerTest.java index c54b9a16c13469967440b35ad8cf205a05b2f5a5..48cda981845e878049bee4ed7bc9f88eca0b81b6 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleanerTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/pvc/WorkspacePVCCleanerTest.java @@ -13,9 +13,7 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -23,7 +21,6 @@ import static org.mockito.Mockito.when; import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.core.notification.EventService; -import org.eclipse.che.api.core.notification.EventSubscriber; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.shared.event.WorkspaceRemovedEvent; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; @@ -42,20 +39,25 @@ import org.testng.annotations.Test; public class WorkspacePVCCleanerTest { @Mock private WorkspaceVolumesStrategy pvcStrategy; - @Mock private EventService eventService; + private EventService eventService; @Mock private KubernetesNamespaceFactory namespaceFactory; @Mock private Workspace workspace; + @Mock WorkspaceRemovedEvent event; private WorkspacePVCCleaner workspacePVCCleaner; @BeforeMethod - public void setUp() { - when(namespaceFactory.isPredefined()).thenReturn(true); + public void setUp() throws Exception { + when(namespaceFactory.isManagingNamespace(any())).thenReturn(false); workspacePVCCleaner = new WorkspacePVCCleaner(true, namespaceFactory, pvcStrategy); + when(workspace.getId()).thenReturn("123"); + when(event.getWorkspace()).thenReturn(workspace); + + eventService = spy(new EventService()); } @Test - public void testDoNotSubscribesCleanerWhenPVCDisabled() { + public void testDoNotSubscribesCleanerWhenPVCDisabled() throws Exception { workspacePVCCleaner = spy(new WorkspacePVCCleaner(false, namespaceFactory, pvcStrategy)); workspacePVCCleaner.subscribe(eventService); @@ -64,58 +66,42 @@ public class WorkspacePVCCleanerTest { } @Test - public void testDoNotSubscribesCleanerWhenPVCEnabledAndNamespaceIsNotPredefined() { - when(namespaceFactory.isPredefined()).thenReturn(false); - workspacePVCCleaner = spy(new WorkspacePVCCleaner(false, namespaceFactory, pvcStrategy)); - + public void testSubscribesDeleteKubernetesOnWorkspaceRemove() throws Exception { workspacePVCCleaner.subscribe(eventService); - verify(eventService, never()).subscribe(any(), eq(WorkspaceRemovedEvent.class)); + verify(eventService).subscribe(any(), eq(WorkspaceRemovedEvent.class)); } @Test - public void testSubscribesDeleteKubernetesOnWorkspaceRemove() throws Exception { + public void testInvokeCleanupWhenWorkspaceRemovedEventPublishedAndNamespaceIsNotManaged() + throws Exception { workspacePVCCleaner.subscribe(eventService); - verify(eventService).subscribe(any(), eq(WorkspaceRemovedEvent.class)); + eventService.publish(event); + + verify(pvcStrategy).cleanup(workspace); } @Test - public void testInvokeCleanupWhenWorkspaceRemovedEventPublished() throws Exception { - doAnswer( - invocationOnMock -> { - final EventSubscriber<WorkspaceRemovedEvent> argument = - invocationOnMock.getArgument(0); - final WorkspaceRemovedEvent event = mock(WorkspaceRemovedEvent.class); - when(event.getWorkspace()).thenReturn(workspace); - argument.onEvent(event); - return invocationOnMock; - }) - .when(eventService) - .subscribe(any(), eq(WorkspaceRemovedEvent.class)); + public void testNotInvokeCleanupWhenWorkspaceRemovedEventPublishedAndNamespaceIsManaged() + throws Exception { + when(namespaceFactory.isManagingNamespace(any())).thenReturn(true); workspacePVCCleaner.subscribe(eventService); - verify(pvcStrategy).cleanup(workspace); + eventService.publish(event); + + verify(pvcStrategy, never()).cleanup(workspace); } @Test public void testDoNotRethrowExceptionWhenErrorOnCleanupOccurs() throws Exception { - doAnswer( - invocationOnMock -> { - final EventSubscriber<WorkspaceRemovedEvent> argument = - invocationOnMock.getArgument(0); - final WorkspaceRemovedEvent event = mock(WorkspaceRemovedEvent.class); - when(event.getWorkspace()).thenReturn(workspace); - argument.onEvent(event); - return invocationOnMock; - }) - .when(eventService) - .subscribe(any(), eq(WorkspaceRemovedEvent.class)); doThrow(InfrastructureException.class).when(pvcStrategy).cleanup(workspace); workspacePVCCleaner.subscribe(eventService); + eventService.publish(event); + verify(pvcStrategy).cleanup(workspace); } } 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 c6b148fbd7675f8e0aede49b4a04fdf4e109f266..bd5ec9c8e1cb5d5139b999de55e2b4dd18039b7d 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 @@ -11,6 +11,8 @@ */ package org.eclipse.che.workspace.infrastructure.openshift.project; +import static java.lang.String.format; + import com.google.common.annotations.VisibleForTesting; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; @@ -94,6 +96,20 @@ public class OpenShiftProject extends KubernetesNamespace { } } + /** + * Deletes the project. Deleting a non-existent projects is not an error as is not an attempt to + * delete a project that is already being deleted. + * + * @throws InfrastructureException if any unexpected exception occurs during project deletion + */ + void delete() throws InfrastructureException { + String workspaceId = getWorkspaceId(); + String projectName = getName(); + + OpenShiftClient osClient = clientFactory.createOC(workspaceId); + delete(projectName, osClient); + } + /** Returns object for managing {@link Route} instances inside project. */ public OpenShiftRoutes routes() { return routes; @@ -128,6 +144,23 @@ public class OpenShiftProject extends KubernetesNamespace { } } + private void delete(String projectName, OpenShiftClient osClient) throws InfrastructureException { + try { + osClient.projects().withName(projectName).delete(); + } catch (KubernetesClientException e) { + if (e.getCode() == 404) { + LOG.warn( + format( + "Tried to delete project '%s' but it doesn't exist in the cluster.", projectName), + e); + } else if (e.getCode() == 409) { + LOG.info(format("The project '%s' is currently being deleted.", projectName), e); + } else { + throw new KubernetesInfrastructureException(e); + } + } + } + private Project get(String projectName, OpenShiftClient client) throws InfrastructureException { try { return client.projects().withName(projectName).get(); 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 fb4f2bb9e6c500c7d1ecfb2b38884cbae1be5bef..f998faffbfe9fef48fc21bbfc567f5fbcc3098d4 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 @@ -12,6 +12,7 @@ package org.eclipse.che.workspace.infrastructure.openshift.project; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; import com.google.common.annotations.VisibleForTesting; @@ -26,9 +27,15 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import javax.inject.Named; +import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.ValidationException; +import org.eclipse.che.api.workspace.server.WorkspaceManager; 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.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; @@ -58,14 +65,16 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { @Named("che.infra.kubernetes.namespace.allow_user_defined") boolean allowUserDefinedNamespaces, OpenShiftClientFactory clientFactory, - OpenShiftClientConfigFactory clientConfigFactory) { + OpenShiftClientConfigFactory clientConfigFactory, + WorkspaceManager workspaceManager) { super( projectName, serviceAccountName, clusterRoleName, defaultNamespaceName, allowUserDefinedNamespaces, - clientFactory); + clientFactory, + workspaceManager); if (allowUserDefinedNamespaces && !clientConfigFactory.isPersonalized()) { LOG.warn( "Users are allowed to list projects but Che server is configured with a service account. " @@ -86,12 +95,21 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { * @throws InfrastructureException if any exception occurs during project preparing */ public OpenShiftProject create(String workspaceId) throws InfrastructureException { - final String projectName = - evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); + final String projectName; + try { + projectName = evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); + } catch (NotFoundException | ServerException | ConflictException | ValidationException e) { + throw new InfrastructureException( + format( + "Failed to evaluate the project name to use for workspace %s. The error message" + + " was: %s", + workspaceId, e.getMessage()), + e); + } OpenShiftProject osProject = doCreateProject(workspaceId, projectName); osProject.prepare(); - if (!isPredefined() && !isNullOrEmpty(getServiceAccountName())) { + if (isCreatingNamespace(workspaceId) && !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 @@ -103,6 +121,37 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { return osProject; } + @Override + public void delete(String workspaceId) throws InfrastructureException { + String projectName; + try { + projectName = evalNamespaceName(workspaceId, EnvironmentContext.getCurrent().getSubject()); + } catch (NotFoundException | ServerException | ConflictException | ValidationException e) { + throw new InfrastructureException( + format( + "Failed to evaluate the project name to use for workspace %s." + + " The error message was: %s", + workspaceId, e.getMessage()), + e); + } + + OpenShiftProject osProject = doCreateProject(workspaceId, projectName); + osProject.delete(); + } + + @VisibleForTesting + @Override + protected String evalNamespaceName(String workspaceId, Subject currentUser) + throws NotFoundException, ServerException, InfrastructureException, ConflictException, + ValidationException { + return super.evalNamespaceName(workspaceId, currentUser); + } + + @Override + protected boolean checkNamespaceExists(String namespaceName) throws InfrastructureException { + return fetchNamespaceObject(namespaceName).isPresent(); + } + /** * Creates a kubernetes namespace for the specified workspace. * @@ -129,9 +178,13 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { @Override protected Optional<KubernetesNamespaceMeta> fetchNamespace(String name) throws InfrastructureException { + return fetchNamespaceObject(name).map(this::asNamespaceMeta); + } + + private Optional<Project> fetchNamespaceObject(String name) throws InfrastructureException { try { Project project = clientFactory.createOC().projects().withName(name).get(); - return Optional.of(asNamespaceMeta(project)); + return Optional.ofNullable(project); } catch (KubernetesClientException e) { if (e.getCode() == 403) { // 403 means that the project does not exist diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemove.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemove.java index 8af34840e7bb00caa54866a7b95b4dd4125b7b44..fd6c7fe1fd1aa799206fa49a095a2e275059311d 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemove.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemove.java @@ -11,20 +11,12 @@ */ package org.eclipse.che.workspace.infrastructure.openshift.project; -import static com.google.common.base.Strings.isNullOrEmpty; - -import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; -import io.fabric8.kubernetes.client.KubernetesClientException; -import javax.inject.Named; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.notification.EventSubscriber; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.shared.event.WorkspaceRemovedEvent; -import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; -import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,45 +30,30 @@ public class RemoveProjectOnWorkspaceRemove implements EventSubscriber<Workspace private static final Logger LOG = LoggerFactory.getLogger(RemoveProjectOnWorkspaceRemove.class); - private final OpenShiftClientFactory clientFactory; - private final String projectName; + private final OpenShiftProjectFactory projectFactory; @Inject - public RemoveProjectOnWorkspaceRemove( - @Nullable @Named("che.infra.kubernetes.namespace") String projectName, - OpenShiftClientFactory clientFactory) { - this.projectName = projectName; - this.clientFactory = clientFactory; + public RemoveProjectOnWorkspaceRemove(OpenShiftProjectFactory projectFactory) { + this.projectFactory = projectFactory; } @Inject public void subscribe(EventService eventService) { - if (isNullOrEmpty(projectName)) { - eventService.subscribe(this); - } + eventService.subscribe(this); } @Override public void onEvent(WorkspaceRemovedEvent event) { + String workspaceId = event.getWorkspace().getId(); try { - doRemoveProject(event.getWorkspace().getId()); + if (projectFactory.isManagingNamespace(workspaceId)) { + projectFactory.delete(workspaceId); + } } catch (InfrastructureException e) { LOG.warn( "Fail to remove OpenShift project for workspace with id {}. Cause: {}", - event.getWorkspace().getId(), + workspaceId, e.getMessage()); } } - - @VisibleForTesting - void doRemoveProject(String projectName) throws InfrastructureException { - try { - clientFactory.createOC(projectName).projects().withName(projectName).delete(); - } catch (KubernetesClientException e) { - if (!(e.getCode() == 403)) { - throw new KubernetesInfrastructureException(e); - } - // project doesn't exist - } - } } diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java index 5963d1fda0d5ca5435034fc36bdffecf7a7a70b9..24da0326e3908e5d328b5ef360a43505011d76bc 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java @@ -11,7 +11,9 @@ */ package org.eclipse.che.workspace.infrastructure.openshift.project; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DESCRIPTION_ANNOTATION; @@ -19,6 +21,7 @@ import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJE import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DISPLAY_NAME_ANNOTATION; import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DISPLAY_NAME_ATTRIBUTE; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -42,7 +45,11 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.che.api.workspace.server.WorkspaceManager; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.shared.Constants; +import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace; @@ -64,12 +71,15 @@ public class OpenShiftProjectFactoryTest { @Mock private OpenShiftClientConfigFactory configFactory; @Mock private OpenShiftClientFactory clientFactory; + @Mock private WorkspaceManager workspaceManager; @Mock private NonNamespaceOperation< Project, ProjectList, DoneableProject, Resource<Project, DoneableProject>> projectOperation; + @Mock private Resource<Project, DoneableProject> projectResource; + @Mock private OpenShiftClient osClient; private OpenShiftProjectFactory projectFactory; @@ -78,6 +88,12 @@ public class OpenShiftProjectFactoryTest { public void setUp() throws Exception { lenient().when(clientFactory.createOC()).thenReturn(osClient); lenient().when(osClient.projects()).thenReturn(projectOperation); + + when(workspaceManager.getWorkspace(any())) + .thenReturn(WorkspaceImpl.builder().setId("1").setAttributes(emptyMap()).build()); + + when(projectOperation.withName(any())).thenReturn(projectResource); + when(projectResource.get()).thenReturn(mock(Project.class)); } @Test( @@ -90,7 +106,7 @@ public class OpenShiftProjectFactoryTest { throws Exception { projectFactory = new OpenShiftProjectFactory( - "projectName", "", "", null, false, clientFactory, configFactory); + "projectName", "", "", null, false, clientFactory, configFactory, workspaceManager); } @Test @@ -112,7 +128,14 @@ public class OpenShiftProjectFactoryTest { projectFactory = new OpenShiftProjectFactory( - "predefined", "", "", "che-default", false, clientFactory, configFactory); + "predefined", + "", + "", + "che-default", + false, + clientFactory, + configFactory, + workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = projectFactory.list(); assertEquals(availableNamespaces.size(), 1); @@ -135,7 +158,14 @@ public class OpenShiftProjectFactoryTest { projectFactory = new OpenShiftProjectFactory( - "predefined", "", "", "che-default", false, clientFactory, configFactory); + "predefined", + "", + "", + "che-default", + false, + clientFactory, + configFactory, + workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = projectFactory.list(); assertEquals(availableNamespaces.size(), 1); @@ -158,7 +188,14 @@ public class OpenShiftProjectFactoryTest { projectFactory = new OpenShiftProjectFactory( - "predefined", "", "", "che-default", false, clientFactory, configFactory); + "predefined", + "", + "", + "che-default", + false, + clientFactory, + configFactory, + workspaceManager); projectFactory.list(); } @@ -171,7 +208,8 @@ public class OpenShiftProjectFactoryTest { createProject("experimental", null, null, "Terminating"))); projectFactory = - new OpenShiftProjectFactory("predefined", "", "", null, true, clientFactory, configFactory); + new OpenShiftProjectFactory( + "predefined", "", "", null, true, clientFactory, configFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = projectFactory.list(); assertEquals(availableNamespaces.size(), 2); @@ -197,7 +235,7 @@ public class OpenShiftProjectFactoryTest { projectFactory = new OpenShiftProjectFactory( - "predefined", "", "", "default", true, clientFactory, configFactory); + "predefined", "", "", "default", true, clientFactory, configFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = projectFactory.list(); @@ -228,7 +266,7 @@ public class OpenShiftProjectFactoryTest { projectFactory = new OpenShiftProjectFactory( - "predefined", "", "", "default", true, clientFactory, configFactory); + "predefined", "", "", "default", true, clientFactory, configFactory, workspaceManager); List<KubernetesNamespaceMeta> availableNamespaces = projectFactory.list(); assertEquals(availableNamespaces.size(), 2); @@ -253,7 +291,8 @@ public class OpenShiftProjectFactoryTest { public void shouldThrownExceptionWhenFailedToGetNamespaces() throws Exception { throwOnTryToGetProjectsList(new KubernetesClientException("connection refused")); projectFactory = - new OpenShiftProjectFactory("predefined", "", "", "", true, clientFactory, configFactory); + new OpenShiftProjectFactory( + "predefined", "", "", "", true, clientFactory, configFactory, workspaceManager); projectFactory.list(); } @@ -264,7 +303,14 @@ public class OpenShiftProjectFactoryTest { projectFactory = spy( new OpenShiftProjectFactory( - "projectName", "", "", "che", false, clientFactory, configFactory)); + "projectName", + "", + "", + "che", + false, + clientFactory, + configFactory, + workspaceManager)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -282,7 +328,9 @@ public class OpenShiftProjectFactoryTest { throws Exception { // given projectFactory = - spy(new OpenShiftProjectFactory("", "", "", "che", false, clientFactory, configFactory)); + spy( + new OpenShiftProjectFactory( + "", "", "", "che", false, clientFactory, configFactory, workspaceManager)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -302,7 +350,14 @@ public class OpenShiftProjectFactoryTest { projectFactory = spy( new OpenShiftProjectFactory( - "", "serviceAccount", "", "che", false, clientFactory, configFactory)); + "", + "serviceAccount", + "", + "<workspaceid>", + false, + clientFactory, + configFactory, + workspaceManager)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -330,7 +385,8 @@ public class OpenShiftProjectFactoryTest { "che", false, clientFactory, - configFactory)); + configFactory, + workspaceManager)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -346,7 +402,9 @@ public class OpenShiftProjectFactoryTest { throws Exception { // given projectFactory = - spy(new OpenShiftProjectFactory("", "", "", "che", false, clientFactory, configFactory)); + spy( + new OpenShiftProjectFactory( + "", "", "", "che", false, clientFactory, configFactory, workspaceManager)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -371,7 +429,8 @@ public class OpenShiftProjectFactoryTest { "che", false, clientFactory, - configFactory)); + configFactory, + workspaceManager)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -384,6 +443,111 @@ public class OpenShiftProjectFactoryTest { verify(toReturnProject, never()).prepare(); } + @Test + public void + testEvalNamespaceUsesNamespaceDefaultIfWorkspaceDoesntRecordNamespaceAndLegacyNamespaceDoesntExist() + throws Exception { + projectFactory = + new OpenShiftProjectFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + configFactory, + workspaceManager); + + when(projectResource.get()).thenThrow(new KubernetesClientException("", 403, null)); + + String namespace = + projectFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false)); + + assertEquals(namespace, "che-123"); + } + + @Test + public void + testEvalNamespaceUsesLegacyNamespaceIfWorkspaceDoesntRecordNamespaceAndLegacyNamespaceExists() + throws Exception { + + projectFactory = + new OpenShiftProjectFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + configFactory, + workspaceManager); + + String namespace = + projectFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false)); + + assertEquals(namespace, "blabol-123-JonDoe-123-JonDoe--"); + } + + @Test + public void testEvalNamespaceUsesWorkspaceRecordedNamespaceIfWorkspaceRecordsIt() + throws Exception { + + projectFactory = + new OpenShiftProjectFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + configFactory, + workspaceManager); + + when(workspaceManager.getWorkspace(eq("42"))) + .thenReturn( + WorkspaceImpl.builder() + .setId("42") + .setAttributes( + singletonMap( + Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE, "wkspcprj")) + .build()); + + String namespace = + projectFactory.evalNamespaceName("42", new SubjectImpl("JonDoe", "123", null, false)); + + assertEquals(namespace, "wkspcprj"); + } + + @Test + public void testEvalNamespaceTreatsWorkspaceRecordedNamespaceLiterally() throws Exception { + + projectFactory = + new OpenShiftProjectFactory( + "blabol-<userid>-<username>-<userid>-<username>--", + "", + "", + "che-<userid>", + false, + clientFactory, + configFactory, + workspaceManager); + + when(workspaceManager.getWorkspace(eq("42"))) + .thenReturn( + WorkspaceImpl.builder() + .setId("42") + .setAttributes( + singletonMap( + Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE, "<userid>")) + .build()); + + String namespace = + projectFactory.evalNamespaceName("42", new SubjectImpl("JonDoe", "123", null, false)); + + // this is an invalid name, but that is not a purpose of this test. + assertEquals(namespace, "<userid>"); + } + private void prepareNamespaceToBeFoundByName(String name, Project project) throws Exception { @SuppressWarnings("unchecked") Resource<Project, DoneableProject> getProjectByNameOperation = mock(Resource.class); diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectTest.java index 37c43b749116ed53935b1bad6dfc6641e02fb54b..74fca56e61a741fae22407c8ea4b03329a4f8649 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectTest.java @@ -17,6 +17,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -160,6 +161,49 @@ public class OpenShiftProjectTest { verify(routes).delete(); } + @Test + public void testDeletesExistingProject() throws Exception { + // given + OpenShiftProject project = new OpenShiftProject(clientFactory, PROJECT_NAME, WORKSPACE_ID); + Resource resource = prepareProjectResource(PROJECT_NAME); + + // when + project.delete(); + + // then + verify(resource).delete(); + } + + @Test + public void testDoesntFailIfDeletedProjectDoesntExist() throws Exception { + // given + OpenShiftProject project = new OpenShiftProject(clientFactory, PROJECT_NAME, WORKSPACE_ID); + Resource resource = prepareProjectResource(PROJECT_NAME); + when(resource.delete()).thenThrow(new KubernetesClientException("err", 404, null)); + + // when + project.delete(); + + // then + verify(resource).delete(); + // and no exception is thrown + } + + @Test + public void testDoesntFailIfDeletedProjectIsBeingDeleted() throws Exception { + // given + OpenShiftProject project = new OpenShiftProject(clientFactory, PROJECT_NAME, WORKSPACE_ID); + Resource resource = prepareProjectResource(PROJECT_NAME); + when(resource.delete()).thenThrow(new KubernetesClientException("err", 409, null)); + + // when + project.delete(); + + // then + verify(resource).delete(); + // and no exception is thrown + } + private MetadataNested prepareProjectRequest() { ProjectRequestOperation projectRequestOperation = mock(ProjectRequestOperation.class); DoneableProjectRequest projectRequest = mock(DoneableProjectRequest.class); diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemoveTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemoveTest.java index be72c96ce2c14c67b8ac0bf1c21c4b43b0e1081e..92ab109e18a0e07db0a0f4b3eb68dcea857a8fd7 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemoveTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/RemoveProjectOnWorkspaceRemoveTest.java @@ -11,9 +11,11 @@ */ package org.eclipse.che.workspace.infrastructure.openshift.project; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,14 +40,15 @@ public class RemoveProjectOnWorkspaceRemoveTest { private static final String WORKSPACE_ID = "workspace123"; @Mock private Workspace workspace; + @Mock private OpenShiftProjectFactory projectFactory; private RemoveProjectOnWorkspaceRemove removeProjectOnWorkspaceRemove; @BeforeMethod public void setUp() throws Exception { - removeProjectOnWorkspaceRemove = spy(new RemoveProjectOnWorkspaceRemove(null, null)); + removeProjectOnWorkspaceRemove = spy(new RemoveProjectOnWorkspaceRemove(projectFactory)); - doNothing().when(removeProjectOnWorkspaceRemove).doRemoveProject(anyString()); + lenient().doNothing().when(projectFactory).delete(anyString()); when(workspace.getId()).thenReturn(WORKSPACE_ID); } @@ -60,9 +63,22 @@ public class RemoveProjectOnWorkspaceRemoveTest { } @Test - public void shouldRemoveProjectOnWorkspaceRemovedEvent() throws Exception { + public void shouldRemoveProjectOnWorkspaceRemovedEventIfFactoryIsManagingNamespaces() + throws Exception { + when(projectFactory.isManagingNamespace(any())).thenReturn(true); + + removeProjectOnWorkspaceRemove.onEvent(new WorkspaceRemovedEvent(workspace)); + + verify(projectFactory).delete(WORKSPACE_ID); + } + + @Test + public void shouldNotRemoveProjectOnWorkspaceRemovedEventIfFactoryIsNotManagingNamespaces() + throws Exception { + when(projectFactory.isManagingNamespace(any())).thenReturn(false); + removeProjectOnWorkspaceRemove.onEvent(new WorkspaceRemovedEvent(workspace)); - verify(removeProjectOnWorkspaceRemove).doRemoveProject(WORKSPACE_ID); + verify(projectFactory, never()).delete(any()); } } 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 a7b34a72d752fc097b30b33c71039bdb6a138992..406e18680754650bdc71a4fff1feaa2b605c82c6 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 @@ -185,5 +185,12 @@ public final class Constants { /** When generating workspace name from generateName, append this many characters. */ public static final int WORKSPACE_GENERATE_NAME_CHARS_APPEND = 5; + /** + * The attribute of the workspace where we store the infrastructure namespace the workspace is + * deployed to + */ + public static final String WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE = + "infrastructureNamespace"; + private Constants() {} } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java index 554e3ea3ae441b17d7a54670c444a9b3a22fbe4f..9a0a667b73c6c454a27e68a43a38397368e43589 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceManager.java @@ -415,7 +415,7 @@ public class WorkspaceManager { .whenComplete( (aVoid, throwable) -> { if (workspace.isTemporary()) { - removeWorkspaceQuietly(workspace); + removeWorkspaceQuietly(workspace.getId()); } }); } @@ -441,13 +441,13 @@ public class WorkspaceManager { runtimes .startAsync(workspace, envName, firstNonNull(options, Collections.emptyMap())) - .thenAccept(aVoid -> handleStartupSuccess(workspace)) + .thenAccept(aVoid -> handleStartupSuccess(workspace.getId())) .exceptionally( ex -> { if (workspace.isTemporary()) { - removeWorkspaceQuietly(workspace); + removeWorkspaceQuietly(workspace.getId()); } else { - handleStartupError(workspace, ex.getCause()); + handleStartupError(workspace.getId(), ex.getCause()); } return null; }); @@ -467,11 +467,14 @@ public class WorkspaceManager { } } - private void removeWorkspaceQuietly(Workspace workspace) { + private void removeWorkspaceQuietly(String workspaceId) { try { - workspaceDao.remove(workspace.getId()); + workspaceDao.remove(workspaceId); } catch (ServerException x) { - LOG.error("Unable to remove temporary workspace '{}'", workspace.getId()); + LOG.error( + "Unable to remove temporary workspace '{}'. Error message was: {}", + workspaceId, + x.getMessage()); } } @@ -493,35 +496,41 @@ public class WorkspaceManager { return workspace; } - private void handleStartupError(Workspace workspace, Throwable t) { - workspace - .getAttributes() - .put( - ERROR_MESSAGE_ATTRIBUTE_NAME, - t instanceof RuntimeException ? t.getCause().getMessage() : t.getMessage()); - workspace.getAttributes().put(STOPPED_ATTRIBUTE_NAME, Long.toString(currentTimeMillis())); - workspace.getAttributes().put(STOPPED_ABNORMALLY_ATTRIBUTE_NAME, Boolean.toString(true)); + private void handleStartupError(String workspaceId, Throwable t) { try { + // we need to reload the workspace because the runtimes might have updated it + Workspace workspace = getWorkspace(workspaceId); + workspace + .getAttributes() + .put( + ERROR_MESSAGE_ATTRIBUTE_NAME, + t instanceof RuntimeException ? t.getCause().getMessage() : t.getMessage()); + workspace.getAttributes().put(STOPPED_ATTRIBUTE_NAME, Long.toString(currentTimeMillis())); + workspace.getAttributes().put(STOPPED_ABNORMALLY_ATTRIBUTE_NAME, Boolean.toString(true)); updateWorkspace(workspace.getId(), workspace); } catch (NotFoundException | ServerException | ValidationException | ConflictException e) { LOG.warn( String.format( "Cannot set error status of the workspace %s. Error is: %s", - workspace.getId(), e.getMessage())); + workspaceId, e.getMessage())); } } - private void handleStartupSuccess(Workspace workspace) { - workspace.getAttributes().remove(STOPPED_ATTRIBUTE_NAME); - workspace.getAttributes().remove(STOPPED_ABNORMALLY_ATTRIBUTE_NAME); - workspace.getAttributes().remove(ERROR_MESSAGE_ATTRIBUTE_NAME); + private void handleStartupSuccess(String workspaceId) { try { + // we need to reload the workspace because the runtimes might have updated it + Workspace workspace = getWorkspace(workspaceId); + + workspace.getAttributes().remove(STOPPED_ATTRIBUTE_NAME); + workspace.getAttributes().remove(STOPPED_ABNORMALLY_ATTRIBUTE_NAME); + workspace.getAttributes().remove(ERROR_MESSAGE_ATTRIBUTE_NAME); + updateWorkspace(workspace.getId(), workspace); } catch (NotFoundException | ServerException | ValidationException | ConflictException e) { LOG.warn( String.format( "Cannot clear error status status of the workspace %s. Error is: %s", - workspace.getId(), e.getMessage())); + workspaceId, e.getMessage())); } } diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java index e46956041b781d2d353e581b090f03bba8b31c04..bc8a7788b1c6fdc0b969622ad1b53e39fb7a9114 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceManagerTest.java @@ -589,9 +589,10 @@ public class WorkspaceManagerTest { final WorkspaceImpl workspace = createAndMockWorkspace(workspaceConfig, NAMESPACE_1); mockAnyWorkspaceStartFailed(new ServerException("start failed")); - workspaceManager.startWorkspace(workspaceConfig, workspace.getNamespace(), false, emptyMap()); - verify(workspaceDao).update(workspaceCaptor.capture()); - Workspace ws = workspaceCaptor.getAllValues().get(workspaceCaptor.getAllValues().size() - 1); + workspaceManager.startWorkspace(workspace.getId(), null, null); + // the first update is capturing the start time, the second update is capturing the error + verify(workspaceDao, times(2)).update(workspaceCaptor.capture()); + Workspace ws = workspaceCaptor.getAllValues().get(1); assertNotNull(ws.getAttributes().get(STOPPED_ATTRIBUTE_NAME)); assertTrue(Boolean.valueOf(ws.getAttributes().get(STOPPED_ABNORMALLY_ATTRIBUTE_NAME))); assertEquals(ws.getAttributes().get(ERROR_MESSAGE_ATTRIBUTE_NAME), "start failed");