Prepare environment for async storage of project sources (#16972)

* Prepare environment for async storage of project sources

Signed-off-by: Vitalii Parfonov <vparfono@redhat.com>
7.20.x
Vitalii Parfonov 2020-07-15 18:50:36 +03:00 committed by GitHub
parent 0c79eaeba0
commit 3dd86e856d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1624 additions and 16 deletions

View File

@ -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

View File

@ -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";

View File

@ -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<String, String> attributes = k8sEnv.getAttributes();
if (isEphemeral(attributes) && parseBoolean(attributes.get(ASYNC_PERSIST_ATTRIBUTE))) {
return GRACE_TERMINATION_PERIOD_ASYNC_STORAGE_WS_SEC;
}
return graceTerminationPeriodSec;
}
}

View File

@ -70,6 +70,14 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-model</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-ssh</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-ssh-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-system</artifactId>

View File

@ -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.
*
* <p>To be valid for async storage workspace MUST have attributes:
*
* <ul>
* <li>{@link org.eclipse.che.api.workspace.shared.Constants#ASYNC_PERSIST_ATTRIBUTE} = 'true'
* <li>{@link org.eclipse.che.api.workspace.shared.Constants#PERSIST_VOLUMES_ATTRIBUTE} = 'false'
* </ul>
*
* <p>If set only {@link org.eclipse.che.api.workspace.shared.Constants#ASYNC_PERSIST_ATTRIBUTE} =
* 'true', {@link ValidationException} is thrown.
*
* <p>If system is configured with other value of properties than below {@link ValidationException},
* is thrown.
*
* <ul>
* <li>che.infra.kubernetes.namespace.default=<username>-che
* <li>che.infra.kubernetes.namespace.allow_user_defined=false
* <li>che.infra.kubernetes.pvc.strategy=common
* <li>che.limits.user.workspaces.run.count=1
* </ul>
*/
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<String, String> attributes) throws ValidationException {
if (parseBoolean(attributes.get(ASYNC_PERSIST_ATTRIBUTE))) {
isEphemeralAttributeValidation(attributes);
pvcStrategyValidation();
alowUserDefinedNamespaceValidation();
nameSpaceStrategyValidation();
runtimesPerUserValidation();
}
}
@Override
public void validateUpdate(Map<String, String> existing, Map<String, String> 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<String, String> 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("<username>")) {
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);
}
}
}

View File

@ -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);

View File

@ -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<WorkspaceAttributeValidator> 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);
}
}

View File

@ -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<Pod, DoneablePod> 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<Pod, DoneablePod> getAsyncStoragePodResource(
String namespace, String workspaceId) throws InfrastructureException {
return clientFactory.create(workspaceId).pods().inNamespace(namespace).withName(ASYNC_STORAGE);
}
private CompletableFuture<Void> deleteAsyncStoragePod(PodResource<Pod, DoneablePod> podResource)
throws InfrastructureException {
Watch toCloseOnException = null;
try {
final CompletableFuture<Void> 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<T> implements Watcher<T> {
private final CompletableFuture<Void> future;
private DeleteWatcher(CompletableFuture<Void> 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));
}
}
}

View File

@ -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. <br>
* Will apply only in case workspace has attributes: asyncPersist: true - persistVolumes:
* false.</br> In case workspace has attributes: asyncPersist: true - persistVolumes: true will
* throw exception.</br> Feature enabled only for 'common' PVC strategy, in other cases will throw
* exception.</br> 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<PersistentVolumeClaim, DoneablePersistentVolumeClaim> 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<SshPairImpl> getOrCreateSshPairs(String userId, OpenShiftEnvironment osEnv)
throws InfrastructureException {
List<SshPairImpl> 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<ConfigMap, DoneableConfigMap> mapResource =
oc.configMaps().inNamespace(namespace).withName(configMapName);
if (mapResource.get() != null) { // map already exist
return;
}
List<SshPairImpl> sshPairs = getOrCreateSshPairs(userId, osEnv);
if (sshPairs == null) {
return;
}
SshPair sshPair = sshPairs.get(0);
Map<String, String> 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<Pod, DoneablePod> 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<Service, DoneableService> 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);
}
}

View File

@ -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, "<username>-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, "<username>-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, "<username>-che", 1);
validator.validate(of(PERSIST_VOLUMES_ATTRIBUTE, "false"));
}
@Test
public void shouldBeFineForPersistentMode() throws ValidationException {
AsyncStorageModeValidator validator =
new AsyncStorageModeValidator("common", false, "<username>-che", 1);
validator.validate(of(PERSIST_VOLUMES_ATTRIBUTE, "true"));
}
@Test
public void shouldBeFineForEmptyAttribute() throws ValidationException {
AsyncStorageModeValidator validator =
new AsyncStorageModeValidator("common", false, "<username>-che", 1);
validator.validate(of());
}
@Test
public void shouldBeFineForAsyncMode() throws ValidationException {
AsyncStorageModeValidator validator =
new AsyncStorageModeValidator("common", false, "<username>-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, "<username>-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, "<username>-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, "<username>-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, "<username>-che", 1);
validator.validateUpdate(of(), of(PERSIST_VOLUMES_ATTRIBUTE, "false"));
}
@Test
public void shouldBeFineForPersistentModeUpdate() throws ValidationException {
AsyncStorageModeValidator validator =
new AsyncStorageModeValidator("common", false, "<username>-che", 1);
validator.validateUpdate(of(), of(PERSIST_VOLUMES_ATTRIBUTE, "true"));
}
@Test
public void shouldBeFineForEmptyAttributeUpdate() throws ValidationException {
AsyncStorageModeValidator validator =
new AsyncStorageModeValidator("common", false, "<username>-che", 1);
validator.validateUpdate(of(), of());
}
@Test
public void shouldBeFineForAsyncModeUpdate() throws ValidationException {
AsyncStorageModeValidator validator =
new AsyncStorageModeValidator("common", false, "<username>-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, "<username>-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, "<username>-che", 1);
validator.validateUpdate(
of(PERSIST_VOLUMES_ATTRIBUTE, "true"), of(ASYNC_PERSIST_ATTRIBUTE, "true"));
}
}

View File

@ -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,

View File

@ -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<Pod, DoneablePod> podResource;
@Mock private MixedOperation mixedOperationPod;
@Mock private NonNamespaceOperation namespacePodOperation;
@Mock private FilterWatchListDeletable<Pod, PodList, Boolean, Watch, Watcher<Pod>> 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();
}
}

View File

@ -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<PersistentVolumeClaim, DoneablePersistentVolumeClaim> pvcResource;
@Mock private Resource<ConfigMap, DoneableConfigMap> mapResource;
@Mock private PodResource<Pod, DoneablePod> podResource;
@Mock private ServiceResource<Service, DoneableService> 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<String, String> 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));
}
}

View File

@ -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("");
}
});

View File

@ -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'.
*
* <p>Should be set/read from {@link WorkspaceConfig#getAttributes}.
*
* <p>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}.

View File

@ -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<String, String> 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<ComponentImpl> components, FileContentProvider contentProvider) throws DevfileException {
try {
Map<String, String> 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.

View File

@ -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<ComponentImpl> 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<ComponentImpl> components = devfile.getComponents();
assertEquals(components.size(), 3);
assertTrue(
components.contains(new ComponentImpl(PLUGIN_COMPONENT_TYPE, ASYNC_STORAGE_PLUGIN_REF)));
}
private ComponentImpl findById(List<ComponentImpl> components, String id) {
return components
.stream()