Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from
Aug 3, 2021
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 @@ -338,6 +339,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 @@ -988,6 +991,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