From 74b47fa68b1c3b7c1cff233fae938aa0d22b72b2 Mon Sep 17 00:00:00 2001 From: Sergii Kabashniuk Date: Tue, 3 Aug 2021 12:32:22 +0300 Subject: [PATCH] feat: REST Service on the Che server-side that will initiate k8s namespace provisioning (#61) * feat: REST Service on the Che server-side that will initiate k8s namespace provisioning Signed-off-by: Sergii Kabashniuk --- .../server/KubernetesNamespaceService.java | 24 ++++ .../namespace/KubernetesNamespaceFactory.java | 16 +++ .../KubernetesNamespaceServiceTest.java | 81 ++++++++++++ .../KubernetesNamespaceFactoryTest.java | 115 ++++++++++++++++++ 4 files changed, 236 insertions(+) diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java index 1940a35313..fd19ca736c 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceService.java @@ -22,10 +22,13 @@ import java.util.List; import java.util.stream.Collectors; import javax.inject.Inject; import javax.ws.rs.GET; +import javax.ws.rs.POST; 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.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.commons.env.EnvironmentContext; 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; @@ -63,6 +66,27 @@ public class KubernetesNamespaceService extends Service { return namespaceFactory.list().stream().map(this::asDto).collect(Collectors.toList()); } + @POST + @Path("provision") + @Produces(APPLICATION_JSON) + @ApiOperation( + value = "Provision k8s namespace where user is able to create workspaces", + notes = + "This operation can be performed only by an authorized user." + + " This is a beta feature that may be significantly changed.", + response = KubernetesNamespaceMetaDto.class) + @ApiResponses({ + @ApiResponse(code = 200, message = "The namespace successfully provisioned"), + @ApiResponse( + code = 500, + message = "Internal server error occurred during namespace provisioning") + }) + public KubernetesNamespaceMetaDto provision() throws InfrastructureException { + return asDto( + namespaceFactory.provision( + new NamespaceResolutionContext(EnvironmentContext.getCurrent().getSubject()))); + } + private KubernetesNamespaceMetaDto asDto(KubernetesNamespaceMeta kubernetesNamespaceMeta) { return DtoFactory.newDto(KubernetesNamespaceMetaDto.class) .withName(kubernetesNamespaceMeta.getName()) 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 f787b5c082..75a0d33328 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 @@ -50,6 +50,7 @@ import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.user.server.PreferenceManager; import org.eclipse.che.api.user.server.UserManager; +import org.eclipse.che.api.workspace.server.model.impl.RuntimeIdentityImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; import org.eclipse.che.commons.annotation.Nullable; @@ -349,6 +350,21 @@ public class KubernetesNamespaceFactory { return namespace; } + public KubernetesNamespaceMeta provision(NamespaceResolutionContext namespaceResolutionContext) + throws InfrastructureException { + KubernetesNamespace namespace = + getOrCreate( + new RuntimeIdentityImpl( + null, + null, + namespaceResolutionContext.getUserId(), + evaluateNamespaceName(namespaceResolutionContext))); + + return fetchNamespace(namespace.getName()) + .orElseThrow( + () -> new InfrastructureException("Not able to find namespace " + namespace.getName())); + } + public KubernetesNamespace get(RuntimeIdentity identity) throws InfrastructureException { String workspaceId = identity.getWorkspaceId(); String namespaceName = identity.getInfrastructureNamespace(); diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java index faf373df2d..9fa5ac97ef 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/api/server/KubernetesNamespaceServiceTest.java @@ -16,6 +16,7 @@ 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.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -24,15 +25,25 @@ 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.ApiExceptionMapper; import org.eclipse.che.api.core.rest.CheJsonProvider; +import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.commons.subject.SubjectImpl; 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.everrest.core.Filter; +import org.everrest.core.GenericContainerRequest; +import org.everrest.core.RequestFilter; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; import org.testng.annotations.Listeners; import org.testng.annotations.Test; @@ -44,6 +55,14 @@ import org.testng.annotations.Test; @Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class}) public class KubernetesNamespaceServiceTest { + @SuppressWarnings("unused") + private static final ApiExceptionMapper MAPPER = new ApiExceptionMapper(); + + @SuppressWarnings("unused") + private static final EnvironmentFilter FILTER = new EnvironmentFilter(); + + private static final Subject SUBJECT = new SubjectImpl("john", "id-123", "token", false); + @SuppressWarnings("unused") // is declared for deploying by everrest-assured private CheJsonProvider jsonProvider = new CheJsonProvider(Collections.emptySet()); @@ -73,7 +92,69 @@ public class KubernetesNamespaceServiceTest { verify(namespaceFactory).list(); } + @Test + public void shouldProvisionNamespace() throws Exception { + // given + KubernetesNamespaceMetaImpl namespaceMeta = + new KubernetesNamespaceMetaImpl( + "ws-namespace", ImmutableMap.of("phase", "active", "default", "true")); + when(namespaceFactory.provision(any(NamespaceResolutionContext.class))) + .thenReturn(namespaceMeta); + // when + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .post(SECURE_PATH + "/kubernetes/namespace/provision"); + // then + + assertEquals(response.getStatusCode(), 200); + KubernetesNamespaceMetaDto actual = unwrapDto(response, KubernetesNamespaceMetaDto.class); + assertEquals(actual.getName(), namespaceMeta.getName()); + assertEquals(actual.getAttributes(), namespaceMeta.getAttributes()); + verify(namespaceFactory).provision(any(NamespaceResolutionContext.class)); + } + + @Test + public void shouldProvisionNamespaceWithCorrectContext() throws Exception { + // given + KubernetesNamespaceMetaImpl namespaceMeta = + new KubernetesNamespaceMetaImpl( + "ws-namespace", ImmutableMap.of("phase", "active", "default", "true")); + when(namespaceFactory.provision(any(NamespaceResolutionContext.class))) + .thenReturn(namespaceMeta); + // when + final Response response = + given() + .auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .when() + .post(SECURE_PATH + "/kubernetes/namespace/provision"); + // then + + assertEquals(response.getStatusCode(), 200); + ArgumentCaptor captor = + ArgumentCaptor.forClass(NamespaceResolutionContext.class); + verify(namespaceFactory).provision(captor.capture()); + NamespaceResolutionContext actualContext = captor.getValue(); + assertEquals(actualContext.getUserId(), SUBJECT.getUserId()); + assertEquals(actualContext.getUserName(), SUBJECT.getUserName()); + Assert.assertNull(actualContext.getWorkspaceId()); + } + private static List unwrapDtoList(Response response, Class dtoClass) { return DtoFactory.getInstance().createListDtoFromJson(response.body().print(), dtoClass); } + + private static T unwrapDto(Response response, Class dtoClass) { + return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass); + } + + @Filter + public static class EnvironmentFilter implements RequestFilter { + public void doFilter(GenericContainerRequest request) { + EnvironmentContext.getCurrent().setSubject(SUBJECT); + } + } } 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 384a70417b..053bb7c1c8 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 @@ -33,6 +33,7 @@ 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 static org.testng.Assert.fail; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.Appender; @@ -57,6 +58,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -76,6 +78,7 @@ import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; import org.mockito.ArgumentCaptor; @@ -1013,6 +1016,118 @@ public class KubernetesNamespaceFactoryTest { assertEquals(namespace, "ns1"); } + @Test + public void shouldHandleProvision() throws InfrastructureException { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", + "", + "-che", + false, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + clientFactory, + cheClientFactory, + userManager, + preferenceManager, + pool)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + when(toReturnNamespace.getName()).thenReturn("jondoe-che"); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); + KubernetesNamespaceMetaImpl namespaceMeta = + new KubernetesNamespaceMetaImpl( + "jondoe-che", ImmutableMap.of("phase", "active", "default", "true")); + doReturn(Optional.of(namespaceMeta)).when(namespaceFactory).fetchNamespace(eq("jondoe-che")); + + // when + NamespaceResolutionContext context = + new NamespaceResolutionContext("workspace123", "user123", "jondoe"); + KubernetesNamespaceMeta actual = namespaceFactory.provision(context); + + // then + assertEquals(actual.getName(), "jondoe-che"); + assertEquals(actual.getAttributes(), ImmutableMap.of("phase", "active", "default", "true")); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = "Not able to find namespace jondoe-cha-cha-cha") + public void shouldFailToProvisionIfNotAbleToFindNamespace() throws InfrastructureException { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", + "", + "-cha-cha-cha", + false, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + clientFactory, + cheClientFactory, + userManager, + preferenceManager, + pool)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + when(toReturnNamespace.getName()).thenReturn("jondoe-cha-cha-cha"); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); + KubernetesNamespaceMetaImpl namespaceMeta = + new KubernetesNamespaceMetaImpl( + "jondoe-cha-cha-cha", ImmutableMap.of("phase", "active", "default", "true")); + doReturn(Optional.empty()).when(namespaceFactory).fetchNamespace(eq("jondoe-cha-cha-cha")); + + // when + NamespaceResolutionContext context = + new NamespaceResolutionContext("workspace123", "user123", "jondoe"); + namespaceFactory.provision(context); + + // then + fail("should not reach this point since exception has to be thrown"); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = "Error occurred when tried to fetch default namespace") + public void shouldFail2ProvisionIfNotAbleToFindNamespace() throws InfrastructureException { + // given + namespaceFactory = + spy( + new KubernetesNamespaceFactory( + "", + "", + "-cha-cha-cha", + false, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + clientFactory, + cheClientFactory, + userManager, + preferenceManager, + pool)); + KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); + when(toReturnNamespace.getName()).thenReturn("jondoe-cha-cha-cha"); + doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); + KubernetesNamespaceMetaImpl namespaceMeta = + new KubernetesNamespaceMetaImpl( + "jondoe-cha-cha-cha", ImmutableMap.of("phase", "active", "default", "true")); + doThrow(new InfrastructureException("Error occurred when tried to fetch default namespace")) + .when(namespaceFactory) + .fetchNamespace(eq("jondoe-cha-cha-cha")); + + // when + NamespaceResolutionContext context = + new NamespaceResolutionContext("workspace123", "user123", "jondoe"); + namespaceFactory.provision(context); + + // then + fail("should not reach this point since exception has to be thrown"); + } + @Test public void testUsernamePlaceholderInLabelsIsNotEvaluated() throws InfrastructureException { List namespaces =