From 82bf4a6495878143eec374511ff0038b0abfbde9 Mon Sep 17 00:00:00 2001 From: Sergii Leshchenko Date: Fri, 27 Sep 2019 11:24:21 +0300 Subject: [PATCH 1/3] Refactor IdentityProviderConfigFactory Signed-off-by: Sergii Leshchenko --- .../oauth/IdentityProviderConfigFactory.java | 168 +++++++++++------- ...=> IdentityProviderConfigFactoryTest.java} | 19 +- 2 files changed, 110 insertions(+), 77 deletions(-) rename infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/{IdentityProviderConfigBuilderTest.java => IdentityProviderConfigFactoryTest.java} (92%) diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java index 38c533b3687..240a85a5899 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java @@ -19,10 +19,14 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.openshift.client.OpenShiftConfig; import io.fabric8.openshift.client.OpenShiftConfigBuilder; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; import java.util.Optional; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +import javax.ws.rs.core.UriBuilder; import org.eclipse.che.api.core.BadRequestException; import org.eclipse.che.api.core.UnauthorizedException; import org.eclipse.che.api.workspace.server.WorkspaceRuntimes; @@ -60,12 +64,9 @@ public class IdentityProviderConfigFactory extends OpenShiftClientConfigFactory private final String oauthIdentityProvider; private final KeycloakServiceClient keycloakServiceClient; - private final KeycloakSettings keycloakSettings; private final Provider workspaceRuntimeProvider; private final String messageToLinkAccount; - private String rootUrl; - @Inject public IdentityProviderConfigFactory( KeycloakServiceClient keycloakServiceClient, @@ -73,23 +74,9 @@ public IdentityProviderConfigFactory( Provider workspaceRuntimeProvider, @Nullable @Named("che.infra.openshift.oauth_identity_provider") String oauthIdentityProvider, @Named("che.api") String apiEndpoint) { - super(); this.keycloakServiceClient = keycloakServiceClient; - this.keycloakSettings = keycloakSettings; this.workspaceRuntimeProvider = workspaceRuntimeProvider; - this.oauthIdentityProvider = oauthIdentityProvider; - rootUrl = apiEndpoint; - if (rootUrl.endsWith("/")) { - rootUrl = rootUrl.substring(0, rootUrl.length() - 1); - } - if (rootUrl.endsWith("/api")) { - rootUrl = rootUrl.substring(0, rootUrl.length() - 4); - } - - String referrer_uri = - rootUrl.replace("http://", "http%3A%2F%2F").replace("https://", "https%3A%2F%2F") - + "%2Fdashboard%2F?redirect_fragment%3D%2Fworkspaces"; messageToLinkAccount = "You should link your account with the " @@ -103,70 +90,117 @@ public IdentityProviderConfigFactory( + "/account/identity?referrer=" + keycloakSettings.get().get(CLIENT_ID_SETTING) + "&referrer_uri=" - + referrer_uri + + buildReferrerURI(apiEndpoint) + "' target='_blank' rel='noopener noreferrer'>Federated Identities page of your Che account"; } + private String buildReferrerURI(String apiEndpoint) { + URI referrerURI = + UriBuilder.fromUri(apiEndpoint) + .replacePath("dashboard/") + .queryParam("redirect_fragment", "/workspaces") + .build(); + try { + return URLEncoder.encode(referrerURI.toString(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException( + "Error occurred during constructing Referrer URI. " + e.getMessage(), e); + } + } + /** - * Builds the Openshift {@link Config} object based on a default {@link Config} object and an + * Builds the OpenShift {@link Config} object based on a default {@link Config} object and an * optional workspace Id. */ public Config buildConfig(Config defaultConfig, @Nullable String workspaceId) throws InfrastructureException { Subject subject = EnvironmentContext.getCurrent().getSubject(); - String workspaceOwnerId = null; - if (workspaceId != null) { - @SuppressWarnings("rawtypes") - Optional context = - workspaceRuntimeProvider.get().getRuntimeContext(workspaceId); - workspaceOwnerId = context.map(c -> c.getIdentity().getOwnerId()).orElse(null); + if (oauthIdentityProvider == null) { + LOG.debug("OAuth Provider is not configured, default config is used."); + return defaultConfig; } - if (oauthIdentityProvider != null - && subject != Subject.ANONYMOUS - && (workspaceOwnerId == null || subject.getUserId().equals(workspaceOwnerId))) { - try { - KeycloakTokenResponse keycloakTokenInfos = - keycloakServiceClient.getIdentityProviderToken(oauthIdentityProvider); - if ("user:full".equals(keycloakTokenInfos.getScope())) { - return new OpenShiftConfigBuilder(OpenShiftConfig.wrap(defaultConfig)) - .withOauthToken(keycloakTokenInfos.getAccessToken()) - .build(); - } else { - throw new InfrastructureException( - "Cannot retrieve user Openshift token: '" - + oauthIdentityProvider - + "' identity provider is not granted full rights: " - + oauthIdentityProvider); - } - } catch (UnauthorizedException e) { - LOG.error("cannot retrieve User Openshift token from the identity provider", e); - - throw new InfrastructureException(messageToLinkAccount); - } catch (BadRequestException e) { - LOG.error( - "cannot retrieve User Openshift token from the '" - + oauthIdentityProvider - + "' identity provider", - e); - if (e.getMessage().endsWith("Invalid token.")) { - throw new InfrastructureException( - "Your session has expired. \nPlease " - + "" - + "login" - + " to Che again to get access to your Openshift account"); - } - throw new InfrastructureException(e.getMessage(), e); - } catch (Exception e) { - LOG.error( - "cannot retrieve User Openshift token from the '" + if (subject == Subject.ANONYMOUS) { + LOG.debug( + "OAuth Provider is configured but default subject is anonymous, default config is used."); + return defaultConfig; + } + + if (workspaceId == null) { + LOG.debug( + "OAuth Provider is configured and this request is not related to any workspace. OAuth token will be retrieved."); + return personalizeConfig(defaultConfig); + } + + Optional context = + workspaceRuntimeProvider.get().getRuntimeContext(workspaceId); + if (!context.isPresent()) { + // there is no cached info for this workspace in workspace API. + // it means that it's not started yet and it's initial call for preparing context + LOG.debug( + "There is no runtime context for the specified workspace '%s'. It's the first workspace " + + "related call, so context is personalized with OAuth token."); + return personalizeConfig(defaultConfig); + } + String workspaceOwnerId = context.map(c -> c.getIdentity().getOwnerId()).orElse(null); + + boolean isRuntimeOwner = subject.getUserId().equals(workspaceOwnerId); + + if (!isRuntimeOwner) { + LOG.debug( + "OAuth Provider is configured, but current subject is not runtime owner, default config is used." + + "Subject user id: '{}'. Runtime owner id: '{}'", + subject.getUserId(), + workspaceOwnerId); + return defaultConfig; + } + + LOG.debug( + "OAuth Provider is configured and current subject is runtime owner. OAuth token will be retrieved."); + return personalizeConfig(defaultConfig); + } + + private Config personalizeConfig(Config defaultConfig) throws InfrastructureException { + try { + KeycloakTokenResponse keycloakTokenInfos = + keycloakServiceClient.getIdentityProviderToken(oauthIdentityProvider); + if ("user:full".equals(keycloakTokenInfos.getScope())) { + return new OpenShiftConfigBuilder(OpenShiftConfig.wrap(defaultConfig)) + .withOauthToken(keycloakTokenInfos.getAccessToken()) + .build(); + } else { + throw new InfrastructureException( + "Cannot retrieve user OpenShift token: '" + oauthIdentityProvider - + "' identity provider", - e); - throw new InfrastructureException(e.getMessage(), e); + + "' identity provider is not granted full rights: " + + oauthIdentityProvider); + } + } catch (UnauthorizedException e) { + LOG.error("Cannot retrieve User OpenShift token from the identity provider", e); + + throw new InfrastructureException(messageToLinkAccount); + } catch (BadRequestException e) { + LOG.error( + "Cannot retrieve User OpenShift token from the '" + + oauthIdentityProvider + + "' identity provider", + e); + if (e.getMessage().endsWith("Invalid token.")) { + throw new InfrastructureException( + "Your session has expired. \nPlease " + + "" + + "login" + + " to Che again to get access to your OpenShift account"); } + throw new InfrastructureException(e.getMessage(), e); + } catch (Exception e) { + LOG.error( + "Cannot retrieve User OpenShift token from the '" + + oauthIdentityProvider + + "' identity provider", + e); + throw new InfrastructureException(e.getMessage(), e); } - return defaultConfig; } } diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigBuilderTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactoryTest.java similarity index 92% rename from infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigBuilderTest.java rename to infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactoryTest.java index a8359d2d607..ae469c97153 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigBuilderTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactoryTest.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertSame; import static org.testng.Assert.fail; import com.google.inject.Provider; @@ -49,7 +49,7 @@ /** @author David Festal */ @Listeners(MockitoTestNGListener.class) -public class IdentityProviderConfigBuilderTest { +public class IdentityProviderConfigFactoryTest { private static final String PROVIDER = "openshift-v3"; private static final String THE_USER_ID = "a_user_id"; private static final String ANOTHER_USER_ID = "another_user_id"; @@ -73,14 +73,14 @@ public class IdentityProviderConfigBuilderTest { + "/account/identity?referrer=" + CLIENT_ID + "&referrer_uri=" - + "http%3A%2F%2Fche-host%2Fdashboard%2F?redirect_fragment%3D%2Fworkspaces" - + "' target='_blank' rel='noopener noreferrer'>Federated Identities page of your Che account"; + + "http%3A%2F%2Fche-host%2Fdashboard%2F%3Fredirect_fragment%3D%2Fworkspaces'" + + " target='_blank' rel='noopener noreferrer'>Federated Identities page of your Che account"; private static final String SESSION_EXPIRED_MESSAGE = "Your session has expired. \nPlease " + "" + "login" - + " to Che again to get access to your Openshift account"; + + " to Che again to get access to your OpenShift account"; private static final Map keycloakSettingsMap = new HashMap(); @@ -138,14 +138,14 @@ public void testFallbackToDefaultConfigWhenProvideIsNull() throws Exception { configBuilder = new IdentityProviderConfigFactory( keycloakServiceClient, keycloakSettings, workspaceRuntimeProvider, null, API_ENDPOINT); - assertTrue(defaultConfig == configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID)); + assertSame(defaultConfig, configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID)); } @Test public void testFallbackToDefaultConfigWhenSubjectIsAnonymous() throws Exception { when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse); doReturn(Subject.ANONYMOUS).when(context).getSubject(); - assertTrue(defaultConfig == configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID)); + assertSame(defaultConfig, configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID)); } @Test @@ -153,15 +153,14 @@ public void testFallbackToDefaultConfigWhenCurrentUserIsDifferentFromWorkspaceOw throws Exception { when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse); when(runtimeIdentity.getOwnerId()).thenReturn(ANOTHER_USER_ID); - assertTrue(defaultConfig == configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID)); + assertSame(defaultConfig, configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID)); } @SuppressWarnings("rawtypes") @Test public void testCreateUserConfigWhenNoRuntimeContext() throws Exception { when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse); - when(workspaceRuntimes.getRuntimeContext(anyString())) - .thenReturn(Optional.empty()); + when(workspaceRuntimes.getRuntimeContext(anyString())).thenReturn(Optional.empty()); Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID); assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN); From 9353991fac673815264c20426945b8d522eeb104 Mon Sep 17 00:00:00 2001 From: Sergii Leshchenko Date: Fri, 27 Sep 2019 11:27:52 +0300 Subject: [PATCH 2/3] Add an ability to check if OpenShift config is personalized Signed-off-by: Sergii Leshchenko --- .../openshift/OpenShiftClientConfigFactory.java | 8 ++++++++ .../multiuser/oauth/IdentityProviderConfigFactory.java | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientConfigFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientConfigFactory.java index db07c8d9578..1a623c315d6 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientConfigFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientConfigFactory.java @@ -33,4 +33,12 @@ public Config buildConfig(Config defaultConfig, @Nullable String workspaceId) throws InfrastructureException { return defaultConfig; } + + /** + * Returns true if implementation personalizes config to the current subject, otherwise returns + * false if default config is always used. + */ + public boolean isPersonalized() { + return false; + } } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java index 240a85a5899..2d741e44186 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/IdentityProviderConfigFactory.java @@ -108,6 +108,12 @@ private String buildReferrerURI(String apiEndpoint) { } } + @Override + public boolean isPersonalized() { + // config is personalized only if OAuth is configured and the current user is not anonymous + return oauthIdentityProvider != null; + } + /** * Builds the OpenShift {@link Config} object based on a default {@link Config} object and an * optional workspace Id. From feb18ac7fc7e4c997483d0a86afd15eb4010f22e Mon Sep 17 00:00:00 2001 From: Sergii Leshchenko Date: Fri, 27 Sep 2019 11:28:33 +0300 Subject: [PATCH 3/3] Added an ability to list namespace available to ws creation Signed-off-by: Sergii Leshchenko --- .../webapp/WEB-INF/classes/che/che.properties | 18 ++ infrastructures/kubernetes/pom.xml | 90 ++++++ .../kubernetes/KubernetesInfraModule.java | 3 + .../server/KubernetesNamespaceService.java | 71 +++++ .../impls/KubernetesNamespaceMetaImpl.java | 89 ++++++ .../api/shared/KubernetesNamespaceMeta.java | 45 +++ .../dto/KubernetesNamespaceMetaDto.java | 34 ++ .../namespace/KubernetesNamespaceFactory.java | 186 ++++++++++- .../KubernetesNamespaceServiceTest.java | 79 +++++ .../KubernetesNamespaceFactoryTest.java | 247 +++++++++++++- infrastructures/openshift/pom.xml | 8 + .../infrastructure/openshift/Constants.java | 47 +++ .../openshift/OpenShiftInfraModule.java | 3 + .../project/OpenShiftProjectFactory.java | 90 +++++- .../project/OpenShiftProjectFactoryTest.java | 301 +++++++++++++++++- 15 files changed, 1277 insertions(+), 34 deletions(-) create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/impls/KubernetesNamespaceMetaImpl.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/KubernetesNamespaceMeta.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/dto/KubernetesNamespaceMetaDto.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/Constants.java diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index dbffa35cf1d..aac01f0f03e 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties @@ -386,6 +386,24 @@ che.infra.kubernetes.ingress.domain= # Ignored for OpenShift infra. Use `che.infra.openshift.project` instead che.infra.kubernetes.namespace= +# Defines Kubernetes default namespace in which user's workspaces are created +# if user does not override it. +# It's possible to use and placeholders (e.g.: che-workspace-). +# In that case, new namespace will be created for each user. +# Is used by OpenShift infra as well to specify Project +# +# BETA It's not fully supported by infra. +# Use che.infra.kubernetes.namespace to configure workspaces' namespace +che.infra.kubernetes.namespace.default=-che + +# Defines if a user is able to specify Kubernetes namespace different from default. +# It's NOT RECOMMENDED to configured true without OAuth configured. +# Is used by OpenShift infra as well to allows users choose Project +# +# BETA It's not fully supported by infra. +# Use che.infra.kubernetes.namespace to configure workspaces' namespace +che.infra.kubernetes.namespace.allow_user_defined=false + # Defines Kubernetes Service Account name which should be specified to be bound to all workspaces pods. # Note that Kubernetes Infrastructure won't create the service account and it should exist. # OpenShift infrastructure will check if project is predefined(if `che.infra.openshift.project` is not empty): diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml index 0a813fa6cc3..f6c8337f5c8 100644 --- a/infrastructures/kubernetes/pom.xml +++ b/infrastructures/kubernetes/pom.xml @@ -22,6 +22,9 @@ infrastructure-kubernetes Infrastructure :: Kubernetes + + ${project.build.directory}/generated-sources/dto/ + com.fasterxml.jackson.core @@ -79,6 +82,10 @@ io.opentracing opentracing-api + + io.swagger + swagger-annotations + javax.annotation javax.annotation-api @@ -185,6 +192,11 @@ h2 test + + com.jayway.restassured + rest-assured + test + org.eclipse.che.core che-core-api-account @@ -215,6 +227,11 @@ org.eclipse.persistence.jpa test + + org.everrest + everrest-assured + test + org.flywaydb flyway-core @@ -243,6 +260,79 @@ + + org.eclipse.che.core + che-core-api-dto-maven-plugin + ${project.version} + + + process-sources + + generate + + + + + + org.eclipse.che.infrastructure + infrastructure-kubernetes + ${project.version} + + + + + org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.dto + + ${dto-generator-out-directory} + org.eclipse.che.workspace.infrastructure.kubernetes.api.server.dto.DtoServerImpls + server + + + + maven-compiler-plugin + + + pre-compile + generate-sources + + compile + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-resource + process-sources + + add-resource + + + + + ${dto-generator-out-directory}/META-INF + META-INF + + + + + + add-source + process-sources + + add-source + + + + ${dto-generator-out-directory} + + + + + org.apache.maven.plugins 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 7c42d9f2f3c..62a227b261e 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 @@ -40,6 +40,7 @@ import org.eclipse.che.api.workspace.shared.Constants; import org.eclipse.che.workspace.infrastructure.docker.environment.dockerimage.DockerImageEnvironment; import org.eclipse.che.workspace.infrastructure.docker.environment.dockerimage.DockerImageEnvironmentFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.KubernetesNamespaceService; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.jpa.JpaKubernetesRuntimeCacheModule; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.DockerimageComponentToWorkspaceApplier; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesComponentToWorkspaceApplier; @@ -78,6 +79,8 @@ public class KubernetesInfraModule extends AbstractModule { @Override protected void configure() { + bind(KubernetesNamespaceService.class); + MapBinder factories = MapBinder.newMapBinder(binder(), String.class, InternalEnvironmentFactory.class); 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 new file mode 100644 index 00000000000..77b3ed0bd59 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java @@ -0,0 +1,71 @@ +/* + * 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.kubernetes.api.server; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import com.google.common.annotations.Beta; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.util.List; +import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import org.eclipse.che.api.core.rest.Service; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +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; + +/** @author Sergii Leshchenko */ +@Api( + value = "kubernetes-namespace", + description = "Kubernetes REST API for working with Namespaces") +@Path("/kubernetes/namespace") +@Beta +public class KubernetesNamespaceService extends Service { + + private final KubernetesNamespaceFactory namespaceFactory; + + @Inject + public KubernetesNamespaceService(KubernetesNamespaceFactory namespaceFactory) { + this.namespaceFactory = namespaceFactory; + } + + @GET + @Produces(APPLICATION_JSON) + @ApiOperation( + value = "Get k8s namespaces where user is able to create workspaces", + notes = + "This operation can be performed only by authorized user." + + "This is under beta and may be significant changed", + response = String.class, + responseContainer = "List") + @ApiResponses({ + @ApiResponse(code = 200, message = "The namespaces successfully fetched"), + @ApiResponse(code = 500, message = "Internal server error occurred during namespaces fetching") + }) + public List getNamespaces() throws InfrastructureException { + return namespaceFactory.list().stream().map(this::asDto).collect(Collectors.toList()); + } + + private KubernetesNamespaceMetaDto asDto(KubernetesNamespaceMeta kubernetesNamespaceMeta) { + return DtoFactory.newDto(KubernetesNamespaceMetaDto.class) + .withName(kubernetesNamespaceMeta.getName()) + .withAttributes(kubernetesNamespaceMeta.getAttributes()); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/impls/KubernetesNamespaceMetaImpl.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/impls/KubernetesNamespaceMetaImpl.java new file mode 100644 index 00000000000..60b985193f7 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/impls/KubernetesNamespaceMetaImpl.java @@ -0,0 +1,89 @@ +/* + * 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.kubernetes.api.server.impls; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; + +/** @author Sergii Leshchenko */ +public class KubernetesNamespaceMetaImpl implements KubernetesNamespaceMeta { + + private String name; + private Map attributes; + + public KubernetesNamespaceMetaImpl(String name) { + this.name = name; + } + + public KubernetesNamespaceMetaImpl(String name, Map attributes) { + this.name = name; + if (attributes != null) { + this.attributes = new HashMap<>(attributes); + } + } + + public KubernetesNamespaceMetaImpl(KubernetesNamespaceMeta namespaceMeta) { + this(namespaceMeta.getName(), namespaceMeta.getAttributes()); + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public Map getAttributes() { + if (attributes == null) { + attributes = new HashMap<>(); + } + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof KubernetesNamespaceMetaImpl)) { + return false; + } + KubernetesNamespaceMetaImpl that = (KubernetesNamespaceMetaImpl) o; + return Objects.equals(getName(), that.getName()) + && Objects.equals(getAttributes(), that.getAttributes()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getAttributes()); + } + + @Override + public String toString() { + return "KubernetesNamespaceMetaImpl{" + + "name='" + + name + + '\'' + + ", attributes=" + + attributes + + '}'; + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/KubernetesNamespaceMeta.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/KubernetesNamespaceMeta.java new file mode 100644 index 00000000000..2c5bb35d6c9 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/KubernetesNamespaceMeta.java @@ -0,0 +1,45 @@ +/* + * 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.kubernetes.api.shared; + +import java.util.Map; + +/** + * Describes meta information about kubernetes namespace. + * + * @author Sergii Leshchenko + */ +public interface KubernetesNamespaceMeta { + + /** + * Attribute that shows if k8s namespace is configured as default. Possible values: true/false. + * Absent value should be considered as false. + */ + String DEFAULT_ATTRIBUTE = "default"; + + /** + * Attributes that contains information about current namespace status. Example values: Active, + * Terminating. Absent value indicates that namespace is not created yet. + */ + String PHASE_ATTRIBUTE = "phase"; + + /** + * Returns the name of namespace. + * + *

Value may be not a name of existing namespace, but predicted name with placeholders inside, + * like . + */ + String getName(); + + /** Returns namespace attributes, which may contains additional info about it like description. */ + Map getAttributes(); +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/dto/KubernetesNamespaceMetaDto.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/dto/KubernetesNamespaceMetaDto.java new file mode 100644 index 00000000000..16652f17d99 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/dto/KubernetesNamespaceMetaDto.java @@ -0,0 +1,34 @@ +/* + * 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.kubernetes.api.shared.dto; + +import java.util.Map; +import org.eclipse.che.dto.shared.DTO; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; + +/** @author Sergii Leshchenko */ +@DTO +public interface KubernetesNamespaceMetaDto extends KubernetesNamespaceMeta { + @Override + String getName(); + + void setName(String name); + + KubernetesNamespaceMetaDto withName(String name); + + @Override + Map getAttributes(); + + void setAttributes(Map attributes); + + KubernetesNamespaceMetaDto withAttributes(Map attributes); +} 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 27b2f806520..243d4546497 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 @@ -11,20 +11,33 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Collections.singletonList; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.client.KubernetesClientException; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.function.Function; +import java.util.stream.Collectors; import javax.inject.Named; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; /** * Helps to create {@link KubernetesNamespace} instances. @@ -42,6 +55,9 @@ public class KubernetesNamespaceFactory { NAMESPACE_NAME_PLACEHOLDERS.put("", Subject::getUserId); } + private final String defaultNamespaceName; + private final boolean allowUserDefinedNamespaces; + private final String namespaceName; private final boolean isPredefined; private final String serviceAccountName; @@ -53,12 +69,23 @@ public KubernetesNamespaceFactory( @Nullable @Named("che.infra.kubernetes.namespace") String namespaceName, @Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName, @Nullable @Named("che.infra.kubernetes.cluster_role_name") String clusterRoleName, - KubernetesClientFactory clientFactory) { + @Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName, + @Named("che.infra.kubernetes.namespace.allow_user_defined") + boolean allowUserDefinedNamespaces, + KubernetesClientFactory clientFactory) + throws ConfigurationException { this.namespaceName = namespaceName; this.isPredefined = !isNullOrEmpty(namespaceName) && hasNoPlaceholders(this.namespaceName); this.serviceAccountName = serviceAccountName; this.clusterRoleName = clusterRoleName; this.clientFactory = clientFactory; + this.defaultNamespaceName = defaultNamespaceName; + this.allowUserDefinedNamespaces = allowUserDefinedNamespaces; + if (isNullOrEmpty(defaultNamespaceName) && !allowUserDefinedNamespaces) { + throw new ConfigurationException( + "che.infra.kubernetes.namespace.default or " + + "che.infra.kubernetes.namespace.allow_user_defined must be configured"); + } } private boolean hasNoPlaceholders(String namespaceName) { @@ -73,6 +100,139 @@ public boolean isPredefined() { return isPredefined; } + /** + * Creates a Kubernetes namespace for the specified workspace. + * + *

Namespace won't be prepared. This method should be used only in case workspace recovering. + * + * @param workspaceId identifier of the workspace + * @return created namespace + */ + public KubernetesNamespace create(String workspaceId, String namespace) { + return doCreateNamespace(workspaceId, namespace); + } + + @VisibleForTesting + KubernetesNamespace doCreateNamespace(String workspaceId, String name) { + return new KubernetesNamespace(clientFactory, name, workspaceId); + } + + /** Returns list of k8s namespaces names where a user is able to run workspaces. */ + public List list() throws InfrastructureException { + if (!allowUserDefinedNamespaces) { + return singletonList(getDefaultNamespace()); + } + + // if user defined namespaces are allowed - fetch all available + List namespaces = fetchNamespaces(); + + // propagate default namespace if it's configured + if (!isNullOrEmpty(defaultNamespaceName)) { + provisionDefaultNamespace(namespaces); + } + return namespaces; + } + + /** + * Returns default namespace, it's based on existing namespace if there is such or just object + * holder if there is no such namespace on cluster. + */ + private KubernetesNamespaceMeta getDefaultNamespace() throws InfrastructureException { + // the default namespace must be configured if user defined are not allowed + // so return only it + String evaluatedName = + evalDefaultNamespaceName( + defaultNamespaceName, EnvironmentContext.getCurrent().getSubject()); + + Optional defaultNamespaceOpt = fetchNamespace(evaluatedName); + + KubernetesNamespaceMeta defaultNamespace = + defaultNamespaceOpt + // if the predefined namespace does not exist - return dummy info and it will be created + // during the first workspace start + .orElseGet(() -> new KubernetesNamespaceMetaImpl(evaluatedName)); + + defaultNamespace.getAttributes().put(DEFAULT_ATTRIBUTE, "true"); + return defaultNamespace; + } + + /** + * Provision default namespace into the specified list. If default namespace is already there - + * just provision the corresponding attributes to it. + * + * @param namespaces list where default namespace should be provisioned + */ + private void provisionDefaultNamespace(List namespaces) { + String evaluatedName = + evalDefaultNamespaceName( + defaultNamespaceName, EnvironmentContext.getCurrent().getSubject()); + + Optional defaultNamespaceOpt = + namespaces.stream().filter(n -> evaluatedName.equals(n.getName())).findAny(); + KubernetesNamespaceMeta defaultNamespace; + if (defaultNamespaceOpt.isPresent()) { + defaultNamespace = defaultNamespaceOpt.get(); + } else { + defaultNamespace = new KubernetesNamespaceMetaImpl(evaluatedName); + namespaces.add(defaultNamespace); + } + + defaultNamespace.getAttributes().put(DEFAULT_ATTRIBUTE, "true"); + } + + /** + * Fetches the specified namespace from a cluster. + * + * @param name name of namespace that should be fetched. + * @return optional with kubernetes namespace meta + * @throws InfrastructureException when any error occurs during namespace fetching + */ + protected Optional fetchNamespace(String name) + throws InfrastructureException { + try { + Namespace namespace = clientFactory.create().namespaces().withName(name).get(); + if (namespace == null) { + return Optional.empty(); + } else { + return Optional.of(asNamespaceMeta(namespace)); + } + } catch (KubernetesClientException e) { + throw new InfrastructureException( + "Error occurred when tried to fetch default namespace. Cause: " + e.getMessage(), e); + } + } + + /** + * Fetched namespace from a k8s cluster. + * + * @return list with available k8s namespace metas. + * @throws InfrastructureException when any error occurs during namespaces fetching + */ + protected List fetchNamespaces() throws InfrastructureException { + try { + return clientFactory + .create() + .namespaces() + .list() + .getItems() + .stream() + .map(this::asNamespaceMeta) + .collect(Collectors.toList()); + } catch (KubernetesClientException e) { + throw new InfrastructureException( + "Error occurred when tried to list all available namespaces. Cause: " + e.getMessage(), + e); + } + } + + private KubernetesNamespaceMeta asNamespaceMeta(Namespace namespace) { + Map attributes = new HashMap<>(2); + if (namespace.getStatus() != null && namespace.getStatus().getPhase() != null) { + attributes.put(PHASE_ATTRIBUTE, namespace.getStatus().getPhase()); + } + return new KubernetesNamespaceMetaImpl(namespace.getMetadata().getName(), attributes); + } + /** * Creates a Kubernetes namespace for the specified workspace. * @@ -117,21 +277,15 @@ protected String evalNamespaceName(String workspaceId, Subject currentUser) { } } - /** - * Creates a Kubernetes namespace for the specified workspace. - * - *

Namespace won't be prepared. This method should be used only in case workspace recovering. - * - * @param workspaceId identifier of the workspace - * @return created namespace - */ - public KubernetesNamespace create(String workspaceId, String namespace) { - return doCreateNamespace(workspaceId, namespace); - } - - @VisibleForTesting - KubernetesNamespace doCreateNamespace(String workspaceId, String name) { - return new KubernetesNamespace(clientFactory, name, workspaceId); + protected String evalDefaultNamespaceName(String defaultNamespace, Subject currentUser) { + checkArgument(!isNullOrEmpty(defaultNamespace)); + String evaluated = defaultNamespace; + for (Entry> placeHolder : + NAMESPACE_NAME_PLACEHOLDERS.entrySet()) { + evaluated = + evaluated.replaceAll(placeHolder.getKey(), placeHolder.getValue().apply(currentUser)); + } + return evaluated; } @VisibleForTesting 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 new file mode 100644 index 00000000000..d87a256b919 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java @@ -0,0 +1,79 @@ +/* + * 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.kubernetes.api.server; + +import static com.jayway.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; +import static org.everrest.assured.JettyHttpServer.SECURE_PATH; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.google.common.collect.ImmutableMap; +import com.jayway.restassured.response.Response; +import java.util.Collections; +import java.util.List; +import org.eclipse.che.api.core.rest.CheJsonProvider; +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.everrest.assured.EverrestJetty; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests for {@link KubernetesNamespaceService} + * + * @author Sergii Leshchenko + */ +@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class}) +public class KubernetesNamespaceServiceTest { + + @SuppressWarnings("unused") // is declared for deploying by everrest-assured + private CheJsonProvider jsonProvider = new CheJsonProvider(Collections.emptySet()); + + @Mock private KubernetesNamespaceFactory namespaceFactory; + + @InjectMocks private KubernetesNamespaceService service; + + @Test + public void shouldReturnNamespaces() throws Exception { + KubernetesNamespaceMetaImpl namespaceMeta = + new KubernetesNamespaceMetaImpl( + "ws-namespace", ImmutableMap.of("phase", "active", "default", "true")); + when(namespaceFactory.list()).thenReturn(singletonList(namespaceMeta)); + + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .get(SECURE_PATH + "/kubernetes/namespace"); + + assertEquals(response.getStatusCode(), 200); + List namespaces = + unwrapDtoList(response, KubernetesNamespaceMetaDto.class); + assertEquals(namespaces.size(), 1); + assertEquals(new KubernetesNamespaceMetaImpl(namespaces.get(0)), namespaceMeta); + verify(namespaceFactory).list(); + } + + private static List unwrapDtoList(Response response, Class dtoClass) { + return DtoFactory.getInstance().createListDtoFromJson(response.body().print(), dtoClass); + } +} 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 e3a249134f7..b1e1b1b46b7 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java @@ -11,20 +11,40 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; +import static java.util.Collections.singletonList; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +import io.fabric8.kubernetes.api.model.DoneableNamespace; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.NamespaceList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import java.util.Arrays; +import java.util.List; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.commons.subject.SubjectImpl; +import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Listeners; import org.testng.annotations.Test; @@ -36,12 +56,175 @@ @Listeners(MockitoTestNGListener.class) public class KubernetesNamespaceFactoryTest { @Mock private KubernetesClientFactory clientFactory; + + @Mock private KubernetesClient k8sClient; + + @Mock + private NonNamespaceOperation< + Namespace, NamespaceList, DoneableNamespace, Resource> + namespaceOperation; + private KubernetesNamespaceFactory namespaceFactory; + @BeforeMethod + public void setUp() throws Exception { + lenient().when(clientFactory.create()).thenReturn(k8sClient); + lenient().when(k8sClient.namespaces()).thenReturn(namespaceOperation); + } + + @Test( + expectedExceptions = ConfigurationException.class, + expectedExceptionsMessageRegExp = + "che.infra.kubernetes.namespace.default or " + + "che.infra.kubernetes.namespace.allow_user_defined must be configured") + public void + shouldThrowExceptionIfNoDefaultNamespaceIsConfiguredAndUserDefinedNamespacesAreNotAllowed() + throws Exception { + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", null, false, clientFactory); + } + + @Test + public void shouldReturnDefaultNamespaceWhenItExistsAndUserDefinedIsNotAllowed() + throws Exception { + prepareNamespaceToBeFoundByName( + "che-default", + new NamespaceBuilder() + .withNewMetadata() + .withName("che-default") + .endMetadata() + .withNewStatus("Active") + .build()); + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", "che-default", false, clientFactory); + + List availableNamespaces = namespaceFactory.list(); + assertEquals(availableNamespaces.size(), 1); + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(0); + assertEquals(defaultNamespace.getName(), "che-default"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + assertEquals(defaultNamespace.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + } + + @Test + public void shouldReturnDefaultNamespaceWhenItDoesNotExistAndUserDefinedIsNotAllowed() + throws Exception { + prepareNamespaceToBeFoundByName("che-default", null); + + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", "che-default", false, clientFactory); + + List availableNamespaces = namespaceFactory.list(); + assertEquals(availableNamespaces.size(), 1); + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(0); + assertEquals(defaultNamespace.getName(), "che-default"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + assertNull( + defaultNamespace + .getAttributes() + .get(PHASE_ATTRIBUTE)); // no phase - means such namespace does not exist + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Error occurred when tried to fetch default namespace. Cause: connection refused") + public void shouldThrownExceptionWhenFailedToGetInfoAboutDefaultNamespace() throws Exception { + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", "che", false, clientFactory); + throwOnTryToGetNamespaceByName("che", new KubernetesClientException("connection refused")); + + namespaceFactory.list(); + } + + @Test + public void shouldReturnListOfExistingNamespacesIfUserDefinedIsAllowed() throws Exception { + prepareListedNamespaces( + Arrays.asList( + createNamespace("my-for-ws", "Active"), + createNamespace("experimental", "Terminating"))); + + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", null, true, clientFactory); + + List availableNamespaces = namespaceFactory.list(); + assertEquals(availableNamespaces.size(), 2); + KubernetesNamespaceMeta forWS = availableNamespaces.get(0); + assertEquals(forWS.getName(), "my-for-ws"); + assertEquals(forWS.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertNull(forWS.getAttributes().get(DEFAULT_ATTRIBUTE)); + + KubernetesNamespaceMeta experimental = availableNamespaces.get(1); + assertEquals(experimental.getName(), "experimental"); + assertEquals(experimental.getAttributes().get(PHASE_ATTRIBUTE), "Terminating"); + } + + @Test + public void shouldReturnListOfExistingNamespacesAlongWithDefaultIfUserDefinedIsAllowed() + throws Exception { + prepareListedNamespaces( + Arrays.asList( + createNamespace("my-for-ws", "Active"), createNamespace("default", "Active"))); + + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", "default", true, clientFactory); + + List availableNamespaces = namespaceFactory.list(); + + assertEquals(availableNamespaces.size(), 2); + KubernetesNamespaceMeta forWS = availableNamespaces.get(0); + assertEquals(forWS.getName(), "my-for-ws"); + assertEquals(forWS.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertNull(forWS.getAttributes().get(DEFAULT_ATTRIBUTE)); + + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(1); + assertEquals(defaultNamespace.getName(), "default"); + assertEquals(defaultNamespace.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + } + + @Test + public void + shouldReturnListOfExistingNamespacesAlongWithNonExistingDefaultIfUserDefinedIsAllowed() + throws Exception { + prepareListedNamespaces(singletonList(createNamespace("my-for-ws", "Active"))); + + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", "default", true, clientFactory); + + List availableNamespaces = namespaceFactory.list(); + assertEquals(availableNamespaces.size(), 2); + KubernetesNamespaceMeta forWS = availableNamespaces.get(0); + assertEquals(forWS.getName(), "my-for-ws"); + assertEquals(forWS.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertNull(forWS.getAttributes().get(DEFAULT_ATTRIBUTE)); + + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(1); + assertEquals(defaultNamespace.getName(), "default"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + assertNull( + defaultNamespace + .getAttributes() + .get(PHASE_ATTRIBUTE)); // no phase - means such namespace does not exist + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Error occurred when tried to list all available namespaces. Cause: connection refused") + public void shouldThrownExceptionWhenFailedToGetNamespaces() throws Exception { + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", null, true, clientFactory); + throwOnTryToGetNamespacesList(new KubernetesClientException("connection refused")); + + namespaceFactory.list(); + } + @Test public void shouldReturnTrueIfNamespaceIsNotEmptyOnCheckingIfNamespaceIsPredefined() { // given - namespaceFactory = new KubernetesNamespaceFactory("predefined", "", "", clientFactory); + namespaceFactory = + new KubernetesNamespaceFactory("predefined", "", "", "che", false, clientFactory); // when boolean isPredefined = namespaceFactory.isPredefined(); @@ -53,7 +236,7 @@ public void shouldReturnTrueIfNamespaceIsNotEmptyOnCheckingIfNamespaceIsPredefin @Test public void shouldReturnTrueIfNamespaceIsEmptyOnCheckingIfNamespaceIsPredefined() { // given - namespaceFactory = new KubernetesNamespaceFactory("", "", "", clientFactory); + namespaceFactory = new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory); // when boolean isPredefined = namespaceFactory.isPredefined(); @@ -65,7 +248,7 @@ public void shouldReturnTrueIfNamespaceIsEmptyOnCheckingIfNamespaceIsPredefined( @Test public void shouldReturnTrueIfNamespaceIsNullOnCheckingIfNamespaceIsPredefined() { // given - namespaceFactory = new KubernetesNamespaceFactory(null, "", "", clientFactory); + namespaceFactory = new KubernetesNamespaceFactory(null, "", "", "che", false, clientFactory); // when boolean isPredefined = namespaceFactory.isPredefined(); @@ -77,7 +260,8 @@ public void shouldReturnTrueIfNamespaceIsNullOnCheckingIfNamespaceIsPredefined() @Test public void shouldCreateAndPrepareNamespaceWithPredefinedValueIfItIsNotEmpty() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("predefined", "", "", clientFactory)); + namespaceFactory = + spy(new KubernetesNamespaceFactory("predefined", "", "", "che", false, clientFactory)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -94,7 +278,7 @@ public void shouldCreateAndPrepareNamespaceWithPredefinedValueIfItIsNotEmpty() t public void shouldCreateAndPrepareNamespaceWithWorkspaceIdAsNameIfConfiguredNameIsNotPredefined() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", clientFactory)); + namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -112,7 +296,7 @@ public void shouldCreateAndPrepareNamespaceWithWorkspaceIdAsNameIfConfiguredName shouldCreateNamespaceAndDoNotPrepareNamespaceOnCreatingNamespaceWithWorkspaceIdAndNameSpecified() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", clientFactory)); + namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -129,7 +313,8 @@ public void shouldCreateAndPrepareNamespaceWithWorkspaceIdAsNameIfConfiguredName public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndNamespaceIsNotPredefined() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "serviceAccount", "", clientFactory)); + namespaceFactory = + spy(new KubernetesNamespaceFactory("", "serviceAccount", "", "che", false, clientFactory)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -152,7 +337,7 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsP namespaceFactory = spy( new KubernetesNamespaceFactory( - "namespace", "serviceAccount", "clusterRole", clientFactory)); + "namespace", "serviceAccount", "clusterRole", "che", false, clientFactory)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -171,7 +356,7 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsP public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProjectIsNotPredefined() throws Exception { // given - namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", clientFactory)); + namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", "che", false, clientFactory)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); @@ -190,10 +375,52 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProject public void testPlaceholder() { namespaceFactory = new KubernetesNamespaceFactory( - "blabol------", "", "", clientFactory); + "blabol------", + "", + "", + "che", + false, + clientFactory); String namespace = namespaceFactory.evalNamespaceName(null, new SubjectImpl("JonDoe", "123", null, false)); assertEquals(namespace, "blabol-123-JonDoe-123-JonDoe--"); } + + private void prepareNamespaceToBeFoundByName(String name, Namespace namespace) throws Exception { + @SuppressWarnings("unchecked") + Resource getNamespaceByNameOperation = mock(Resource.class); + when(namespaceOperation.withName(name)).thenReturn(getNamespaceByNameOperation); + + when(getNamespaceByNameOperation.get()).thenReturn(namespace); + } + + private void throwOnTryToGetNamespaceByName(String namespaceName, Throwable e) throws Exception { + @SuppressWarnings("unchecked") + Resource getNamespaceByNameOperation = mock(Resource.class); + when(namespaceOperation.withName(namespaceName)).thenReturn(getNamespaceByNameOperation); + + when(getNamespaceByNameOperation.get()).thenThrow(e); + } + + private void prepareListedNamespaces(List namespaces) throws Exception { + @SuppressWarnings("unchecked") + NamespaceList namespaceList = mock(NamespaceList.class); + when(namespaceOperation.list()).thenReturn(namespaceList); + + when(namespaceList.getItems()).thenReturn(namespaces); + } + + private void throwOnTryToGetNamespacesList(Throwable e) throws Exception { + when(namespaceOperation.list()).thenThrow(e); + } + + private Namespace createNamespace(String name, String phase) { + return new NamespaceBuilder() + .withNewMetadata() + .withName(name) + .endMetadata() + .withNewStatus(phase) + .build(); + } } diff --git a/infrastructures/openshift/pom.xml b/infrastructures/openshift/pom.xml index f11c1accd01..73091ba5eed 100644 --- a/infrastructures/openshift/pom.xml +++ b/infrastructures/openshift/pom.xml @@ -59,6 +59,10 @@ javax.inject javax.inject + + javax.ws.rs + javax.ws.rs-api + org.eclipse.che.core che-core-api-core @@ -83,6 +87,10 @@ org.eclipse.che.core che-core-commons-annotations + + org.eclipse.che.core + che-core-commons-inject + org.eclipse.che.core che-core-commons-tracing diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/Constants.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/Constants.java new file mode 100644 index 00000000000..a94b4a0b579 --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/Constants.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * Constants for OpenShift implementation of spi. + * + * @author Sergii Leshchenko + */ +public final class Constants { + private Constants() {} + + /** + * Attribute name of {@link + * org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta} that + * stores project display name. + */ + public static final String PROJECT_DISPLAY_NAME_ATTRIBUTE = "displayName"; + + /** + * Attribute name of {@link + * org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta} that + * stores project description. + */ + public static final String PROJECT_DESCRIPTION_ATTRIBUTE = "description"; + + /** + * Annotation name of {@link io.fabric8.openshift.api.model.Project} that stores project display + * name. + */ + public static final String PROJECT_DISPLAY_NAME_ANNOTATION = "openshift.io/display-name"; + + /** + * Annotation name of {@link io.fabric8.openshift.api.model.Project} that stores project + * description. + */ + public static final String PROJECT_DESCRIPTION_ANNOTATION = "openshift.io/description"; +} 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 f25f99b45c8..4c8f27ce830 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 @@ -40,6 +40,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientTermination; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesEnvironmentProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.StartSynchronizerFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.KubernetesNamespaceService; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.jpa.JpaKubernetesRuntimeCacheModule; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.DockerimageComponentToWorkspaceApplier; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesComponentToWorkspaceApplier; @@ -83,6 +84,8 @@ public class OpenShiftInfraModule extends AbstractModule { @Override protected void configure() { + bind(KubernetesNamespaceService.class); + MapBinder factories = MapBinder.newMapBinder(binder(), String.class, InternalEnvironmentFactory.class); 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 35b4d669eff..fb4f2bb9e6c 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java @@ -12,16 +12,31 @@ package org.eclipse.che.workspace.infrastructure.openshift.project; import static com.google.common.base.Strings.isNullOrEmpty; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.openshift.api.model.Project; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; import javax.inject.Named; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.eclipse.che.workspace.infrastructure.openshift.Constants; +import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientConfigFactory; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Helps to create {@link OpenShiftProject} instances. @@ -30,6 +45,7 @@ */ @Singleton public class OpenShiftProjectFactory extends KubernetesNamespaceFactory { + private static final Logger LOG = LoggerFactory.getLogger(OpenShiftProjectFactory.class); private final OpenShiftClientFactory clientFactory; @@ -38,8 +54,24 @@ public OpenShiftProjectFactory( @Nullable @Named("che.infra.openshift.project") String projectName, @Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName, @Nullable @Named("che.infra.kubernetes.cluster_role_name") String clusterRoleName, - OpenShiftClientFactory clientFactory) { - super(projectName, serviceAccountName, clusterRoleName, clientFactory); + @Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName, + @Named("che.infra.kubernetes.namespace.allow_user_defined") + boolean allowUserDefinedNamespaces, + OpenShiftClientFactory clientFactory, + OpenShiftClientConfigFactory clientConfigFactory) { + super( + projectName, + serviceAccountName, + clusterRoleName, + defaultNamespaceName, + allowUserDefinedNamespaces, + clientFactory); + if (allowUserDefinedNamespaces && !clientConfigFactory.isPersonalized()) { + LOG.warn( + "Users are allowed to list projects but Che server is configured with a service account. " + + "All users will receive the same list of projects. Consider configuring OpenShift " + + "OAuth to personalize credentials that will be used for cluster access."); + } this.clientFactory = clientFactory; } @@ -93,4 +125,58 @@ OpenShiftWorkspaceServiceAccount doCreateServiceAccount(String workspaceId, Stri return new OpenShiftWorkspaceServiceAccount( workspaceId, projectName, getServiceAccountName(), getClusterRoleName(), clientFactory); } + + @Override + protected Optional fetchNamespace(String name) + throws InfrastructureException { + try { + Project project = clientFactory.createOC().projects().withName(name).get(); + return Optional.of(asNamespaceMeta(project)); + } catch (KubernetesClientException e) { + if (e.getCode() == 403) { + // 403 means that the project does not exist + // or a user really is not permitted to access it which is Che Server misconfiguration + return Optional.empty(); + } else { + throw new InfrastructureException( + "Error occurred when tried to fetch default project. Cause: " + e.getMessage(), e); + } + } + } + + @Override + protected List fetchNamespaces() throws InfrastructureException { + try { + return clientFactory + .createOC() + .projects() + .list() + .getItems() + .stream() + .map(this::asNamespaceMeta) + .collect(Collectors.toList()); + } catch (KubernetesClientException e) { + throw new InfrastructureException( + "Error occurred when tried to list all available projects. Cause: " + e.getMessage(), e); + } + } + + private KubernetesNamespaceMeta asNamespaceMeta(io.fabric8.openshift.api.model.Project project) { + Map attributes = new HashMap<>(4); + ObjectMeta metadata = project.getMetadata(); + Map annotations = metadata.getAnnotations(); + String displayName = annotations.get(Constants.PROJECT_DISPLAY_NAME_ANNOTATION); + if (displayName != null) { + attributes.put(Constants.PROJECT_DISPLAY_NAME_ATTRIBUTE, displayName); + } + String description = annotations.get(Constants.PROJECT_DESCRIPTION_ANNOTATION); + if (description != null) { + attributes.put(Constants.PROJECT_DESCRIPTION_ATTRIBUTE, description); + } + + if (project.getStatus() != null && project.getStatus().getPhase() != null) { + attributes.put(PHASE_ATTRIBUTE, project.getStatus().getPhase()); + } + return new KubernetesNamespaceMetaImpl(metadata.getName(), attributes); + } } diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java index 48859495fe2..5963d1fda0d 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java @@ -11,18 +11,46 @@ */ package org.eclipse.che.workspace.infrastructure.openshift.project; +import static java.util.Collections.singletonList; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DESCRIPTION_ANNOTATION; +import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DESCRIPTION_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DISPLAY_NAME_ANNOTATION; +import static org.eclipse.che.workspace.infrastructure.openshift.Constants.PROJECT_DISPLAY_NAME_ATTRIBUTE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.openshift.api.model.DoneableProject; +import io.fabric8.openshift.api.model.Project; +import io.fabric8.openshift.api.model.ProjectBuilder; +import io.fabric8.openshift.api.model.ProjectList; +import io.fabric8.openshift.client.OpenShiftClient; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.inject.ConfigurationException; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace; +import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientConfigFactory; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Listeners; import org.testng.annotations.Test; @@ -33,13 +61,210 @@ */ @Listeners(MockitoTestNGListener.class) public class OpenShiftProjectFactoryTest { + + @Mock private OpenShiftClientConfigFactory configFactory; @Mock private OpenShiftClientFactory clientFactory; + + @Mock + private NonNamespaceOperation< + Project, ProjectList, DoneableProject, Resource> + projectOperation; + + @Mock private OpenShiftClient osClient; + private OpenShiftProjectFactory projectFactory; + @BeforeMethod + public void setUp() throws Exception { + lenient().when(clientFactory.createOC()).thenReturn(osClient); + lenient().when(osClient.projects()).thenReturn(projectOperation); + } + + @Test( + expectedExceptions = ConfigurationException.class, + expectedExceptionsMessageRegExp = + "che.infra.kubernetes.namespace.default or " + + "che.infra.kubernetes.namespace.allow_user_defined must be configured") + public void + shouldThrowExceptionIfNoDefaultNamespaceIsConfiguredAndUserDefinedNamespacesAreNotAllowed() + throws Exception { + projectFactory = + new OpenShiftProjectFactory( + "projectName", "", "", null, false, clientFactory, configFactory); + } + + @Test + public void shouldReturnDefaultProjectWhenItExistsAndUserDefinedIsNotAllowed() throws Exception { + prepareNamespaceToBeFoundByName( + "che-default", + new ProjectBuilder() + .withNewMetadata() + .withName("che-default") + .withAnnotations( + ImmutableMap.of( + PROJECT_DISPLAY_NAME_ANNOTATION, + "Default Che Project", + PROJECT_DESCRIPTION_ANNOTATION, + "some description")) + .endMetadata() + .withNewStatus("Active") + .build()); + + projectFactory = + new OpenShiftProjectFactory( + "predefined", "", "", "che-default", false, clientFactory, configFactory); + + List availableNamespaces = projectFactory.list(); + assertEquals(availableNamespaces.size(), 1); + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(0); + assertEquals(defaultNamespace.getName(), "che-default"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + assertEquals( + defaultNamespace.getAttributes().get(PROJECT_DISPLAY_NAME_ATTRIBUTE), + "Default Che Project"); + assertEquals( + defaultNamespace.getAttributes().get(PROJECT_DESCRIPTION_ATTRIBUTE), "some description"); + assertEquals(defaultNamespace.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + } + + @Test + public void shouldReturnDefaultProjectWhenItDoesNotExistAndUserDefinedIsNotAllowed() + throws Exception { + throwOnTryToGetProjectByName( + "che-default", new KubernetesClientException("forbidden", 403, null)); + + projectFactory = + new OpenShiftProjectFactory( + "predefined", "", "", "che-default", false, clientFactory, configFactory); + + List availableNamespaces = projectFactory.list(); + assertEquals(availableNamespaces.size(), 1); + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(0); + assertEquals(defaultNamespace.getName(), "che-default"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + assertNull( + defaultNamespace + .getAttributes() + .get(PHASE_ATTRIBUTE)); // no phase - means such project does not exist + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Error occurred when tried to fetch default project. Cause: connection refused") + public void shouldThrownExceptionWhenFailedToGetInfoAboutDefaultNamespace() throws Exception { + throwOnTryToGetProjectByName( + "che-default", new KubernetesClientException("connection refused")); + + projectFactory = + new OpenShiftProjectFactory( + "predefined", "", "", "che-default", false, clientFactory, configFactory); + + projectFactory.list(); + } + + @Test + public void shouldReturnListOfExistingProjectsIfUserDefinedIsAllowed() throws Exception { + prepareListedProjects( + Arrays.asList( + createProject("my-for-ws", "Project for Workspaces", "some description", "Active"), + createProject("experimental", null, null, "Terminating"))); + + projectFactory = + new OpenShiftProjectFactory("predefined", "", "", null, true, clientFactory, configFactory); + + List availableNamespaces = projectFactory.list(); + assertEquals(availableNamespaces.size(), 2); + KubernetesNamespaceMeta forWS = availableNamespaces.get(0); + assertEquals(forWS.getName(), "my-for-ws"); + assertEquals( + forWS.getAttributes().get(PROJECT_DISPLAY_NAME_ATTRIBUTE), "Project for Workspaces"); + assertEquals(forWS.getAttributes().get(PROJECT_DESCRIPTION_ATTRIBUTE), "some description"); + assertEquals(forWS.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + + KubernetesNamespaceMeta experimental = availableNamespaces.get(1); + assertEquals(experimental.getName(), "experimental"); + assertEquals(experimental.getAttributes().get(PHASE_ATTRIBUTE), "Terminating"); + } + + @Test + public void shouldReturnListOfExistingProjectsAlongWithDefaultIfUserDefinedIsAllowed() + throws Exception { + prepareListedProjects( + Arrays.asList( + createProject("my-for-ws", "Project for Workspaces", "some description", "Active"), + createProject("default", "Default Che Project", "some description", "Active"))); + + projectFactory = + new OpenShiftProjectFactory( + "predefined", "", "", "default", true, clientFactory, configFactory); + + List availableNamespaces = projectFactory.list(); + + assertEquals(availableNamespaces.size(), 2); + KubernetesNamespaceMeta forWS = availableNamespaces.get(0); + assertEquals(forWS.getName(), "my-for-ws"); + assertEquals( + forWS.getAttributes().get(PROJECT_DISPLAY_NAME_ATTRIBUTE), "Project for Workspaces"); + assertEquals(forWS.getAttributes().get(PROJECT_DESCRIPTION_ATTRIBUTE), "some description"); + assertEquals(forWS.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertNull(forWS.getAttributes().get(DEFAULT_ATTRIBUTE)); + + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(1); + assertEquals(defaultNamespace.getName(), "default"); + assertEquals( + defaultNamespace.getAttributes().get(PROJECT_DISPLAY_NAME_ATTRIBUTE), + "Default Che Project"); + assertEquals( + defaultNamespace.getAttributes().get(PROJECT_DESCRIPTION_ATTRIBUTE), "some description"); + assertEquals(defaultNamespace.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + } + + @Test + public void shouldReturnListOfExistingProjectsAlongWithNonExistingDefaultIfUserDefinedIsAllowed() + throws Exception { + prepareListedProjects(singletonList(createProject("my-for-ws", "", "", "Active"))); + + projectFactory = + new OpenShiftProjectFactory( + "predefined", "", "", "default", true, clientFactory, configFactory); + + List availableNamespaces = projectFactory.list(); + assertEquals(availableNamespaces.size(), 2); + KubernetesNamespaceMeta forWS = availableNamespaces.get(0); + assertEquals(forWS.getName(), "my-for-ws"); + assertEquals(forWS.getAttributes().get(PHASE_ATTRIBUTE), "Active"); + assertNull(forWS.getAttributes().get(DEFAULT_ATTRIBUTE)); + + KubernetesNamespaceMeta defaultNamespace = availableNamespaces.get(1); + assertEquals(defaultNamespace.getName(), "default"); + assertEquals(defaultNamespace.getAttributes().get(DEFAULT_ATTRIBUTE), "true"); + assertNull( + defaultNamespace + .getAttributes() + .get(PHASE_ATTRIBUTE)); // no phase - means such namespace does not exist + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Error occurred when tried to list all available projects. Cause: connection refused") + public void shouldThrownExceptionWhenFailedToGetNamespaces() throws Exception { + throwOnTryToGetProjectsList(new KubernetesClientException("connection refused")); + projectFactory = + new OpenShiftProjectFactory("predefined", "", "", "", true, clientFactory, configFactory); + + projectFactory.list(); + } + @Test public void shouldCreateAndPrepareProjectWithPredefinedValueIfItIsNotEmpty() throws Exception { // given - projectFactory = spy(new OpenShiftProjectFactory("projectName", "", "", clientFactory)); + projectFactory = + spy( + new OpenShiftProjectFactory( + "projectName", "", "", "che", false, clientFactory, configFactory)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -56,7 +281,8 @@ public void shouldCreateAndPrepareProjectWithPredefinedValueIfItIsNotEmpty() thr public void shouldCreateAndPrepareProjectWithWorkspaceIdAsNameIfConfiguredValueIsEmtpy() throws Exception { // given - projectFactory = spy(new OpenShiftProjectFactory("", "", "", clientFactory)); + projectFactory = + spy(new OpenShiftProjectFactory("", "", "", "che", false, clientFactory, configFactory)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -73,7 +299,10 @@ public void shouldCreateAndPrepareProjectWithWorkspaceIdAsNameIfConfiguredValueI public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsNotPredefined() throws Exception { // given - projectFactory = spy(new OpenShiftProjectFactory("", "serviceAccount", "", clientFactory)); + projectFactory = + spy( + new OpenShiftProjectFactory( + "", "serviceAccount", "", "che", false, clientFactory, configFactory)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -95,7 +324,13 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsP projectFactory = spy( new OpenShiftProjectFactory( - "namespace", "serviceAccount", "clusterRole", clientFactory)); + "namespace", + "serviceAccount", + "clusterRole", + "che", + false, + clientFactory, + configFactory)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -110,7 +345,8 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsP public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProjectIsNotPredefined() throws Exception { // given - projectFactory = spy(new OpenShiftProjectFactory("", "", "", clientFactory)); + projectFactory = + spy(new OpenShiftProjectFactory("", "", "", "che", false, clientFactory, configFactory)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -129,7 +365,13 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProject projectFactory = spy( new OpenShiftProjectFactory( - "projectName", "serviceAccountName", "clusterRole", clientFactory)); + "projectName", + "serviceAccountName", + "clusterRole", + "che", + false, + clientFactory, + configFactory)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProject(any(), any()); @@ -141,4 +383,51 @@ public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProject verify(projectFactory).doCreateProject("workspace123", "name"); verify(toReturnProject, never()).prepare(); } + + private void prepareNamespaceToBeFoundByName(String name, Project project) throws Exception { + @SuppressWarnings("unchecked") + Resource getProjectByNameOperation = mock(Resource.class); + when(projectOperation.withName(name)).thenReturn(getProjectByNameOperation); + + when(getProjectByNameOperation.get()).thenReturn(project); + } + + private void throwOnTryToGetProjectByName(String name, KubernetesClientException e) + throws Exception { + @SuppressWarnings("unchecked") + Resource getProjectByNameOperation = mock(Resource.class); + when(projectOperation.withName(name)).thenReturn(getProjectByNameOperation); + + when(getProjectByNameOperation.get()).thenThrow(e); + } + + private void prepareListedProjects(List projects) throws Exception { + @SuppressWarnings("unchecked") + ProjectList projectList = mock(ProjectList.class); + when(projectOperation.list()).thenReturn(projectList); + + when(projectList.getItems()).thenReturn(projects); + } + + private void throwOnTryToGetProjectsList(Throwable e) throws Exception { + when(projectOperation.list()).thenThrow(e); + } + + private Project createProject(String name, String displayName, String description, String phase) { + Map annotations = new HashMap<>(); + if (displayName != null) { + annotations.put(PROJECT_DISPLAY_NAME_ANNOTATION, displayName); + } + if (description != null) { + annotations.put(PROJECT_DESCRIPTION_ANNOTATION, description); + } + + return new ProjectBuilder() + .withNewMetadata() + .withName(name) + .withAnnotations(annotations) + .endMetadata() + .withNewStatus(phase) + .build(); + } }