Skip to content

Commit

Permalink
feat: REST Service on the Che server-side that will initiate k8s name…
Browse files Browse the repository at this point in the history
…space provisioning (eclipse-che#61)

* feat: REST Service on the Che server-side that will initiate k8s namespace provisioning


Signed-off-by: Sergii Kabashniuk <skabashniuk@redhat.com>
  • Loading branch information
skabashnyuk authored and xbaran4 committed Aug 4, 2021
1 parent b68106d commit f8abc10
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
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;
Expand Down Expand Up @@ -63,6 +66,27 @@ public List<KubernetesNamespaceMetaDto> getNamespaces() throws InfrastructureExc
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
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;
Expand Down Expand Up @@ -349,6 +350,21 @@ public KubernetesNamespace getOrCreate(RuntimeIdentity identity) throws Infrastr
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand All @@ -24,15 +25,25 @@
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;

Expand All @@ -44,6 +55,14 @@
@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());

Expand Down Expand Up @@ -73,7 +92,69 @@ public void shouldReturnNamespaces() throws Exception {
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<NamespaceResolutionContext> 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 <T> List<T> unwrapDtoList(Response response, Class<T> dtoClass) {
return DtoFactory.getInstance().createListDtoFromJson(response.body().print(), dtoClass);
}

private static <T> T unwrapDto(Response response, Class<T> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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;
Expand All @@ -57,6 +58,7 @@
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;
Expand All @@ -76,6 +78,7 @@
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;
Expand Down Expand Up @@ -1013,6 +1016,118 @@ public void testEvalNamespaceNameWhenPreparedNamespacesFound() throws Infrastruc
assertEquals(namespace, "ns1");
}

@Test
public void shouldHandleProvision() throws InfrastructureException {
// given
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-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(
"",
"",
"<username>-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(
"",
"",
"<username>-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<Namespace> namespaces =
Expand Down

0 comments on commit f8abc10

Please sign in to comment.