From 82ea3023adb519b96c5b7d03847a968c87d1efaa Mon Sep 17 00:00:00 2001 From: Sergii Leshchenko Date: Wed, 11 Sep 2019 15:41:27 +0300 Subject: [PATCH] Add an ability to list available k8s namespaces Signed-off-by: Sergii Leshchenko --- infrastructures/kubernetes/pom.xml | 80 ++++++ .../kubernetes/KubernetesInfraModule.java | 3 + .../server/KubernetesNamespaceService.java | 67 +++++ .../impls/KubernetesNamespaceMetaImpl.java | 85 ++++++ .../api/shared/KubernetesNamespaceMeta.java | 27 ++ .../dto/KubernetesNamespaceMetaDto.java | 32 +++ .../namespace/KubernetesNamespaceFactory.java | 26 ++ .../KubernetesNamespaceFactoryTest.java | 15 ++ .../KubernetesNamespaceMetaFactoryTest.java | 187 ++++++++++++++ .../KubernetesNamespaceMetaTest.java | 242 ++++++++++++++++++ .../infrastructure/openshift/Constants.java | 47 ++++ .../openshift/OpenShiftInfraModule.java | 3 + .../project/OpenShiftProjectFactory.java | 44 ++++ .../project/OpenShiftProjectFactoryTest.java | 15 ++ 14 files changed, 873 insertions(+) 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/namespace/KubernetesNamespaceMetaFactoryTest.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaTest.java create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/Constants.java diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml index eb6805a90e14..79c650e0fd7a 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 @@ -251,6 +258,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 4f4eb0e03868..1963d3221d24 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.bootstrapper.KubernetesBootstrapperFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.jpa.JpaKubernetesRuntimeCacheModule; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.DockerimageComponentToWorkspaceApplier; @@ -79,6 +80,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 000000000000..7152e0e81d20 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java @@ -0,0 +1,67 @@ +/* + * 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 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") +public class KubernetesNamespaceService extends Service { + + private final KubernetesNamespaceFactory namespaceFactory; + + @Inject + public KubernetesNamespaceService(KubernetesNamespaceFactory namespaceFactory) { + this.namespaceFactory = namespaceFactory; + } + + @GET + @Produces(APPLICATION_JSON) + @ApiOperation( + value = "Get existing k8s namespaces where user is able to create workspaces", + notes = "This operation can be performed only by authorized user", + 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 000000000000..530ecfe30a0b --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/impls/KubernetesNamespaceMetaImpl.java @@ -0,0 +1,85 @@ +/* + * 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); + } + } + + @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 000000000000..c35bf0af229e --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/KubernetesNamespaceMeta.java @@ -0,0 +1,27 @@ +/* + * 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 { + /** Returns the name of namespace. */ + 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 000000000000..4eb8ea68cd1b --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/shared/dto/KubernetesNamespaceMetaDto.java @@ -0,0 +1,32 @@ +/* + * 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.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; + +/** @author Sergii Leshchenko */ +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 ecbbd5d97e2d..d40197047e9e 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 @@ -12,14 +12,20 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Collections.singletonList; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; +import io.fabric8.kubernetes.api.model.Namespace; +import java.util.List; +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.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. @@ -56,6 +62,26 @@ public boolean isPredefined() { return isPredefined; } + /** Returns lists of existing namespaces where a user is able to run workspaces. */ + public List list() throws InfrastructureException { + if (isPredefined) { + Namespace predefinedNamespace = + clientFactory.create().namespaces().withName(namespaceName).get(); + return singletonList( + new KubernetesNamespaceMetaImpl(predefinedNamespace.getMetadata().getName())); + } else { + return clientFactory + .create() + .namespaces() + .list() + .getItems() + .stream() + .filter(n -> !"Terminating".equals(n.getStatus().getPhase())) + .map(n -> new KubernetesNamespaceMetaImpl(n.getMetadata().getName())) + .collect(Collectors.toList()); + } + } + /** * Creates a Kubernetes namespace for the specified workspace. * 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 e12e333b60c0..baaff6aadadb 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 @@ -37,6 +37,21 @@ public class KubernetesNamespaceFactoryTest { @Mock private KubernetesClientFactory clientFactory; private KubernetesNamespaceFactory namespaceFactory; + @Test + public void shouldReturnListOfExistingNamespacesWhenNamespaceIsNotPredefined() { + // TODO Implement me before merging + } + + @Test + public void shouldReturnPredefinedNamespaceOnFetchingAllAvailable() { + // TODO Implement me before merging + } + + @Test + public void shouldReturnPredefinedNamespaceWithResolvedPlaceHolderOnFetchingAllAvailable() { + // TODO Implement me before merging + } + @Test public void shouldReturnTrueIfNamespaceIsNotEmptyOnCheckingIfNamespaceIsPredefined() { // given diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaFactoryTest.java new file mode 100644 index 000000000000..40b44fc2abe2 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaFactoryTest.java @@ -0,0 +1,187 @@ +/* + * 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.namespace; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +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.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests {@link KubernetesNamespaceFactory}. + * + * @author Sergii Leshchenko + */ +@Listeners(MockitoTestNGListener.class) +public class KubernetesNamespaceMetaFactoryTest { + @Mock private KubernetesClientFactory clientFactory; + private KubernetesNamespaceFactory namespaceFactory; + + @Test + public void shouldReturnTrueIfNamespaceIsNotEmptyOnCheckingIfNamespaceIsPredefined() { + // given + namespaceFactory = new KubernetesNamespaceFactory("predefined", "", "", clientFactory); + + // when + boolean isPredefined = namespaceFactory.isPredefined(); + + // then + assertTrue(isPredefined); + } + + @Test + public void shouldReturnTrueIfNamespaceIsEmptyOnCheckingIfNamespaceIsPredefined() { + // given + namespaceFactory = new KubernetesNamespaceFactory("", "", "", clientFactory); + + // when + boolean isPredefined = namespaceFactory.isPredefined(); + + // then + assertFalse(isPredefined); + } + + @Test + public void shouldReturnTrueIfNamespaceIsNullOnCheckingIfNamespaceIsPredefined() { + // given + namespaceFactory = new KubernetesNamespaceFactory(null, "", "", clientFactory); + + // when + boolean isPredefined = namespaceFactory.isPredefined(); + + // then + assertFalse(isPredefined); + } + + @Test + public void shouldCreateAndPrepareNamespaceWithPredefinedValueIfItIsNotEmpty() throws Exception { + // given + namespaceFactory = spy(new KubernetesNamespaceFactory("predefined", "", "", clientFactory)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); + + // when + KubernetesNamespace namespace = namespaceFactory.create("workspace123"); + + // then + assertEquals(toReturnNamespace, namespace); + verify(namespaceFactory).doCreateNamespace("workspace123", "predefined"); + verify(toReturnNamespace).prepare(); + } + + @Test + public void shouldCreateAndPrepareNamespaceWithWorkspaceIdAsNameIfConfiguredNameIsNotPredefined() + throws Exception { + // given + namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", clientFactory)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); + + // when + KubernetesNamespace namespace = namespaceFactory.create("workspace123"); + + // then + assertEquals(toReturnNamespace, namespace); + verify(namespaceFactory).doCreateNamespace("workspace123", "workspace123"); + verify(toReturnNamespace).prepare(); + } + + @Test + public void + shouldCreateNamespaceAndDoNotPrepareNamespaceOnCreatingNamespaceWithWorkspaceIdAndNameSpecified() + throws Exception { + // given + namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", clientFactory)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); + + // when + KubernetesNamespace namespace = namespaceFactory.create("workspace123", "name"); + + // then + assertEquals(toReturnNamespace, namespace); + verify(namespaceFactory).doCreateNamespace("workspace123", "name"); + verify(toReturnNamespace, never()).prepare(); + } + + @Test + public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndNamespaceIsNotPredefined() + throws Exception { + // given + namespaceFactory = spy(new KubernetesNamespaceFactory("", "serviceAccount", "", clientFactory)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); + + KubernetesWorkspaceServiceAccount serviceAccount = + mock(KubernetesWorkspaceServiceAccount.class); + doReturn(serviceAccount).when(namespaceFactory).doCreateServiceAccount(any(), any()); + + // when + namespaceFactory.create("workspace123"); + + // then + verify(namespaceFactory).doCreateServiceAccount("workspace123", "workspace123"); + verify(serviceAccount).prepare(); + } + + @Test + public void shouldNotPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsPredefined() + throws Exception { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "namespace", "serviceAccount", "clusterRole", clientFactory)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); + + KubernetesWorkspaceServiceAccount serviceAccount = + mock(KubernetesWorkspaceServiceAccount.class); + doReturn(serviceAccount).when(namespaceFactory).doCreateServiceAccount(any(), any()); + + // when + namespaceFactory.create("workspace123"); + + // then + verify(namespaceFactory, never()).doCreateServiceAccount(any(), any()); + } + + @Test + public void shouldNotPrepareWorkspaceServiceAccountIfItIsNotConfiguredAndProjectIsNotPredefined() + throws Exception { + // given + namespaceFactory = spy(new KubernetesNamespaceFactory("", "", "", clientFactory)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespace(any(), any()); + + KubernetesWorkspaceServiceAccount serviceAccount = + mock(KubernetesWorkspaceServiceAccount.class); + doReturn(serviceAccount).when(namespaceFactory).doCreateServiceAccount(any(), any()); + + // when + namespaceFactory.create("workspace123"); + + // then + verify(namespaceFactory, never()).doCreateServiceAccount(any(), any()); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaTest.java new file mode 100644 index 000000000000..f80f44046c0c --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceMetaTest.java @@ -0,0 +1,242 @@ +/* + * 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.namespace; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import io.fabric8.kubernetes.api.model.DoneableNamespace; +import io.fabric8.kubernetes.api.model.DoneableServiceAccount; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.NamespaceFluent.MetadataNested; +import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.Watcher.Action; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests {@link KubernetesNamespace} + * + * @author Sergii Leshchenko + */ +@Listeners(MockitoTestNGListener.class) +public class KubernetesNamespaceMetaTest { + + public static final String NAMESPACE = "testNamespace"; + public static final String WORKSPACE_ID = "workspace123"; + + @Mock private KubernetesDeployments deployments; + @Mock private KubernetesServices services; + @Mock private KubernetesIngresses ingresses; + @Mock private KubernetesPersistentVolumeClaims pvcs; + @Mock private KubernetesSecrets secrets; + @Mock private KubernetesConfigsMaps configMaps; + @Mock private KubernetesClientFactory clientFactory; + @Mock private KubernetesClient kubernetesClient; + @Mock private NonNamespaceOperation namespaceOperation; + @Mock private Resource serviceAccountResource; + + private KubernetesNamespace k8sNamespace; + + @BeforeMethod + public void setUp() throws Exception { + lenient().when(clientFactory.create(anyString())).thenReturn(kubernetesClient); + + lenient().doReturn(namespaceOperation).when(kubernetesClient).namespaces(); + + final MixedOperation mixedOperation = mock(MixedOperation.class); + final NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class); + lenient().doReturn(mixedOperation).when(kubernetesClient).serviceAccounts(); + lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation); + lenient().when(namespaceOperation.withName(anyString())).thenReturn(serviceAccountResource); + lenient().when(serviceAccountResource.get()).thenReturn(mock(ServiceAccount.class)); + + k8sNamespace = + new KubernetesNamespace( + clientFactory, + WORKSPACE_ID, + NAMESPACE, + deployments, + services, + pvcs, + ingresses, + secrets, + configMaps); + } + + @Test + public void testKubernetesNamespacePreparingWhenNamespaceExists() throws Exception { + // given + prepareNamespace(NAMESPACE); + KubernetesNamespace namespace = new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID); + + // when + namespace.prepare(); + } + + @Test + public void testKubernetesNamespacePreparingCreationWhenNamespaceDoesNotExist() throws Exception { + // given + MetadataNested namespaceMeta = prepareCreateNamespaceRequest(); + + Resource resource = prepareNamespaceResource(NAMESPACE); + doThrow(new KubernetesClientException("error", 403, null)).when(resource).get(); + KubernetesNamespace namespace = new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID); + + // when + namespace.prepare(); + + // then + verify(namespaceMeta).withName(NAMESPACE); + } + + @Test + public void testKubernetesNamespaceCleaningUp() throws Exception { + // when + k8sNamespace.cleanUp(); + + verify(ingresses).delete(); + verify(services).delete(); + verify(deployments).delete(); + verify(secrets).delete(); + verify(configMaps).delete(); + } + + @Test + public void testKubernetesNamespaceCleaningUpIfExceptionsOccurs() throws Exception { + doThrow(new InfrastructureException("err1.")).when(services).delete(); + doThrow(new InfrastructureException("err2.")).when(deployments).delete(); + + InfrastructureException error = null; + // when + try { + k8sNamespace.cleanUp(); + + } catch (InfrastructureException e) { + error = e; + } + + // then + assertNotNull(error); + String message = error.getMessage(); + assertEquals(message, "Error(s) occurs while cleaning up the namespace. err1. err2."); + verify(ingresses).delete(); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testThrowsInfrastructureExceptionWhenFailedToGetNamespaceServiceAccounts() + throws Exception { + prepareCreateNamespaceRequest(); + final Resource resource = prepareNamespaceResource(NAMESPACE); + doThrow(new KubernetesClientException("error", 403, null)).when(resource).get(); + doThrow(KubernetesClientException.class).when(kubernetesClient).serviceAccounts(); + + new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID).prepare(); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testThrowsInfrastructureExceptionWhenServiceAccountEventNotPublished() + throws Exception { + prepareCreateNamespaceRequest(); + final Resource resource = prepareNamespaceResource(NAMESPACE); + doThrow(new KubernetesClientException("error", 403, null)).when(resource).get(); + when(serviceAccountResource.get()).thenReturn(null); + + new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID).prepare(); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testThrowsInfrastructureExceptionWhenWatcherClosed() throws Exception { + prepareCreateNamespaceRequest(); + final Resource resource = prepareNamespaceResource(NAMESPACE); + doThrow(new KubernetesClientException("error", 403, null)).when(resource).get(); + when(serviceAccountResource.get()).thenReturn(null); + doAnswer( + (Answer) + invocation -> { + final Watcher watcher = invocation.getArgument(0); + watcher.onClose(mock(KubernetesClientException.class)); + return mock(Watch.class); + }) + .when(serviceAccountResource) + .watch(any()); + + new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID).prepare(); + } + + @Test + public void testStopsWaitingServiceAccountEventJustAfterEventReceived() throws Exception { + prepareCreateNamespaceRequest(); + final Resource resource = prepareNamespaceResource(NAMESPACE); + doThrow(new KubernetesClientException("error", 403, null)).when(resource).get(); + when(serviceAccountResource.get()).thenReturn(null); + doAnswer( + invocation -> { + final Watcher watcher = invocation.getArgument(0); + watcher.eventReceived(Action.ADDED, mock(ServiceAccount.class)); + return mock(Watch.class); + }) + .when(serviceAccountResource) + .watch(any()); + + new KubernetesNamespace(clientFactory, NAMESPACE, WORKSPACE_ID).prepare(); + + verify(serviceAccountResource).get(); + verify(serviceAccountResource).watch(any()); + } + + private MetadataNested prepareCreateNamespaceRequest() { + DoneableNamespace namespace = mock(DoneableNamespace.class); + MetadataNested metadataNested = mock(MetadataNested.class); + + doReturn(namespace).when(namespaceOperation).createNew(); + doReturn(metadataNested).when(namespace).withNewMetadata(); + doReturn(metadataNested).when(metadataNested).withName(anyString()); + doReturn(namespace).when(metadataNested).endMetadata(); + return metadataNested; + } + + private Resource prepareNamespaceResource(String namespaceName) { + Resource namespaceResource = mock(Resource.class); + doReturn(namespaceResource).when(namespaceOperation).withName(namespaceName); + kubernetesClient.namespaces().withName(namespaceName).get(); + return namespaceResource; + } + + private void prepareNamespace(String namespaceName) { + Namespace namespace = mock(Namespace.class); + Resource namespaceResource = prepareNamespaceResource(namespaceName); + doReturn(namespace).when(namespaceResource).get(); + } +} 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 000000000000..a94b4a0b5799 --- /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 fe2fc1dcdfe1..bb6fe2162042 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.bootstrapper.KubernetesBootstrapperFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.jpa.JpaKubernetesRuntimeCacheModule; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.DockerimageComponentToWorkspaceApplier; @@ -84,6 +85,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 5df0c5d90caf..cb3121db2c83 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,14 +12,24 @@ package org.eclipse.che.workspace.infrastructure.openshift.project; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Collections.singletonList; 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.openshift.api.model.Project; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.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.OpenShiftClientFactory; /** @@ -48,6 +58,24 @@ public OpenShiftProjectFactory( this.clientFactory = clientFactory; } + @Override + public List list() throws InfrastructureException { + if (isPredefined()) { + Project predefinedProject = clientFactory.createOC().projects().withName(projectName).get(); + return singletonList(asNamespaceMeta(predefinedProject)); + } else { + return clientFactory + .createOC() + .projects() + .list() + .getItems() + .stream() + .filter(n -> !"Terminating".equals(n.getStatus().getPhase())) + .map(this::asNamespaceMeta) + .collect(Collectors.toList()); + } + } + /** * Creates a OpenShift project for the specified workspace. * @@ -97,4 +125,20 @@ OpenShiftWorkspaceServiceAccount doCreateServiceAccount(String workspaceId, Stri return new OpenShiftWorkspaceServiceAccount( workspaceId, projectName, serviceAccountName, clusterRoleName, clientFactory); } + + private KubernetesNamespaceMeta asNamespaceMeta(io.fabric8.openshift.api.model.Project project) { + Map attributes = new HashMap<>(); + 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); + } + + 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 48859495fe22..49308db017cf 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 @@ -36,6 +36,21 @@ public class OpenShiftProjectFactoryTest { @Mock private OpenShiftClientFactory clientFactory; private OpenShiftProjectFactory projectFactory; + @Test + public void shouldReturnListOfExistingProjectsWhenProjectIsNotPredefined() { + // TODO Implement me before merging + } + + @Test + public void shouldReturnPredefinedProjectOnFetchingAllAvailable() { + // TODO Implement me before merging + } + + @Test + public void shouldReturnPredefinedProjectWithResolvedPlaceHoldersOnFetchingAllAvailable() { + // TODO Implement me before merging + } + @Test public void shouldCreateAndPrepareProjectWithPredefinedValueIfItIsNotEmpty() throws Exception { // given