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 66d0012638..10f1f93fcc 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 @@ -46,5 +46,13 @@ public final class Constants { public static final String POD_STATUS_PHASE_FAILED = "Failed"; public static final String POD_STATUS_PHASE_SUCCEEDED = "Succeeded"; + /** DevWorkspace labels and annotations for mounting secrets and configmaps. */ + public static final String DEV_WORKSPACE_MOUNT_LABEL = + "controller.devfile.io/mount-to-devworkspace"; + + public static final String DEV_WORKSPACE_MOUNT_PATH_ANNOTATION = + "controller.devfile.io/mount-path"; + public static final String DEV_WORKSPACE_MOUNT_AS_ANNOTATION = "controller.devfile.io/mount-as"; + private Constants() {} } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java index 3d2d9b7673..f520c54734 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java @@ -48,6 +48,9 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesDev import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.RemoveNamespaceOnWorkspaceRemove; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPreferencesConfigurator; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserProfileConfigurator; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.PerWorkspacePVCStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.UniqueWorkspacePVCStrategy; @@ -98,6 +101,11 @@ public class KubernetesInfraModule extends AbstractModule { workspaceAttributeValidators.addBinding().to(K8sInfraNamespaceWsAttributeValidator.class); workspaceAttributeValidators.addBinding().to(AsyncStorageModeValidator.class); + Multibinder namespaceConfigurators = + Multibinder.newSetBinder(binder(), NamespaceConfigurator.class); + namespaceConfigurators.addBinding().to(UserProfileConfigurator.class); + namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class); + bind(KubernetesNamespaceService.class); MapBinder factories = diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java index a155685d57..e7409b9368 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java @@ -35,6 +35,7 @@ import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.dto.KubernetesNamespaceMetaDto; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.NamespaceProvisioner; /** @author Sergii Leshchenko */ @Tag(name = "kubernetes-namespace", description = "Kubernetes REST API for working with Namespaces") @@ -43,10 +44,13 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesN public class KubernetesNamespaceService extends Service { private final KubernetesNamespaceFactory namespaceFactory; + private final NamespaceProvisioner namespaceProvisioner; @Inject - public KubernetesNamespaceService(KubernetesNamespaceFactory namespaceFactory) { + public KubernetesNamespaceService( + KubernetesNamespaceFactory namespaceFactory, NamespaceProvisioner namespaceProvisioner) { this.namespaceFactory = namespaceFactory; + this.namespaceProvisioner = namespaceProvisioner; } @GET @@ -88,7 +92,7 @@ public class KubernetesNamespaceService extends Service { }) public KubernetesNamespaceMetaDto provision() throws InfrastructureException { return asDto( - namespaceFactory.provision( + namespaceProvisioner.provision( new NamespaceResolutionContext(EnvironmentContext.getCurrent().getSubject()))); } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java index 7871c8a834..3e0e533f36 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java @@ -53,7 +53,6 @@ import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.user.server.PreferenceManager; import org.eclipse.che.api.user.server.UserManager; -import org.eclipse.che.api.workspace.server.model.impl.RuntimeIdentityImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; import org.eclipse.che.commons.annotation.Nullable; @@ -255,7 +254,7 @@ public class KubernetesNamespaceFactory { * @return optional with kubernetes namespace meta * @throws InfrastructureException when any error occurs during namespace fetching */ - protected Optional fetchNamespace(String name) + public Optional fetchNamespace(String name) throws InfrastructureException { try { Namespace namespace = clientFactory.create().namespaces().withName(name).get(); @@ -372,21 +371,6 @@ public class KubernetesNamespaceFactory { return namespace; } - public KubernetesNamespaceMeta provision(NamespaceResolutionContext namespaceResolutionContext) - throws InfrastructureException { - KubernetesNamespace namespace = - getOrCreate( - new RuntimeIdentityImpl( - null, - null, - namespaceResolutionContext.getUserId(), - evaluateNamespaceName(namespaceResolutionContext))); - - return fetchNamespace(namespace.getName()) - .orElseThrow( - () -> new InfrastructureException("Not able to find namespace " + namespace.getName())); - } - public KubernetesNamespace get(RuntimeIdentity identity) throws InfrastructureException { String workspaceId = identity.getWorkspaceId(); String namespaceName = identity.getInfrastructureNamespace(); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java new file mode 100644 index 0000000000..e42e9fb1d4 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2012-2021 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.kubernetes.namespace.configurator; + +import io.fabric8.kubernetes.api.model.Secret; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.NamespaceProvisioner; + +/** + * Configures user's namespace after provisioning in {@link NamespaceProvisioner} with whatever is + * needed. Such as creating user profile and preferences {@link Secret} in user namespace. + * + * @author Pavol Baran + */ +public interface NamespaceConfigurator { + + /** + * Configures user's namespace after provisioning. + * + * @param namespaceResolutionContext users namespace context + * @throws InfrastructureException when any error occurs + */ + public void configure(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException; +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java new file mode 100644 index 0000000000..fb69ac251b --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2012-2021 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.kubernetes.namespace.configurator; + +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_AS_ANNOTATION; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_LABEL; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_PATH_ANNOTATION; + +import com.google.common.annotations.VisibleForTesting; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import javax.inject.Inject; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.user.User; +import org.eclipse.che.api.user.server.PreferenceManager; +import org.eclipse.che.api.user.server.UserManager; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; + +/** + * Creates {@link Secret} with user preferences. This serves as a way for DevWorkspaces to acquire + * information about the user. + * + * @author Pavol Baran + */ +public class UserPreferencesConfigurator implements NamespaceConfigurator { + private static final String USER_PREFERENCES_SECRET_NAME = "user-preferences"; + private static final String USER_PREFERENCES_SECRET_MOUNT_PATH = "/config/user/preferences"; + private static final int PREFERENCE_NAME_MAX_LENGTH = 253; + + private final KubernetesNamespaceFactory namespaceFactory; + private final KubernetesClientFactory clientFactory; + private final UserManager userManager; + private final PreferenceManager preferenceManager; + + @Inject + public UserPreferencesConfigurator( + KubernetesNamespaceFactory namespaceFactory, + KubernetesClientFactory clientFactory, + UserManager userManager, + PreferenceManager preferenceManager) { + this.namespaceFactory = namespaceFactory; + this.clientFactory = clientFactory; + this.userManager = userManager; + this.preferenceManager = preferenceManager; + } + + @Override + public void configure(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + Secret userPreferencesSecret = preparePreferencesSecret(namespaceResolutionContext); + String namespace = namespaceFactory.evaluateNamespaceName(namespaceResolutionContext); + + try { + clientFactory + .create() + .secrets() + .inNamespace(namespace) + .createOrReplace(userPreferencesSecret); + } catch (KubernetesClientException e) { + throw new InfrastructureException( + "Error occurred while trying to create user preferences secret.", e); + } + } + + private Secret preparePreferencesSecret(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + Base64.Encoder enc = Base64.getEncoder(); + User user; + Map preferences; + + try { + user = userManager.getById(namespaceResolutionContext.getUserId()); + preferences = preferenceManager.find(user.getId()); + } catch (NotFoundException | ServerException e) { + throw new InfrastructureException( + String.format( + "Preferences of user with id:%s cannot be retrieved.", + namespaceResolutionContext.getUserId()), + e); + } + + if (preferences == null || preferences.isEmpty()) { + throw new InfrastructureException( + String.format( + "Preferences of user with id:%s are empty. Cannot create user preferences secrets.", + namespaceResolutionContext.getUserId())); + } + + Map preferencesEncoded = new HashMap<>(); + preferences.forEach( + (key, value) -> + preferencesEncoded.put( + normalizePreferenceName(key), enc.encodeToString(value.getBytes()))); + return new SecretBuilder() + .addToData(preferencesEncoded) + .withNewMetadata() + .withName(USER_PREFERENCES_SECRET_NAME) + .addToLabels(DEV_WORKSPACE_MOUNT_LABEL, "true") + .addToAnnotations(DEV_WORKSPACE_MOUNT_AS_ANNOTATION, "file") + .addToAnnotations(DEV_WORKSPACE_MOUNT_PATH_ANNOTATION, USER_PREFERENCES_SECRET_MOUNT_PATH) + .endMetadata() + .build(); + } + + /** + * Some preferences names are not compatible with k8s restrictions on key field in secret. The + * keys of data must consist of alphanumeric characters, -, _ or . This method replaces illegal + * characters with - + * + * @param name original preference name + * @return k8s compatible preference name used as a key field in Secret + */ + @VisibleForTesting + String normalizePreferenceName(String name) { + name = name.replaceAll("[^-._a-zA-Z0-9]+", "-").replaceAll("-+", "-"); + return name.substring(0, Math.min(name.length(), PREFERENCE_NAME_MAX_LENGTH)); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java new file mode 100644 index 0000000000..ecfe5a2845 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012-2021 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.kubernetes.namespace.configurator; + +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_AS_ANNOTATION; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_LABEL; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_PATH_ANNOTATION; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import javax.inject.Inject; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.user.User; +import org.eclipse.che.api.user.server.UserManager; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; + +/** + * Creates {@link Secret} with user profile information such as his id, name and email. This serves + * as a way for DevWorkspaces to acquire information about the user. + * + * @author Pavol Baran + */ +public class UserProfileConfigurator implements NamespaceConfigurator { + private static final String USER_PROFILE_SECRET_NAME = "user-profile"; + private static final String USER_PROFILE_SECRET_MOUNT_PATH = "/config/user/profile"; + + private final KubernetesNamespaceFactory namespaceFactory; + private final KubernetesClientFactory clientFactory; + private final UserManager userManager; + + @Inject + public UserProfileConfigurator( + KubernetesNamespaceFactory namespaceFactory, + KubernetesClientFactory clientFactory, + UserManager userManager) { + this.namespaceFactory = namespaceFactory; + this.clientFactory = clientFactory; + this.userManager = userManager; + } + + @Override + public void configure(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + Secret userProfileSecret = prepareProfileSecret(namespaceResolutionContext); + String namespace = namespaceFactory.evaluateNamespaceName(namespaceResolutionContext); + try { + clientFactory.create().secrets().inNamespace(namespace).createOrReplace(userProfileSecret); + } catch (KubernetesClientException e) { + throw new InfrastructureException( + "Error occurred while trying to create user profile secret.", e); + } + } + + private Secret prepareProfileSecret(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + User user; + try { + user = userManager.getById(namespaceResolutionContext.getUserId()); + } catch (NotFoundException | ServerException e) { + throw new InfrastructureException( + String.format( + "Could not find current user with id:%s.", namespaceResolutionContext.getUserId()), + e); + } + + Base64.Encoder enc = Base64.getEncoder(); + final Map userProfileData = new HashMap<>(); + userProfileData.put("id", enc.encodeToString(user.getId().getBytes())); + userProfileData.put("name", enc.encodeToString(user.getName().getBytes())); + userProfileData.put("email", enc.encodeToString(user.getEmail().getBytes())); + + return new SecretBuilder() + .addToData(userProfileData) + .withNewMetadata() + .withName(USER_PROFILE_SECRET_NAME) + .addToLabels(DEV_WORKSPACE_MOUNT_LABEL, "true") + .addToAnnotations(DEV_WORKSPACE_MOUNT_AS_ANNOTATION, "file") + .addToAnnotations(DEV_WORKSPACE_MOUNT_PATH_ANNOTATION, USER_PROFILE_SECRET_MOUNT_PATH) + .endMetadata() + .build(); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java new file mode 100644 index 0000000000..eb96ea011a --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012-2021 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.kubernetes.provision; + +import io.fabric8.kubernetes.api.model.Namespace; +import java.util.Set; +import javax.inject.Inject; +import org.eclipse.che.api.workspace.server.model.impl.RuntimeIdentityImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; + +/** + * Provisions the k8s {@link Namespace}. After provisioning, configures the namespace through {@link + * NamespaceConfigurator}. + * + * @author Pavol Baran + */ +public class NamespaceProvisioner { + private final KubernetesNamespaceFactory namespaceFactory; + private final Set namespaceConfigurators; + + @Inject + public NamespaceProvisioner( + KubernetesNamespaceFactory namespaceFactory, + Set namespaceConfigurators) { + this.namespaceFactory = namespaceFactory; + this.namespaceConfigurators = namespaceConfigurators; + } + + /** Tests for this method are in KubernetesFactoryTest. */ + public KubernetesNamespaceMeta provision(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + KubernetesNamespace namespace = + namespaceFactory.getOrCreate( + new RuntimeIdentityImpl( + null, + null, + namespaceResolutionContext.getUserId(), + namespaceFactory.evaluateNamespaceName(namespaceResolutionContext))); + + KubernetesNamespaceMeta namespaceMeta = + namespaceFactory + .fetchNamespace(namespace.getName()) + .orElseThrow( + () -> + new InfrastructureException( + "Not able to find namespace " + namespace.getName())); + configureNamespace(namespaceResolutionContext); + return namespaceMeta; + } + + private void configureNamespace(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + for (NamespaceConfigurator configurator : namespaceConfigurators) { + configurator.configure(namespaceResolutionContext); + } + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java index 9336f742da..1ed81fd830 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java @@ -35,6 +35,7 @@ import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.dto.KubernetesNamespaceMetaDto; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.NamespaceProvisioner; import org.everrest.assured.EverrestJetty; import org.everrest.core.Filter; import org.everrest.core.GenericContainerRequest; @@ -67,6 +68,7 @@ public class KubernetesNamespaceServiceTest { private CheJsonProvider jsonProvider = new CheJsonProvider(Collections.emptySet()); @Mock private KubernetesNamespaceFactory namespaceFactory; + @Mock private NamespaceProvisioner namespaceProvisioner; @InjectMocks private KubernetesNamespaceService service; @@ -98,7 +100,7 @@ public class KubernetesNamespaceServiceTest { KubernetesNamespaceMetaImpl namespaceMeta = new KubernetesNamespaceMetaImpl( "ws-namespace", ImmutableMap.of("phase", "active", "default", "true")); - when(namespaceFactory.provision(any(NamespaceResolutionContext.class))) + when(namespaceProvisioner.provision(any(NamespaceResolutionContext.class))) .thenReturn(namespaceMeta); // when final Response response = @@ -113,7 +115,7 @@ public class KubernetesNamespaceServiceTest { KubernetesNamespaceMetaDto actual = unwrapDto(response, KubernetesNamespaceMetaDto.class); assertEquals(actual.getName(), namespaceMeta.getName()); assertEquals(actual.getAttributes(), namespaceMeta.getAttributes()); - verify(namespaceFactory).provision(any(NamespaceResolutionContext.class)); + verify(namespaceProvisioner).provision(any(NamespaceResolutionContext.class)); } @Test @@ -122,7 +124,7 @@ public class KubernetesNamespaceServiceTest { KubernetesNamespaceMetaImpl namespaceMeta = new KubernetesNamespaceMetaImpl( "ws-namespace", ImmutableMap.of("phase", "active", "default", "true")); - when(namespaceFactory.provision(any(NamespaceResolutionContext.class))) + when(namespaceProvisioner.provision(any(NamespaceResolutionContext.class))) .thenReturn(namespaceMeta); // when final Response response = @@ -136,7 +138,7 @@ public class KubernetesNamespaceServiceTest { assertEquals(response.getStatusCode(), 200); ArgumentCaptor captor = ArgumentCaptor.forClass(NamespaceResolutionContext.class); - verify(namespaceFactory).provision(captor.capture()); + verify(namespaceProvisioner).provision(captor.capture()); NamespaceResolutionContext actualContext = captor.getValue(); assertEquals(actualContext.getUserId(), SUBJECT.getUserId()); assertEquals(actualContext.getUserName(), SUBJECT.getUserName()); diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java index e51309fa2a..47ed7f0fb6 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java @@ -12,6 +12,7 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE; import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; @@ -86,6 +87,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesCl import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.NamespaceProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -1248,7 +1250,7 @@ public class KubernetesNamespaceFactoryTest { // when NamespaceResolutionContext context = new NamespaceResolutionContext("workspace123", "user123", "jondoe"); - KubernetesNamespaceMeta actual = namespaceFactory.provision(context); + KubernetesNamespaceMeta actual = testProvisioning(context); // then assertEquals(actual.getName(), "jondoe-che"); @@ -1288,7 +1290,7 @@ public class KubernetesNamespaceFactoryTest { // when NamespaceResolutionContext context = new NamespaceResolutionContext("workspace123", "user123", "jondoe"); - namespaceFactory.provision(context); + testProvisioning(context); // then fail("should not reach this point since exception has to be thrown"); @@ -1329,7 +1331,8 @@ public class KubernetesNamespaceFactoryTest { // when NamespaceResolutionContext context = new NamespaceResolutionContext("workspace123", "user123", "jondoe"); - namespaceFactory.provision(context); + + testProvisioning(context); // then fail("should not reach this point since exception has to be thrown"); @@ -1535,4 +1538,9 @@ public class KubernetesNamespaceFactoryTest { .endStatus() .build(); } + + private KubernetesNamespaceMeta testProvisioning(NamespaceResolutionContext context) + throws InfrastructureException { + return new NamespaceProvisioner(namespaceFactory, emptySet()).provision(context); + } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java new file mode 100644 index 0000000000..8bf7f86989 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2012-2021 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.kubernetes.namespace.configurator; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.user.server.PreferenceManager; +import org.eclipse.che.api.user.server.UserManager; +import org.eclipse.che.api.user.server.model.impl.UserImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests for {@link UserPreferencesConfigurator}. + * + * @author Pavol Baran + */ +@Listeners(MockitoTestNGListener.class) +public class UserPreferencesConfiguratorTest { + + private static final String USER_ID = "user-id"; + private static final String USER_NAME = "user-name"; + private static final String USER_EMAIL = "user-email"; + private static final String USER_NAMESPACE = "user-namespace"; + + @Mock private KubernetesNamespaceFactory namespaceFactory; + @Mock private KubernetesClientFactory clientFactory; + @Mock private UserManager userManager; + @Mock private PreferenceManager preferenceManager; + + @InjectMocks private UserPreferencesConfigurator userPreferencesConfigurator; + + private KubernetesServer kubernetesServer; + private NamespaceResolutionContext context; + + @BeforeMethod + public void setUp() throws InfrastructureException, NotFoundException, ServerException { + context = new NamespaceResolutionContext(null, USER_ID, USER_NAME); + kubernetesServer = new KubernetesServer(true, true); + kubernetesServer.before(); + + Map preferences = new HashMap<>(); + preferences.put("preference-name", "preference"); + + lenient() + .when(userManager.getById(USER_ID)) + .thenReturn(new UserImpl(USER_ID, USER_EMAIL, USER_NAME)); + lenient().when(namespaceFactory.evaluateNamespaceName(any())).thenReturn(USER_NAMESPACE); + lenient().when(clientFactory.create()).thenReturn(kubernetesServer.getClient()); + lenient().when(preferenceManager.find(USER_ID)).thenReturn(preferences); + } + + @AfterMethod + public void cleanUp() { + kubernetesServer.after(); + } + + @Test + public void shouldCreatePreferencesSecret() throws InfrastructureException { + userPreferencesConfigurator.configure(context); + List secrets = + kubernetesServer.getClient().secrets().inNamespace(USER_NAMESPACE).list().getItems(); + assertEquals(secrets.size(), 1); + assertEquals(secrets.get(0).getMetadata().getName(), "user-preferences"); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Preferences of user with id:" + USER_ID + " cannot be retrieved.") + public void shouldNotCreateSecretOnException() throws ServerException, InfrastructureException { + when(preferenceManager.find(USER_ID)).thenThrow(new ServerException("test exception")); + userPreferencesConfigurator.configure(context); + fail("InfrastructureException should have been thrown."); + } + + @Test + public void shouldNormalizePreferenceName() { + assertEquals( + userPreferencesConfigurator.normalizePreferenceName("codename:bond"), "codename-bond"); + assertEquals(userPreferencesConfigurator.normalizePreferenceName("some--:pref"), "some-pref"); + assertEquals( + userPreferencesConfigurator.normalizePreferenceName("pref[name].sub"), "pref-name-.sub"); + } + + @Test + public void shouldKeepPreferenceName() { + assertEquals( + userPreferencesConfigurator.normalizePreferenceName("codename.bond"), "codename.bond"); + assertEquals(userPreferencesConfigurator.normalizePreferenceName("pref_name"), "pref_name"); + assertEquals( + userPreferencesConfigurator.normalizePreferenceName("some-name.over_rainbow"), + "some-name.over_rainbow"); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java new file mode 100644 index 0000000000..33d175742f --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012-2021 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.kubernetes.namespace.configurator; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import java.util.List; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.user.server.UserManager; +import org.eclipse.che.api.user.server.model.impl.UserImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests for {@link UserProfileConfigurator}. + * + * @author Pavol Baran + */ +@Listeners(MockitoTestNGListener.class) +public class UserProfileConfiguratorTest { + private static final String USER_ID = "user-id"; + private static final String USER_NAME = "user-name"; + private static final String USER_EMAIL = "user-email"; + private static final String USER_NAMESPACE = "user-namespace"; + + @Mock private KubernetesNamespaceFactory namespaceFactory; + @Mock private KubernetesClientFactory clientFactory; + @Mock private UserManager userManager; + + @InjectMocks private UserProfileConfigurator userProfileConfigurator; + + private KubernetesServer kubernetesServer; + private NamespaceResolutionContext context; + + @BeforeMethod + public void setUp() throws InfrastructureException, NotFoundException, ServerException { + context = new NamespaceResolutionContext(null, USER_ID, USER_NAME); + kubernetesServer = new KubernetesServer(true, true); + kubernetesServer.before(); + + when(userManager.getById(USER_ID)).thenReturn(new UserImpl(USER_ID, USER_EMAIL, USER_NAME)); + when(namespaceFactory.evaluateNamespaceName(any())).thenReturn(USER_NAMESPACE); + when(clientFactory.create()).thenReturn(kubernetesServer.getClient()); + } + + @AfterMethod + public void cleanUp() { + kubernetesServer.after(); + } + + @Test + public void shouldCreateProfileSecret() throws InfrastructureException { + userProfileConfigurator.configure(context); + List secrets = + kubernetesServer.getClient().secrets().inNamespace(USER_NAMESPACE).list().getItems(); + assertEquals(secrets.size(), 1); + assertEquals(secrets.get(0).getMetadata().getName(), "user-profile"); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = "Could not find current user with id:" + USER_ID + ".") + public void shouldNotCreateSecretOnException() + throws NotFoundException, ServerException, InfrastructureException { + when(userManager.getById(USER_ID)).thenThrow(new ServerException("test exception")); + userProfileConfigurator.configure(context); + fail("InfrastructureException should have been thrown."); + } +} 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 2610d524e9..0bfd6c80f5 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 @@ -53,6 +53,9 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesDev import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPreferencesConfigurator; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserProfileConfigurator; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.PerWorkspacePVCStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.UniqueWorkspacePVCStrategy; @@ -110,6 +113,11 @@ public class OpenShiftInfraModule extends AbstractModule { workspaceAttributeValidators.addBinding().to(K8sInfraNamespaceWsAttributeValidator.class); workspaceAttributeValidators.addBinding().to(AsyncStorageModeValidator.class); + Multibinder namespaceConfigurators = + Multibinder.newSetBinder(binder(), NamespaceConfigurator.class); + namespaceConfigurators.addBinding().to(UserProfileConfigurator.class); + namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class); + bind(KubernetesNamespaceService.class); MapBinder factories = diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java index 220f633075..547f55c02a 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java @@ -207,7 +207,7 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { } @Override - protected Optional fetchNamespace(String name) + public Optional fetchNamespace(String name) throws InfrastructureException { return fetchNamespaceObject(name).map(this::asNamespaceMeta); }