diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml
index d02883b0d..f6f3c23af 100644
--- a/assembly/assembly-wsmaster-war/pom.xml
+++ b/assembly/assembly-wsmaster-war/pom.xml
@@ -44,6 +44,10 @@
com.redhat.che
che-plugin-analytics-wsmaster
+
+ com.redhat.che
+ fabric8-cdn-support
+
com.redhat.che
fabric8-end2end-flow
diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/rh-che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/rh-che.properties
index ca81aee0a..e119f7687 100644
--- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/rh-che.properties
+++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/rh-che.properties
@@ -41,3 +41,11 @@ che.fabric8.end2end.protect.secret_key=NULL
# End2End Flow bot protection: option to enable the use of the client IP
# in the captcha server-side verification
che.fabric8.end2end.protect.verify_with_ip=false
+
+# Full identifier of the default editor whose CDN resources
+# should be prefetched during the dashboard client-side load
+# - Can be the identifier of an editor plugin in the plugin registry, like that:
+# org.eclipse.che.editor.theia:1.0.0
+# - or the full path to a custom editor plugin, like that:
+# https://raw.githubusercontent.com/davidfestal/che-theia-cdn/master/plugins/org.eclipse.che.editor.theia.david:1.0.2
+che.fabric8.cdn.prefetch.editor=NULL
diff --git a/assembly/fabric8-ide-dashboard-war/pom.xml b/assembly/fabric8-ide-dashboard-war/pom.xml
index 1b670f964..88c8aa75a 100644
--- a/assembly/fabric8-ide-dashboard-war/pom.xml
+++ b/assembly/fabric8-ide-dashboard-war/pom.xml
@@ -212,7 +212,7 @@
- >
+
diff --git a/assembly/fabric8-ide-dashboard-war/src/components/ide-fetcher/che-ide-fetcher.service.ts b/assembly/fabric8-ide-dashboard-war/src/components/ide-fetcher/che-ide-fetcher.service.ts
new file mode 100644
index 000000000..00d81694f
--- /dev/null
+++ b/assembly/fabric8-ide-dashboard-war/src/components/ide-fetcher/che-ide-fetcher.service.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2016-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
+ */
+'use strict';
+import {CheBranding} from '../branding/che-branding.factory';
+
+const IDE_FETCHER_CALLBACK_ID = 'cheIdeFetcherCallback';
+
+/**
+ * Provides a way to download IDE .js and then cache it before trying to load the IDE
+ * @author Florent Benoit
+ * @author Oleksii Orel
+ * @author David Festal
+ */
+export class CheIdeFetcher {
+
+ static $inject = ['$log', '$http', '$window', 'cheBranding'];
+
+ private $log: ng.ILogService;
+ private $http: ng.IHttpService;
+ private $window: ng.IWindowService;
+ private cheBranding: CheBranding;
+ private userAgent: string;
+
+ /**
+ * Default constructor that is using resource injection
+ */
+ constructor($log: ng.ILogService, $http: ng.IHttpService, $window: ng.IWindowService, cheBranding: CheBranding) {
+ this.$log = $log;
+ this.$http = $http;
+ this.$window = $window;
+ this.cheBranding = cheBranding;
+
+ this.userAgent = this.getUserAgent();
+
+ const callback = () => {
+ this.findMappingFile();
+ this.cheBranding.unregisterCallback(IDE_FETCHER_CALLBACK_ID);
+ };
+ this.cheBranding.registerCallback(IDE_FETCHER_CALLBACK_ID, callback.bind(this));
+ }
+
+ /**
+ * Gets user agent.
+ * @returns {string}
+ */
+ getUserAgent(): string {
+ const userAgent = this.$window.navigator.userAgent.toLowerCase();
+ const docMode = (this.$window.document).documentMode;
+
+ if (userAgent.indexOf('webkit') !== -1) {
+ return 'safari';
+ } else if (userAgent.indexOf('msie') !== -1) {
+ if (docMode >= 10 && docMode < 11) {
+ return 'ie10';
+ } else if (docMode >= 9 && docMode < 11) {
+ return 'ie9';
+ } else if (docMode >= 8 && docMode < 11) {
+ return 'ie8';
+ }
+ } else if (userAgent.indexOf('gecko') !== -1) {
+ return 'gecko1_8';
+ }
+
+ return 'unknown';
+ }
+
+ /**
+ * Finds mapping file.
+ */
+ findMappingFile(): void {
+ // get the content of the compilation mapping file
+ const randVal = Math.floor((Math.random() * 1000000) + 1);
+ const resourcesPath = this.cheBranding.getIdeResourcesPath();
+ if (!resourcesPath) {
+ this.$log.log('Unable to get IDE resources path');
+ return;
+ }
+
+ const fileMappingUrl = `${resourcesPath}compilation-mappings.txt?uid=${randVal}`;
+
+ this.$http.get(fileMappingUrl).then((response: {data: any}) => {
+ if (!response || !response.data) {
+ return;
+ }
+ let urlToLoad = this.getIdeUrlMappingFile(response.data);
+ // load the url
+ if (angular.isDefined(urlToLoad)) {
+ this.$log.log('Preloading IDE javascript', urlToLoad);
+ this.$http.get(urlToLoad, { cache: true});
+ } else {
+ this.$log.error('Unable to find the IDE javascript file to cache');
+ }
+ }, (error: any) => {
+ this.$log.log('unable to find compilation mapping file', error);
+ });
+
+ this.$http.get('/api/cdn-support/paths', {cache: false}).then((response: {data: any}) => {
+ if (!response || !response.data) {
+ return;
+ }
+ const data = response.data;
+ data.forEach((entry) => {
+ let urlToLoad = entry.cdn;
+ // load the url
+ if (angular.isDefined(urlToLoad)) {
+ this.$log.log('Preloading Theia resource', urlToLoad);
+ const link = document.createElement("link");
+ link.rel='prefetch';
+ link.href=urlToLoad;
+ document.head.appendChild(link);
+ } else {
+ this.$log.error('Unable to find the Theia resource file to cache');
+ }
+ });
+ }, (error: any) => {
+ this.$log.log('Unable to find Theia CDN resources to cache', error);
+ });
+ }
+
+ /**
+ * Gets URL of mapping file.
+ * @param data {string}
+ * @returns {string}
+ */
+ getIdeUrlMappingFile(data: string): string {
+ let mappingFileUrl: string;
+ let javascriptFileName: string;
+ const mappings = data.split(new RegExp('^\\n', 'gm'));
+ const isPasses = mappings.some((mapping: string) => {
+ const subMappings = mapping.split('\n');
+ const userAgent = subMappings.find((subMapping: string) => {
+ return subMapping.startsWith('user.agent ');
+ }).split(' ')[1];
+ javascriptFileName = subMappings.find((subMapping: string) => {
+ return subMapping.endsWith('.cache.js');
+ });
+ return javascriptFileName && userAgent && this.userAgent === userAgent;
+ });
+ if (isPasses && javascriptFileName) {
+ mappingFileUrl = this.cheBranding.getIdeResourcesPath() + javascriptFileName;
+ }
+
+ return mappingFileUrl;
+ }
+
+}
diff --git a/dev-scripts/deploy_custom_rh-che.sh b/dev-scripts/deploy_custom_rh-che.sh
index 2be554b6d..d57630288 100755
--- a/dev-scripts/deploy_custom_rh-che.sh
+++ b/dev-scripts/deploy_custom_rh-che.sh
@@ -306,7 +306,8 @@ CHE_CONFIG_YAML=$(yq ".\"data\".\"CHE_KEYCLOAK_REALM\" = \"NULL\" |
.\"data\".\"che.jdbc.username\" = \"$RH_CHE_JDBC_USERNAME\" |
.\"data\".\"che.jdbc.password\" = \"$RH_CHE_JDBC_PASSWORD\" |
.\"data\".\"che.jdbc.url\" = \"$RH_CHE_JDBC_URL\" |
- .\"data\".\"CHE_LOG_LEVEL\" = \"plaintext\" " ${RH_CHE_CONFIG})
+ .\"data\".\"CHE_LOG_LEVEL\" = \"INFO\" |
+ .\"data\".\"CHE_LOGS_APPENDERS_IMPL\" = \"plaintext\" " ${RH_CHE_CONFIG})
CHE_CONFIG_YAML=$(echo "$CHE_CONFIG_YAML" | \
yq ".\"data\".\"CHE_HOST\" = \"rhche-$RH_CHE_PROJECT_NAMESPACE.devtools-dev.ext.devshift.net\" |
diff --git a/dockerfiles/che-fabric8/Dockerfile b/dockerfiles/che-fabric8/Dockerfile
index 644c972ca..2fab89014 100644
--- a/dockerfiles/che-fabric8/Dockerfile
+++ b/dockerfiles/che-fabric8/Dockerfile
@@ -23,6 +23,7 @@ RUN yum -y update && \
curl -sSL "https://${DOCKER_BUCKET}/builds/Linux/x86_64/docker-${DOCKER_VERSION}" -o /usr/bin/docker && \
chmod +x /usr/bin/docker && \
yum -y remove openssl && \
+ yum -y install skopeo \
yum clean all && \
echo "%root ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
sed -i 's/Defaults requiretty/#Defaults requiretty/g' /etc/sudoers && \
diff --git a/openshift/rh-che.config.yaml b/openshift/rh-che.config.yaml
index 1176c9527..5459b6edc 100644
--- a/openshift/rh-che.config.yaml
+++ b/openshift/rh-che.config.yaml
@@ -70,3 +70,4 @@ data:
CHE_INFRA_KUBERNETES_SERVICE__ACCOUNT__NAME: "che-workspace"
CHE_WORKSPACE_STORAGE: "/home/user/che/workspaces"
CHE_WORKSPACE_PLUGIN__BROKER_INIT_IMAGE: "eclipse/che-init-plugin-broker:v0.7.1"
+ CHE_FABRIC8_CDN_PREFETCH_EDITOR: 'NULL'
diff --git a/plugins/fabric8-cdn-support/pom.xml b/plugins/fabric8-cdn-support/pom.xml
new file mode 100644
index 000000000..cda0cc645
--- /dev/null
+++ b/plugins/fabric8-cdn-support/pom.xml
@@ -0,0 +1,114 @@
+
+
+
+ 4.0.0
+
+ fabric8-ide-plugins-parent
+ com.redhat.che
+ 1.0.0-SNAPSHOT
+ ../pom.xml
+
+ fabric8-cdn-support
+ Fabric8 IDE :: Plugins :: CDN Support
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+
+
+ com.google.guava
+ guava
+
+
+ com.google.inject
+ guice
+
+
+ javax.inject
+ javax.inject
+
+
+ javax.ws.rs
+ javax.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-workspace
+
+
+ org.eclipse.che.core
+ che-core-api-workspace-shared
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.core
+ che-core-commons-inject
+
+
+ org.slf4j
+ slf4j-api
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ org.eclipse.che.core
+ che-core-db
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
diff --git a/plugins/fabric8-cdn-support/src/main/java/com/redhat/che/cdn/CdnSupportService.java b/plugins/fabric8-cdn-support/src/main/java/com/redhat/che/cdn/CdnSupportService.java
new file mode 100644
index 000000000..0ec8c8dea
--- /dev/null
+++ b/plugins/fabric8-cdn-support/src/main/java/com/redhat/che/cdn/CdnSupportService.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (c) 2016-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 com.redhat.che.cdn;
+
+import static java.lang.String.format;
+import static java.util.Spliterators.spliteratorUnknownSize;
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.runAsync;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Stream.of;
+import static java.util.stream.StreamSupport.stream;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import org.eclipse.che.api.core.ApiException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.api.core.util.LineConsumer;
+import org.eclipse.che.api.core.util.ListLineConsumer;
+import org.eclipse.che.api.core.util.ProcessUtil;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.wsplugins.PluginMetaRetriever;
+import org.eclipse.che.api.workspace.server.wsplugins.model.PluginMeta;
+import org.eclipse.che.api.workspace.shared.Constants;
+import org.eclipse.che.commons.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+@Path("cdn-support")
+public class CdnSupportService extends Service {
+ private static final Logger LOG = LoggerFactory.getLogger(CdnSupportService.class);
+ private static final ObjectMapper YAML_PARSER = new ObjectMapper(new YAMLFactory());
+ private static final ObjectMapper JSON_PARSER = new ObjectMapper(new JsonFactory());
+ private static final String SKOPEO_BINARY = "skopeo";
+ private static final String[] SKOPEO_HELP_ARGS = new String[] {"--help"};
+ private static final long SKOPEO_HELP_TIMEOUT_SECONDS = 2;
+ private static final String SKOPEO_INSPECT_ARG = "inspect";
+ private static final String SKOPEO_IMAGE_PREFIX = "docker://";
+ private static final long SKOPEO_INSPECT_TIMEOUT_SECONDS = 10;
+ private static final String TAR_BINARY = "tar";
+ private static final String TAR_ARG = "-xvf";
+ private static final long TAR_TIMEOUT_SECONDS = 5;
+ private static final String CHE_PLUGIN_YAML_FILE_NAME = "che-plugin.yaml";
+ private static final String CHE_PLUGIN_TGZ_FILE_NAME = "che-plugin.tar.gz";
+ private static final String TEMP_DIR_PREFIX = "che-plugin-archive-dir";
+ private static final String LABEL_NAME = "che-plugin.cdn.artifacts";
+
+ private static final CompletableFuture>[] FUTURE_ARRAY = new CompletableFuture>[0];
+ private static final CommandRunner RUNNER = new CommandRunner();
+
+ private final String editorToPrefetch;
+ private final CommandRunner commandRunner;
+ private final PluginMetaRetriever pluginMetaRetriever;
+ @VisibleForTesting String editorDefinitionUrl = null;
+ @VisibleForTesting String dockerImage = null;
+
+ @Inject
+ public CdnSupportService(
+ PluginMetaRetriever pluginMetaRetriever,
+ @Nullable @Named("che.fabric8.cdn.prefetch.editor") String editorToPrefetch) {
+ this(RUNNER, pluginMetaRetriever, editorToPrefetch);
+ }
+
+ @VisibleForTesting
+ CdnSupportService(
+ CommandRunner commandRunner,
+ PluginMetaRetriever pluginMetaRetriever,
+ @Nullable @Named("che.fabric8.cdn.prefetch.editor") String editorToPrefetch) {
+ this.pluginMetaRetriever = pluginMetaRetriever;
+ this.editorToPrefetch = editorToPrefetch;
+ this.commandRunner = commandRunner;
+
+ // Test that the skopeo process is available
+
+ try {
+ commandRunner.runCommand(
+ SKOPEO_BINARY,
+ SKOPEO_HELP_ARGS,
+ null,
+ SKOPEO_HELP_TIMEOUT_SECONDS,
+ TimeUnit.SECONDS,
+ null,
+ null);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "The `skopeo` command is not available. Please check that is has been correctly installed in the Che server Docker image",
+ e);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ LOG.warn("Exception during the `skopeo --help` command execution", e);
+ }
+ }
+
+ @GET
+ @Path("paths")
+ @Produces(APPLICATION_JSON)
+ public String getPaths() throws Exception {
+ if (editorToPrefetch == null) {
+ throw new NotFoundException("No editor is configured for CDN resource pre-fetching");
+ }
+
+ String url = retrieveEditorPluginUrl();
+ if (!url.equals(editorDefinitionUrl)) {
+ editorDefinitionUrl = url;
+ dockerImage = null;
+ } else {
+ LOG.debug("Editor full definition didn't change");
+ }
+
+ if (dockerImage == null) {
+ dockerImage = readDockerImageName(editorDefinitionUrl);
+ }
+
+ if (dockerImage == null) {
+ throw new ServerException(
+ format(
+ "Plugin container image not found in the plugin descriptor of '%s'",
+ editorToPrefetch));
+ }
+
+ JsonNode json = inspectDockerImage();
+ return json.path("Labels").path(LABEL_NAME).asText("[]");
+ }
+
+ private JsonNode inspectDockerImage()
+ throws IOException, TimeoutException, InterruptedException, ExecutionException,
+ ServerException {
+ LOG.debug("Running {} on image {}", SKOPEO_BINARY, dockerImage);
+ final ListLineConsumer out = commandRunner.newOutputConsumer();
+ final ListLineConsumer err = commandRunner.newErrorConsumer();
+ Process skopeoInspect =
+ commandRunner.runCommand(
+ SKOPEO_BINARY,
+ new String[] {SKOPEO_INSPECT_ARG, SKOPEO_IMAGE_PREFIX + dockerImage},
+ null,
+ SKOPEO_INSPECT_TIMEOUT_SECONDS,
+ TimeUnit.SECONDS,
+ out,
+ err);
+ if (skopeoInspect.exitValue() != 0) {
+ String message =
+ format(
+ "%s failed when trying to retrieve the CDN label of docker image %s - exit code: %d - error output: %s",
+ SKOPEO_BINARY, dockerImage, skopeoInspect.exitValue(), err.getText());
+ LOG.warn(message);
+ throw new ServerException(message);
+ }
+ String skopeoOutput = out.getText();
+ LOG.debug("Result of running skopeo on image {}: {}", dockerImage, skopeoOutput);
+
+ JsonNode json = JSON_PARSER.readTree(skopeoOutput);
+ return json;
+ }
+
+ private String retrieveEditorPluginUrl()
+ throws InfrastructureException, NotFoundException, ServerException {
+ LOG.debug("Searching the editor plugin entry in the plugin registry");
+
+ Collection plugins =
+ pluginMetaRetriever.get(
+ ImmutableMap.of(Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, editorToPrefetch));
+ if (plugins.size() != 1) {
+ throw new NotFoundException("Editor '" + editorToPrefetch + "' is unknown");
+ }
+
+ PluginMeta editor = plugins.toArray(new PluginMeta[] {})[0];
+ if (editor == null) {
+ throw new NotFoundException("Editor '" + editorToPrefetch + "' is unknown");
+ }
+
+ LOG.debug("Retrieving editor URL");
+
+ String url = editor.getUrl();
+ if (url == null) {
+ throw new ServerException(format("URL of editor '%s' is null", editorToPrefetch));
+ }
+ return url;
+ }
+
+ private String readDockerImageName(String editorDefinitionUrl)
+ throws JsonProcessingException, IOException, TimeoutException, InterruptedException,
+ ExecutionException, ApiException {
+ LOG.debug("Creating temp folder");
+ java.nio.file.Path archiveDir =
+ Files.createTempDirectory(FileSystems.getDefault().getPath("/tmp"), TEMP_DIR_PREFIX);
+ java.nio.file.Path archive = archiveDir.resolve(CHE_PLUGIN_TGZ_FILE_NAME);
+
+ LOG.debug("Downloading Editor definition at URL {} into {}", editorDefinitionUrl, archive);
+ try (InputStream in = URI.create(editorDefinitionUrl).toURL().openStream()) {
+ Files.copy(in, archive);
+ } catch (IOException e) {
+ LOG.warn("Exception while downloading", e);
+ throw e;
+ }
+
+ LOG.debug("Unzipping archive");
+ final ListLineConsumer err = commandRunner.newErrorConsumer();
+ Process tar =
+ commandRunner.runCommand(
+ TAR_BINARY,
+ new String[] {TAR_ARG, archive.toRealPath().toString()},
+ archiveDir.toRealPath().toFile(),
+ TAR_TIMEOUT_SECONDS,
+ TimeUnit.SECONDS,
+ null,
+ err);
+ if (tar.exitValue() != 0) {
+ String message =
+ format(
+ "Tar command failed with error status: %d and error log: %s",
+ tar.exitValue(), err.getText());
+ LOG.warn(message);
+ throw new ServerException(message);
+ }
+ java.nio.file.Path pluginYaml = archiveDir.resolve(CHE_PLUGIN_YAML_FILE_NAME);
+
+ LOG.debug("Parse Plugin YAML file: {}", pluginYaml.toAbsolutePath().toString());
+ JsonNode json = YAML_PARSER.readTree(pluginYaml.toFile());
+
+ LOG.debug("Plugin YAML file content: {}", json.toString());
+ return stream(spliteratorUnknownSize(json.path("containers").elements(), 0), false)
+ .findFirst()
+ .map(node -> node.path("image").asText())
+ .orElse(null);
+ }
+
+ @VisibleForTesting
+ static class CommandRunner {
+ @VisibleForTesting
+ Process runCommand(
+ String command,
+ String[] arguments,
+ File directory,
+ long timeout,
+ TimeUnit timeUnit,
+ LineConsumer outputConsumer,
+ LineConsumer errorConsumer)
+ throws IOException, TimeoutException, InterruptedException, ExecutionException {
+ final String[] commandLine = new String[arguments.length + 1];
+ Lists.asList(command, arguments).toArray(commandLine);
+ ProcessBuilder processBuilder = new ProcessBuilder();
+ if (directory != null) {
+ processBuilder.directory(directory);
+ }
+ processBuilder.command(commandLine);
+ LOG.debug("Command: {}", processBuilder.command());
+ LOG.debug("Directory: {}", processBuilder.directory());
+ Process process = processBuilder.start();
+ CompletableFuture readers =
+ allOf(
+ of(outputConsumer, errorConsumer)
+ .map(
+ (LineConsumer consumer) -> {
+ return runAsync(
+ () -> {
+ if (consumer != null) {
+ InputStream is =
+ consumer == outputConsumer
+ ? process.getInputStream()
+ : process.getErrorStream();
+ // consume logs until process ends
+ try (BufferedReader inputReader =
+ new BufferedReader(new InputStreamReader(is))) {
+ String line;
+ while ((line = inputReader.readLine()) != null) {
+ consumer.writeLine(line);
+ }
+ } catch (IOException e) {
+ LOG.error(
+ format(
+ "Failed to complete reading of the process '%s' output or error due to occurred error",
+ Joiner.on(" ").join(commandLine)),
+ e);
+ }
+ }
+ });
+ })
+ .collect(toList())
+ .toArray(FUTURE_ARRAY));
+ try {
+ if (!process.waitFor(timeout, timeUnit)) {
+ try {
+ ProcessUtil.kill(process);
+ } catch (RuntimeException x) {
+ LOG.error(
+ "An error occurred while killing process '{}'", Joiner.on(" ").join(commandLine));
+ }
+ throw new TimeoutException(
+ format(
+ "Process '%s' was terminated by timeout %s %s.",
+ Joiner.on(" ").join(commandLine), timeout, timeUnit.name().toLowerCase()));
+ }
+ } finally {
+ readers.get(2, TimeUnit.SECONDS);
+ }
+
+ return process;
+ }
+
+ @VisibleForTesting
+ ListLineConsumer newOutputConsumer() {
+ return new ListLineConsumer();
+ }
+
+ @VisibleForTesting
+ ListLineConsumer newErrorConsumer() {
+ return new ListLineConsumer();
+ }
+ }
+}
diff --git a/plugins/fabric8-cdn-support/src/main/java/com/redhat/che/cdn/CdnSupportWsMasterModule.java b/plugins/fabric8-cdn-support/src/main/java/com/redhat/che/cdn/CdnSupportWsMasterModule.java
new file mode 100644
index 000000000..e3031b84f
--- /dev/null
+++ b/plugins/fabric8-cdn-support/src/main/java/com/redhat/che/cdn/CdnSupportWsMasterModule.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2016-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 com.redhat.che.cdn;
+
+import com.google.inject.AbstractModule;
+import org.eclipse.che.inject.DynaModule;
+
+/**
+ * Module that allows pushing workspace events to the Segment Analytics tracking tool
+ *
+ * @author David festal
+ */
+@DynaModule
+public class CdnSupportWsMasterModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(CdnSupportService.class);
+ }
+}
diff --git a/plugins/fabric8-cdn-support/src/test/java/com/redhat/che/cdn/CdnSupportServiceTest.java b/plugins/fabric8-cdn-support/src/test/java/com/redhat/che/cdn/CdnSupportServiceTest.java
new file mode 100644
index 000000000..43dcc4eeb
--- /dev/null
+++ b/plugins/fabric8-cdn-support/src/test/java/com/redhat/che/cdn/CdnSupportServiceTest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (c) 2016-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 com.redhat.che.cdn;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.util.ListLineConsumer;
+import org.eclipse.che.api.workspace.server.wsplugins.PluginMetaRetriever;
+import org.eclipse.che.api.workspace.server.wsplugins.model.PluginMeta;
+import org.eclipse.che.api.workspace.shared.Constants;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+import org.testng.collections.Lists;
+
+@Listeners(MockitoTestNGListener.class)
+public class CdnSupportServiceTest {
+ private static String EDITOR_REF = "editor";
+ private static String EDITOR_URL = "http://editorURL";
+ private static String IMAGE_REF = "imageRef";
+
+ @Mock private CdnSupportService.CommandRunner commandRunner;
+ @Mock private PluginMetaRetriever metaRetriever;
+ @Mock private URLConnection urlConnection;
+ @Mock private PluginMeta pluginMeta;
+ @Mock private Process tarProcess;
+ @Mock private Process skopeoHelpProcess;
+ @Mock private Process skopeoInspectProcess;
+ @Mock private ListLineConsumer skopeoOutputConsumer;
+ @Mock private ListLineConsumer skopeoErrorConsumer;
+ @Mock private ListLineConsumer tarErrorConsumer;
+
+ private CdnSupportService service;
+
+ @BeforeClass
+ public void registerURLHandler() {
+ URL.setURLStreamHandlerFactory(
+ new URLStreamHandlerFactory() {
+
+ @Override
+ public URLStreamHandler createURLStreamHandler(String protocol) {
+ if ("http".equals(protocol)) {
+ return new URLStreamHandler() {
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ return urlConnection;
+ }
+ };
+ } else {
+ return null;
+ }
+ }
+ });
+ }
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ service = null;
+ }
+
+ private void setupURLConnectionInputStream(String resourceName) throws IOException {
+ InputStream is = this.getClass().getResourceAsStream(resourceName);
+ doReturn(is).when(urlConnection).getInputStream();
+ }
+
+ @Test(
+ expectedExceptions = {RuntimeException.class},
+ expectedExceptionsMessageRegExp =
+ "The `skopeo` command is not available. Please check that is has been correctly installed in the Che server Docker image")
+ public void throwAtStartIfNoSkopeoBinary() throws Exception {
+ doThrow(IOException.class)
+ .when(commandRunner)
+ .runCommand("skopeo", new String[] {"--help"}, null, 2, TimeUnit.SECONDS, null, null);
+ new CdnSupportService(commandRunner, metaRetriever, "");
+ }
+
+ @Test(
+ expectedExceptions = {NotFoundException.class},
+ expectedExceptionsMessageRegExp = "No editor is configured for CDN resource pre-fetching")
+ public void throwWhenNoPreferredEditor() throws Exception {
+ service = new CdnSupportService(commandRunner, metaRetriever, null);
+ service.getPaths();
+ }
+
+ @Test(
+ expectedExceptions = {NotFoundException.class},
+ expectedExceptionsMessageRegExp = "Editor 'unknownEditor' is unknown")
+ public void throwWhenEditorNotFound() throws Exception {
+ doReturn(Collections.emptyList()).when(metaRetriever).get(any());
+ service = new CdnSupportService(commandRunner, metaRetriever, "unknownEditor");
+ service.getPaths();
+ }
+
+ @Test(
+ expectedExceptions = {NotFoundException.class},
+ expectedExceptionsMessageRegExp = "Editor 'unknownEditor' is unknown")
+ public void throwWhenEditorIsNull() throws Exception {
+ doReturn(Lists.newArrayList(new PluginMeta[] {null}))
+ .when(metaRetriever)
+ .get(any());
+ service = new CdnSupportService(commandRunner, metaRetriever, "unknownEditor");
+ service.getPaths();
+ }
+
+ @Test(
+ expectedExceptions = {ServerException.class},
+ expectedExceptionsMessageRegExp = "URL of editor 'editor' is null")
+ public void throwWhenEditorPluginUrlIsNull() throws Exception {
+ doReturn(Lists.newArrayList(new PluginMeta[] {pluginMeta}))
+ .when(metaRetriever)
+ .get(any());
+ doReturn(null).when(pluginMeta).getUrl();
+ service = new CdnSupportService(commandRunner, metaRetriever, EDITOR_REF);
+
+ try {
+ service.getPaths();
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ verify(metaRetriever).get(of(Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, EDITOR_REF));
+ }
+ }
+
+ @Test(
+ expectedExceptions = {ServerException.class},
+ expectedExceptionsMessageRegExp =
+ "skopeo failed when trying to retrieve the CDN label of docker image imageRef - exit code: 1 - error output: skopeo error output")
+ public void throwWhenSkopeoFailsWithNonZeroCode() throws Exception {
+ doReturn(Lists.newArrayList(new PluginMeta[] {pluginMeta}))
+ .when(metaRetriever)
+ .get(any());
+
+ doReturn(EDITOR_URL).when(pluginMeta).getUrl();
+
+ lenient()
+ .when(commandRunner.runCommand(eq("skopeo"), any(), any(), anyLong(), any(), any(), any()))
+ .thenReturn(skopeoHelpProcess, skopeoInspectProcess);
+ doReturn(0).when(skopeoHelpProcess).exitValue();
+ doReturn(1).when(skopeoInspectProcess).exitValue();
+ when(commandRunner.newOutputConsumer()).thenReturn(skopeoOutputConsumer);
+ when(commandRunner.newErrorConsumer()).thenReturn(skopeoErrorConsumer);
+ when(skopeoErrorConsumer.getText()).thenReturn("skopeo error output");
+
+ service = new CdnSupportService(commandRunner, metaRetriever, EDITOR_REF);
+ service.editorDefinitionUrl = EDITOR_URL;
+ service.dockerImage = IMAGE_REF;
+
+ try {
+ service.getPaths();
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ verify(metaRetriever).get(of(Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, EDITOR_REF));
+ }
+ }
+
+ @Test
+ public void reuseExistingImageRefAndReturnLabelWhenSkopeoSucceeds() throws Exception {
+ doReturn(Lists.newArrayList(new PluginMeta[] {pluginMeta}))
+ .when(metaRetriever)
+ .get(any());
+
+ doReturn(EDITOR_URL).when(pluginMeta).getUrl();
+
+ lenient()
+ .when(commandRunner.runCommand(eq("skopeo"), any(), any(), anyLong(), any(), any(), any()))
+ .thenReturn(skopeoHelpProcess, skopeoInspectProcess);
+ doReturn(0).when(skopeoInspectProcess).exitValue();
+ when(commandRunner.newOutputConsumer()).thenReturn(skopeoOutputConsumer);
+ when(commandRunner.newErrorConsumer()).thenReturn(skopeoErrorConsumer);
+ when(skopeoOutputConsumer.getText())
+ .thenReturn("{\"Labels\": { \"che-plugin.cdn.artifacts\": \"cdnJsonContent\" }}");
+
+ service = new CdnSupportService(commandRunner, metaRetriever, EDITOR_REF);
+ service.editorDefinitionUrl = EDITOR_URL;
+ service.dockerImage = IMAGE_REF;
+
+ assertEquals(service.getPaths(), "cdnJsonContent");
+
+ verify(metaRetriever).get(of(Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, EDITOR_REF));
+ verify(commandRunner, never())
+ .runCommand(eq("tar"), any(), any(), anyLong(), any(), any(), any());
+ }
+
+ @Test(
+ expectedExceptions = {ServerException.class},
+ expectedExceptionsMessageRegExp =
+ "Tar command failed with error status: 1 and error log: tar error output content")
+ public void searchForImageRefAndThrowWhenTarFails() throws Exception {
+ doReturn(Lists.newArrayList(new PluginMeta[] {pluginMeta}))
+ .when(metaRetriever)
+ .get(any());
+
+ doReturn(EDITOR_URL).when(pluginMeta).getUrl();
+
+ lenient()
+ .when(commandRunner.runCommand(eq("skopeo"), any(), any(), anyLong(), any(), any(), any()))
+ .thenReturn(skopeoHelpProcess);
+ doReturn(0).when(skopeoHelpProcess).exitValue();
+ lenient()
+ .when(commandRunner.runCommand(eq("tar"), any(), any(), anyLong(), any(), any(), any()))
+ .thenReturn(tarProcess);
+ doReturn(1).when(tarProcess).exitValue();
+ when(commandRunner.newErrorConsumer()).thenReturn(tarErrorConsumer);
+ when(tarErrorConsumer.getText()).thenReturn("tar error output content");
+
+ setupURLConnectionInputStream("/che-editor-plugin.tar.gz");
+ service = new CdnSupportService(commandRunner, metaRetriever, EDITOR_REF);
+
+ try {
+ assertEquals(service.getPaths(), "{}");
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ verify(metaRetriever).get(of(Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, EDITOR_REF));
+ verify(commandRunner, times(1))
+ .runCommand(eq("skopeo"), any(), any(), anyLong(), any(), any(), any());
+ verify(commandRunner, times(1))
+ .runCommand(eq("tar"), any(), any(), anyLong(), any(), any(), any());
+ }
+ }
+
+ @Test
+ public void searchForImageRefAndReturnLabelWhenSkopeoSucceeds() throws Exception {
+ doReturn(Lists.newArrayList(new PluginMeta[] {pluginMeta}))
+ .when(metaRetriever)
+ .get(any());
+
+ doReturn(EDITOR_URL).when(pluginMeta).getUrl();
+
+ lenient()
+ .when(commandRunner.runCommand(eq("skopeo"), any(), any(), anyLong(), any(), any(), any()))
+ .thenReturn(skopeoHelpProcess, skopeoInspectProcess);
+ doReturn(0).when(skopeoHelpProcess).exitValue();
+ doReturn(0).when(skopeoInspectProcess).exitValue();
+ lenient()
+ .when(commandRunner.runCommand(eq("tar"), any(), any(), anyLong(), any(), any(), any()))
+ .thenCallRealMethod();
+ doReturn(0).when(tarProcess).exitValue();
+ when(commandRunner.newOutputConsumer()).thenReturn(skopeoOutputConsumer);
+ when(commandRunner.newErrorConsumer()).thenReturn(tarErrorConsumer, skopeoErrorConsumer);
+ when(skopeoErrorConsumer.getText()).thenReturn("skopeo error output content");
+ when(skopeoOutputConsumer.getText())
+ .thenReturn("{\"Labels\": { \"che-plugin.cdn.artifacts\": \"cdnJsonContent\" }}");
+
+ setupURLConnectionInputStream("/che-editor-plugin.tar.gz");
+ service = new CdnSupportService(commandRunner, metaRetriever, EDITOR_REF);
+
+ assertEquals(service.getPaths(), "cdnJsonContent");
+
+ verify(metaRetriever).get(of(Constants.WORKSPACE_TOOLING_EDITOR_ATTRIBUTE, EDITOR_REF));
+ verify(commandRunner, times(2))
+ .runCommand(eq("skopeo"), any(), any(), anyLong(), any(), any(), any());
+ verify(commandRunner, times(1))
+ .runCommand(eq("tar"), any(), any(), anyLong(), any(), any(), any());
+ }
+}
diff --git a/plugins/fabric8-cdn-support/src/test/resources/che-editor-plugin.tar.gz b/plugins/fabric8-cdn-support/src/test/resources/che-editor-plugin.tar.gz
new file mode 100644
index 000000000..9aba015c0
Binary files /dev/null and b/plugins/fabric8-cdn-support/src/test/resources/che-editor-plugin.tar.gz differ
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 5c0a07fce..d50be9e7e 100644
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -30,5 +30,6 @@
fabric8-multi-tenant-manager
che-plugin-analytics
fabric8-end2end-flow
+ fabric8-cdn-support
diff --git a/pom.xml b/pom.xml
index 158b1cd97..3b8b7c127 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,12 @@
${rh.che.plugins.version}
jar
+
+ com.redhat.che
+ fabric8-cdn-support
+ ${rh.che.plugins.version}
+ jar
+
com.redhat.che
fabric8-end2end-flow