From 3dd86e856d45f8ce1df6f668b45e0b58e10f40d5 Mon Sep 17 00:00:00 2001 From: Vitalii Parfonov Date: Wed, 15 Jul 2020 18:50:36 +0300 Subject: [PATCH] Prepare environment for async storage of project sources (#16972) * Prepare environment for async storage of project sources Signed-off-by: Vitalii Parfonov --- .../webapp/WEB-INF/classes/che/che.properties | 10 +- .../infrastructure/kubernetes/Constants.java | 3 + .../PodTerminationGracePeriodProvisioner.java | 23 +- infrastructures/openshift/pom.xml | 8 + .../openshift/AsyncStorageModeValidator.java | 158 +++++++ .../OpenShiftEnvironmentProvisioner.java | 10 + .../openshift/OpenShiftInfraModule.java | 9 +- .../provision/AsyncStoragePodInterceptor.java | 157 +++++++ .../provision/AsyncStorageProvisioner.java | 384 ++++++++++++++++++ .../AsyncStorageModeValidatorTest.java | 224 ++++++++++ .../OpenShiftEnvironmentProvisionerTest.java | 6 + .../AsyncStoragePodInterceptorTest.java | 173 ++++++++ .../AsyncStorageProvisionerTest.java | 342 ++++++++++++++++ .../JpaEntitiesCascadeRemovalTest.java | 3 + .../che/api/workspace/shared/Constants.java | 14 + .../convert/DefaultEditorProvisioner.java | 32 ++ .../convert/DefaultEditorProvisionerTest.java | 84 +++- 17 files changed, 1624 insertions(+), 16 deletions(-) create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidator.java create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptor.java create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisioner.java create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidatorTest.java create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptorTest.java create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisionerTest.java 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 ef4078327c..14a1153b5b 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 @@ -330,7 +330,7 @@ che.infra.kubernetes.pvc.strategy=common che.infra.kubernetes.pvc.precreate_subpaths=true # Defines the settings of PVC name for che workspaces. -# Each PVC strategy suplies this value differently. +# Each PVC strategy supplies this value differently. # See doc for che.infra.kubernetes.pvc.strategy property che.infra.kubernetes.pvc.name=claim-che-workspace @@ -615,3 +615,11 @@ che.workspace.devfile.default_editor.plugins=eclipse/che-machine-exec-plugin/nig # which will be mount into workspace containers as a files or env variables. # Only secrets that match ALL given labels will be selected. che.workspace.provision.secret.labels=app.kubernetes.io/part-of=che.eclipse.org,app.kubernetes.io/component=workspace-secret + + +# Plugin is added in case async storage feature will be enabled in workspace config +# and supported by environment +che.workspace.devfile.async.storage.plugin=eclipse/che-async-pv-plugin/nightly + +# Docker image for the Che async storage +che.infra.kubernetes.async.storage.image=quay.io/eclipse/che-workspace-data-sync-storage:latest diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Constants.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Constants.java index e3c485e324..83e7718b51 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Constants.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Constants.java @@ -31,6 +31,9 @@ public final class Constants { /** The label that contains a value with workspace id to which object belongs to. */ public static final String CHE_WORKSPACE_ID_LABEL = "che.workspace_id"; + /** The label that contains a value with user id to which object belongs to. */ + public static final String CHE_USER_ID_LABEL = "che.user_id"; + /** The label that contains name of deployment responsible for Pod. */ public static final String CHE_DEPLOYMENT_NAME_LABEL = "che.deployment_name"; diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PodTerminationGracePeriodProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PodTerminationGracePeriodProvisioner.java index c4b61b4bf3..1fc9d94b76 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PodTerminationGracePeriodProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PodTerminationGracePeriodProvisioner.java @@ -11,7 +11,12 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.provision; +import static java.lang.Boolean.parseBoolean; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.EphemeralWorkspaceUtility.isEphemeral; + import io.fabric8.kubernetes.api.model.PodSpec; +import java.util.Map; import javax.inject.Inject; import javax.inject.Named; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; @@ -31,6 +36,14 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.Kubernete */ public class PodTerminationGracePeriodProvisioner implements ConfigurationProvisioner { private final long graceTerminationPeriodSec; + /** + * This value will activate if workspace configured to use Async Storage. We can't set default + * grace termination period because we need to give some time on workspace stop action for backup + * changes to the persistent storage. At the moment no way to predict this time because it depends + * on amount of files, size of files and network ability. This is some empirical number of seconds + * which should be enough for most projects. + */ + private static final long GRACE_TERMINATION_PERIOD_ASYNC_STORAGE_WS_SEC = 60; @Inject public PodTerminationGracePeriodProvisioner( @@ -48,7 +61,7 @@ public class PodTerminationGracePeriodProvisioner implements ConfigurationProvis for (PodData pod : k8sEnv.getPodsData().values()) { if (!isTerminationGracePeriodSet(pod.getSpec())) { - pod.getSpec().setTerminationGracePeriodSeconds(graceTerminationPeriodSec); + pod.getSpec().setTerminationGracePeriodSeconds(getGraceTerminationPeriodSec(k8sEnv)); } } } @@ -60,4 +73,12 @@ public class PodTerminationGracePeriodProvisioner implements ConfigurationProvis private boolean isTerminationGracePeriodSet(final PodSpec podSpec) { return podSpec.getTerminationGracePeriodSeconds() != null; } + + private long getGraceTerminationPeriodSec(KubernetesEnvironment k8sEnv) { + Map attributes = k8sEnv.getAttributes(); + if (isEphemeral(attributes) && parseBoolean(attributes.get(ASYNC_PERSIST_ATTRIBUTE))) { + return GRACE_TERMINATION_PERIOD_ASYNC_STORAGE_WS_SEC; + } + return graceTerminationPeriodSec; + } } diff --git a/infrastructures/openshift/pom.xml b/infrastructures/openshift/pom.xml index 29c83b5efa..cd658a2c3b 100644 --- a/infrastructures/openshift/pom.xml +++ b/infrastructures/openshift/pom.xml @@ -70,6 +70,14 @@ org.eclipse.che.core che-core-api-model + + org.eclipse.che.core + che-core-api-ssh + + + org.eclipse.che.core + che-core-api-ssh-shared + org.eclipse.che.core che-core-api-system diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidator.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidator.java new file mode 100644 index 0000000000..a0cff2c040 --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidator.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift; + +import static java.lang.Boolean.parseBoolean; +import static java.lang.String.format; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy.COMMON_STRATEGY; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.EphemeralWorkspaceUtility.isEphemeral; + +import com.google.common.base.Strings; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Named; +import org.eclipse.che.api.core.ValidationException; +import org.eclipse.che.api.workspace.server.WorkspaceAttributeValidator; +import org.eclipse.che.commons.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Validates the workspace attributes before workspace creation and updating if async storage + * configure. + * + *

To be valid for async storage workspace MUST have attributes: + * + *

    + *
  • {@link org.eclipse.che.api.workspace.shared.Constants#ASYNC_PERSIST_ATTRIBUTE} = 'true' + *
  • {@link org.eclipse.che.api.workspace.shared.Constants#PERSIST_VOLUMES_ATTRIBUTE} = 'false' + *
+ * + *

If set only {@link org.eclipse.che.api.workspace.shared.Constants#ASYNC_PERSIST_ATTRIBUTE} = + * 'true', {@link ValidationException} is thrown. + * + *

If system is configured with other value of properties than below {@link ValidationException}, + * is thrown. + * + *

    + *
  • che.infra.kubernetes.namespace.default=-che + *
  • che.infra.kubernetes.namespace.allow_user_defined=false + *
  • che.infra.kubernetes.pvc.strategy=common + *
  • che.limits.user.workspaces.run.count=1 + *
+ */ +public class AsyncStorageModeValidator implements WorkspaceAttributeValidator { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncStorageModeValidator.class); + + private final String pvcStrategy; + private final boolean allowUserDefinedNamespaces; + private final String defaultNamespaceName; + private final int runtimesPerUser; + + @Inject + public AsyncStorageModeValidator( + @Named("che.infra.kubernetes.pvc.strategy") String pvcStrategy, + @Named("che.infra.kubernetes.namespace.allow_user_defined") + boolean allowUserDefinedNamespaces, + @Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName, + @Named("che.limits.user.workspaces.run.count") int runtimesPerUser) { + + this.pvcStrategy = pvcStrategy; + this.allowUserDefinedNamespaces = allowUserDefinedNamespaces; + this.defaultNamespaceName = defaultNamespaceName; + this.runtimesPerUser = runtimesPerUser; + } + + @Override + public void validate(Map attributes) throws ValidationException { + if (parseBoolean(attributes.get(ASYNC_PERSIST_ATTRIBUTE))) { + isEphemeralAttributeValidation(attributes); + pvcStrategyValidation(); + alowUserDefinedNamespaceValidation(); + nameSpaceStrategyValidation(); + runtimesPerUserValidation(); + } + } + + @Override + public void validateUpdate(Map existing, Map update) + throws ValidationException { + if (parseBoolean(update.get(ASYNC_PERSIST_ATTRIBUTE))) { + if (isEphemeral(existing) || isEphemeral(update)) { + pvcStrategyValidation(); + alowUserDefinedNamespaceValidation(); + nameSpaceStrategyValidation(); + runtimesPerUserValidation(); + } else { + String message = + "Workspace configuration not valid: Asynchronous storage available only for NOT persistent storage"; + LOG.warn(message); + throw new ValidationException(message); + } + } + } + + private void isEphemeralAttributeValidation(Map attributes) + throws ValidationException { + if (!isEphemeral(attributes)) { + String message = + "Workspace configuration not valid: Asynchronous storage available only for NOT persistent storage"; + LOG.warn(message); + throw new ValidationException(message); + } + } + + private void runtimesPerUserValidation() throws ValidationException { + if (runtimesPerUser > 1) { + String message = + format( + "Workspace configuration not valid: Asynchronous storage available only if 'che.limits.user.workspaces.run.count' set to 1, but got %s", + runtimesPerUser); + LOG.warn(message); + throw new ValidationException(message); + } + } + + private void nameSpaceStrategyValidation() throws ValidationException { + if (Strings.isNullOrEmpty(defaultNamespaceName) + || !defaultNamespaceName.contains("")) { + String message = + "Workspace configuration not valid: Asynchronous storage available only for 'per-user' namespace strategy"; + LOG.warn(message); + throw new ValidationException(message); + } + } + + private void alowUserDefinedNamespaceValidation() throws ValidationException { + if (allowUserDefinedNamespaces) { + String message = + format( + "Workspace configuration not valid: Asynchronous storage available only if 'che.infra.kubernetes.namespace.allow_user_defined' set to 'false', but got '%s'", + allowUserDefinedNamespaces); + LOG.warn(message); + throw new ValidationException(message); + } + } + + private void pvcStrategyValidation() throws ValidationException { + if (!COMMON_STRATEGY.equals(pvcStrategy)) { + String message = + format( + "Workspace configuration not valid: Asynchronous storage available only for 'common' PVC strategy, but got %s", + pvcStrategy); + LOG.warn(message); + throw new ValidationException(message); + } + } +} diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java index 80deeac3c4..dd8ab53666 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java @@ -36,6 +36,8 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpoli import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter; import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStoragePodInterceptor; +import org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftUniqueNamesProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftPreviewUrlExposer; @@ -67,7 +69,9 @@ public class OpenShiftEnvironmentProvisioner private final PodTerminationGracePeriodProvisioner podTerminationGracePeriodProvisioner; private final ImagePullSecretProvisioner imagePullSecretProvisioner; private final ProxySettingsProvisioner proxySettingsProvisioner; + private final AsyncStoragePodInterceptor asyncStoragePodObserver; private final ServiceAccountProvisioner serviceAccountProvisioner; + private final AsyncStorageProvisioner asyncStorageProvisioner; private final CertificateProvisioner certificateProvisioner; private final SshKeysProvisioner sshKeysProvisioner; private final GitConfigProvisioner gitConfigProvisioner; @@ -88,6 +92,8 @@ public class OpenShiftEnvironmentProvisioner PodTerminationGracePeriodProvisioner podTerminationGracePeriodProvisioner, ImagePullSecretProvisioner imagePullSecretProvisioner, ProxySettingsProvisioner proxySettingsProvisioner, + AsyncStorageProvisioner asyncStorageProvisioner, + AsyncStoragePodInterceptor asyncStoragePodObserver, ServiceAccountProvisioner serviceAccountProvisioner, CertificateProvisioner certificateProvisioner, SshKeysProvisioner sshKeysProvisioner, @@ -106,6 +112,8 @@ public class OpenShiftEnvironmentProvisioner this.podTerminationGracePeriodProvisioner = podTerminationGracePeriodProvisioner; this.imagePullSecretProvisioner = imagePullSecretProvisioner; this.proxySettingsProvisioner = proxySettingsProvisioner; + this.asyncStorageProvisioner = asyncStorageProvisioner; + this.asyncStoragePodObserver = asyncStoragePodObserver; this.serviceAccountProvisioner = serviceAccountProvisioner; this.certificateProvisioner = certificateProvisioner; this.sshKeysProvisioner = sshKeysProvisioner; @@ -124,6 +132,7 @@ public class OpenShiftEnvironmentProvisioner "Start provisioning OpenShift environment for workspace '{}'", identity.getWorkspaceId()); // 1 stage - update environment according Infrastructure specific if (pvcEnabled) { + asyncStoragePodObserver.intercept(osEnv, identity); logsVolumeMachineProvisioner.provision(osEnv, identity); } @@ -144,6 +153,7 @@ public class OpenShiftEnvironmentProvisioner imagePullSecretProvisioner.provision(osEnv, identity); proxySettingsProvisioner.provision(osEnv, identity); serviceAccountProvisioner.provision(osEnv, identity); + asyncStorageProvisioner.provision(osEnv, identity); certificateProvisioner.provision(osEnv, identity); sshKeysProvisioner.provision(osEnv, identity); vcsSslCertificateProvisioner.provision(osEnv, identity); diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java index ae839cbead..223579d2a9 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java @@ -82,6 +82,7 @@ import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftE import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironmentFactory; import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProjectFactory; import org.eclipse.che.workspace.infrastructure.openshift.project.RemoveProjectOnWorkspaceRemove; +import org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftPreviewUrlCommandProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftCookiePathStrategy; import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftExternalServerExposer; @@ -93,9 +94,10 @@ import org.eclipse.che.workspace.infrastructure.openshift.wsplugins.brokerphases public class OpenShiftInfraModule extends AbstractModule { @Override protected void configure() { - Multibinder.newSetBinder(binder(), WorkspaceAttributeValidator.class) - .addBinding() - .to(K8sInfraNamespaceWsAttributeValidator.class); + Multibinder workspaceAttributeValidators = + Multibinder.newSetBinder(binder(), WorkspaceAttributeValidator.class); + workspaceAttributeValidators.addBinding().to(K8sInfraNamespaceWsAttributeValidator.class); + workspaceAttributeValidators.addBinding().to(AsyncStorageModeValidator.class); bind(KubernetesNamespaceService.class); @@ -222,5 +224,6 @@ public class OpenShiftInfraModule extends AbstractModule { bind(ExternalServiceExposureStrategy.class).to(OpenShiftServerExposureStrategy.class); bind(CookiePathStrategy.class).to(OpenShiftCookiePathStrategy.class); bind(NonTlsDistributedClusterModeNotifier.class); + bind(AsyncStorageProvisioner.class); } } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptor.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptor.java new file mode 100644 index 0000000000..3758306634 --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptor.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift.provision; + +import static java.lang.String.format; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy.COMMON_STRATEGY; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.EphemeralWorkspaceUtility.isEphemeral; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner.ASYNC_STORAGE; + +import io.fabric8.kubernetes.api.model.DoneablePod; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.PodResource; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.inject.Inject; +import javax.inject.Named; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; +import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This interceptor checks whether the starting workspace is configured with persistent storage and + * makes sure to stop the async storage pod (if any is running) to prevent "Multi-Attach error for + * volume". After the async storage pod is stopped and deleted, the workspace start is resumed. + */ +public class AsyncStoragePodInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncStoragePodInterceptor.class); + private static final int DELETE_POD_TIMEOUT_IN_MIN = 5; + + private final OpenShiftClientFactory clientFactory; + private final String pvcStrategy; + + @Inject + public AsyncStoragePodInterceptor( + @Named("che.infra.kubernetes.pvc.strategy") String pvcStrategy, + OpenShiftClientFactory openShiftClientFactory) { + this.pvcStrategy = pvcStrategy; + this.clientFactory = openShiftClientFactory; + } + + public void intercept(OpenShiftEnvironment osEnv, RuntimeIdentity identity) + throws InfrastructureException { + if (!COMMON_STRATEGY.equals(pvcStrategy) || isEphemeral(osEnv.getAttributes())) { + return; + } + + String namespace = identity.getInfrastructureNamespace(); + String workspaceId = identity.getWorkspaceId(); + + PodResource asyncStoragePodResource = + getAsyncStoragePodResource(namespace, workspaceId); + + if (asyncStoragePodResource.get() == null) { // pod doesn't exist + return; + } + + try { + deleteAsyncStoragePod(asyncStoragePodResource) + .get(DELETE_POD_TIMEOUT_IN_MIN, TimeUnit.MINUTES); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new InfrastructureException( + format( + "Interrupted while waiting for pod '%s' removal. " + ex.getMessage(), ASYNC_STORAGE), + ex); + } catch (ExecutionException ex) { + throw new InfrastructureException( + format( + "Error occurred while waiting for pod '%s' removal. " + ex.getMessage(), + ASYNC_STORAGE), + ex); + } catch (TimeoutException ex) { + throw new InfrastructureException( + format("Pod '%s' removal timeout reached " + ex.getMessage(), ASYNC_STORAGE), ex); + } + } + + private PodResource getAsyncStoragePodResource( + String namespace, String workspaceId) throws InfrastructureException { + return clientFactory.create(workspaceId).pods().inNamespace(namespace).withName(ASYNC_STORAGE); + } + + private CompletableFuture deleteAsyncStoragePod(PodResource podResource) + throws InfrastructureException { + Watch toCloseOnException = null; + try { + final CompletableFuture deleteFuture = new CompletableFuture<>(); + final Watch watch = podResource.watch(new DeleteWatcher<>(deleteFuture)); + toCloseOnException = watch; + + Boolean deleteSucceeded = podResource.withPropagationPolicy("Background").delete(); + if (deleteSucceeded == null || !deleteSucceeded) { + deleteFuture.complete(null); + } + return deleteFuture.whenComplete( + (v, e) -> { + if (e != null) { + LOG.warn("Failed to remove pod {} cause {}", ASYNC_STORAGE, e.getMessage()); + } + watch.close(); + }); + } catch (KubernetesClientException e) { + if (toCloseOnException != null) { + toCloseOnException.close(); + } + throw new KubernetesInfrastructureException(e); + } catch (Exception e) { + if (toCloseOnException != null) { + toCloseOnException.close(); + } + throw e; + } + } + + private static class DeleteWatcher implements Watcher { + + private final CompletableFuture future; + + private DeleteWatcher(CompletableFuture future) { + this.future = future; + } + + @Override + public void eventReceived(Action action, T hasMetadata) { + if (action == Action.DELETED) { + future.complete(null); + } + } + + @Override + public void onClose(KubernetesClientException e) { + // if event about removing is received then this completion has no effect + future.completeExceptionally( + new RuntimeException( + "WebSocket connection is closed. But event about removing is not received.", e)); + } + } +} diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisioner.java new file mode 100644 index 0000000000..6af22f963d --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisioner.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift.provision; + +import static com.google.common.collect.ImmutableMap.of; +import static java.lang.Boolean.parseBoolean; +import static java.lang.String.format; +import static java.lang.String.valueOf; +import static java.util.Collections.singletonList; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_USER_ID_LABEL; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.NOT_ABLE_TO_PROVISION_SSH_KEYS; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.NOT_ABLE_TO_PROVISION_SSH_KEYS_MESSAGE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy.COMMON_STRATEGY; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.EphemeralWorkspaceUtility.isEphemeral; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.DoneableConfigMap; +import io.fabric8.kubernetes.api.model.DoneablePersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.DoneablePod; +import io.fabric8.kubernetes.api.model.DoneableService; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.IntOrStringBuilder; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpec; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.PodResource; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Named; +import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.ssh.server.SshManager; +import org.eclipse.che.api.ssh.server.model.impl.SshPairImpl; +import org.eclipse.che.api.ssh.shared.model.SshPair; +import org.eclipse.che.api.workspace.server.model.impl.WarningImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.Names; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil; +import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configure environment for Async Storage feature (details described in issue + * https://github.com/eclipse/che/issues/15384) This environment will allow backup on workspace stop + * event and restore on restart created earlier.
+ * Will apply only in case workspace has attributes: asyncPersist: true - persistVolumes: + * false.
In case workspace has attributes: asyncPersist: true - persistVolumes: true will + * throw exception.
Feature enabled only for 'common' PVC strategy, in other cases will throw + * exception.
During provision will be created: - storage Pod - service for rsync connection + * via SSH - configmap, with public part of SSH key - PVC for storing backups; + */ +public class AsyncStorageProvisioner { + + private static final int SERVICE_PORT = 2222; + /** + * The authorized_keys file in SSH specifies the SSH keys that can be used for logging into the + * user account for which the file is configured. + */ + private static final String AUTHORIZED_KEYS = "authorized_keys"; + /** + * Name of the asynchronous storage Pod and Service. Rsync command will use this Service name for + * communications: e.g.: rsync ${RSYNC_OPTIONS} --rsh="ssh ${SSH_OPTIONS}" async-storage:/{PATH} + */ + static final String ASYNC_STORAGE = "async-storage"; + /** The name suffix for ConfigMap with SSH configuration */ + static final String ASYNC_STORAGE_CONFIG = "async-storage-config"; + /** The path of mount storage volume for file persist */ + private static final String ASYNC_STORAGE_DATA_PATH = "/" + ASYNC_STORAGE; + /** The path to the authorized_keys */ + private static final String SSH_KEY_PATH = "/.ssh/" + AUTHORIZED_KEYS; + /** The name of SSH key pair for rsync */ + static final String SSH_KEY_NAME = "rsync-via-ssh"; + /** The name of volume for mounting configuration map and authorized_keys */ + private static final String CONFIG_MAP_VOLUME_NAME = "async-storage-configvolume"; + /** */ + private static final String STORAGE_VOLUME = "async-storage-data"; + + private static final Logger LOG = LoggerFactory.getLogger(AsyncStorageProvisioner.class); + + private final String sidecarImagePullPolicy; + private final String pvcQuantity; + private final String asyncStorageImage; + private final String pvcAccessMode; + private final String pvcStrategy; + private final String pvcName; + private final String pvcStorageClassName; + private final SshManager sshManager; + private final OpenShiftClientFactory clientFactory; + + @Inject + public AsyncStorageProvisioner( + @Named("che.workspace.sidecar.image_pull_policy") String sidecarImagePullPolicy, + @Named("che.infra.kubernetes.pvc.quantity") String pvcQuantity, + @Named("che.infra.kubernetes.async.storage.image") String asyncStorageImage, + @Named("che.infra.kubernetes.pvc.access_mode") String pvcAccessMode, + @Named("che.infra.kubernetes.pvc.strategy") String pvcStrategy, + @Named("che.infra.kubernetes.pvc.name") String pvcName, + @Named("che.infra.kubernetes.pvc.storage_class_name") String pvcStorageClassName, + SshManager sshManager, + OpenShiftClientFactory openShiftClientFactory) { + this.sidecarImagePullPolicy = sidecarImagePullPolicy; + this.pvcQuantity = pvcQuantity; + this.asyncStorageImage = asyncStorageImage; + this.pvcAccessMode = pvcAccessMode; + this.pvcStrategy = pvcStrategy; + this.pvcName = pvcName; + this.pvcStorageClassName = pvcStorageClassName; + this.sshManager = sshManager; + this.clientFactory = openShiftClientFactory; + } + + public void provision(OpenShiftEnvironment osEnv, RuntimeIdentity identity) + throws InfrastructureException { + if (!parseBoolean(osEnv.getAttributes().get(ASYNC_PERSIST_ATTRIBUTE))) { + return; + } + + if (!COMMON_STRATEGY.equals(pvcStrategy)) { + String message = + format( + "Workspace configuration not valid: Asynchronous storage available only for 'common' PVC strategy, but got %s", + pvcStrategy); + LOG.warn(message); + osEnv.addWarning(new WarningImpl(4200, message)); + throw new InfrastructureException(message); + } + + if (!isEphemeral(osEnv.getAttributes())) { + String message = + format( + "Workspace configuration not valid: Asynchronous storage available only if '%s' attribute set to false", + ASYNC_PERSIST_ATTRIBUTE); + LOG.warn(message); + osEnv.addWarning(new WarningImpl(4200, message)); + throw new InfrastructureException(message); + } + + String namespace = identity.getInfrastructureNamespace(); + String userId = identity.getOwnerId(); + KubernetesClient oc = clientFactory.create(identity.getWorkspaceId()); + String configMapName = namespace + ASYNC_STORAGE_CONFIG; + + createPvcIfNotExist(oc, namespace, userId); + createConfigMapIfNotExist(oc, namespace, configMapName, userId, osEnv); + createAsyncStoragePodIfNotExist(oc, namespace, configMapName, userId); + createStorageServiceIfNotExist(oc, namespace, userId); + } + + private void createPvcIfNotExist(KubernetesClient oc, String namespace, String userId) { + Resource claimResource = + oc.persistentVolumeClaims().inNamespace(namespace).withName(pvcName); + + if (claimResource.get() != null) { + return; // pvc already exist + } + PersistentVolumeClaim pvc = + KubernetesObjectUtil.newPVC(pvcName, pvcAccessMode, pvcQuantity, pvcStorageClassName); + KubernetesObjectUtil.putLabel(pvc.getMetadata(), CHE_USER_ID_LABEL, userId); + oc.persistentVolumeClaims().inNamespace(namespace).create(pvc); + } + + /** Get or create new pair of SSH keys, this is need for securing rsync connection */ + private List getOrCreateSshPairs(String userId, OpenShiftEnvironment osEnv) + throws InfrastructureException { + List sshPairs; + try { + sshPairs = sshManager.getPairs(userId, "internal"); + } catch (ServerException e) { + String message = format("Unable to get SSH Keys. Cause: %s", e.getMessage()); + LOG.warn(message); + osEnv.addWarning( + new WarningImpl( + NOT_ABLE_TO_PROVISION_SSH_KEYS, + format(NOT_ABLE_TO_PROVISION_SSH_KEYS_MESSAGE, message))); + throw new InfrastructureException(e); + } + if (sshPairs.isEmpty()) { + try { + sshPairs = singletonList(sshManager.generatePair(userId, "internal", SSH_KEY_NAME)); + } catch (ServerException | ConflictException e) { + String message = + format( + "Unable to generate the SSH key for async storage service. Cause: %S", + e.getMessage()); + LOG.warn(message); + osEnv.addWarning( + new WarningImpl( + NOT_ABLE_TO_PROVISION_SSH_KEYS, + format(NOT_ABLE_TO_PROVISION_SSH_KEYS_MESSAGE, message))); + throw new InfrastructureException(e); + } + } + return sshPairs; + } + + /** Create configmap with public part of SSH key */ + private void createConfigMapIfNotExist( + KubernetesClient oc, + String namespace, + String configMapName, + String userId, + OpenShiftEnvironment osEnv) + throws InfrastructureException { + Resource mapResource = + oc.configMaps().inNamespace(namespace).withName(configMapName); + if (mapResource.get() != null) { // map already exist + return; + } + + List sshPairs = getOrCreateSshPairs(userId, osEnv); + if (sshPairs == null) { + return; + } + SshPair sshPair = sshPairs.get(0); + Map sshConfigData = of(AUTHORIZED_KEYS, sshPair.getPublicKey() + "\n"); + ConfigMap configMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName(configMapName) + .withNamespace(namespace) + .withLabels(of(CHE_USER_ID_LABEL, userId)) + .endMetadata() + .withData(sshConfigData) + .build(); + oc.configMaps().inNamespace(namespace).create(configMap); + } + + /** + * Create storage Pod with container with mounted volume for storing project source backups, SSH + * key and exposed port for rsync connection + */ + private void createAsyncStoragePodIfNotExist( + KubernetesClient oc, String namespace, String configMap, String userId) { + PodResource podResource = + oc.pods().inNamespace(namespace).withName(ASYNC_STORAGE); + if (podResource.get() != null) { + return; // pod already exist + } + + String containerName = Names.generateName(ASYNC_STORAGE); + + Volume storageVolume = + new VolumeBuilder() + .withName(STORAGE_VOLUME) + .withPersistentVolumeClaim( + new PersistentVolumeClaimVolumeSourceBuilder() + .withClaimName(pvcName) + .withReadOnly(false) + .build()) + .build(); + + Volume sshKeyVolume = + new VolumeBuilder() + .withName(CONFIG_MAP_VOLUME_NAME) + .withConfigMap( + new ConfigMapVolumeSourceBuilder() + .withName(configMap) + .withDefaultMode(0600) + .build()) + .build(); + + VolumeMount storageVolumeMount = + new VolumeMountBuilder() + .withMountPath(ASYNC_STORAGE_DATA_PATH) + .withName(STORAGE_VOLUME) + .withReadOnly(false) + .build(); + + VolumeMount sshVolumeMount = + new VolumeMountBuilder() + .withMountPath(SSH_KEY_PATH) + .withSubPath(AUTHORIZED_KEYS) + .withName(CONFIG_MAP_VOLUME_NAME) + .withReadOnly(true) + .build(); + + Container container = + new ContainerBuilder() + .withName(containerName) + .withImage(asyncStorageImage) + .withImagePullPolicy(sidecarImagePullPolicy) + .withNewResources() + .addToLimits("memory", new Quantity("512Mi")) + .addToRequests("memory", new Quantity("256Mi")) + .endResources() + .withPorts( + new ContainerPortBuilder() + .withContainerPort(SERVICE_PORT) + .withProtocol("TCP") + .build()) + .withVolumeMounts(storageVolumeMount, sshVolumeMount) + .build(); + + PodSpecBuilder podSpecBuilder = new PodSpecBuilder(); + PodSpec podSpec = + podSpecBuilder.withContainers(container).withVolumes(storageVolume, sshKeyVolume).build(); + + Pod pod = + new PodBuilder() + .withApiVersion("v1") + .withKind("Pod") + .withNewMetadata() + .withName(ASYNC_STORAGE) + .withNamespace(namespace) + .withLabels(of("app", ASYNC_STORAGE, CHE_USER_ID_LABEL, userId)) + .endMetadata() + .withSpec(podSpec) + .build(); + + oc.pods().inNamespace(namespace).create(pod); + } + + /** Create service for serving rsync connection */ + private void createStorageServiceIfNotExist( + KubernetesClient oc, String namespace, String userId) { + ServiceResource serviceResource = + oc.services().inNamespace(namespace).withName(ASYNC_STORAGE); + if (serviceResource.get() != null) { + return; // service already exist + } + + ObjectMeta meta = new ObjectMeta(); + meta.setName(ASYNC_STORAGE); + meta.setNamespace(namespace); + meta.setLabels(of(CHE_USER_ID_LABEL, userId)); + + IntOrString targetPort = + new IntOrStringBuilder().withIntVal(SERVICE_PORT).withStrVal(valueOf(SERVICE_PORT)).build(); + + ServicePort port = + new ServicePortBuilder() + .withName("rsync-port") + .withProtocol("TCP") + .withPort(SERVICE_PORT) + .withTargetPort(targetPort) + .build(); + ServiceSpec spec = new ServiceSpec(); + spec.setPorts(singletonList(port)); + spec.setSelector(of("app", ASYNC_STORAGE)); + + Service service = new Service(); + service.setApiVersion("v1"); + service.setKind("Service"); + service.setMetadata(meta); + service.setSpec(spec); + + oc.services().inNamespace(namespace).create(service); + } +} diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidatorTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidatorTest.java new file mode 100644 index 0000000000..6f2573bc0c --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/AsyncStorageModeValidatorTest.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift; + +import static com.google.common.collect.ImmutableMap.of; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.PERSIST_VOLUMES_ATTRIBUTE; + +import org.eclipse.che.api.core.ValidationException; +import org.testng.annotations.Test; + +public class AsyncStorageModeValidatorTest { + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for 'common' PVC strategy, but got not-common") + public void shouldThrowExceptionIfNotCommonStrategy() throws ValidationException { + AsyncStorageModeValidator validator = new AsyncStorageModeValidator("not-common", false, "", 1); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for 'per-user' namespace strategy") + public void shouldThrowExceptionIfNotPerUserNamespaceStrategy() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "my-name", 1); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for 'per-user' namespace strategy") + public void shouldThrowExceptionWithNullNamespaceStrategy() throws ValidationException { + AsyncStorageModeValidator validator = new AsyncStorageModeValidator("common", false, null, 1); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only if 'che.infra.kubernetes.namespace.allow_user_defined' set to 'false', but got 'true'") + public void shouldThrowExceptionIfUserDefineNamespaceAllowed() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", true, "-che", 1); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only if 'che.limits.user.workspaces.run.count' set to 1, but got 2") + public void shouldThrowExceptionIfMoreThanOneRuntimeEnabled() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 2); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test + public void shouldBeFineForEphemeralMode() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validate(of(PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test + public void shouldBeFineForPersistentMode() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validate(of(PERSIST_VOLUMES_ATTRIBUTE, "true")); + } + + @Test + public void shouldBeFineForEmptyAttribute() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validate(of()); + } + + @Test + public void shouldBeFineForAsyncMode() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for NOT persistent storage") + public void shouldThrowExceptionIfAsyncAttributeForNotEphemeral() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validate(of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "true")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for 'common' PVC strategy, but got not-common") + public void shouldThrowExceptionIfNotCommonStrategyUpdate() throws ValidationException { + AsyncStorageModeValidator validator = new AsyncStorageModeValidator("not-common", false, "", 1); + + validator.validateUpdate( + of(), of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for 'per-user' namespace strategy") + public void shouldThrowExceptionIfNotPerUserNamespaceStrategyUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "my-name", 1); + + validator.validateUpdate( + of(), of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only if 'che.infra.kubernetes.namespace.allow_user_defined' set to 'false', but got 'true'") + public void shouldThrowExceptionIfUserDefineNamespaceAllowedUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", true, "-che", 1); + + validator.validateUpdate( + of(), of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only if 'che.limits.user.workspaces.run.count' set to 1, but got 2") + public void shouldThrowExceptionIfMoreThanOneRuntimeEnabledUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 2); + + validator.validateUpdate( + of(), of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test + public void shouldBeFineForEphemeralModeUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validateUpdate(of(), of(PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test + public void shouldBeFineForPersistentModeUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validateUpdate(of(), of(PERSIST_VOLUMES_ATTRIBUTE, "true")); + } + + @Test + public void shouldBeFineForEmptyAttributeUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validateUpdate(of(), of()); + } + + @Test + public void shouldBeFineForAsyncModeUpdate() throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validateUpdate( + of(), of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "false")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for NOT persistent storage") + public void shouldThrowExceptionIfAsyncAttributeForNotEphemeralUpdate() + throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validateUpdate( + of(), of(ASYNC_PERSIST_ATTRIBUTE, "true", PERSIST_VOLUMES_ATTRIBUTE, "true")); + } + + @Test( + expectedExceptions = ValidationException.class, + expectedExceptionsMessageRegExp = + "Workspace configuration not valid: Asynchronous storage available only for NOT persistent storage") + public void shouldThrowExceptionIfAsyncAttributeForNotEphemeralUpdate2() + throws ValidationException { + AsyncStorageModeValidator validator = + new AsyncStorageModeValidator("common", false, "-che", 1); + + validator.validateUpdate( + of(PERSIST_VOLUMES_ATTRIBUTE, "true"), of(ASYNC_PERSIST_ATTRIBUTE, "true")); + } +} diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java index c99a44a34f..27d2504dc8 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java @@ -30,6 +30,8 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.limits.ram. import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpolicy.RestartPolicyRewriter; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStoragePodInterceptor; +import org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftUniqueNamesProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner; import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftPreviewUrlExposer; @@ -62,6 +64,8 @@ public class OpenShiftEnvironmentProvisionerTest { @Mock private ImagePullSecretProvisioner imagePullSecretProvisioner; @Mock private ProxySettingsProvisioner proxySettingsProvisioner; @Mock private ServiceAccountProvisioner serviceAccountProvisioner; + @Mock private AsyncStorageProvisioner asyncStorageProvisioner; + @Mock private AsyncStoragePodInterceptor asyncStoragePodObserver; @Mock private CertificateProvisioner certificateProvisioner; @Mock private SshKeysProvisioner sshKeysProvisioner; @Mock private GitConfigProvisioner gitConfigProvisioner; @@ -88,6 +92,8 @@ public class OpenShiftEnvironmentProvisionerTest { podTerminationGracePeriodProvisioner, imagePullSecretProvisioner, proxySettingsProvisioner, + asyncStorageProvisioner, + asyncStoragePodObserver, serviceAccountProvisioner, certificateProvisioner, sshKeysProvisioner, diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptorTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptorTest.java new file mode 100644 index 0000000000..f8d2724ad8 --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStoragePodInterceptorTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift.provision; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Collections.emptyMap; +import static java.util.UUID.randomUUID; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.PERSIST_VOLUMES_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner.ASYNC_STORAGE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.DoneablePod; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.PodResource; +import io.fabric8.openshift.client.OpenShiftClient; +import java.util.UUID; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class AsyncStoragePodInterceptorTest { + + private static final String WORKSPACE_ID = UUID.randomUUID().toString(); + private static final String NAMESPACE = UUID.randomUUID().toString(); + + @Mock private OpenShiftEnvironment openShiftEnvironment; + @Mock private RuntimeIdentity identity; + @Mock private OpenShiftClientFactory clientFactory; + @Mock private OpenShiftClient osClient; + @Mock private PodResource podResource; + @Mock private MixedOperation mixedOperationPod; + @Mock private NonNamespaceOperation namespacePodOperation; + @Mock private FilterWatchListDeletable> deletable; + + private AsyncStoragePodInterceptor asyncStoragePodInterceptor; + + @BeforeMethod + public void setUp() { + asyncStoragePodInterceptor = new AsyncStoragePodInterceptor("common", clientFactory); + } + + @Test + public void shouldDoNothingIfNotCommonStrategy() throws Exception { + AsyncStoragePodInterceptor asyncStoragePodInterceptor = + new AsyncStoragePodInterceptor(randomUUID().toString(), clientFactory); + asyncStoragePodInterceptor.intercept(openShiftEnvironment, identity); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test + public void shouldDoNothingIfEphemeralWorkspace() throws Exception { + when(openShiftEnvironment.getAttributes()).thenReturn(of(PERSIST_VOLUMES_ATTRIBUTE, "false")); + asyncStoragePodInterceptor.intercept(openShiftEnvironment, identity); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test + public void shouldDoNothingIfWorkspaceConfiguredWithAsyncStorage() throws Exception { + when(openShiftEnvironment.getAttributes()) + .thenReturn(of(PERSIST_VOLUMES_ATTRIBUTE, "false", ASYNC_PERSIST_ATTRIBUTE, "true")); + asyncStoragePodInterceptor.intercept(openShiftEnvironment, identity); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test + public void shouldDoNothingIfPodDoesNotExist() throws InfrastructureException { + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + + when(clientFactory.create(WORKSPACE_ID)).thenReturn(osClient); + when(openShiftEnvironment.getAttributes()).thenReturn(emptyMap()); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + when(podResource.get()).thenReturn(null); + + asyncStoragePodInterceptor.intercept(openShiftEnvironment, identity); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + verifyNoMoreInteractions(osClient); + } + + @Test + public void shouldDoDeletePodIfWorkspaceWithEmptyAttributes() throws InfrastructureException { + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + + when(clientFactory.create(WORKSPACE_ID)).thenReturn(osClient); + when(openShiftEnvironment.getAttributes()).thenReturn(emptyMap()); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + + ObjectMeta meta = new ObjectMeta(); + meta.setName(ASYNC_STORAGE); + Pod pod = new Pod(); + pod.setMetadata(meta); + + when(podResource.get()).thenReturn(pod); + when(podResource.withPropagationPolicy("Background")).thenReturn(deletable); + + Watch watch = mock(Watch.class); + when(podResource.watch(any())).thenReturn(watch); + + asyncStoragePodInterceptor.intercept(openShiftEnvironment, identity); + verify(deletable).delete(); + verify(watch).close(); + } + + @Test + public void shouldDoDeletePodIfWorkspaceConfigureToPersistentStorage() + throws InfrastructureException { + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + + when(clientFactory.create(WORKSPACE_ID)).thenReturn(osClient); + when(openShiftEnvironment.getAttributes()) + .thenReturn(ImmutableMap.of(PERSIST_VOLUMES_ATTRIBUTE, "true")); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + + ObjectMeta meta = new ObjectMeta(); + meta.setName(ASYNC_STORAGE); + Pod pod = new Pod(); + pod.setMetadata(meta); + + when(podResource.get()).thenReturn(pod); + when(podResource.withPropagationPolicy("Background")).thenReturn(deletable); + + Watch watch = mock(Watch.class); + when(podResource.watch(any())).thenReturn(watch); + + asyncStoragePodInterceptor.intercept(openShiftEnvironment, identity); + verify(deletable).delete(); + verify(watch).close(); + } +} diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisionerTest.java new file mode 100644 index 0000000000..8bc01e28bc --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/AsyncStorageProvisionerTest.java @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift.provision; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.UUID.randomUUID; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.PERSIST_VOLUMES_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner.ASYNC_STORAGE; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner.ASYNC_STORAGE_CONFIG; +import static org.eclipse.che.workspace.infrastructure.openshift.provision.AsyncStorageProvisioner.SSH_KEY_NAME; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.DoneableConfigMap; +import io.fabric8.kubernetes.api.model.DoneablePersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.DoneablePod; +import io.fabric8.kubernetes.api.model.DoneableService; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.PodResource; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import io.fabric8.openshift.client.OpenShiftClient; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.ssh.server.SshManager; +import org.eclipse.che.api.ssh.server.model.impl.SshPairImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class AsyncStorageProvisionerTest { + + private static final String WORKSPACE_ID = UUID.randomUUID().toString(); + private static final String NAMESPACE = UUID.randomUUID().toString(); + private static final String CONFIGMAP_NAME = NAMESPACE + ASYNC_STORAGE_CONFIG; + private static final String VPC_NAME = UUID.randomUUID().toString(); + private static final String USER = "user"; + + @Mock private OpenShiftEnvironment openShiftEnvironment; + @Mock private RuntimeIdentity identity; + @Mock private OpenShiftClientFactory clientFactory; + @Mock private OpenShiftClient osClient; + @Mock private SshManager sshManager; + @Mock private Resource pvcResource; + @Mock private Resource mapResource; + @Mock private PodResource podResource; + @Mock private ServiceResource serviceResource; + @Mock private MixedOperation mixedOperationPvc; + @Mock private MixedOperation mixedOperationConfigMap; + @Mock private MixedOperation mixedOperationPod; + @Mock private MixedOperation mixedOperationService; + @Mock private NonNamespaceOperation namespacePvcOperation; + @Mock private NonNamespaceOperation namespaceConfigMapOperation; + @Mock private NonNamespaceOperation namespacePodOperation; + @Mock private NonNamespaceOperation namespaceServiceOperation; + + private Map attributes; + private AsyncStorageProvisioner asyncStorageProvisioner; + private SshPairImpl sshPair; + + @BeforeMethod + public void setUp() { + asyncStorageProvisioner = + new AsyncStorageProvisioner( + "Always", + "10Gi", + "org/image:tag", + "ReadWriteOnce", + "common", + VPC_NAME, + "storage", + sshManager, + clientFactory); + attributes = new HashMap<>(2); + attributes.put(ASYNC_PERSIST_ATTRIBUTE, "true"); + attributes.put(PERSIST_VOLUMES_ATTRIBUTE, "false"); + sshPair = new SshPairImpl(USER, "internal", SSH_KEY_NAME, "", ""); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void shouldThrowExceptionIfNotCommonStrategy() throws Exception { + AsyncStorageProvisioner asyncStorageProvisioner = + new AsyncStorageProvisioner( + "Always", + "10Gi", + "org/image:tag", + "ReadWriteOnce", + randomUUID().toString(), + VPC_NAME, + "storageClass", + sshManager, + clientFactory); + when(openShiftEnvironment.getAttributes()).thenReturn(attributes); + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verifyNoMoreInteractions(sshManager); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void shouldThrowExceptionIfAsyncStorageForNotEphemeralWorkspace() throws Exception { + Map attributes = new HashMap<>(2); + attributes.put(ASYNC_PERSIST_ATTRIBUTE, "true"); + attributes.put(PERSIST_VOLUMES_ATTRIBUTE, "true"); + when(openShiftEnvironment.getAttributes()).thenReturn(attributes); + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verifyNoMoreInteractions(sshManager); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test + public void shouldDoNothingIfNotSetAttribute() throws InfrastructureException { + when(openShiftEnvironment.getAttributes()).thenReturn(emptyMap()); + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verifyNoMoreInteractions(sshManager); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test + public void shouldDoNothingIfAttributesAsyncPersistOnly() throws InfrastructureException { + when(openShiftEnvironment.getAttributes()) + .thenReturn(singletonMap(PERSIST_VOLUMES_ATTRIBUTE, "false")); + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verifyNoMoreInteractions(sshManager); + verifyNoMoreInteractions(clientFactory); + verifyNoMoreInteractions(identity); + } + + @Test + public void shouldCreateAll() throws InfrastructureException, ServerException, ConflictException { + when(openShiftEnvironment.getAttributes()).thenReturn(attributes); + when(clientFactory.create(anyString())).thenReturn(osClient); + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + when(identity.getOwnerId()).thenReturn(USER); + when(sshManager.getPairs(USER, "internal")).thenReturn(singletonList(sshPair)); + + when(osClient.persistentVolumeClaims()).thenReturn(mixedOperationPvc); + when(mixedOperationPvc.inNamespace(NAMESPACE)).thenReturn(namespacePvcOperation); + when(namespacePvcOperation.withName(VPC_NAME)).thenReturn(pvcResource); + when(pvcResource.get()).thenReturn(null); + + when(osClient.configMaps()).thenReturn(mixedOperationConfigMap); + when(mixedOperationConfigMap.inNamespace(NAMESPACE)).thenReturn(namespaceConfigMapOperation); + when(namespaceConfigMapOperation.withName(anyString())).thenReturn(mapResource); + when(mapResource.get()).thenReturn(null); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + when(podResource.get()).thenReturn(null); + + when(osClient.services()).thenReturn(mixedOperationService); + when(mixedOperationService.inNamespace(NAMESPACE)).thenReturn(namespaceServiceOperation); + when(namespaceServiceOperation.withName(ASYNC_STORAGE)).thenReturn(serviceResource); + when(serviceResource.get()).thenReturn(null); + + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verify(identity, times(1)).getInfrastructureNamespace(); + verify(identity, times(1)).getOwnerId(); + verify(sshManager, times(1)).getPairs(USER, "internal"); + verify(sshManager, never()).generatePair(USER, "internal", SSH_KEY_NAME); + verify(osClient.services().inNamespace(NAMESPACE), times(1)).create(any(Service.class)); + verify(osClient.configMaps().inNamespace(NAMESPACE), times(1)).create(any(ConfigMap.class)); + verify(osClient.pods().inNamespace(NAMESPACE), times(1)).create(any(Pod.class)); + verify(osClient.persistentVolumeClaims().inNamespace(NAMESPACE), times(1)) + .create(any(PersistentVolumeClaim.class)); + } + + @Test + public void shouldNotCreateConfigMap() + throws InfrastructureException, ServerException, ConflictException { + when(openShiftEnvironment.getAttributes()).thenReturn(attributes); + when(clientFactory.create(anyString())).thenReturn(osClient); + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + when(identity.getOwnerId()).thenReturn(USER); + + when(osClient.persistentVolumeClaims()).thenReturn(mixedOperationPvc); + when(mixedOperationPvc.inNamespace(NAMESPACE)).thenReturn(namespacePvcOperation); + when(namespacePvcOperation.withName(VPC_NAME)).thenReturn(pvcResource); + when(pvcResource.get()).thenReturn(null); + + when(osClient.configMaps()).thenReturn(mixedOperationConfigMap); + when(mixedOperationConfigMap.inNamespace(NAMESPACE)).thenReturn(namespaceConfigMapOperation); + when(namespaceConfigMapOperation.withName(CONFIGMAP_NAME)).thenReturn(mapResource); + ObjectMeta meta = new ObjectMeta(); + meta.setName(CONFIGMAP_NAME); + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(meta); + when(mapResource.get()).thenReturn(configMap); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + when(podResource.get()).thenReturn(null); + + when(osClient.services()).thenReturn(mixedOperationService); + when(mixedOperationService.inNamespace(NAMESPACE)).thenReturn(namespaceServiceOperation); + when(namespaceServiceOperation.withName(ASYNC_STORAGE)).thenReturn(serviceResource); + when(serviceResource.get()).thenReturn(null); + + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verify(identity, times(1)).getInfrastructureNamespace(); + verify(identity, times(1)).getOwnerId(); + verify(identity, times(1)).getWorkspaceId(); + verify(sshManager, never()).getPairs(USER, "internal"); + verify(sshManager, never()).generatePair(USER, "internal", SSH_KEY_NAME); + verify(osClient.services().inNamespace(NAMESPACE), times(1)).create(any(Service.class)); + verify(osClient.configMaps().inNamespace(NAMESPACE), never()).create(any(ConfigMap.class)); + verify(osClient.pods().inNamespace(NAMESPACE), times(1)).create(any(Pod.class)); + verify(osClient.persistentVolumeClaims().inNamespace(NAMESPACE), times(1)) + .create(any(PersistentVolumeClaim.class)); + } + + @Test + public void shouldNotCreatePod() + throws InfrastructureException, ServerException, ConflictException { + when(openShiftEnvironment.getAttributes()).thenReturn(attributes); + when(clientFactory.create(anyString())).thenReturn(osClient); + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + when(identity.getOwnerId()).thenReturn(USER); + when(sshManager.getPairs(USER, "internal")).thenReturn(singletonList(sshPair)); + + when(osClient.persistentVolumeClaims()).thenReturn(mixedOperationPvc); + when(mixedOperationPvc.inNamespace(NAMESPACE)).thenReturn(namespacePvcOperation); + when(namespacePvcOperation.withName(VPC_NAME)).thenReturn(pvcResource); + when(pvcResource.get()).thenReturn(null); + + when(osClient.configMaps()).thenReturn(mixedOperationConfigMap); + when(mixedOperationConfigMap.inNamespace(NAMESPACE)).thenReturn(namespaceConfigMapOperation); + when(namespaceConfigMapOperation.withName(CONFIGMAP_NAME)).thenReturn(mapResource); + when(mapResource.get()).thenReturn(null); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + ObjectMeta meta = new ObjectMeta(); + meta.setName(ASYNC_STORAGE); + Pod pod = new Pod(); + pod.setMetadata(meta); + when(podResource.get()).thenReturn(pod); + + when(osClient.services()).thenReturn(mixedOperationService); + when(mixedOperationService.inNamespace(NAMESPACE)).thenReturn(namespaceServiceOperation); + when(namespaceServiceOperation.withName(ASYNC_STORAGE)).thenReturn(serviceResource); + when(serviceResource.get()).thenReturn(null); + + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verify(identity, times(1)).getInfrastructureNamespace(); + verify(identity, times(1)).getOwnerId(); + verify(sshManager, times(1)).getPairs(USER, "internal"); + verify(sshManager, never()).generatePair(USER, "internal", SSH_KEY_NAME); + verify(osClient.services().inNamespace(NAMESPACE), times(1)).create(any(Service.class)); + verify(osClient.configMaps().inNamespace(NAMESPACE), times(1)).create(any(ConfigMap.class)); + verify(osClient.pods().inNamespace(NAMESPACE), never()).create(any(Pod.class)); + verify(osClient.persistentVolumeClaims().inNamespace(NAMESPACE), times(1)) + .create(any(PersistentVolumeClaim.class)); + } + + @Test + public void shouldNotCreateService() + throws InfrastructureException, ServerException, ConflictException { + when(openShiftEnvironment.getAttributes()).thenReturn(attributes); + when(clientFactory.create(anyString())).thenReturn(osClient); + when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + when(identity.getOwnerId()).thenReturn(USER); + when(sshManager.getPairs(USER, "internal")).thenReturn(singletonList(sshPair)); + + when(osClient.persistentVolumeClaims()).thenReturn(mixedOperationPvc); + when(mixedOperationPvc.inNamespace(NAMESPACE)).thenReturn(namespacePvcOperation); + when(namespacePvcOperation.withName(VPC_NAME)).thenReturn(pvcResource); + when(pvcResource.get()).thenReturn(null); + + when(osClient.configMaps()).thenReturn(mixedOperationConfigMap); + when(mixedOperationConfigMap.inNamespace(NAMESPACE)).thenReturn(namespaceConfigMapOperation); + when(namespaceConfigMapOperation.withName(CONFIGMAP_NAME)).thenReturn(mapResource); + when(mapResource.get()).thenReturn(null); + + when(osClient.pods()).thenReturn(mixedOperationPod); + when(mixedOperationPod.inNamespace(NAMESPACE)).thenReturn(namespacePodOperation); + when(namespacePodOperation.withName(ASYNC_STORAGE)).thenReturn(podResource); + when(podResource.get()).thenReturn(null); + + when(osClient.services()).thenReturn(mixedOperationService); + when(mixedOperationService.inNamespace(NAMESPACE)).thenReturn(namespaceServiceOperation); + when(namespaceServiceOperation.withName(ASYNC_STORAGE)).thenReturn(serviceResource); + ObjectMeta meta = new ObjectMeta(); + meta.setName(ASYNC_STORAGE); + Service service = new Service(); + service.setMetadata(meta); + when(serviceResource.get()).thenReturn(service); + + asyncStorageProvisioner.provision(openShiftEnvironment, identity); + verify(identity, times(1)).getInfrastructureNamespace(); + verify(identity, times(1)).getOwnerId(); + verify(sshManager, times(1)).getPairs(USER, "internal"); + verify(sshManager, never()).generatePair(USER, "internal", SSH_KEY_NAME); + verify(osClient.services().inNamespace(NAMESPACE), never()).create(any(Service.class)); + verify(osClient.configMaps().inNamespace(NAMESPACE), times(1)).create(any(ConfigMap.class)); + verify(osClient.pods().inNamespace(NAMESPACE), times(1)).create(any(Pod.class)); + verify(osClient.persistentVolumeClaims().inNamespace(NAMESPACE), times(1)) + .create(any(PersistentVolumeClaim.class)); + } +} diff --git a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java index 9a8c24d69d..ec557918e9 100644 --- a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java +++ b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java @@ -288,6 +288,9 @@ public class JpaEntitiesCascadeRemovalTest { bind(String[].class) .annotatedWith(Names.named("che.workspace.devfile.default_editor.plugins")) .toInstance(new String[] {"default/plugin/0.0.1"}); + bind(String.class) + .annotatedWith(Names.named("che.workspace.devfile.async.storage.plugin")) + .toInstance(""); } }); 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 234f2f7387..ca2d73b814 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 @@ -121,6 +121,20 @@ public final class Constants { */ public static final String PERSIST_VOLUMES_ATTRIBUTE = "persistVolumes"; + /** + * The attribute allows to configure workspace with async storage support this configuration. Make + * sense only in case org.eclipse.che.api.workspace.shared.Constants#PERSIST_VOLUMES_ATTRIBUTE set + * to 'false'. + * + *

Should be set/read from {@link WorkspaceConfig#getAttributes}. + * + *

Value is expected to be boolean, and if set to 'true' special plugin will be added to + * workspace. It will provide ability to backup/restore project source to the async storage. + * Workspace volumes still would be created as `emptyDir`. During stopping workspace project + * source will be sent to the storage Pod and restore from it on next restarts. + */ + public static final String ASYNC_PERSIST_ATTRIBUTE = "asyncPersist"; + /** * Contains a list of workspace tooling plugins that should be used in a workspace. Should be * set/read from {@link WorkspaceConfig#getAttributes}. diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisioner.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisioner.java index df81a9be4d..5490f4b248 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisioner.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisioner.java @@ -15,7 +15,10 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static org.eclipse.che.api.workspace.server.devfile.Constants.EDITOR_COMPONENT_TYPE; import static org.eclipse.che.api.workspace.server.devfile.Constants.EDITOR_FREE_DEVFILE_ATTRIBUTE; import static org.eclipse.che.api.workspace.server.devfile.Constants.PLUGIN_COMPONENT_TYPE; +import static org.eclipse.che.api.workspace.shared.Constants.ASYNC_PERSIST_ATTRIBUTE; +import static org.eclipse.che.api.workspace.shared.Constants.PERSIST_VOLUMES_ATTRIBUTE; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,6 +45,7 @@ public class DefaultEditorProvisioner { private final String defaultEditorRef; private final String defaultEditor; private final Map defaultPluginsToRefs; + private final String asyncStoragePluginRef; private final ComponentFQNParser componentFQNParser; private final PluginFQNParser pluginFQNParser; @@ -49,10 +53,12 @@ public class DefaultEditorProvisioner { public DefaultEditorProvisioner( @Named("che.workspace.devfile.default_editor") String defaultEditorRef, @Named("che.workspace.devfile.default_editor.plugins") String[] defaultPluginsRefs, + @Named("che.workspace.devfile.async.storage.plugin") String asyncStoragePluginRef, ComponentFQNParser componentFQNParser, PluginFQNParser pluginFQNParser) throws DevfileException { this.defaultEditorRef = isNullOrEmpty(defaultEditorRef) ? null : defaultEditorRef; + this.asyncStoragePluginRef = asyncStoragePluginRef; this.componentFQNParser = componentFQNParser; this.pluginFQNParser = pluginFQNParser; this.defaultEditor = @@ -102,6 +108,10 @@ public class DefaultEditorProvisioner { if (isDefaultEditorUsed) { provisionDefaultPlugins(components, contentProvider); } + if ("false".equals(devfile.getAttributes().get(PERSIST_VOLUMES_ATTRIBUTE)) + && "true".equals(devfile.getAttributes().get(ASYNC_PERSIST_ATTRIBUTE))) { + provisionAsyncStoragePlugin(components, contentProvider); + } } /** @@ -122,6 +132,28 @@ public class DefaultEditorProvisioner { } } + /** + * Provision the for async storage service, it will provide ability backup and restore project + * source using special storage. Will torn on only if workspace start in Ephemeral mode and has + * attribute 'asyncPersist = true' + * + * @param components The set of components currently present in the Devfile + * @param contentProvider content provider for plugin references retrieval + * @throws DevfileException - A DevfileException containing any caught InfrastructureException + */ + private void provisionAsyncStoragePlugin( + List components, FileContentProvider contentProvider) throws DevfileException { + try { + Map missingPluginsIdToRef = + Collections.singletonMap( + componentFQNParser.getPluginPublisherAndName(asyncStoragePluginRef), + asyncStoragePluginRef); + addMissingPlugins(components, contentProvider, missingPluginsIdToRef); + } catch (InfrastructureException e) { + throw new DevfileException(e.getMessage(), e); + } + } + /** * Checks if any of the Devfile's components are also in the list of missing default plugins, and * removes them. diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisionerTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisionerTest.java index 6df5192eef..2a0e4500e3 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisionerTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/convert/DefaultEditorProvisionerTest.java @@ -17,16 +17,19 @@ import static org.eclipse.che.api.workspace.server.devfile.Constants.PLUGIN_COMP import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +import com.google.common.collect.ImmutableMap; import java.util.List; import org.eclipse.che.api.workspace.server.devfile.FileContentProvider; import org.eclipse.che.api.workspace.server.devfile.convert.component.ComponentFQNParser; import org.eclipse.che.api.workspace.server.model.impl.devfile.ComponentImpl; import org.eclipse.che.api.workspace.server.model.impl.devfile.DevfileImpl; import org.eclipse.che.api.workspace.server.wsplugins.PluginFQNParser; +import org.eclipse.che.api.workspace.shared.Constants; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.Listeners; @@ -50,6 +53,7 @@ public class DefaultEditorProvisionerTest { private static final String TERMINAL_PLUGIN_NAME = "theia-terminal"; private static final String TERMINAL_PLUGIN_VERSION = "0.0.4"; + private static final String ASYNC_STORAGE_PLUGIN_REF = "eclipse/che-async-pv-plugin/nightly"; private static final String TERMINAL_PLUGIN_REF = EDITOR_PUBLISHER + "/" + TERMINAL_PLUGIN_NAME + "/" + TERMINAL_PLUGIN_VERSION; @@ -65,7 +69,9 @@ public class DefaultEditorProvisionerTest { @Test public void shouldNotProvisionDefaultEditorIfItIsNotConfigured() throws Exception { // given - provisioner = new DefaultEditorProvisioner(null, new String[] {}, fqnParser, pluginFQNParser); + provisioner = + new DefaultEditorProvisioner( + null, new String[] {}, ASYNC_STORAGE_PLUGIN_REF, fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); // when @@ -82,6 +88,7 @@ public class DefaultEditorProvisionerTest { new DefaultEditorProvisioner( EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF, COMMAND_PLUGIN_REF}, + "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); @@ -103,7 +110,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl defaultEditorWithDifferentVersion = @@ -129,7 +136,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl defaultEditorWithDifferentVersion = @@ -156,7 +163,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl editorWithNameSimilarToDefault = @@ -180,7 +187,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); devfile.getAttributes().put(EDITOR_FREE_DEVFILE_ATTRIBUTE, "true"); @@ -200,7 +207,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl pluginWithNameSimilarToDefault = @@ -224,7 +231,7 @@ public class DefaultEditorProvisionerTest { throws Exception { // given provisioner = - new DefaultEditorProvisioner(EDITOR_REF, new String[] {}, fqnParser, pluginFQNParser); + new DefaultEditorProvisioner(EDITOR_REF, new String[] {}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl nonDefaultEditor = new ComponentImpl(EDITOR_COMPONENT_TYPE, "anypublisher/anyname/v" + EDITOR_VERSION); @@ -244,7 +251,7 @@ public class DefaultEditorProvisionerTest { throws Exception { // given provisioner = - new DefaultEditorProvisioner(EDITOR_REF, new String[] {}, fqnParser, pluginFQNParser); + new DefaultEditorProvisioner(EDITOR_REF, new String[] {}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl myTheiaEditor = new ComponentImpl( @@ -265,7 +272,7 @@ public class DefaultEditorProvisionerTest { throws Exception { // given provisioner = - new DefaultEditorProvisioner(EDITOR_REF, new String[] {}, fqnParser, pluginFQNParser); + new DefaultEditorProvisioner(EDITOR_REF, new String[] {}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl myTheiaEditor = new ComponentImpl( @@ -300,7 +307,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); ComponentImpl myTerminal = new ComponentImpl( @@ -323,7 +330,7 @@ public class DefaultEditorProvisionerTest { // given provisioner = new DefaultEditorProvisioner( - EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, fqnParser, pluginFQNParser); + EDITOR_REF, new String[] {TERMINAL_PLUGIN_REF}, "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); String meta = "apiVersion: v2\n" @@ -360,6 +367,7 @@ public class DefaultEditorProvisionerTest { new DefaultEditorProvisioner( EDITOR_REF, new String[] {EDITOR_PUBLISHER + "/" + "my-plugin/v2.0"}, + "", fqnParser, pluginFQNParser); DevfileImpl devfile = new DevfileImpl(); @@ -389,6 +397,7 @@ public class DefaultEditorProvisionerTest { new DefaultEditorProvisioner( EDITOR_REF, new String[] {EDITOR_PUBLISHER + "/" + "my-plugin/v2.0", referencePluginRef}, + "", fqnParser, pluginFQNParser); String meta = @@ -416,6 +425,59 @@ public class DefaultEditorProvisionerTest { assertTrue(components.contains(myPlugin)); } + @Test + public void shouldProvisionAsyncStoragePluginsIfWorkspaceHasOnlyOneAttribute() throws Exception { + // given + provisioner = + new DefaultEditorProvisioner( + EDITOR_REF, + new String[] {TERMINAL_PLUGIN_REF}, + ASYNC_STORAGE_PLUGIN_REF, + fqnParser, + pluginFQNParser); + + DevfileImpl devfile = new DevfileImpl(); + devfile.setAttributes(ImmutableMap.of(Constants.ASYNC_PERSIST_ATTRIBUTE, "true")); + // when + provisioner.apply(devfile, fileContentProvider); + + // then + List components = devfile.getComponents(); + assertEquals(components.size(), 2); + + assertFalse( + components.contains(new ComponentImpl(PLUGIN_COMPONENT_TYPE, ASYNC_STORAGE_PLUGIN_REF))); + } + + @Test + public void shouldProvisionAsyncStoragePluginsIfWorkspaceHasBothAttributes() throws Exception { + // given + provisioner = + new DefaultEditorProvisioner( + EDITOR_REF, + new String[] {TERMINAL_PLUGIN_REF}, + ASYNC_STORAGE_PLUGIN_REF, + fqnParser, + pluginFQNParser); + + DevfileImpl devfile = new DevfileImpl(); + devfile.setAttributes( + ImmutableMap.of( + Constants.ASYNC_PERSIST_ATTRIBUTE, + "true", + Constants.PERSIST_VOLUMES_ATTRIBUTE, + "false")); + // when + provisioner.apply(devfile, fileContentProvider); + + // then + List components = devfile.getComponents(); + assertEquals(components.size(), 3); + + assertTrue( + components.contains(new ComponentImpl(PLUGIN_COMPONENT_TYPE, ASYNC_STORAGE_PLUGIN_REF))); + } + private ComponentImpl findById(List components, String id) { return components .stream()