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
parent
2f2113b9a7
commit
25a7d7f24e
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
11
pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue