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 <skabashniuk@redhat.com>
pull/72/head
Sergii Kabashniuk 2021-08-03 12:32:22 +03:00 committed by GitHub
parent 7bb2641d15
commit 74b47fa68b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 236 additions and 0 deletions

View File

@ -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())

View File

@ -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();

View File

@ -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<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);
}
}
}

View File

@ -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(
"",
"",
"<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 =