diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index beacc908e1..07d0a66027 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -135,6 +135,10 @@ org.eclipse.che.core che-core-api-factory-github + + org.eclipse.che.core + che-core-api-infraproxy + org.eclipse.che.core che-core-api-logger diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index c5b6e0e4ad..15c1484969 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -34,6 +34,7 @@ import org.eclipse.che.api.factory.server.FactoryCreateValidator; import org.eclipse.che.api.factory.server.FactoryEditValidator; import org.eclipse.che.api.factory.server.FactoryParametersResolver; import org.eclipse.che.api.factory.server.github.GithubFactoryParametersResolver; +import org.eclipse.che.api.infraproxy.server.InfraProxyModule; import org.eclipse.che.api.metrics.WsMasterMetricsModule; import org.eclipse.che.api.system.server.ServiceTermination; import org.eclipse.che.api.system.server.SystemModule; @@ -409,6 +410,10 @@ public class WsMasterModule extends AbstractModule { bind(PermissionChecker.class).to(PermissionCheckerImpl.class); bindConstant().annotatedWith(Names.named("che.agents.auth_enabled")).to(true); + + if (OpenShiftInfrastructure.NAME.equals(infrastructure)) { + install(new InfraProxyModule()); + } } private void configureJwtProxySecureProvisioner(String infrastructure) { diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml index e2586dc55e..2d291e533a 100644 --- a/infrastructures/kubernetes/pom.xml +++ b/infrastructures/kubernetes/pom.xml @@ -69,6 +69,10 @@ com.squareup.okhttp3 okhttp + + com.squareup.okio + okio + io.fabric8 kubernetes-client @@ -247,6 +251,11 @@ mockito-core test + + org.mockito + mockito-core + test + org.mockito mockito-testng diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/DirectKubernetesAPIAccessHelper.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/DirectKubernetesAPIAccessHelper.java new file mode 100644 index 0000000000..31f77f0480 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/DirectKubernetesAPIAccessHelper.java @@ -0,0 +1,178 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import okhttp3.Call; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import okio.BufferedSink; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.annotation.Nullable; + +public class DirectKubernetesAPIAccessHelper { + private static final String DEFAULT_MEDIA_TYPE = + javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE + .withCharset(StandardCharsets.UTF_8.name()) + .toString(); + + private DirectKubernetesAPIAccessHelper() {} + + /** + * This method just performs an HTTP request of given {@code httpMethod} on an URL composed of the + * {@code masterUrl} and {@code relativeUri} using the provided {@code httpClient}, optionally + * sending the provided {@code body}. + * + * @param masterUrl the base of the final URL + * @param httpClient the HTTP client to perform the request with + * @param httpMethod the HTTP method of the request + * @param relativeUri the relative URI that should be appended ot the {@code masterUrl} + * @param body the body to send with the request, if any + * @return the HTTP response received + * @throws InfrastructureException on failure to validate or perform the request + */ + public static Response call( + String masterUrl, + OkHttpClient httpClient, + String httpMethod, + URI relativeUri, + @Nullable HttpHeaders headers, + @Nullable InputStream body) + throws InfrastructureException { + if (relativeUri.isAbsolute() || relativeUri.isOpaque()) { + throw new InfrastructureException( + "The direct infrastructure URL must be relative and not opaque."); + } + + try { + URL fullUrl = new URI(masterUrl).resolve(relativeUri).toURL(); + okhttp3.Response response = callApi(httpClient, fullUrl, httpMethod, headers, body); + return convertResponse(response); + } catch (URISyntaxException | MalformedURLException e) { + throw new InfrastructureException("Could not compose the direct URI.", e); + } catch (IOException e) { + throw new InfrastructureException("Error sending the direct infrastructure request.", e); + } + } + + private static okhttp3.Response callApi( + OkHttpClient httpClient, + URL url, + String httpMethod, + @Nullable HttpHeaders headers, + @Nullable InputStream body) + throws IOException { + String mediaType = inputMediaType(headers); + + RequestBody requestBody = + body == null ? null : new InputStreamBasedRequestBody(body, mediaType); + + Call httpCall = + httpClient.newCall(prepareRequest(url, httpMethod, requestBody, toOkHttpHeaders(headers))); + + return httpCall.execute(); + } + + private static Request prepareRequest( + URL url, String httpMethod, RequestBody requestBody, Headers headers) { + return new Request.Builder().url(url).method(httpMethod, requestBody).headers(headers).build(); + } + + private static Response convertResponse(okhttp3.Response response) { + Response.ResponseBuilder responseBuilder = Response.status(response.code()); + + convertResponseHeaders(responseBuilder, response); + convertResponseBody(responseBuilder, response); + + return responseBuilder.build(); + } + + private static void convertResponseHeaders( + Response.ResponseBuilder responseBuilder, okhttp3.Response response) { + for (int i = 0; i < response.headers().size(); ++i) { + String name = response.headers().name(i); + String value = response.headers().value(i); + responseBuilder.header(name, value); + } + } + + private static void convertResponseBody( + Response.ResponseBuilder responseBuilder, okhttp3.Response response) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + responseBuilder.entity(responseBody.byteStream()); + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + responseBuilder.type(contentType.toString()); + } + } + } + + private static String inputMediaType(@Nullable HttpHeaders headers) { + javax.ws.rs.core.MediaType mediaTypeHeader = headers == null ? null : headers.getMediaType(); + return mediaTypeHeader == null ? DEFAULT_MEDIA_TYPE : mediaTypeHeader.toString(); + } + + private static Headers toOkHttpHeaders(HttpHeaders headers) { + Headers.Builder headersBuilder = new Headers.Builder(); + + if (headers != null) { + for (Map.Entry> e : headers.getRequestHeaders().entrySet()) { + String name = e.getKey(); + List values = e.getValue(); + for (String value : values) { + headersBuilder.add(name, value); + } + } + } + + return headersBuilder.build(); + } + + private static final class InputStreamBasedRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType mediaType; + + private InputStreamBasedRequestBody(InputStream is, String contentType) { + this.inputStream = is; + this.mediaType = contentType == null ? null : MediaType.parse(contentType); + } + + @Override + public MediaType contentType() { + return mediaType; + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + byte[] buffer = new byte[1024]; + int cnt; + while ((cnt = inputStream.read(buffer)) != -1) { + sink.write(buffer, 0, cnt); + } + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java index 7b9d022b7f..d07cba726f 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java @@ -45,7 +45,7 @@ import org.eclipse.che.commons.annotation.Nullable; public class KubernetesClientFactory { /** {@link OkHttpClient} instance shared by all Kubernetes clients. */ - private OkHttpClient httpClient; + private final OkHttpClient httpClient; /** * Default Kubernetes {@link Config} that will be the base configuration to create per-workspace @@ -129,11 +129,28 @@ public class KubernetesClientFactory { return httpClient; } + /** + * Unlike {@link #getHttpClient()} method, this method always returns an HTTP client that contains + * interceptors that augment the request with authentication information available in the global + * context. + * + *

Unlike {@link #getHttpClient()}, this method creates a new HTTP client instance each time it + * is called. + * + * @return HTTP client with authorization set up + * @throws InfrastructureException if it is not possible to build the client with authentication + * infromation + */ + public OkHttpClient getAuthenticatedHttpClient() throws InfrastructureException { + throw new InfrastructureException( + "Impersonating the current user is not supported in the Kubernetes Client."); + } + /** * Retrieves the default Kubernetes {@link Config} that will be the base configuration to create * per-workspace configurations. */ - protected Config getDefaultConfig() { + public Config getDefaultConfig() { return defaultConfig; } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructure.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructure.java index e488198631..5ccdc298e2 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructure.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructure.java @@ -14,9 +14,13 @@ package org.eclipse.che.workspace.infrastructure.kubernetes; import static java.lang.String.format; import com.google.common.collect.ImmutableSet; +import java.io.InputStream; +import java.net.URI; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.workspace.server.NoEnvironmentFactory.NoEnvInternalEnvironment; @@ -27,6 +31,7 @@ import org.eclipse.che.api.workspace.server.spi.RuntimeInfrastructure; import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; import org.eclipse.che.api.workspace.server.spi.provision.InternalEnvironmentProvisioner; import org.eclipse.che.api.workspace.shared.Constants; +import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; @@ -41,6 +46,7 @@ public class KubernetesInfrastructure extends RuntimeInfrastructure { private final KubernetesRuntimeContextFactory runtimeContextFactory; private final KubernetesRuntimeStateCache runtimeStatusesCache; private final KubernetesNamespaceFactory namespaceFactory; + private final KubernetesClientFactory kubernetesClientFactory; @Inject public KubernetesInfrastructure( @@ -48,7 +54,8 @@ public class KubernetesInfrastructure extends RuntimeInfrastructure { KubernetesRuntimeContextFactory runtimeContextFactory, Set internalEnvProvisioners, KubernetesRuntimeStateCache runtimeStatusesCache, - KubernetesNamespaceFactory namespaceFactory) { + KubernetesNamespaceFactory namespaceFactory, + KubernetesClientFactory kubernetesClientFactory) { super( NAME, ImmutableSet.of(KubernetesEnvironment.TYPE, Constants.NO_ENVIRONMENT_RECIPE_TYPE), @@ -57,6 +64,7 @@ public class KubernetesInfrastructure extends RuntimeInfrastructure { this.runtimeContextFactory = runtimeContextFactory; this.runtimeStatusesCache = runtimeStatusesCache; this.namespaceFactory = namespaceFactory; + this.kubernetesClientFactory = kubernetesClientFactory; } @Override @@ -81,6 +89,19 @@ public class KubernetesInfrastructure extends RuntimeInfrastructure { return NamespaceNameValidator.isValid(name); } + @Override + public Response sendDirectInfrastructureRequest( + String httpMethod, URI relativeUri, @Nullable HttpHeaders headers, @Nullable InputStream body) + throws InfrastructureException { + return DirectKubernetesAPIAccessHelper.call( + kubernetesClientFactory.getDefaultConfig().getMasterUrl(), + kubernetesClientFactory.getAuthenticatedHttpClient(), + httpMethod, + relativeUri, + headers, + body); + } + @Override protected KubernetesRuntimeContext internalPrepare( RuntimeIdentity id, InternalEnvironment environment) throws InfrastructureException { diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/DirectKubernetesAPIAccessHelperTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/DirectKubernetesAPIAccessHelperTest.java new file mode 100644 index 0000000000..c856a87ad8 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/DirectKubernetesAPIAccessHelperTest.java @@ -0,0 +1,257 @@ +/* + * 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; + +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.google.common.collect.ImmutableMap; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedHashMap; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.lang.IoUtil; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class DirectKubernetesAPIAccessHelperTest { + + @Mock private OkHttpClient client; + @Mock private Call call; + @Mock private HttpHeaders headers; + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + + @BeforeMethod + public void setup() { + when(headers.getRequestHeaders()).thenReturn(new MultivaluedHashMap<>()); + when(client.newCall(requestCaptor.capture())).thenReturn(call); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testFailsOnAbsoluteUrlSuppliedAsRelative() throws Exception { + DirectKubernetesAPIAccessHelper.call( + "https://master/", client, "GET", URI.create("https://not-this-way"), headers, null); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testFailsOnOpaqueUrlSuppliedAsRelative() throws Exception { + DirectKubernetesAPIAccessHelper.call( + "https://master/", client, "GET", URI.create("opaque:not-this-way"), headers, null); + } + + @Test + public void testSendsDataAsApplicationJsonUtf8IfNotSpecifiedInRequest() throws Exception { + // given + setupResponse(new Response.Builder().code(200)); + + // when + DirectKubernetesAPIAccessHelper.call( + "https://master/", + client, + "POST", + URI.create("somewhere/over/the/rainbow"), + headers, + new ByteArrayInputStream( + "Žluťoučký kůň úpěl ďábelské ódy.".getBytes(StandardCharsets.UTF_8))); + + // then + assertEquals( + requestCaptor.getValue().body().contentType(), + MediaType.get("application/json;charset=UTF-8")); + + Buffer expectedBody = new Buffer(); + expectedBody.write(StandardCharsets.UTF_8.encode("Žluťoučký kůň úpěl ďábelské ódy.")); + + Buffer body = new Buffer(); + requestCaptor.getValue().body().writeTo(body); + + assertEquals(body, expectedBody); + } + + @Test + public void testSendsRequestHeaders() throws Exception { + // given + when(headers.getRequestHeaders()) + .thenReturn( + new MultivaluedHashMap<>( + ImmutableMap.of( + "ducks", "many", "geese", "volumes", "Content-Type", "text/literary"))); + setupResponse(new Response.Builder().code(200)); + + // when + javax.ws.rs.core.Response response = + DirectKubernetesAPIAccessHelper.call( + "https://master/", + client, + "POST", + URI.create("somewhere/over/the/rainbow"), + headers, + new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8))); + + // then + assertEquals(requestCaptor.getValue().header("ducks"), "many"); + assertEquals(requestCaptor.getValue().header("geese"), "volumes"); + assertEquals(requestCaptor.getValue().header("Content-Type"), "text/literary"); + } + + @Test + public void testBodySentIntact() throws Exception { + // given + when(headers.getRequestHeaders()) + .thenReturn( + new MultivaluedHashMap<>( + ImmutableMap.of( + "ducks", "many", "geese", "volumes", "Content-Type", "text/literary"))); + setupResponse(new Response.Builder().code(200)); + + // when + javax.ws.rs.core.Response response = + DirectKubernetesAPIAccessHelper.call( + "https://master/", + client, + "POST", + URI.create("somewhere/over/the/rainbow"), + headers, + new ByteArrayInputStream( + "Žluťoučký kůň úpěl ďábelské ódy.".getBytes(StandardCharsets.UTF_16BE))); + + // then + Buffer expectedBody = new Buffer(); + expectedBody.write(StandardCharsets.UTF_16BE.encode("Žluťoučký kůň úpěl ďábelské ódy.")); + + Buffer body = new Buffer(); + requestCaptor.getValue().body().writeTo(body); + + assertEquals(body, expectedBody); + } + + @Test + public void testHonorsRequestCharset() throws Exception { + // given + when(headers.getRequestHeaders()) + .thenReturn( + new MultivaluedHashMap<>( + ImmutableMap.of("Content-Type", "text/plain;charset=utf-16be"))); + when(headers.getMediaType()) + .thenReturn(javax.ws.rs.core.MediaType.valueOf("text/plain;charset=utf-16be")); + + setupResponse(new Response.Builder().code(200)); + + // when + DirectKubernetesAPIAccessHelper.call( + "https://master/", + client, + "POST", + URI.create("somewhere/over/the/rainbow"), + headers, + new ByteArrayInputStream( + "Žluťoučký kůň úpěl ďábelské ódy.".getBytes(StandardCharsets.UTF_16BE))); + + // then + Request req = requestCaptor.getValue(); + + assertEquals(req.header("Content-Type"), "text/plain;charset=utf-16be"); + assertEquals(req.body().contentType(), MediaType.parse("text/plain;charset=utf-16be")); + + Buffer expectedBody = new Buffer(); + expectedBody.write(StandardCharsets.UTF_16BE.encode("Žluťoučký kůň úpěl ďábelské ódy.")); + + Buffer body = new Buffer(); + req.body().writeTo(body); + + assertEquals(body, expectedBody); + } + + @Test + public void testResponseContainsHeaders() throws Exception { + // given + setupResponse(new Response.Builder().code(200).header("header", "value")); + + // when + javax.ws.rs.core.Response response = + DirectKubernetesAPIAccessHelper.call( + "https://master/", + client, + "POST", + URI.create("somewhere/over/the/rainbow"), + headers, + new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8))); + + // then + assertEquals(response.getStringHeaders().get("header").get(0), "value"); + } + + @Test + public void testResponseContainsBody() throws Exception { + // given + setupResponse( + new Response.Builder() + .code(200) + .body(ResponseBody.create(MediaType.get("application/json"), "true"))); + + // when + javax.ws.rs.core.Response response = + DirectKubernetesAPIAccessHelper.call( + "https://master/", + client, + "POST", + URI.create("somewhere/over/the/rainbow"), + headers, + new ByteArrayInputStream("null".getBytes(StandardCharsets.UTF_8))); + + // then + assertEquals( + response.getMediaType(), + javax.ws.rs.core.MediaType.valueOf("application/json; charset=utf-8")); + assertEquals(IoUtil.readAndCloseQuietly((InputStream) response.getEntity()), "true"); + } + + @Test + public void testEmptyHeadersHandled() throws Exception { + setupResponse(new Response.Builder().code(200)); + + // when + javax.ws.rs.core.Response response = + DirectKubernetesAPIAccessHelper.call( + "https://master/", client, "GET", URI.create("somewhere/over/the/rainbow"), null, null); + + // then + assertEquals(200, response.getStatus()); + } + + private void setupResponse(Response.Builder response) throws Exception { + when(call.execute()) + .thenAnswer( + inv -> + response + .request(requestCaptor.getValue()) + .message("") + .protocol(Protocol.HTTP_1_1) + .build()); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructureTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructureTest.java new file mode 100644 index 0000000000..84dcac31f4 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfrastructureTest.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; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.fabric8.kubernetes.client.Config; +import java.net.URI; +import java.util.Collections; +import javax.ws.rs.core.HttpHeaders; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class KubernetesInfrastructureTest { + + @Mock private KubernetesClientFactory factory; + private KubernetesInfrastructure infra; + + @BeforeMethod + public void setup() { + infra = + new KubernetesInfrastructure( + mock(EventService.class), + mock(KubernetesRuntimeContextFactory.class), + Collections.emptySet(), + mock(KubernetesRuntimeStateCache.class), + mock(KubernetesNamespaceFactory.class), + factory); + + when(factory.getDefaultConfig()).thenReturn(mock(Config.class)); + } + + @Test + public void testUsesAuthenticatedKubernetesClient() throws Exception { + // when + try { + infra.sendDirectInfrastructureRequest( + "GET", URI.create("somewhere/over/the/rainbow"), mock(HttpHeaders.class), null); + } catch (Exception e) { + // we don't care that this fails, because it fails during the execution of the HTTP request + // that we intentionally don't set up fully. + // it is enough for this test to verify that the code is trying to use the authenticated HTTP + // client. + } + + // then + verify(factory).getAuthenticatedHttpClient(); + } +} diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java index c086f15952..8918e0954e 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java @@ -115,6 +115,15 @@ public class OpenShiftClientFactory extends KubernetesClientFactory { return createOC(buildConfig(getDefaultConfig(), null)); } + @Override + public OkHttpClient getAuthenticatedHttpClient() throws InfrastructureException { + if (!configBuilder.isPersonalized()) { + throw new InfrastructureException( + "Not able to construct impersonating openshift API client."); + } + return clientForConfig(buildConfig(getDefaultConfig(), null)); + } + @Override protected Config buildDefaultConfig(String masterUrl, Boolean doTrustCerts) { OpenShiftConfigBuilder configBuilder = new OpenShiftConfigBuilder(); @@ -206,18 +215,19 @@ public class OpenShiftClientFactory extends KubernetesClientFactory { } private OpenShiftClient createOC(Config config) { + return new UnclosableOpenShiftClient(clientForConfig(config), config); + } + + private OkHttpClient clientForConfig(Config config) { OkHttpClient clientHttpClient = getHttpClient().newBuilder().authenticator(Authenticator.NONE).build(); OkHttpClient.Builder builder = clientHttpClient.newBuilder(); builder.interceptors().clear(); - clientHttpClient = - builder - .addInterceptor( - new OpenShiftOAuthInterceptor(clientHttpClient, OpenShiftConfig.wrap(config))) - .addInterceptor(new ImpersonatorInterceptor(config)) - .build(); - - return new UnclosableOpenShiftClient(clientHttpClient, config); + return builder + .addInterceptor( + new OpenShiftOAuthInterceptor(clientHttpClient, OpenShiftConfig.wrap(config))) + .addInterceptor(new ImpersonatorInterceptor(config)) + .build(); } /** Decorates the {@link DefaultOpenShiftClient} so that it can not be closed from the outside. */ diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructure.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructure.java index da1fb05314..12a493b5c3 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructure.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructure.java @@ -15,9 +15,13 @@ import static java.lang.String.format; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; +import java.io.InputStream; +import java.net.URI; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.workspace.server.NoEnvironmentFactory.NoEnvInternalEnvironment; @@ -28,6 +32,8 @@ import org.eclipse.che.api.workspace.server.spi.RuntimeInfrastructure; import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; import org.eclipse.che.api.workspace.server.spi.provision.InternalEnvironmentProvisioner; import org.eclipse.che.api.workspace.shared.Constants; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.workspace.infrastructure.kubernetes.DirectKubernetesAPIAccessHelper; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.NamespaceNameValidator; @@ -43,6 +49,7 @@ public class OpenShiftInfrastructure extends RuntimeInfrastructure { private final OpenShiftRuntimeContextFactory runtimeContextFactory; private final KubernetesRuntimeStateCache runtimeStatusesCache; private final OpenShiftProjectFactory projectFactory; + private final OpenShiftClientFactory openShiftClientFactory; @Inject public OpenShiftInfrastructure( @@ -50,7 +57,8 @@ public class OpenShiftInfrastructure extends RuntimeInfrastructure { OpenShiftRuntimeContextFactory runtimeContextFactory, Set internalEnvProvisioners, KubernetesRuntimeStateCache runtimeStatusesCache, - OpenShiftProjectFactory projectFactory) { + OpenShiftProjectFactory projectFactory, + OpenShiftClientFactory openShiftClientFactory) { super( NAME, ImmutableSet.of( @@ -62,6 +70,7 @@ public class OpenShiftInfrastructure extends RuntimeInfrastructure { this.runtimeContextFactory = runtimeContextFactory; this.runtimeStatusesCache = runtimeStatusesCache; this.projectFactory = projectFactory; + this.openShiftClientFactory = openShiftClientFactory; } @Override @@ -92,6 +101,19 @@ public class OpenShiftInfrastructure extends RuntimeInfrastructure { return runtimeContextFactory.create(asOpenShiftEnv(environment), identity, this); } + @Override + public Response sendDirectInfrastructureRequest( + String httpMethod, URI relativeUri, @Nullable HttpHeaders headers, @Nullable InputStream body) + throws InfrastructureException { + return DirectKubernetesAPIAccessHelper.call( + openShiftClientFactory.getDefaultConfig().getMasterUrl(), + openShiftClientFactory.getAuthenticatedHttpClient(), + httpMethod, + relativeUri, + headers, + body); + } + private OpenShiftEnvironment asOpenShiftEnv(InternalEnvironment source) throws InfrastructureException { if (source instanceof NoEnvInternalEnvironment) { diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructureTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructureTest.java new file mode 100644 index 0000000000..4f8f4ddd4c --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfrastructureTest.java @@ -0,0 +1,66 @@ +/* + * 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; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.fabric8.kubernetes.client.Config; +import java.net.URI; +import java.util.Collections; +import javax.ws.rs.core.HttpHeaders; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; +import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProjectFactory; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class OpenShiftInfrastructureTest { + @Mock private OpenShiftClientFactory factory; + private OpenShiftInfrastructure infra; + + @BeforeMethod + public void setup() { + infra = + new OpenShiftInfrastructure( + mock(EventService.class), + mock(OpenShiftRuntimeContextFactory.class), + Collections.emptySet(), + mock(KubernetesRuntimeStateCache.class), + mock(OpenShiftProjectFactory.class), + factory); + + when(factory.getDefaultConfig()).thenReturn(mock(Config.class)); + } + + @Test + public void testUsesAuthenticatedKubernetesClient() throws Exception { + // when + try { + infra.sendDirectInfrastructureRequest( + "GET", URI.create("somewhere/over/the/rainbow"), mock(HttpHeaders.class), null); + } catch (Exception e) { + // we don't care that this fails, because it fails during the execution of the HTTP request + // that we intentionally don't set up fully. + // it is enough for this test to verify that the code is trying to use the authenticated HTTP + // client. + } + + // then + verify(factory).getAuthenticatedHttpClient(); + } +} diff --git a/pom.xml b/pom.xml index ad22ea85e0..5423ba96b0 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ 2.4.0 0.1.54 3.12.6 + 1.15.0 1.11 1.19 1.3.3 @@ -251,6 +252,11 @@ okhttp ${com.squareup.okhttp3.version} + + com.squareup.okio + okio + ${com.squareup.okio.version} + commons-codec commons-codec @@ -764,6 +770,11 @@ che-core-api-git-shared ${che.version} + + org.eclipse.che.core + che-core-api-infraproxy + ${che.version} + org.eclipse.che.core che-core-api-infrastructure-local diff --git a/wsmaster/che-core-api-infraproxy/pom.xml b/wsmaster/che-core-api-infraproxy/pom.xml new file mode 100644 index 0000000000..607e808deb --- /dev/null +++ b/wsmaster/che-core-api-infraproxy/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + che-master-parent + org.eclipse.che.core + 7.23.0-SNAPSHOT + + che-core-api-infraproxy + jar + Che Core :: API :: InfraProxy + Provides direct HTTP access to the underlying infrastructure web API. + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.guava + guava + + + com.google.inject + guice + + + io.swagger + swagger-annotations + + + javax.inject + javax.inject + + + javax.ws.rs + javax.ws.rs-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-commons-annotations + + + com.jayway.restassured + rest-assured + test + + + org.everrest + everrest-assured + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + org.testng + testng + test + + + diff --git a/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfraProxyModule.java b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfraProxyModule.java new file mode 100644 index 0000000000..788f3f61f0 --- /dev/null +++ b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfraProxyModule.java @@ -0,0 +1,22 @@ +/* + * 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.api.infraproxy.server; + +import com.google.inject.AbstractModule; + +/** Guice module class configuring the infra proxy. */ +public class InfraProxyModule extends AbstractModule { + @Override + protected void configure() { + bind(InfrastructureApiService.class); + } +} diff --git a/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java new file mode 100644 index 0000000000..f001f97d8d --- /dev/null +++ b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java @@ -0,0 +1,153 @@ +/* + * 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.api.infraproxy.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import io.swagger.annotations.Api; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import javax.inject.Inject; +import javax.inject.Named; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.core.ForbiddenException; +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.RuntimeInfrastructure; +import org.eclipse.che.commons.annotation.Nullable; + +/** + * We use this to give our clients the direct access to the underlying infrastructure REST API. This + * is only allowed when we can properly impersonate the user - e.g. on OpenShift with OpenShift + * OAuth switched on. + */ +@Api(InfrastructureApiService.PATH_PREFIX) +@Beta +@Path(InfrastructureApiService.PATH_PREFIX) +public class InfrastructureApiService extends Service { + static final String PATH_PREFIX = "/unsupported/k8s"; + private static final int PATH_PREFIX_LENGTH = PATH_PREFIX.length(); + + private final boolean allowed; + private final RuntimeInfrastructure runtimeInfrastructure; + private final ObjectMapper mapper; + + @Context private MediaType mediaType; + + private static boolean determineAllowed(String infra, String identityProvider) { + return "openshift".equals(infra) + && identityProvider != null + && identityProvider.startsWith("openshift"); + } + + @Inject + public InfrastructureApiService( + @Nullable @Named("che.infra.openshift.oauth_identity_provider") String identityProvider, + RuntimeInfrastructure runtimeInfrastructure) { + this(System.getenv("CHE_INFRASTRUCTURE_ACTIVE"), identityProvider, runtimeInfrastructure); + } + + @VisibleForTesting + InfrastructureApiService(String infraName, String identityProvider, RuntimeInfrastructure infra) { + this.runtimeInfrastructure = infra; + this.mapper = new ObjectMapper(); + this.allowed = determineAllowed(infraName, identityProvider); + } + + @GET + @Path("{path:.+}") + public Response get(@Context HttpHeaders headers) + throws InfrastructureException, ApiException, IOException { + return request("GET", headers, null); + } + + @HEAD + @Path("{path:.+}") + public Response head(@Context HttpHeaders headers) + throws InfrastructureException, ApiException, IOException { + return request("HEAD", headers, null); + } + + @POST + @Path("{path:.+}") + public Response post(@Context HttpHeaders headers, InputStream body) + throws InfrastructureException, IOException, ApiException { + return request("POST", headers, body); + } + + @DELETE + @Path("{path:.+}") + public Response delete(@Context HttpHeaders headers, InputStream body) + throws InfrastructureException, IOException, ApiException { + return request("DELETE", headers, body); + } + + @PUT + @Path("{path:.+}") + public Response put(@Context HttpHeaders headers, InputStream body) + throws InfrastructureException, IOException, ApiException { + return request("PUT", headers, body); + } + + @OPTIONS + @Path("{path:.+}") + public Response options(@Context HttpHeaders headers) + throws InfrastructureException, ApiException, IOException { + return request("OPTIONS", headers, null); + } + + @PATCH + @Path("{path:.+}") + public Response patch(@Context HttpHeaders headers, InputStream body) + throws InfrastructureException, IOException, ApiException { + return request("PATCH", headers, body); + } + + private void auth() throws ApiException { + if (!allowed) { + throw new ForbiddenException( + "Interaction with backing infrastructure is only allowed in multi-user mode with OpenShift OAuth"); + } + } + + private Response request(String method, HttpHeaders headers, @Nullable InputStream body) + throws ApiException, IOException, InfrastructureException { + auth(); + return runtimeInfrastructure.sendDirectInfrastructureRequest( + method, relativizeRequestAndStripPrefix(), headers, body); + } + + /** + * We need to strip our prefix from the request path before sending it to the infrastructure. The + * infrastructure is unaware of where we deployed our proxy. + * + * @return the relative URI composed from the current request + */ + private URI relativizeRequestAndStripPrefix() { + URI unstrippedRelative = uriInfo.getBaseUri().relativize(uriInfo.getRequestUri()); + String str = unstrippedRelative.toString(); + return URI.create(str.substring(PATH_PREFIX_LENGTH)); + } +} diff --git a/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/PATCH.java b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/PATCH.java new file mode 100644 index 0000000000..02862c21db --- /dev/null +++ b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/PATCH.java @@ -0,0 +1,30 @@ +/* + * 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.api.infraproxy.server; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.ws.rs.HttpMethod; + +/** + * Kubernetes API accepts PATCH requests but JAX-RS doesn't provide the annotation for such requests + * out of the box. So we need to declare one. + */ +@Target(METHOD) +@Retention(RUNTIME) +@HttpMethod("PATCH") +@Documented +public @interface PATCH {} diff --git a/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java b/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java new file mode 100644 index 0000000000..11e55de725 --- /dev/null +++ b/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java @@ -0,0 +1,217 @@ +/* + * 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.api.infraproxy.server; + +import static com.jayway.restassured.RestAssured.given; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.jayway.restassured.response.Response; +import org.eclipse.che.api.core.rest.ApiExceptionMapper; +import org.eclipse.che.api.workspace.server.spi.RuntimeInfrastructure; +import org.everrest.assured.EverrestJetty; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners({EverrestJetty.class, MockitoTestNGListener.class}) +public class InfrastructureApiServiceTest { + @SuppressWarnings("unused") // is declared for use by everrest-assured + ApiExceptionMapper exceptionMapper = new ApiExceptionMapper(); + + @Mock RuntimeInfrastructure infra; + InfrastructureApiService apiService; + + @BeforeMethod + public void setup() throws Exception { + apiService = new InfrastructureApiService("openshift", "openshift-identityProvider", infra); + } + + @Test + public void testFailsAuthWhenNotOnOpenShift() throws Exception { + // given + apiService = new InfrastructureApiService("not-openshift", "openshift-identityProvider", infra); + + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .when() + .get("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 403); + } + + @Test + public void testFailsAuthWhenNotUsingOpenShiftIdentityProvider() throws Exception { + // given + apiService = new InfrastructureApiService("openshift", "not-openshift-identityProvider", infra); + + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .when() + .get("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 403); + } + + @Test + public void testGet() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), eq(null))) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .when() + .get("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getContentType(), "application/json;charset=utf-8"); + } + + @Test + public void testPost() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any())) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .body("true") + .when() + .post("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getContentType(), "application/json;charset=utf-8"); + } + + @Test + public void testPut() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any())) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .body("true") + .when() + .put("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getContentType(), "application/json;charset=utf-8"); + } + + @Test + public void testHead() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any())) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .when() + .head("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + } + + @Test + public void testDelete() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any())) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .body("true") + .when() + .delete("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getContentType(), "application/json;charset=utf-8"); + } + + @Test + public void testOptions() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any())) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .when() + .options("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getContentType(), "application/json;charset=utf-8"); + } + + @Test + public void testPatch() throws Exception { + // given + when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any())) + .thenReturn( + javax.ws.rs.core.Response.ok() + .header("Content-Type", "application/json; charset=utf-8") + .build()); + // when + Response response = + given() + .contentType("application/json; charset=utf-8") + .body("true") + .when() + .patch("/unsupported/k8s/nazdar/"); + + // then + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getContentType(), "application/json;charset=utf-8"); + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/spi/RuntimeInfrastructure.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/spi/RuntimeInfrastructure.java index 61891318a1..1378d228f1 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/spi/RuntimeInfrastructure.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/spi/RuntimeInfrastructure.java @@ -13,14 +13,19 @@ package org.eclipse.che.api.workspace.server.spi; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; +import java.io.InputStream; +import java.net.URI; import java.util.Collection; import java.util.Objects; import java.util.Set; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; import org.eclipse.che.api.core.ValidationException; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; import org.eclipse.che.api.workspace.server.spi.provision.InternalEnvironmentProvisioner; +import org.eclipse.che.commons.annotation.Nullable; /** * Starting point of describing the contract which infrastructure provider should implement for @@ -152,4 +157,21 @@ public abstract class RuntimeInfrastructure { protected abstract RuntimeContext internalPrepare( RuntimeIdentity identity, InternalEnvironment environment) throws ValidationException, InfrastructureException; + + /** + * This is a very dangerous method that should be used with care. + * + *

The implementation of this method needs to make sure that it properly impersonates the + * current user when performing the request. + * + * @param httpMethod the http method to use + * @param relativeUri the URI to request - this must be a relative URI that is appended to the + * master URL of the infrastructure + * @param headers the HTTP headers to send + * @param body the optional body of the request + * @return the response from the backing infrastructure + */ + public abstract Response sendDirectInfrastructureRequest( + String httpMethod, URI relativeUri, @Nullable HttpHeaders headers, @Nullable InputStream body) + throws InfrastructureException; } diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java index 5fc7e05a77..4977ba0df3 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java @@ -42,6 +42,8 @@ import static org.testng.AssertJUnit.assertTrue; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.io.InputStream; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -53,6 +55,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; import org.eclipse.che.account.spi.AccountImpl; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; @@ -1070,6 +1074,12 @@ public class WorkspaceRuntimesTest { public RuntimeContext internalPrepare(RuntimeIdentity id, InternalEnvironment environment) { throw new UnsupportedOperationException(); } + + @Override + public Response sendDirectInfrastructureRequest( + String httpMethod, URI relativeUri, HttpHeaders headers, InputStream body) { + throw new UnsupportedOperationException(); + } } private static class TestInternalRuntime extends InternalRuntime { diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml index 05f1bfa805..b3825cf34d 100644 --- a/wsmaster/pom.xml +++ b/wsmaster/pom.xml @@ -32,6 +32,7 @@ che-core-api-workspace che-core-api-workspace-activity che-core-api-user-shared + che-core-api-infraproxy che-core-api-devfile-shared che-core-api-devfile che-core-api-account