Provides direct access to the kubernetes API (#18367)

Fixes #18326 - Provides direct access to the kubernetes API on /api/unsupported/k8s.
Restricted to OpenShift with OpenShift OAuth.

Signed-off-by: Lukas Krejci <lkrejci@redhat.com>
7.24.x
Lukas Krejci 2020-11-24 16:12:44 +01:00 committed by GitHub
parent 2f2113b9a7
commit 25a7d7f24e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1223 additions and 12 deletions

View File

@ -135,6 +135,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-factory-github</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-infraproxy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-logger</artifactId>

View File

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

View File

@ -69,6 +69,10 @@
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
@ -247,6 +251,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-testng</artifactId>

View File

@ -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<String, List<String>> e : headers.getRequestHeaders().entrySet()) {
String name = e.getKey();
List<String> 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);
}
}
}
}

View File

@ -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.
*
* <p>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;
}

View File

@ -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<InternalEnvironmentProvisioner> 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 {

View File

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

View File

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

View File

@ -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. */

View File

@ -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<InternalEnvironmentProvisioner> 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) {

View File

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

11
pom.xml
View File

@ -60,6 +60,7 @@
<com.jayway.restassured.version>2.4.0</com.jayway.restassured.version>
<com.jcraft.jsch.version>0.1.54</com.jcraft.jsch.version>
<com.squareup.okhttp3.version>3.12.6</com.squareup.okhttp3.version>
<com.squareup.okio.version>1.15.0</com.squareup.okio.version>
<commons-codec.version>1.11</commons-codec.version>
<commons-compress.version>1.19</commons-compress.version>
<commons-fileupload.version>1.3.3</commons-fileupload.version>
@ -251,6 +252,11 @@
<artifactId>okhttp</artifactId>
<version>${com.squareup.okhttp3.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>${com.squareup.okio.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
@ -764,6 +770,11 @@
<artifactId>che-core-api-git-shared</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-infraproxy</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-infrastructure-local</artifactId>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-master-parent</artifactId>
<groupId>org.eclipse.che.core</groupId>
<version>7.23.0-SNAPSHOT</version>
</parent>
<artifactId>che-core-api-infraproxy</artifactId>
<packaging>jar</packaging>
<name>Che Core :: API :: InfraProxy</name>
<description>Provides direct HTTP access to the underlying infrastructure web API.</description>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -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 {}

View File

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

View File

@ -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.
*
* <p>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;
}

View File

@ -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<RuntimeContext> {

View File

@ -32,6 +32,7 @@
<module>che-core-api-workspace</module>
<module>che-core-api-workspace-activity</module>
<module>che-core-api-user-shared</module>
<module>che-core-api-infraproxy</module>
<module>che-core-api-devfile-shared</module>
<module>che-core-api-devfile</module>
<module>che-core-api-account</module>