From 5d38d7a715bee75b90b1de8fc4bc1a930023f9e4 Mon Sep 17 00:00:00 2001
From: Michal Vala <sparkoo@users.noreply.github.com>
Date: Thu, 24 Oct 2019 11:39:56 +0200
Subject: [PATCH] Preview url server support (#14713)

Signed-off-by: Michal Vala <mvala@redhat.com>
---
 .../core/model/workspace/config/Command.java  |  10 +
 .../core/model/workspace/devfile/Command.java |   3 +
 .../model/workspace/devfile/PreviewUrl.java   |  37 +
 .../KubernetesEnvironmentProvisioner.java     |   7 +-
 .../kubernetes/KubernetesInfraModule.java     |   7 +
 .../kubernetes/KubernetesInternalRuntime.java |   7 +
 .../infrastructure/kubernetes/Warnings.java   |   4 +
 .../model/KubernetesRuntimeCommandImpl.java   |  60 +-
 .../namespace/KubernetesIngresses.java        |  16 +
 ...ubernetesPreviewUrlCommandProvisioner.java |  46 ++
 .../PreviewUrlCommandProvisioner.java         | 118 +++
 .../kubernetes/server/PreviewUrlExposer.java  | 110 +++
 .../kubernetes/util/Ingresses.java            |  64 ++
 .../kubernetes/util/Services.java             |  52 ++
 .../KubernetesEnvironmentProvisionerTest.java |   8 +-
 .../KubernetesInternalRuntimeTest.java        |   5 +-
 ...netesPreviewUrlCommandProvisionerTest.java | 186 +++++
 .../external/PreviewUrlExposerTest.java       | 213 +++++
 .../kubernetes/util/IngressesTest.java        | 118 +++
 .../kubernetes/util/ServicesTest.java         | 120 +++
 .../OpenShiftEnvironmentProvisioner.java      |   8 +-
 .../openshift/OpenShiftInfraModule.java       |   7 +
 .../openshift/OpenShiftInternalRuntime.java   |   3 +
 .../environment/OpenShiftEnvironment.java     |   8 +
 ...OpenShiftPreviewUrlCommandProvisioner.java |  54 ++
 .../server/OpenShiftPreviewUrlExposer.java    |  41 +
 .../infrastructure/openshift/util/Routes.java |  60 ++
 .../OpenShiftEnvironmentProvisionerTest.java  |   8 +-
 .../OpenShiftInternalRuntimeTest.java         |   3 +
 ...ShiftPreviewUrlCommandProvisionerTest.java | 186 +++++
 .../OpenShiftPreviewUrlExposerTest.java       | 182 +++++
 .../openshift/util/RoutesTest.java            | 107 +++
 .../shared/dto/devfile/DevfileCommandDto.java |   6 +
 .../shared/dto/devfile/PreviewUrlDto.java     |  34 +
 .../api/workspace/server/DtoConverter.java    |  30 +-
 .../PreviewUrlLinksVariableGenerator.java     | 109 +++
 .../server/WorkspaceLinksGenerator.java       |   7 +
 .../workspace/server/devfile/Constants.java   |   4 +-
 .../devfile/convert/CommandConverter.java     |   1 +
 .../server/model/impl/CommandImpl.java        |  84 +-
 .../model/impl/devfile/CommandImpl.java       |  61 +-
 .../model/impl/devfile/PreviewUrlImpl.java    |  74 ++
 .../main/resources/schema/1.0.0/devfile.json  |   1 +
 .../resources/schema/1.0.1-beta/devfile.json  | 755 ++++++++++++++++++
 .../PreviewUrlLinksVariableGeneratorTest.java | 250 ++++++
 .../server/WorkspaceLinksGeneratorTest.java   |   6 +-
 .../validator/DevfileSchemaValidatorTest.java |  25 +-
 .../server/spi/tck/WorkspaceDaoTest.java      |   6 +-
 ...evfile_command_with_empty_preview_url.yaml |  32 +
 .../devfile_command_with_preview_url.yaml     |  34 +
 ...le_command_with_preview_url_only_path.yaml |  33 +
 ...le_command_with_preview_url_only_port.yaml |  33 +
 ...and_with_preview_url_port_is_negative.yaml |  34 +
 ...mmand_with_preview_url_port_is_string.yaml |  34 +
 ...and_with_preview_url_port_is_too_high.yaml |  34 +
 .../2__add_preview_url_to_devfile_command.sql |  21 +
 .../che/core/db/jpa/TestObjectsFactory.java   |   2 +-
 57 files changed, 3468 insertions(+), 100 deletions(-)
 create mode 100644 core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/PreviewUrl.java
 create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisioner.java
 create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PreviewUrlCommandProvisioner.java
 create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/PreviewUrlExposer.java
 create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Ingresses.java
 create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Services.java
 create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisionerTest.java
 create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/PreviewUrlExposerTest.java
 create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/IngressesTest.java
 create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ServicesTest.java
 create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisioner.java
 create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposer.java
 create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/util/Routes.java
 create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisionerTest.java
 create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposerTest.java
 create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/util/RoutesTest.java
 create mode 100644 wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/PreviewUrlDto.java
 create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGenerator.java
 create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/PreviewUrlImpl.java
 create mode 100644 wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.1-beta/devfile.json
 create mode 100644 wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGeneratorTest.java
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_empty_preview_url.yaml
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url.yaml
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_path.yaml
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_port.yaml
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_negative.yaml
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_string.yaml
 create mode 100644 wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_too_high.yaml
 create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.4.0/2__add_preview_url_to_devfile_command.sql

diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java
index 238a6d368d..197d80ffe7 100644
--- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java
+++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/Command.java
@@ -12,6 +12,7 @@
 package org.eclipse.che.api.core.model.workspace.config;
 
 import java.util.Map;
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
 
 /**
  * Command that can be used to create {@link Process} in a machine
@@ -33,6 +34,12 @@ public interface Command {
    */
   String MACHINE_NAME_ATTRIBUTE = "machineName";
 
+  /**
+   * Optional {@link Command} attribute to store full url of the view of the command. This url
+   * should be opened on command run.
+   */
+  String PREVIEW_URL_ATTRIBUTE = "previewUrl";
+
   /**
    * {@link Command} attribute which indicates in which plugin command must be run. If specified
    * plugin has multiple containers then first containers should be used. Attribute value has the
@@ -66,6 +73,9 @@ public interface Command {
   /** Returns command type (i.e. 'maven') */
   String getType();
 
+  /** @return preview url of the command or null if no preview url specified */
+  PreviewUrl getPreviewUrl();
+
   /**
    * Returns attributes related to this command.
    *
diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Command.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Command.java
index daeba19585..d07b2506fd 100644
--- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Command.java
+++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/Command.java
@@ -18,6 +18,9 @@ public interface Command {
   /** Returns the name of the command. It is mandatory and unique per commands set. */
   String getName();
 
+  /** Returns preview url of the command. Optional parameter, can be null if not specified. */
+  PreviewUrl getPreviewUrl();
+
   /**
    * Returns the command actions. Now the only one command must be specified in list but there are
    * plans to implement supporting multiple actions commands. It is mandatory.
diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/PreviewUrl.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/PreviewUrl.java
new file mode 100644
index 0000000000..35476cef93
--- /dev/null
+++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/devfile/PreviewUrl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.core.model.workspace.devfile;
+
+/**
+ * Preview url is optional parameter of {@link Command}. It is used to construct proper
+ * service+ingress/route and to compose valid path to the application. Typical use-case for
+ * applications that doesn't have UI on root path. Preview url also partially replaces endpoint,
+ * that is not needed to expose the application.
+ */
+public interface PreviewUrl {
+
+  /**
+   * {@code port} specifies where application, that is executed by command, listens. It is used to
+   * create service+ingress/route pair to make application accessible.
+   *
+   * @return applications's listen port
+   */
+  int getPort();
+
+  /**
+   * Specifies path and/or query parameters that should be opened after command execution.
+   *
+   * @return path and/or query params to open or {@code null} when not defined
+   */
+  String getPath();
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java
index 5759e086cb..2c8725d610 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisioner.java
@@ -35,6 +35,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.env.EnvVars
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.limits.ram.RamLimitRequestProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpolicy.RestartPolicyRewriter;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -73,6 +74,7 @@ public interface KubernetesEnvironmentProvisioner<T extends KubernetesEnvironmen
     private final CertificateProvisioner certificateProvisioner;
     private final VcsSshKeysProvisioner vcsSshKeysProvisioner;
     private final GitUserProfileProvisioner gitUserProfileProvisioner;
+    private final PreviewUrlExposer<KubernetesEnvironment> previewUrlExposer;
 
     @Inject
     public KubernetesEnvironmentProvisionerImpl(
@@ -92,7 +94,8 @@ public interface KubernetesEnvironmentProvisioner<T extends KubernetesEnvironmen
         ServiceAccountProvisioner serviceAccountProvisioner,
         CertificateProvisioner certificateProvisioner,
         VcsSshKeysProvisioner vcsSshKeysProvisioner,
-        GitUserProfileProvisioner gitUserProfileProvisioner) {
+        GitUserProfileProvisioner gitUserProfileProvisioner,
+        PreviewUrlExposer<KubernetesEnvironment> previewUrlExposer) {
       this.pvcEnabled = pvcEnabled;
       this.volumesStrategy = volumesStrategy;
       this.uniqueNamesProvisioner = uniqueNamesProvisioner;
@@ -110,6 +113,7 @@ public interface KubernetesEnvironmentProvisioner<T extends KubernetesEnvironmen
       this.certificateProvisioner = certificateProvisioner;
       this.vcsSshKeysProvisioner = vcsSshKeysProvisioner;
       this.gitUserProfileProvisioner = gitUserProfileProvisioner;
+      this.previewUrlExposer = previewUrlExposer;
     }
 
     @Traced
@@ -129,6 +133,7 @@ public interface KubernetesEnvironmentProvisioner<T extends KubernetesEnvironmen
       // 2 stage - converting Che model env to Kubernetes env
       LOG.debug("Provisioning servers & env vars converters for workspace '{}'", workspaceId);
       serversConverter.provision(k8sEnv, identity);
+      previewUrlExposer.expose(k8sEnv);
       envVarsConverter.provision(k8sEnv, identity);
       if (pvcEnabled) {
         volumesStrategy.provision(k8sEnv, identity);
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java
index 62a227b261..444914a66c 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java
@@ -57,9 +57,12 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.Workspa
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesCheApiExternalEnvVarProvider;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesCheApiInternalEnvVarProvider;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesPreviewUrlCommandProvisioner;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.PreviewUrlCommandProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.env.LogsRootEnvVariableProvider;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.IngressAnnotationsProvider;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.DefaultHostExternalServiceExposureStrategy;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServiceExposureStrategy;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.IngressServiceExposureStrategyProvider;
@@ -133,6 +136,10 @@ public class KubernetesInfraModule extends AbstractModule {
         .toProvider(IngressServiceExposureStrategyProvider.class);
 
     bind(ServersConverter.class).to(new TypeLiteral<ServersConverter<KubernetesEnvironment>>() {});
+    bind(PreviewUrlExposer.class)
+        .to(new TypeLiteral<PreviewUrlExposer<KubernetesEnvironment>>() {});
+    bind(PreviewUrlCommandProvisioner.class)
+        .to(new TypeLiteral<KubernetesPreviewUrlCommandProvisioner>() {});
 
     Multibinder<EnvVarProvider> envVarProviders =
         Multibinder.newSetBinder(binder(), EnvVarProvider.class);
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java
index 67ddb32a08..090eb74077 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntime.java
@@ -82,6 +82,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.model.KubernetesRunti
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace;
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.event.PodEvent;
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.PreviewUrlCommandProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerResolver;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.IngressPathTransformInverter;
 import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool;
@@ -118,6 +119,7 @@ public class KubernetesInternalRuntime<E extends KubernetesEnvironment>
   private final SidecarToolingProvisioner<E> toolingProvisioner;
   private final IngressPathTransformInverter ingressPathTransformInverter;
   private final RuntimeHangingDetector runtimeHangingDetector;
+  private final PreviewUrlCommandProvisioner previewUrlCommandProvisioner;
   protected final Tracer tracer;
 
   @Inject
@@ -140,6 +142,7 @@ public class KubernetesInternalRuntime<E extends KubernetesEnvironment>
       SidecarToolingProvisioner<E> toolingProvisioner,
       IngressPathTransformInverter ingressPathTransformInverter,
       RuntimeHangingDetector runtimeHangingDetector,
+      PreviewUrlCommandProvisioner previewUrlCommandProvisioner,
       Tracer tracer,
       @Assisted KubernetesRuntimeContext<E> context,
       @Assisted KubernetesNamespace namespace) {
@@ -162,6 +165,7 @@ public class KubernetesInternalRuntime<E extends KubernetesEnvironment>
     this.ingressPathTransformInverter = ingressPathTransformInverter;
     this.runtimeHangingDetector = runtimeHangingDetector;
     this.startSynchronizer = startSynchronizerFactory.create(context.getIdentity());
+    this.previewUrlCommandProvisioner = previewUrlCommandProvisioner;
     this.tracer = tracer;
   }
 
@@ -203,6 +207,9 @@ public class KubernetesInternalRuntime<E extends KubernetesEnvironment>
 
       startMachines();
 
+      previewUrlCommandProvisioner.provision(context.getEnvironment(), namespace);
+      runtimeStates.updateCommands(context.getIdentity(), context.getEnvironment().getCommands());
+
       startSynchronizer.checkFailure();
 
       final Map<String, CompletableFuture<Void>> machinesFutures = new LinkedHashMap<>();
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Warnings.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Warnings.java
index 7f831b4ef1..8a6ab9b366 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Warnings.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Warnings.java
@@ -51,5 +51,9 @@ public final class Warnings {
       "Unable to provision git configuration into runtime. "
           + "Internal server error occurred during operating with user management: '%s'";
 
+  public static final int NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL = 4110;
+  public static final String NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL_MESSAGE =
+      "Not able to provision objects for PreviewUrl. Message: '%s'";
+
   private Warnings() {}
 }
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/model/KubernetesRuntimeCommandImpl.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/model/KubernetesRuntimeCommandImpl.java
index 5ca13b4ba8..fd8ad5e12a 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/model/KubernetesRuntimeCommandImpl.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/model/KubernetesRuntimeCommandImpl.java
@@ -14,9 +14,11 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.model;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.StringJoiner;
 import javax.persistence.CollectionTable;
 import javax.persistence.Column;
 import javax.persistence.ElementCollection;
+import javax.persistence.Embedded;
 import javax.persistence.Entity;
 import javax.persistence.FetchType;
 import javax.persistence.GeneratedValue;
@@ -25,6 +27,8 @@ import javax.persistence.JoinColumn;
 import javax.persistence.MapKeyColumn;
 import javax.persistence.Table;
 import org.eclipse.che.api.core.model.workspace.config.Command;
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
 
 /**
  * Data object for {@link Command}.
@@ -49,6 +53,8 @@ public class KubernetesRuntimeCommandImpl implements Command {
   @Column(name = "type", nullable = false)
   private String type;
 
+  @Embedded private PreviewUrlImpl previewUrl;
+
   @ElementCollection(fetch = FetchType.EAGER)
   @CollectionTable(
       name = "k8s_runtime_command_attributes",
@@ -69,6 +75,9 @@ public class KubernetesRuntimeCommandImpl implements Command {
     this.name = command.getName();
     this.commandLine = command.getCommandLine();
     this.type = command.getType();
+    if (command.getPreviewUrl() != null) {
+      this.previewUrl = new PreviewUrlImpl(command.getPreviewUrl());
+    }
     this.attributes = command.getAttributes();
   }
 
@@ -90,6 +99,15 @@ public class KubernetesRuntimeCommandImpl implements Command {
     this.commandLine = commandLine;
   }
 
+  @Override
+  public PreviewUrl getPreviewUrl() {
+    return previewUrl;
+  }
+
+  public void setPreviewUrl(PreviewUrlImpl previewUrl) {
+    this.previewUrl = previewUrl;
+  }
+
   @Override
   public String getType() {
     return type;
@@ -112,48 +130,36 @@ public class KubernetesRuntimeCommandImpl implements Command {
   }
 
   @Override
-  public boolean equals(Object obj) {
-    if (this == obj) {
+  public boolean equals(Object o) {
+    if (this == o) {
       return true;
     }
-    if (!(obj instanceof KubernetesRuntimeCommandImpl)) {
+    if (o == null || getClass() != o.getClass()) {
       return false;
     }
-    final KubernetesRuntimeCommandImpl that = (KubernetesRuntimeCommandImpl) obj;
+    KubernetesRuntimeCommandImpl that = (KubernetesRuntimeCommandImpl) o;
     return Objects.equals(id, that.id)
         && Objects.equals(name, that.name)
         && Objects.equals(commandLine, that.commandLine)
         && Objects.equals(type, that.type)
-        && getAttributes().equals(that.getAttributes());
+        && Objects.equals(previewUrl, that.previewUrl)
+        && Objects.equals(attributes, that.attributes);
   }
 
   @Override
   public int hashCode() {
-    int hash = 7;
-    hash = 31 * hash + Objects.hashCode(id);
-    hash = 31 * hash + Objects.hashCode(name);
-    hash = 31 * hash + Objects.hashCode(commandLine);
-    hash = 31 * hash + Objects.hashCode(type);
-    hash = 31 * hash + getAttributes().hashCode();
-    return hash;
+    return Objects.hash(id, name, commandLine, type, previewUrl, attributes);
   }
 
   @Override
   public String toString() {
-    return "KubernetesRuntimeCommandImpl{"
-        + "id="
-        + id
-        + ", name='"
-        + name
-        + '\''
-        + ", commandLine='"
-        + commandLine
-        + '\''
-        + ", type='"
-        + type
-        + '\''
-        + ", attributes="
-        + attributes
-        + '}';
+    return new StringJoiner(", ", KubernetesRuntimeCommandImpl.class.getSimpleName() + "[", "]")
+        .add("id=" + id)
+        .add("name='" + name + "'")
+        .add("commandLine='" + commandLine + "'")
+        .add("type='" + type + "'")
+        .add("previewUrl=" + previewUrl)
+        .add("attributes=" + attributes)
+        .toString();
   }
 }
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesIngresses.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesIngresses.java
index 422bb194bf..080e6b0a12 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesIngresses.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesIngresses.java
@@ -20,6 +20,7 @@ import io.fabric8.kubernetes.client.KubernetesClientException;
 import io.fabric8.kubernetes.client.Watch;
 import io.fabric8.kubernetes.client.Watcher;
 import io.fabric8.kubernetes.client.dsl.Resource;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -63,6 +64,21 @@ public class KubernetesIngresses {
     }
   }
 
+  public List<Ingress> get() throws InfrastructureException {
+    try {
+      return clientFactory
+          .create(workspaceId)
+          .extensions()
+          .ingresses()
+          .inNamespace(namespace)
+          .withLabel(CHE_WORKSPACE_ID_LABEL, workspaceId)
+          .list()
+          .getItems();
+    } catch (KubernetesClientException e) {
+      throw new KubernetesInfrastructureException(e);
+    }
+  }
+
   public Ingress wait(String name, long timeout, TimeUnit timeoutUnit, Predicate<Ingress> predicate)
       throws InfrastructureException {
     CompletableFuture<Ingress> future = new CompletableFuture<>();
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisioner.java
new file mode 100644
index 0000000000..2746bd8160
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisioner.java
@@ -0,0 +1,46 @@
+/*
+ * 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.provision;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import io.fabric8.kubernetes.api.model.extensions.IngressRule;
+import java.util.List;
+import java.util.Optional;
+import javax.inject.Singleton;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace;
+import org.eclipse.che.workspace.infrastructure.kubernetes.util.Ingresses;
+
+/**
+ * Extends {@link PreviewUrlCommandProvisioner} where needed. For Kubernetes, we work with {@link
+ * Ingress}es and {@link KubernetesNamespace}.
+ */
+@Singleton
+public class KubernetesPreviewUrlCommandProvisioner
+    extends PreviewUrlCommandProvisioner<KubernetesEnvironment, Ingress> {
+
+  @Override
+  protected List<Ingress> loadExposureObjects(KubernetesNamespace namespace)
+      throws InfrastructureException {
+    return namespace.ingresses().get();
+  }
+
+  @Override
+  protected Optional<String> findHostForServicePort(
+      List<Ingress> ingresses, Service service, int port) {
+    return Ingresses.findIngressRuleForServicePort(ingresses, service, port)
+        .map(IngressRule::getHost);
+  }
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PreviewUrlCommandProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PreviewUrlCommandProvisioner.java
new file mode 100644
index 0000000000..286475c902
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/PreviewUrlCommandProvisioner.java
@@ -0,0 +1,118 @@
+/*
+ * 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.provision;
+
+import static org.eclipse.che.api.core.model.workspace.config.Command.PREVIEW_URL_ATTRIBUTE;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL_MESSAGE;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.model.impl.WarningImpl;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace;
+import org.eclipse.che.workspace.infrastructure.kubernetes.util.Services;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Updates commands with proper Preview URL attribute.
+ *
+ * <p>Goes through all {@link CommandImpl}s in given {@link KubernetesEnvironment} and for ones that
+ * have defined Preview URL tries to find matching {@link Service} and {@link Ingress} from given
+ * {@link KubernetesNamespace}. When found, it composes full URL and set it as attribute of a
+ * command with key {@link
+ * org.eclipse.che.api.core.model.workspace.config.Command#PREVIEW_URL_ATTRIBUTE}.
+ *
+ * @param <E> type of the environment
+ */
+public abstract class PreviewUrlCommandProvisioner<
+    E extends KubernetesEnvironment, T extends HasMetadata> {
+
+  private static final Logger LOG = LoggerFactory.getLogger(PreviewUrlCommandProvisioner.class);
+
+  public void provision(E env, KubernetesNamespace namespace) throws InfrastructureException {
+    injectsPreviewUrlToCommands(env, namespace);
+  }
+
+  /**
+   * Go through all commands, find matching service and exposed host. Then construct full preview
+   * url from this data and set it as Command's parameter under `previewUrl` key.
+   *
+   * @param env environment to get commands
+   * @param namespace current kubernetes namespace where we're looking for services and ingresses
+   */
+  private void injectsPreviewUrlToCommands(E env, KubernetesNamespace namespace)
+      throws InfrastructureException {
+    if (env.getCommands() == null) {
+      return;
+    }
+
+    List<T> exposureObjects = loadExposureObjects(namespace);
+    List<Service> services = namespace.services().get();
+    for (CommandImpl command :
+        env.getCommands()
+            .stream()
+            .filter(c -> c.getPreviewUrl() != null)
+            .collect(Collectors.toList())) {
+      Optional<Service> foundService =
+          Services.findServiceWithPort(services, command.getPreviewUrl().getPort());
+      if (!foundService.isPresent()) {
+        String message =
+            String.format(
+                "unable to find service for port '%s' for command '%s'",
+                command.getPreviewUrl().getPort(), command.getName());
+        LOG.warn(message);
+        env.addWarning(
+            new WarningImpl(
+                NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL,
+                String.format(NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL_MESSAGE, message)));
+        continue;
+      }
+
+      Optional<String> foundHost =
+          findHostForServicePort(
+              exposureObjects, foundService.get(), command.getPreviewUrl().getPort());
+      if (foundHost.isPresent()) {
+        command.getAttributes().put(PREVIEW_URL_ATTRIBUTE, foundHost.get());
+      } else {
+        String message =
+            String.format(
+                "unable to find ingress for service '%s' and port '%s'",
+                foundService.get(), command.getPreviewUrl().getPort());
+        LOG.warn(message);
+        env.addWarning(
+            new WarningImpl(
+                NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL,
+                String.format(NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL_MESSAGE, message)));
+      }
+    }
+  }
+
+  /**
+   * Loads exposure objects of running infrastructure. Kubernetes implementation should return
+   * `List<Ingress>`, OpenShift implementation `List<Route>`.
+   */
+  protected abstract List<T> loadExposureObjects(KubernetesNamespace namespace)
+      throws InfrastructureException;
+
+  protected abstract Optional<String> findHostForServicePort(
+      List<T> ingressList, Service service, int port) throws InternalInfrastructureException;
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/PreviewUrlExposer.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/PreviewUrlExposer.java
new file mode 100644
index 0000000000..1c7fe20669
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/PreviewUrlExposer.java
@@ -0,0 +1,110 @@
+/*
+ * 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.server;
+
+import static org.eclipse.che.commons.lang.NameGenerator.generate;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_PREFIX;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_UNIQUE_PART_SIZE;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposer;
+import org.eclipse.che.workspace.infrastructure.kubernetes.util.Ingresses;
+import org.eclipse.che.workspace.infrastructure.kubernetes.util.Services;
+
+/**
+ * For Commands that have defined Preview URL, tries to find existing {@link Service} and {@link
+ * Ingress}. When not found, we create new ones and put them into given {@link
+ * KubernetesEnvironment}.
+ *
+ * @param <T> type of the environment
+ */
+@Singleton
+public class PreviewUrlExposer<T extends KubernetesEnvironment> {
+
+  private final ExternalServerExposer<T> externalServerExposer;
+
+  @Inject
+  public PreviewUrlExposer(ExternalServerExposer<T> externalServerExposer) {
+    this.externalServerExposer = externalServerExposer;
+  }
+
+  public void expose(T env) throws InternalInfrastructureException {
+    List<CommandImpl> previewUrlCommands =
+        env.getCommands()
+            .stream()
+            .filter(c -> c.getPreviewUrl() != null)
+            .collect(Collectors.toList());
+
+    List<ServicePort> portsToProvision = new ArrayList<>();
+    for (CommandImpl command : previewUrlCommands) {
+      int port = command.getPreviewUrl().getPort();
+      Optional<Service> foundService =
+          Services.findServiceWithPort(env.getServices().values(), port);
+      if (foundService.isPresent()) {
+        if (!hasMatchingEndpoint(env, foundService.get(), port)) {
+          ServicePort servicePort =
+              Services.findPort(foundService.get(), port)
+                  .orElseThrow(
+                      () ->
+                          new InternalInfrastructureException(
+                              String.format(
+                                  "Port '%d' in service '%s' not found. This is not expected, please report a bug!",
+                                  port, foundService.get().getMetadata().getName())));
+          externalServerExposer.expose(
+              env,
+              null,
+              foundService.get().getMetadata().getName(),
+              servicePort,
+              Collections.emptyMap());
+        }
+      } else {
+        portsToProvision.add(createServicePort(port));
+      }
+    }
+
+    if (!portsToProvision.isEmpty()) {
+      Service service =
+          new ServerServiceBuilder()
+              .withName(generate(SERVER_PREFIX, SERVER_UNIQUE_PART_SIZE) + "-previewUrl")
+              .withPorts(portsToProvision)
+              .build();
+      env.getServices().put(service.getMetadata().getName(), service);
+      portsToProvision.forEach(
+          port ->
+              externalServerExposer.expose(
+                  env, null, service.getMetadata().getName(), port, Collections.emptyMap()));
+    }
+  }
+
+  private ServicePort createServicePort(int port) {
+    return new ServicePort("server-" + port, null, port, "TCP", new IntOrString(port));
+  }
+
+  protected boolean hasMatchingEndpoint(T env, Service service, int port) {
+    return Ingresses.findIngressRuleForServicePort(env.getIngresses().values(), service, port)
+        .isPresent();
+  }
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Ingresses.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Ingresses.java
new file mode 100644
index 0000000000..133ba3ac79
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Ingresses.java
@@ -0,0 +1,64 @@
+/*
+ * 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.util;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import io.fabric8.kubernetes.api.model.extensions.IngressBackend;
+import io.fabric8.kubernetes.api.model.extensions.IngressRule;
+import java.util.Collection;
+import java.util.Optional;
+
+/** Util class that helps working with k8s Ingresses */
+public class Ingresses {
+
+  /**
+   * In given {@code ingresses} finds {@link IngressRule} for given {@code service} and {@code
+   * port}.
+   *
+   * @return found {@link IngressRule} or {@link Optional#empty()}
+   */
+  public static Optional<IngressRule> findIngressRuleForServicePort(
+      Collection<Ingress> ingresses, Service service, int port) {
+    Optional<ServicePort> foundPort = Services.findPort(service, port);
+    if (!foundPort.isPresent()) {
+      return Optional.empty();
+    }
+
+    for (Ingress ingress : ingresses) {
+      for (IngressRule rule : ingress.getSpec().getRules()) {
+        for (HTTPIngressPath path : rule.getHttp().getPaths()) {
+          IngressBackend backend = path.getBackend();
+          if (backend.getServiceName().equals(service.getMetadata().getName())
+              && matchesServicePort(backend.getServicePort(), foundPort.get())) {
+            return Optional.of(rule);
+          }
+        }
+      }
+    }
+    return Optional.empty();
+  }
+
+  private static boolean matchesServicePort(IntOrString backendPort, ServicePort servicePort) {
+    if (backendPort.getStrVal() != null && backendPort.getStrVal().equals(servicePort.getName())) {
+      return true;
+    }
+    if (backendPort.getIntVal() != null && backendPort.getIntVal().equals(servicePort.getPort())) {
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Services.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Services.java
new file mode 100644
index 0000000000..f7c99955db
--- /dev/null
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Services.java
@@ -0,0 +1,52 @@
+/*
+ * 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.util;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import java.util.Collection;
+import java.util.Optional;
+
+/** Utility class to help work with {@link Service}s */
+public class Services {
+
+  /**
+   * Try to find port in given service.
+   *
+   * @return {@link Optional} of found {@link ServicePort}, or {@link Optional#empty()} when not
+   *     found.
+   */
+  public static Optional<ServicePort> findPort(Service service, int port) {
+    if (service == null || service.getSpec() == null || service.getSpec().getPorts() == null) {
+      return Optional.empty();
+    }
+    return service
+        .getSpec()
+        .getPorts()
+        .stream()
+        .filter(p -> p.getPort() != null && p.getPort() == port)
+        .findFirst();
+  }
+
+  /**
+   * Go through all given services and finds one that has given port.
+   *
+   * @return {@link Optional} of found {@link Service}, or {@link Optional#empty()} when not found.
+   */
+  public static Optional<Service> findServiceWithPort(Collection<Service> services, int port) {
+    if (services == null) {
+      return Optional.empty();
+    }
+    return services.stream().filter(s -> Services.findPort(s, port).isPresent()).findFirst();
+  }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java
index 7a466bfa1e..970ce43c3a 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesEnvironmentProvisionerTest.java
@@ -33,6 +33,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.env.EnvVars
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.limits.ram.RamLimitRequestProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpolicy.RestartPolicyRewriter;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.testng.MockitoTestNGListener;
@@ -66,6 +67,7 @@ public class KubernetesEnvironmentProvisionerTest {
   @Mock private CertificateProvisioner certificateProvisioner;
   @Mock private VcsSshKeysProvisioner vcsSshKeysProvisioner;
   @Mock private GitUserProfileProvisioner gitUserProfileProvisioner;
+  @Mock private PreviewUrlExposer previewUrlExposer;
 
   private KubernetesEnvironmentProvisioner<KubernetesEnvironment> k8sInfraProvisioner;
 
@@ -91,7 +93,8 @@ public class KubernetesEnvironmentProvisionerTest {
             serviceAccountProvisioner,
             certificateProvisioner,
             vcsSshKeysProvisioner,
-            gitUserProfileProvisioner);
+            gitUserProfileProvisioner,
+            previewUrlExposer);
     provisionOrder =
         inOrder(
             logsVolumeMachineProvisioner,
@@ -108,7 +111,8 @@ public class KubernetesEnvironmentProvisionerTest {
             proxySettingsProvisioner,
             serviceAccountProvisioner,
             certificateProvisioner,
-            gitUserProfileProvisioner);
+            gitUserProfileProvisioner,
+            previewUrlExposer);
   }
 
   @Test
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java
index 66db6b05ff..fe41dd62bc 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInternalRuntimeTest.java
@@ -121,6 +121,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesS
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesServices;
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.event.PodEvent;
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesPreviewUrlCommandProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerResolver;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.IngressPathTransformInverter;
 import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool;
@@ -192,6 +193,7 @@ public class KubernetesInternalRuntimeTest {
   @Mock private InternalEnvironmentProvisioner internalEnvironmentProvisioner;
   @Mock private IngressPathTransformInverter pathTransformInverter;
   @Mock private RuntimeHangingDetector runtimeHangingDetector;
+  @Mock private KubernetesPreviewUrlCommandProvisioner previewUrlCommandProvisioner;
 
   @Mock
   private KubernetesEnvironmentProvisioner<KubernetesEnvironment> kubernetesEnvironmentProvisioner;
@@ -238,7 +240,7 @@ public class KubernetesInternalRuntimeTest {
     when(startSynchronizerFactory.create(any())).thenReturn(startSynchronizer);
 
     internalRuntime =
-        new KubernetesInternalRuntime<>(
+        new KubernetesInternalRuntime<KubernetesEnvironment>(
             13,
             5,
             new URLRewriter.NoOpURLRewriter(),
@@ -257,6 +259,7 @@ public class KubernetesInternalRuntimeTest {
             toolingProvisioner,
             pathTransformInverter,
             runtimeHangingDetector,
+            previewUrlCommandProvisioner,
             tracer,
             context,
             namespace);
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisionerTest.java
new file mode 100644
index 0000000000..64ab0fd8c7
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/KubernetesPreviewUrlCommandProvisionerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.provision;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressRuleValue;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import io.fabric8.kubernetes.api.model.extensions.IngressBackend;
+import io.fabric8.kubernetes.api.model.extensions.IngressRule;
+import io.fabric8.kubernetes.api.model.extensions.IngressSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.Warnings;
+import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesIngresses;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesServices;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+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 KubernetesPreviewUrlCommandProvisionerTest {
+
+  private KubernetesPreviewUrlCommandProvisioner previewUrlCommandProvisioner;
+  @Mock private KubernetesEnvironment mockEnvironment;
+  @Mock private KubernetesNamespace mockNamespace;
+  @Mock private KubernetesServices mockServices;
+  @Mock private KubernetesIngresses mockIngresses;
+
+  @BeforeMethod
+  public void setUp() {
+    previewUrlCommandProvisioner = new KubernetesPreviewUrlCommandProvisioner();
+  }
+
+  @Test
+  public void shouldDoNothingWhenGetCommandsIsNull() throws InfrastructureException {
+    Mockito.when(mockEnvironment.getCommands()).thenReturn(null);
+
+    previewUrlCommandProvisioner.provision(mockEnvironment, mockNamespace);
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandsDefined() throws InfrastructureException {
+    Mockito.when(mockEnvironment.getCommands()).thenReturn(Collections.emptyList());
+    Mockito.when(mockNamespace.ingresses()).thenReturn(mockIngresses);
+    Mockito.when(mockNamespace.services()).thenReturn(mockServices);
+
+    previewUrlCommandProvisioner.provision(mockEnvironment, mockNamespace);
+  }
+
+  @Test
+  public void shouldDoNothingWhenCommandsWithoutPreviewUrlDefined() throws InfrastructureException {
+    List<CommandImpl> commands =
+        Arrays.asList(new CommandImpl("a", "a", "a"), new CommandImpl("b", "b", "b"));
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockNamespace.ingresses()).thenReturn(mockIngresses);
+    Mockito.when(mockNamespace.services()).thenReturn(mockServices);
+
+    previewUrlCommandProvisioner.provision(env, mockNamespace);
+
+    assertTrue(commands.containsAll(env.getCommands()));
+    assertTrue(env.getCommands().containsAll(commands));
+    assertTrue(env.getWarnings().isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenCantFindServiceForPreviewurl() throws InfrastructureException {
+    List<CommandImpl> commands =
+        Collections.singletonList(
+            new CommandImpl("a", "a", "a", new PreviewUrlImpl(8080, null), Collections.emptyMap()));
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockNamespace.ingresses()).thenReturn(mockIngresses);
+    Mockito.when(mockNamespace.services()).thenReturn(mockServices);
+    Mockito.when(mockServices.get()).thenReturn(Collections.emptyList());
+
+    previewUrlCommandProvisioner.provision(env, mockNamespace);
+
+    assertTrue(commands.containsAll(env.getCommands()));
+    assertTrue(env.getCommands().containsAll(commands));
+    assertEquals(
+        env.getWarnings().get(0).getCode(), Warnings.NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL);
+  }
+
+  @Test
+  public void shouldDoNothingWhenCantFindIngressForPreviewUrl() throws InfrastructureException {
+    int port = 8080;
+    List<CommandImpl> commands =
+        Collections.singletonList(
+            new CommandImpl("a", "a", "a", new PreviewUrlImpl(port, null), Collections.emptyMap()));
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockNamespace.services()).thenReturn(mockServices);
+    Service service = new Service();
+    ServiceSpec spec = new ServiceSpec();
+    spec.setPorts(
+        Collections.singletonList(new ServicePort("a", null, port, "TCP", new IntOrString(port))));
+    service.setSpec(spec);
+    Mockito.when(mockServices.get()).thenReturn(Collections.singletonList(service));
+
+    Mockito.when(mockNamespace.ingresses()).thenReturn(mockIngresses);
+    Mockito.when(mockIngresses.get()).thenReturn(Collections.emptyList());
+
+    previewUrlCommandProvisioner.provision(env, mockNamespace);
+
+    assertTrue(commands.containsAll(env.getCommands()));
+    assertTrue(env.getCommands().containsAll(commands));
+    assertEquals(
+        env.getWarnings().get(0).getCode(), Warnings.NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL);
+  }
+
+  @Test
+  public void shouldUpdateCommandWhenServiceAndIngressFound() throws InfrastructureException {
+    final int PORT = 8080;
+    final String SERVICE_PORT_NAME = "service-" + PORT;
+    List<CommandImpl> commands =
+        Collections.singletonList(
+            new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap()));
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockNamespace.services()).thenReturn(mockServices);
+    Service service = new Service();
+    ObjectMeta metadata = new ObjectMeta();
+    metadata.setName("servicename");
+    service.setMetadata(metadata);
+    ServiceSpec spec = new ServiceSpec();
+    spec.setPorts(
+        Collections.singletonList(
+            new ServicePort(SERVICE_PORT_NAME, null, PORT, "TCP", new IntOrString(PORT))));
+    service.setSpec(spec);
+    Mockito.when(mockServices.get()).thenReturn(Collections.singletonList(service));
+
+    Ingress ingress = new Ingress();
+    IngressSpec ingressSpec = new IngressSpec();
+    IngressRule rule =
+        new IngressRule(
+            "testhost",
+            new HTTPIngressRuleValue(
+                Collections.singletonList(
+                    new HTTPIngressPath(
+                        new IngressBackend("servicename", new IntOrString(SERVICE_PORT_NAME)),
+                        null))));
+    ingressSpec.setRules(Collections.singletonList(rule));
+    ingress.setSpec(ingressSpec);
+    Mockito.when(mockNamespace.ingresses()).thenReturn(mockIngresses);
+    Mockito.when(mockIngresses.get()).thenReturn(Collections.singletonList(ingress));
+
+    previewUrlCommandProvisioner.provision(env, mockNamespace);
+
+    assertTrue(env.getCommands().get(0).getAttributes().containsKey("previewUrl"));
+    assertEquals(env.getCommands().get(0).getAttributes().get("previewUrl"), "testhost");
+    assertTrue(env.getWarnings().isEmpty());
+  }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/PreviewUrlExposerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/PreviewUrlExposerTest.java
new file mode 100644
index 0000000000..c6dc47b30f
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/PreviewUrlExposerTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.server.external;
+
+import static java.util.Collections.singletonList;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressRuleValue;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import io.fabric8.kubernetes.api.model.extensions.IngressBackend;
+import io.fabric8.kubernetes.api.model.extensions.IngressRule;
+import io.fabric8.kubernetes.api.model.extensions.IngressSpec;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+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 PreviewUrlExposerTest {
+
+  private PreviewUrlExposer<KubernetesEnvironment> previewUrlExposer;
+
+  @Mock private ExternalServiceExposureStrategy externalServiceExposureStrategy;
+
+  @BeforeMethod
+  public void setUp() {
+    ExternalServerExposer<KubernetesEnvironment> externalServerExposer =
+        new ExternalServerExposer<>(externalServiceExposureStrategy, Collections.emptyMap(), null);
+    previewUrlExposer = new PreviewUrlExposer<>(externalServerExposer);
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandsDefined() throws InternalInfrastructureException {
+    KubernetesEnvironment env = KubernetesEnvironment.builder().build();
+
+    previewUrlExposer.expose(env);
+
+    assertTrue(env.getCommands().isEmpty());
+    assertTrue(env.getServices().isEmpty());
+    assertTrue(env.getIngresses().isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandWithPreviewUrlDefined()
+      throws InternalInfrastructureException {
+    CommandImpl command = new CommandImpl("a", "a", "a");
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .build();
+
+    previewUrlExposer.expose(env);
+
+    assertEquals(env.getCommands().get(0), command);
+    assertTrue(env.getServices().isEmpty());
+    assertTrue(env.getIngresses().isEmpty());
+  }
+
+  @Test
+  public void shouldNotProvisionWhenServiceAndIngressFound()
+      throws InternalInfrastructureException {
+    final int PORT = 8080;
+    final String SERVER_PORT_NAME = "server-" + PORT;
+
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap());
+
+    Service service = new Service();
+    ObjectMeta serviceMeta = new ObjectMeta();
+    serviceMeta.setName("servicename");
+    service.setMetadata(serviceMeta);
+    ServiceSpec serviceSpec = new ServiceSpec();
+    serviceSpec.setPorts(
+        singletonList(new ServicePort(SERVER_PORT_NAME, null, PORT, "TCP", new IntOrString(PORT))));
+    service.setSpec(serviceSpec);
+
+    Ingress ingress = new Ingress();
+    ObjectMeta ingressMeta = new ObjectMeta();
+    ingressMeta.setName("ingressname");
+    ingress.setMetadata(ingressMeta);
+    IngressSpec ingressSpec = new IngressSpec();
+    IngressRule ingressRule = new IngressRule();
+    ingressRule.setHost("ingresshost");
+    IngressBackend ingressBackend =
+        new IngressBackend("servicename", new IntOrString(SERVER_PORT_NAME));
+    ingressRule.setHttp(
+        new HTTPIngressRuleValue(singletonList(new HTTPIngressPath(ingressBackend, null))));
+    ingressSpec.setRules(singletonList(ingressRule));
+    ingress.setSpec(ingressSpec);
+
+    Map<String, Service> services = new HashMap<>();
+    services.put("servicename", service);
+    Map<String, Ingress> ingresses = new HashMap<>();
+    ingresses.put("ingressname", ingress);
+
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .setServices(services)
+            .setIngresses(ingresses)
+            .build();
+
+    assertEquals(env.getIngresses().size(), 1);
+    previewUrlExposer.expose(env);
+    assertEquals(env.getIngresses().size(), 1);
+  }
+
+  @Test
+  public void shouldProvisionIngressWhenNotFound() throws InternalInfrastructureException {
+    Mockito.when(
+            externalServiceExposureStrategy.getExternalPath(Mockito.anyString(), Mockito.any()))
+        .thenReturn("some-server-path");
+
+    final int PORT = 8080;
+    final String SERVER_PORT_NAME = "server-" + PORT;
+    final String SERVICE_NAME = "servicename";
+
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap());
+
+    Service service = new Service();
+    ObjectMeta serviceMeta = new ObjectMeta();
+    serviceMeta.setName(SERVICE_NAME);
+    service.setMetadata(serviceMeta);
+    ServiceSpec serviceSpec = new ServiceSpec();
+    serviceSpec.setPorts(
+        singletonList(new ServicePort(SERVER_PORT_NAME, null, PORT, "TCP", new IntOrString(PORT))));
+    service.setSpec(serviceSpec);
+
+    Map<String, Service> services = new HashMap<>();
+    services.put(SERVICE_NAME, service);
+
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .setServices(services)
+            .setIngresses(new HashMap<>())
+            .build();
+
+    previewUrlExposer.expose(env);
+    assertEquals(env.getIngresses().size(), 1);
+    Ingress provisionedIngress = env.getIngresses().values().iterator().next();
+    IngressBackend provisionedIngressBackend =
+        provisionedIngress.getSpec().getRules().get(0).getHttp().getPaths().get(0).getBackend();
+    assertEquals(provisionedIngressBackend.getServicePort().getStrVal(), SERVER_PORT_NAME);
+    assertEquals(provisionedIngressBackend.getServiceName(), SERVICE_NAME);
+  }
+
+  @Test
+  public void shouldProvisionServiceAndIngressWhenNotFound()
+      throws InternalInfrastructureException {
+    Mockito.when(
+            externalServiceExposureStrategy.getExternalPath(Mockito.anyString(), Mockito.any()))
+        .thenReturn("some-server-path");
+
+    final int PORT = 8080;
+    final String SERVER_PORT_NAME = "server-" + PORT;
+
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap());
+
+    KubernetesEnvironment env =
+        KubernetesEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .setIngresses(new HashMap<>())
+            .setServices(new HashMap<>())
+            .build();
+
+    previewUrlExposer.expose(env);
+
+    assertEquals(env.getIngresses().size(), 1);
+    assertEquals(env.getServices().size(), 1);
+
+    Service provisionedService = env.getServices().values().iterator().next();
+    ServicePort provisionedServicePort = provisionedService.getSpec().getPorts().get(0);
+    assertEquals(provisionedServicePort.getName(), SERVER_PORT_NAME);
+    assertEquals(provisionedServicePort.getPort().intValue(), PORT);
+
+    Ingress provisionedIngress = env.getIngresses().values().iterator().next();
+    IngressBackend provisionedIngressBackend =
+        provisionedIngress.getSpec().getRules().get(0).getHttp().getPaths().get(0).getBackend();
+    assertEquals(provisionedIngressBackend.getServicePort().getStrVal(), SERVER_PORT_NAME);
+    assertEquals(
+        provisionedIngressBackend.getServiceName(), provisionedService.getMetadata().getName());
+  }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/IngressesTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/IngressesTest.java
new file mode 100644
index 0000000000..39e3ab4b75
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/IngressesTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.util;
+
+import static java.util.Collections.singletonList;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.extensions.HTTPIngressRuleValue;
+import io.fabric8.kubernetes.api.model.extensions.Ingress;
+import io.fabric8.kubernetes.api.model.extensions.IngressBackend;
+import io.fabric8.kubernetes.api.model.extensions.IngressRule;
+import io.fabric8.kubernetes.api.model.extensions.IngressSpec;
+import java.util.Optional;
+import org.testng.annotations.Test;
+
+public class IngressesTest {
+
+  @Test
+  public void findHostWhenPortDefinedByString() {
+    final String SERVER_PORT_NAME = "server-8080";
+    final int PORT = 8080;
+
+    Service service = createService(SERVER_PORT_NAME, PORT);
+    Ingress ingress = createIngress(new IngressBackend("servicename", new IntOrString(PORT)));
+
+    Optional<IngressRule> foundRule =
+        Ingresses.findIngressRuleForServicePort(singletonList(ingress), service, PORT);
+    assertTrue(foundRule.isPresent());
+    assertEquals(foundRule.get().getHost(), "ingresshost");
+  }
+
+  @Test
+  public void findHostWhenPortDefinedByInt() {
+    final String SERVER_PORT_NAME = "server-8080";
+    final int PORT = 8080;
+
+    Service service = createService(SERVER_PORT_NAME, PORT);
+    Ingress ingress =
+        createIngress(new IngressBackend("servicename", new IntOrString(SERVER_PORT_NAME)));
+
+    Optional<IngressRule> foundRule =
+        Ingresses.findIngressRuleForServicePort(singletonList(ingress), service, PORT);
+    assertTrue(foundRule.isPresent());
+    assertEquals(foundRule.get().getHost(), "ingresshost");
+  }
+
+  @Test
+  public void emptyWhenPortByStringAndNotFound() {
+    final String SERVER_PORT_NAME = "server-8080";
+    final int PORT = 8080;
+
+    Service service = createService(SERVER_PORT_NAME, PORT);
+    Ingress ingress =
+        createIngress(new IngressBackend("servicename", new IntOrString("does not exist")));
+
+    Optional<IngressRule> foundRule =
+        Ingresses.findIngressRuleForServicePort(singletonList(ingress), service, PORT);
+    assertFalse(foundRule.isPresent());
+  }
+
+  @Test
+  public void emptyWhenPortByIntAndNotFound() {
+    final String SERVER_PORT_NAME = "server-8080";
+    final int PORT = 8080;
+
+    Service service = createService(SERVER_PORT_NAME, PORT);
+    Ingress ingress = createIngress(new IngressBackend("servicename", new IntOrString(666)));
+
+    Optional<IngressRule> foundRule =
+        Ingresses.findIngressRuleForServicePort(singletonList(ingress), service, PORT);
+    assertFalse(foundRule.isPresent());
+  }
+
+  private Service createService(String serverPortName, int port) {
+    Service service = new Service();
+    ObjectMeta serviceMeta = new ObjectMeta();
+    serviceMeta.setName("servicename");
+    service.setMetadata(serviceMeta);
+    ServiceSpec serviceSpec = new ServiceSpec();
+    serviceSpec.setPorts(
+        singletonList(new ServicePort(serverPortName, null, port, "TCP", new IntOrString(port))));
+    service.setSpec(serviceSpec);
+    return service;
+  }
+
+  private Ingress createIngress(IngressBackend backend) {
+    Ingress ingress = new Ingress();
+    ObjectMeta ingressMeta = new ObjectMeta();
+    ingressMeta.setName("ingressname");
+    ingress.setMetadata(ingressMeta);
+    IngressSpec ingressSpec = new IngressSpec();
+    IngressRule ingressRule = new IngressRule();
+    ingressRule.setHost("ingresshost");
+    ingressRule.setHttp(
+        new HTTPIngressRuleValue(singletonList(new HTTPIngressPath(backend, null))));
+    ingressSpec.setRules(singletonList(ingressRule));
+    ingress.setSpec(ingressSpec);
+    return ingress;
+  }
+}
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ServicesTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ServicesTest.java
new file mode 100644
index 0000000000..f7ff7465d2
--- /dev/null
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ServicesTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.util;
+
+import static org.testng.Assert.assertEquals;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class ServicesTest {
+
+  @Test(dataProvider = "nullService")
+  public void testFindPortShouldReturnEmptyWhenSomethingIsNull(Service service) {
+    assertEquals(Services.findPort(service, 1), Optional.empty());
+  }
+
+  @Test
+  public void testFindPortWhenExists() {
+    final int PORT = 1234;
+
+    Service service = new Service();
+    ServiceSpec spec = new ServiceSpec();
+    ServicePort port = new ServicePort();
+    port.setPort(PORT);
+    spec.setPorts(Arrays.asList(port, new ServicePort()));
+    service.setSpec(spec);
+
+    assertEquals(Services.findPort(service, PORT).get(), port);
+  }
+
+  @Test(dataProvider = "nullServices")
+  public void testFindServiceWithPortShouldReturnEmptyWhenSomethingIsNull(
+      Collection<Service> services) {
+    assertEquals(Services.findServiceWithPort(services, 1), Optional.empty());
+  }
+
+  @Test
+  public void testFindServiceWhenExists() {
+    final int PORT = 1234;
+
+    Service service = new Service();
+    ServiceSpec spec = new ServiceSpec();
+    ServicePort port = new ServicePort();
+    port.setPort(PORT);
+    spec.setPorts(Collections.singletonList(port));
+    service.setSpec(spec);
+
+    assertEquals(
+        Services.findServiceWithPort(Arrays.asList(service, new Service()), PORT).get(), service);
+  }
+
+  @DataProvider
+  public Object[][] nullService() {
+    List<Service> nullServices = createNullServices();
+    Object[][] returnObjects = new Object[nullServices.size()][1];
+    for (int i = 0; i < nullServices.size(); i++) {
+      returnObjects[i][0] = nullServices.get(i);
+    }
+
+    return returnObjects;
+  }
+
+  @DataProvider
+  public Object[][] nullServices() {
+    List<Service> nullServices = createNullServices();
+
+    Object[][] returnObjects = new Object[nullServices.size() + 1][1];
+    for (int i = 0; i < nullServices.size(); i++) {
+      returnObjects[i][0] = Collections.singletonList(nullServices.get(i));
+    }
+    returnObjects[returnObjects.length - 1][0] = null;
+
+    return returnObjects;
+  }
+
+  private List<Service> createNullServices() {
+    List<Service> nullServices = new ArrayList<>();
+
+    nullServices.add(null);
+
+    Service service = new Service();
+    service.setSpec(null);
+    nullServices.add(service);
+
+    service = new Service();
+    ServiceSpec spec = new ServiceSpec();
+    spec.setPorts(null);
+    service.setSpec(spec);
+    nullServices.add(service);
+
+    service = new Service();
+    spec = new ServiceSpec();
+    ServicePort port = new ServicePort();
+    port.setPort(null);
+    spec.setPorts(Collections.singletonList(port));
+    service.setSpec(spec);
+    nullServices.add(service);
+
+    return nullServices;
+  }
+}
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java
index 251898f71b..505eef3232 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisioner.java
@@ -33,9 +33,11 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.env.EnvVars
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.limits.ram.RamLimitRequestProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.restartpolicy.RestartPolicyRewriter;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
 import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
 import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftUniqueNamesProvisioner;
 import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner;
+import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftPreviewUrlExposer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,6 +70,7 @@ public class OpenShiftEnvironmentProvisioner
   private final CertificateProvisioner certificateProvisioner;
   private final VcsSshKeysProvisioner vcsSshKeysProvisioner;
   private final GitUserProfileProvisioner gitUserProfileProvisioner;
+  private final PreviewUrlExposer<OpenShiftEnvironment> previewUrlExposer;
 
   @Inject
   public OpenShiftEnvironmentProvisioner(
@@ -86,7 +89,8 @@ public class OpenShiftEnvironmentProvisioner
       ServiceAccountProvisioner serviceAccountProvisioner,
       CertificateProvisioner certificateProvisioner,
       VcsSshKeysProvisioner vcsSshKeysProvisioner,
-      GitUserProfileProvisioner gitUserProfileProvisioner) {
+      GitUserProfileProvisioner gitUserProfileProvisioner,
+      OpenShiftPreviewUrlExposer previewUrlEndpointsProvisioner) {
     this.pvcEnabled = pvcEnabled;
     this.volumesStrategy = volumesStrategy;
     this.uniqueNamesProvisioner = uniqueNamesProvisioner;
@@ -103,6 +107,7 @@ public class OpenShiftEnvironmentProvisioner
     this.certificateProvisioner = certificateProvisioner;
     this.vcsSshKeysProvisioner = vcsSshKeysProvisioner;
     this.gitUserProfileProvisioner = gitUserProfileProvisioner;
+    this.previewUrlExposer = previewUrlEndpointsProvisioner;
   }
 
   @Override
@@ -121,6 +126,7 @@ public class OpenShiftEnvironmentProvisioner
 
     // 2 stage - converting Che model env to OpenShift env
     serversConverter.provision(osEnv, identity);
+    previewUrlExposer.expose(osEnv);
     envVarsConverter.provision(osEnv, identity);
     if (pvcEnabled) {
       volumesStrategy.provision(osEnv, identity);
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java
index 4c8f27ce83..82c692a319 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java
@@ -57,8 +57,10 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.Workspa
 import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesCheApiExternalEnvVarProvider;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.KubernetesCheApiInternalEnvVarProvider;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.PreviewUrlCommandProvisioner;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.env.LogsRootEnvVariableProvider;
 import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposer;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServiceExposureStrategy;
 import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.DefaultSecureServersFactory;
@@ -75,8 +77,10 @@ import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftE
 import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironmentFactory;
 import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProjectFactory;
 import org.eclipse.che.workspace.infrastructure.openshift.project.RemoveProjectOnWorkspaceRemove;
+import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftPreviewUrlCommandProvisioner;
 import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftCookiePathStrategy;
 import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftExternalServerExposer;
+import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftPreviewUrlExposer;
 import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftServerExposureStrategy;
 import org.eclipse.che.workspace.infrastructure.openshift.wsplugins.brokerphases.OpenshiftBrokerEnvironmentFactory;
 
@@ -119,6 +123,9 @@ public class OpenShiftInfraModule extends AbstractModule {
     bind(new TypeLiteral<ExternalServerExposer<OpenShiftEnvironment>>() {})
         .to(OpenShiftExternalServerExposer.class);
     bind(ServersConverter.class).to(new TypeLiteral<ServersConverter<OpenShiftEnvironment>>() {});
+    bind(PreviewUrlExposer.class).to(new TypeLiteral<OpenShiftPreviewUrlExposer>() {});
+    bind(PreviewUrlCommandProvisioner.class)
+        .to(new TypeLiteral<OpenShiftPreviewUrlCommandProvisioner>() {});
 
     Multibinder<EnvVarProvider> envVarProviders =
         Multibinder.newSetBinder(binder(), EnvVarProvider.class);
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java
index 1d431479f6..58679758e2 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntime.java
@@ -43,6 +43,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.util.UnrecoverablePod
 import org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.SidecarToolingProvisioner;
 import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
 import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProject;
+import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftPreviewUrlCommandProvisioner;
 import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftServerResolver;
 
 /**
@@ -72,6 +73,7 @@ public class OpenShiftInternalRuntime extends KubernetesInternalRuntime<OpenShif
       OpenShiftEnvironmentProvisioner kubernetesEnvironmentProvisioner,
       SidecarToolingProvisioner<OpenShiftEnvironment> toolingProvisioner,
       RuntimeHangingDetector runtimeHangingDetector,
+      OpenShiftPreviewUrlCommandProvisioner previewUrlCommandProvisioner,
       Tracer tracer,
       @Assisted OpenShiftRuntimeContext context,
       @Assisted OpenShiftProject project) {
@@ -94,6 +96,7 @@ public class OpenShiftInternalRuntime extends KubernetesInternalRuntime<OpenShif
         toolingProvisioner,
         null,
         runtimeHangingDetector,
+        previewUrlCommandProvisioner,
         tracer,
         context,
         project);
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironment.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironment.java
index d78b459767..a3a365427d 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironment.java
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironment.java
@@ -19,10 +19,12 @@ import io.fabric8.kubernetes.api.model.Service;
 import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.api.model.extensions.Ingress;
 import io.fabric8.openshift.api.model.Route;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.eclipse.che.api.core.model.workspace.Warning;
+import org.eclipse.che.api.core.model.workspace.config.Command;
 import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment;
 import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig;
 import org.eclipse.che.api.workspace.server.spi.environment.InternalRecipe;
@@ -140,6 +142,12 @@ public class OpenShiftEnvironment extends KubernetesEnvironment {
       return this;
     }
 
+    @Override
+    public Builder setCommands(List<? extends Command> commands) {
+      super.setCommands(new ArrayList<>(commands));
+      return this;
+    }
+
     @Override
     public Builder setPods(Map<String, Pod> pods) {
       this.pods.putAll(pods);
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisioner.java
new file mode 100644
index 0000000000..0867c03257
--- /dev/null
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisioner.java
@@ -0,0 +1,54 @@
+/*
+ * 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.provision;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.openshift.api.model.Route;
+import java.util.List;
+import java.util.Optional;
+import javax.inject.Singleton;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace;
+import org.eclipse.che.workspace.infrastructure.kubernetes.provision.PreviewUrlCommandProvisioner;
+import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
+import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProject;
+import org.eclipse.che.workspace.infrastructure.openshift.util.Routes;
+
+/**
+ * Extends {@link PreviewUrlCommandProvisioner} where needed. For OpenShift, we work with {@link
+ * Route}s and {@link OpenShiftProject}. Other than that, logic is the same as for k8s.
+ */
+@Singleton
+public class OpenShiftPreviewUrlCommandProvisioner
+    extends PreviewUrlCommandProvisioner<OpenShiftEnvironment, Route> {
+
+  @Override
+  protected List<Route> loadExposureObjects(KubernetesNamespace namespace)
+      throws InfrastructureException {
+    if (!(namespace instanceof OpenShiftProject)) {
+      throw new InternalInfrastructureException(
+          String.format(
+              "OpenShiftProject instance expected, but got '%s'. Please report a bug!",
+              namespace.getClass().getCanonicalName()));
+    }
+    OpenShiftProject project = (OpenShiftProject) namespace;
+
+    return project.routes().get();
+  }
+
+  @Override
+  protected Optional<String> findHostForServicePort(List<Route> routes, Service service, int port) {
+    return Routes.findRouteForServicePort(routes, service, port).map(r -> r.getSpec().getHost());
+  }
+}
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposer.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposer.java
new file mode 100644
index 0000000000..8310c6bce8
--- /dev/null
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposer.java
@@ -0,0 +1,41 @@
+/*
+ * 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.server;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.openshift.api.model.Route;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.PreviewUrlExposer;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposer;
+import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
+import org.eclipse.che.workspace.infrastructure.openshift.util.Routes;
+
+/**
+ * Extends {@link PreviewUrlExposer} with OpenShift capabilities. We work with {@link Route} and
+ * {@link OpenShiftEnvironment}.
+ */
+@Singleton
+public class OpenShiftPreviewUrlExposer extends PreviewUrlExposer<OpenShiftEnvironment> {
+
+  @Inject
+  public OpenShiftPreviewUrlExposer(
+      ExternalServerExposer<OpenShiftEnvironment> externalServerExposer) {
+    super(externalServerExposer);
+  }
+
+  @Override
+  protected boolean hasMatchingEndpoint(OpenShiftEnvironment env, Service service, int port) {
+    return Routes.findRouteForServicePort(env.getRoutes().values(), service, port).isPresent();
+  }
+}
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/util/Routes.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/util/Routes.java
new file mode 100644
index 0000000000..286d65a6e2
--- /dev/null
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/util/Routes.java
@@ -0,0 +1,60 @@
+/*
+ * 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.util;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.openshift.api.model.Route;
+import io.fabric8.openshift.api.model.RouteSpec;
+import java.util.Collection;
+import java.util.Optional;
+import org.eclipse.che.workspace.infrastructure.kubernetes.util.Services;
+
+/** Util class that helps working with OpenShift Routes */
+public class Routes {
+
+  /**
+   * In given {@code routes} finds route that for given {@code service} and {@code port}
+   *
+   * @return found {@link Route} or {@link Optional#empty()}
+   */
+  public static Optional<Route> findRouteForServicePort(
+      Collection<Route> routes, Service service, int port) {
+    Optional<ServicePort> foundPort = Services.findPort(service, port);
+    if (!foundPort.isPresent()) {
+      return Optional.empty();
+    }
+
+    for (Route route : routes) {
+      RouteSpec spec = route.getSpec();
+      if (spec.getTo().getName().equals(service.getMetadata().getName())
+          && matchesPort(foundPort.get(), spec.getPort().getTargetPort())) {
+        return Optional.of(route);
+      }
+    }
+    return Optional.empty();
+  }
+
+  private static boolean matchesPort(ServicePort servicePort, IntOrString routePort) {
+    if (routePort.getStrVal() != null && routePort.getStrVal().equals(servicePort.getName())) {
+      return true;
+    }
+
+    if (routePort.getIntVal() != null && routePort.getIntVal().equals(servicePort.getPort())) {
+      return true;
+    }
+
+    return false;
+  }
+}
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java
index 42ef2a69cc..53b7f01c38 100644
--- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftEnvironmentProvisionerTest.java
@@ -31,6 +31,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.Serv
 import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
 import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftUniqueNamesProvisioner;
 import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner;
+import org.eclipse.che.workspace.infrastructure.openshift.server.OpenShiftPreviewUrlExposer;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.testng.MockitoTestNGListener;
@@ -63,6 +64,7 @@ public class OpenShiftEnvironmentProvisionerTest {
   @Mock private CertificateProvisioner certificateProvisioner;
   @Mock private VcsSshKeysProvisioner vcsSshKeysProvisioner;
   @Mock private GitUserProfileProvisioner gitUserProfileProvisioner;
+  @Mock private OpenShiftPreviewUrlExposer previewUrlEndpointsProvisioner;
 
   private OpenShiftEnvironmentProvisioner osInfraProvisioner;
 
@@ -87,7 +89,8 @@ public class OpenShiftEnvironmentProvisionerTest {
             serviceAccountProvisioner,
             certificateProvisioner,
             vcsSshKeysProvisioner,
-            gitUserProfileProvisioner);
+            gitUserProfileProvisioner,
+            previewUrlEndpointsProvisioner);
     provisionOrder =
         inOrder(
             logsVolumeMachineProvisioner,
@@ -104,7 +107,8 @@ public class OpenShiftEnvironmentProvisionerTest {
             serviceAccountProvisioner,
             certificateProvisioner,
             vcsSshKeysProvisioner,
-            gitUserProfileProvisioner);
+            gitUserProfileProvisioner,
+            previewUrlEndpointsProvisioner);
   }
 
   @Test
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java
index b317cc0d7d..9d14dc0cfe 100644
--- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInternalRuntimeTest.java
@@ -82,6 +82,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.SidecarTool
 import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
 import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProject;
 import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftRoutes;
+import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftPreviewUrlCommandProvisioner;
 import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
@@ -139,6 +140,7 @@ public class OpenShiftInternalRuntimeTest {
   @Mock private SidecarToolingProvisioner<OpenShiftEnvironment> toolingProvisioner;
   @Mock private UnrecoverablePodEventListenerFactory unrecoverablePodEventListenerFactory;
   @Mock private RuntimeHangingDetector runtimeHangingDetector;
+  @Mock private OpenShiftPreviewUrlCommandProvisioner previewUrlCommandProvisioner;
 
   @Mock(answer = Answers.RETURNS_MOCKS)
   private Tracer tracer;
@@ -175,6 +177,7 @@ public class OpenShiftInternalRuntimeTest {
             kubernetesEnvironmentProvisioner,
             toolingProvisioner,
             runtimeHangingDetector,
+            previewUrlCommandProvisioner,
             tracer,
             context,
             project);
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisionerTest.java
new file mode 100644
index 0000000000..47aa2a04da
--- /dev/null
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftPreviewUrlCommandProvisionerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.provision;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.openshift.api.model.Route;
+import io.fabric8.openshift.api.model.RoutePort;
+import io.fabric8.openshift.api.model.RouteSpec;
+import io.fabric8.openshift.api.model.RouteTargetReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.workspace.infrastructure.kubernetes.Warnings;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespace;
+import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesServices;
+import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
+import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProject;
+import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftRoutes;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+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 OpenShiftPreviewUrlCommandProvisionerTest {
+
+  private OpenShiftPreviewUrlCommandProvisioner previewUrlCommandProvisioner;
+  @Mock private OpenShiftEnvironment mockEnvironment;
+  @Mock private OpenShiftProject mockProject;
+  @Mock private KubernetesServices mockServices;
+  @Mock private OpenShiftRoutes mockRoutes;
+
+  @BeforeMethod
+  public void setUp() {
+    previewUrlCommandProvisioner = new OpenShiftPreviewUrlCommandProvisioner();
+  }
+
+  @Test
+  public void shouldDoNothingWhenGetCommandsIsNull() throws InfrastructureException {
+    Mockito.when(mockEnvironment.getCommands()).thenReturn(null);
+
+    previewUrlCommandProvisioner.provision(mockEnvironment, mockProject);
+  }
+
+  @Test(expectedExceptions = InternalInfrastructureException.class)
+  public void throwsInfrastructureExceptionWhenK8sNamespaces() throws InfrastructureException {
+    KubernetesNamespace namespace = Mockito.mock(KubernetesNamespace.class);
+    previewUrlCommandProvisioner.provision(mockEnvironment, namespace);
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandsDefined() throws InfrastructureException {
+    Mockito.when(mockEnvironment.getCommands()).thenReturn(Collections.emptyList());
+    Mockito.when(mockProject.routes()).thenReturn(mockRoutes);
+    Mockito.when(mockProject.services()).thenReturn(mockServices);
+
+    previewUrlCommandProvisioner.provision(mockEnvironment, mockProject);
+  }
+
+  @Test
+  public void shouldDoNothingWhenCommandsWithoutPreviewUrlDefined() throws InfrastructureException {
+    List<CommandImpl> commands =
+        Arrays.asList(new CommandImpl("a", "a", "a"), new CommandImpl("b", "b", "b"));
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockProject.routes()).thenReturn(mockRoutes);
+    Mockito.when(mockProject.services()).thenReturn(mockServices);
+
+    previewUrlCommandProvisioner.provision(env, mockProject);
+
+    assertTrue(commands.containsAll(env.getCommands()));
+    assertTrue(env.getCommands().containsAll(commands));
+    assertTrue(env.getWarnings().isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenCantFindServiceForPreviewurl() throws InfrastructureException {
+    List<CommandImpl> commands =
+        Collections.singletonList(
+            new CommandImpl("a", "a", "a", new PreviewUrlImpl(8080, null), Collections.emptyMap()));
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockProject.routes()).thenReturn(mockRoutes);
+    Mockito.when(mockProject.services()).thenReturn(mockServices);
+    Mockito.when(mockServices.get()).thenReturn(Collections.emptyList());
+
+    previewUrlCommandProvisioner.provision(env, mockProject);
+
+    assertTrue(commands.containsAll(env.getCommands()));
+    assertTrue(env.getCommands().containsAll(commands));
+    assertEquals(
+        env.getWarnings().get(0).getCode(), Warnings.NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL);
+  }
+
+  @Test
+  public void shouldDoNothingWhenCantFindRouteForPreviewUrl() throws InfrastructureException {
+    int port = 8080;
+    List<CommandImpl> commands =
+        Collections.singletonList(
+            new CommandImpl("a", "a", "a", new PreviewUrlImpl(port, null), Collections.emptyMap()));
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockProject.services()).thenReturn(mockServices);
+    Service service = new Service();
+    ServiceSpec spec = new ServiceSpec();
+    spec.setPorts(
+        Collections.singletonList(new ServicePort("a", null, port, "TCP", new IntOrString(port))));
+    service.setSpec(spec);
+    Mockito.when(mockServices.get()).thenReturn(Collections.singletonList(service));
+
+    Mockito.when(mockProject.routes()).thenReturn(mockRoutes);
+    Mockito.when(mockRoutes.get()).thenReturn(Collections.emptyList());
+
+    previewUrlCommandProvisioner.provision(env, mockProject);
+
+    assertTrue(commands.containsAll(env.getCommands()));
+    assertTrue(env.getCommands().containsAll(commands));
+    assertEquals(
+        env.getWarnings().get(0).getCode(), Warnings.NOT_ABLE_TO_PROVISION_OBJECTS_FOR_PREVIEW_URL);
+  }
+
+  @Test
+  public void shouldUpdateCommandWhenServiceAndIngressFound() throws InfrastructureException {
+    int port = 8080;
+    List<CommandImpl> commands =
+        Collections.singletonList(
+            new CommandImpl("a", "a", "a", new PreviewUrlImpl(port, null), Collections.emptyMap()));
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder().setCommands(new ArrayList<>(commands)).build();
+
+    Mockito.when(mockProject.services()).thenReturn(mockServices);
+    Service service = new Service();
+    ObjectMeta metadata = new ObjectMeta();
+    metadata.setName("servicename");
+    service.setMetadata(metadata);
+    ServiceSpec spec = new ServiceSpec();
+    spec.setPorts(
+        Collections.singletonList(
+            new ServicePort("8080", null, port, "TCP", new IntOrString(port))));
+    service.setSpec(spec);
+    Mockito.when(mockServices.get()).thenReturn(Collections.singletonList(service));
+
+    Route route = new Route();
+    RouteSpec routeSpec = new RouteSpec();
+    routeSpec.setPort(new RoutePort(new IntOrString("8080")));
+    routeSpec.setTo(new RouteTargetReference("a", "servicename", 1));
+    routeSpec.setHost("testhost");
+    route.setSpec(routeSpec);
+
+    Mockito.when(mockProject.routes()).thenReturn(mockRoutes);
+    Mockito.when(mockRoutes.get()).thenReturn(Collections.singletonList(route));
+
+    previewUrlCommandProvisioner.provision(env, mockProject);
+
+    assertTrue(env.getCommands().get(0).getAttributes().containsKey("previewUrl"));
+    assertEquals(env.getCommands().get(0).getAttributes().get("previewUrl"), "testhost");
+    assertTrue(env.getWarnings().isEmpty());
+  }
+}
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposerTest.java
new file mode 100644
index 0000000000..20b8bc09f6
--- /dev/null
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/server/OpenShiftPreviewUrlExposerTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.server;
+
+import static java.util.Collections.singletonList;
+import static org.testng.Assert.*;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.openshift.api.model.Route;
+import io.fabric8.openshift.api.model.RoutePort;
+import io.fabric8.openshift.api.model.RouteSpec;
+import io.fabric8.openshift.api.model.RouteTargetReference;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
+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 OpenShiftPreviewUrlExposerTest {
+
+  private OpenShiftPreviewUrlExposer previewUrlEndpointsProvisioner;
+
+  @BeforeMethod
+  public void setUp() {
+    OpenShiftExternalServerExposer externalServerExposer = new OpenShiftExternalServerExposer();
+    previewUrlEndpointsProvisioner = new OpenShiftPreviewUrlExposer(externalServerExposer);
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandsDefined() throws InternalInfrastructureException {
+    OpenShiftEnvironment env = OpenShiftEnvironment.builder().build();
+
+    previewUrlEndpointsProvisioner.expose(env);
+
+    assertTrue(env.getCommands().isEmpty());
+    assertTrue(env.getServices().isEmpty());
+    assertTrue(env.getRoutes().isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandWithPreviewUrlDefined()
+      throws InternalInfrastructureException {
+    CommandImpl command = new CommandImpl("a", "a", "a");
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder().setCommands(singletonList(new CommandImpl(command))).build();
+
+    previewUrlEndpointsProvisioner.expose(env);
+
+    assertEquals(env.getCommands().get(0), command);
+    assertTrue(env.getServices().isEmpty());
+    assertTrue(env.getRoutes().isEmpty());
+  }
+
+  @Test
+  public void shouldNotProvisionWhenServiceAndRouteFound() throws InternalInfrastructureException {
+    final int PORT = 8080;
+    final String SERVER_PORT_NAME = "server-" + PORT;
+
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap());
+
+    Service service = new Service();
+    ObjectMeta serviceMeta = new ObjectMeta();
+    serviceMeta.setName("servicename");
+    service.setMetadata(serviceMeta);
+    ServiceSpec serviceSpec = new ServiceSpec();
+    serviceSpec.setPorts(
+        singletonList(new ServicePort(SERVER_PORT_NAME, null, PORT, "TCP", new IntOrString(PORT))));
+    service.setSpec(serviceSpec);
+
+    Route route = new Route();
+    RouteSpec routeSpec = new RouteSpec();
+    routeSpec.setPort(new RoutePort(new IntOrString(SERVER_PORT_NAME)));
+    routeSpec.setTo(new RouteTargetReference("routekind", "servicename", 1));
+    route.setSpec(routeSpec);
+
+    Map<String, Service> services = new HashMap<>();
+    services.put("servicename", service);
+    Map<String, Route> routes = new HashMap<>();
+    routes.put("routename", route);
+
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .setServices(services)
+            .setRoutes(routes)
+            .build();
+
+    assertEquals(env.getRoutes().size(), 1);
+    previewUrlEndpointsProvisioner.expose(env);
+    assertEquals(env.getRoutes().size(), 1);
+  }
+
+  @Test
+  public void shouldProvisionRouteWhenNotFound() throws InternalInfrastructureException {
+    final int PORT = 8080;
+    final String SERVER_PORT_NAME = "server-" + PORT;
+    final String SERVICE_NAME = "servicename";
+
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap());
+
+    Service service = new Service();
+    ObjectMeta serviceMeta = new ObjectMeta();
+    serviceMeta.setName(SERVICE_NAME);
+    service.setMetadata(serviceMeta);
+    ServiceSpec serviceSpec = new ServiceSpec();
+    serviceSpec.setPorts(
+        singletonList(new ServicePort(SERVER_PORT_NAME, null, PORT, "TCP", new IntOrString(PORT))));
+    service.setSpec(serviceSpec);
+
+    Map<String, Service> services = new HashMap<>();
+    services.put(SERVICE_NAME, service);
+
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .setServices(services)
+            .setRoutes(new HashMap<>())
+            .build();
+
+    previewUrlEndpointsProvisioner.expose(env);
+    assertEquals(env.getRoutes().size(), 1);
+    Route provisionedRoute = env.getRoutes().values().iterator().next();
+    assertEquals(provisionedRoute.getSpec().getTo().getName(), SERVICE_NAME);
+    assertEquals(
+        provisionedRoute.getSpec().getPort().getTargetPort().getStrVal(), SERVER_PORT_NAME);
+  }
+
+  @Test
+  public void shouldProvisionServiceAndRouteWhenNotFound() throws InternalInfrastructureException {
+    final int PORT = 8080;
+    final String SERVER_PORT_NAME = "server-" + PORT;
+
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(PORT, null), Collections.emptyMap());
+
+    OpenShiftEnvironment env =
+        OpenShiftEnvironment.builder()
+            .setCommands(singletonList(new CommandImpl(command)))
+            .setRoutes(new HashMap<>())
+            .setServices(new HashMap<>())
+            .build();
+
+    previewUrlEndpointsProvisioner.expose(env);
+
+    assertEquals(env.getRoutes().size(), 1);
+    assertEquals(env.getServices().size(), 1);
+
+    Service provisionedService = env.getServices().values().iterator().next();
+    ServicePort provisionedServicePort = provisionedService.getSpec().getPorts().get(0);
+    assertEquals(provisionedServicePort.getName(), SERVER_PORT_NAME);
+    assertEquals(provisionedServicePort.getPort().intValue(), PORT);
+
+    Route provisionedRoute = env.getRoutes().values().iterator().next();
+    assertEquals(
+        provisionedRoute.getSpec().getTo().getName(), provisionedService.getMetadata().getName());
+    assertEquals(
+        provisionedRoute.getSpec().getPort().getTargetPort().getStrVal(), SERVER_PORT_NAME);
+  }
+}
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/util/RoutesTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/util/RoutesTest.java
new file mode 100644
index 0000000000..e503f16329
--- /dev/null
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/util/RoutesTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.util;
+
+import static org.testng.Assert.*;
+
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.openshift.api.model.Route;
+import io.fabric8.openshift.api.model.RoutePort;
+import io.fabric8.openshift.api.model.RouteSpec;
+import io.fabric8.openshift.api.model.RouteTargetReference;
+import java.util.Collections;
+import java.util.Optional;
+import org.testng.annotations.Test;
+
+public class RoutesTest {
+
+  @Test
+  public void shouldFindRouteWhenPortDefinedByString() {
+    int portInt = 8080;
+    String portString = "8080";
+
+    Service service = createService(portString, portInt);
+    Route route = createRoute(new IntOrString(portString));
+
+    Optional<Route> foundRoute =
+        Routes.findRouteForServicePort(Collections.singletonList(route), service, portInt);
+    assertTrue(foundRoute.isPresent());
+    assertEquals(foundRoute.get().getSpec().getHost(), "testhost");
+  }
+
+  @Test
+  public void shouldFindRouteWhenPortDefinedByInt() {
+    int portInt = 8080;
+    String portString = "8080";
+
+    Service service = createService(portString, portInt);
+    Route route = createRoute(new IntOrString(portInt));
+
+    Optional<Route> foundRoute =
+        Routes.findRouteForServicePort(Collections.singletonList(route), service, portInt);
+    assertTrue(foundRoute.isPresent());
+    assertEquals(foundRoute.get().getSpec().getHost(), "testhost");
+  }
+
+  @Test
+  public void shouldReturnEmptyWhenNotFoundByInt() {
+    int portInt = 8080;
+    String portString = "8080";
+
+    Service service = createService(portString, portInt);
+    Route route = createRoute(new IntOrString(666));
+
+    Optional<Route> foundRoute =
+        Routes.findRouteForServicePort(Collections.singletonList(route), service, portInt);
+    assertFalse(foundRoute.isPresent());
+  }
+
+  @Test
+  public void shouldReturnEmptyWhenNotFoundByString() {
+    int portInt = 8080;
+    String portString = "8080";
+
+    Service service = createService(portString, portInt);
+    Route route = createRoute(new IntOrString("666"));
+
+    Optional<Route> foundRoute =
+        Routes.findRouteForServicePort(Collections.singletonList(route), service, portInt);
+    assertFalse(foundRoute.isPresent());
+  }
+
+  private Route createRoute(IntOrString port) {
+    Route route = new Route();
+    RouteSpec routeSpec = new RouteSpec();
+    routeSpec.setPort(new RoutePort(port));
+    routeSpec.setTo(new RouteTargetReference("a", "servicename", 1));
+    routeSpec.setHost("testhost");
+    route.setSpec(routeSpec);
+    return route;
+  }
+
+  private Service createService(String portString, int portInt) {
+    Service service = new Service();
+    ObjectMeta metadata = new ObjectMeta();
+    metadata.setName("servicename");
+    service.setMetadata(metadata);
+    ServiceSpec spec = new ServiceSpec();
+    spec.setPorts(
+        Collections.singletonList(new ServicePort(portString, null, portInt, "TCP", null)));
+    service.setSpec(spec);
+    return service;
+  }
+}
diff --git a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/DevfileCommandDto.java b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/DevfileCommandDto.java
index 25949c990b..2a8d77a115 100644
--- a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/DevfileCommandDto.java
+++ b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/DevfileCommandDto.java
@@ -26,6 +26,12 @@ public interface DevfileCommandDto extends Command {
 
   DevfileCommandDto withName(String name);
 
+  void setPreviewUrl(PreviewUrlDto previewUrl);
+
+  DevfileCommandDto withPreviewUrl(PreviewUrlDto previewUrl);
+
+  PreviewUrlDto getPreviewUrl();
+
   @Override
   List<DevfileActionDto> getActions();
 
diff --git a/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/PreviewUrlDto.java b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/PreviewUrlDto.java
new file mode 100644
index 0000000000..e35aca4d88
--- /dev/null
+++ b/wsmaster/che-core-api-workspace-shared/src/main/java/org/eclipse/che/api/workspace/shared/dto/devfile/PreviewUrlDto.java
@@ -0,0 +1,34 @@
+/*
+ * 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.workspace.shared.dto.devfile;
+
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
+import org.eclipse.che.dto.shared.DTO;
+
+@DTO
+public interface PreviewUrlDto extends PreviewUrl {
+
+  @Override
+  int getPort();
+
+  @Override
+  String getPath();
+
+  void setPort(int port);
+
+  PreviewUrlDto withPort(int port);
+
+  void setPath(String path);
+
+  PreviewUrlDto withPath(String path);
+}
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java
index 0f5cb5c018..16a6d26161 100644
--- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/DtoConverter.java
@@ -35,6 +35,7 @@ import org.eclipse.che.api.core.model.workspace.devfile.Endpoint;
 import org.eclipse.che.api.core.model.workspace.devfile.Entrypoint;
 import org.eclipse.che.api.core.model.workspace.devfile.Env;
 import org.eclipse.che.api.core.model.workspace.devfile.Metadata;
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
 import org.eclipse.che.api.core.model.workspace.devfile.Project;
 import org.eclipse.che.api.core.model.workspace.devfile.Source;
 import org.eclipse.che.api.core.model.workspace.runtime.Machine;
@@ -64,6 +65,7 @@ import org.eclipse.che.api.workspace.shared.dto.devfile.EndpointDto;
 import org.eclipse.che.api.workspace.shared.dto.devfile.EntrypointDto;
 import org.eclipse.che.api.workspace.shared.dto.devfile.EnvDto;
 import org.eclipse.che.api.workspace.shared.dto.devfile.MetadataDto;
+import org.eclipse.che.api.workspace.shared.dto.devfile.PreviewUrlDto;
 import org.eclipse.che.api.workspace.shared.dto.devfile.ProjectDto;
 import org.eclipse.che.api.workspace.shared.dto.devfile.SourceDto;
 
@@ -188,10 +190,30 @@ public final class DtoConverter {
       org.eclipse.che.api.core.model.workspace.devfile.Command command) {
     List<DevfileActionDto> actions =
         command.getActions().stream().map(DtoConverter::asDto).collect(toList());
-    return newDto(DevfileCommandDto.class)
-        .withName(command.getName())
-        .withActions(actions)
-        .withAttributes(command.getAttributes());
+
+    DevfileCommandDto commandDto =
+        newDto(DevfileCommandDto.class)
+            .withName(command.getName())
+            .withActions(actions)
+            .withAttributes(command.getAttributes());
+
+    if (command.getPreviewUrl() != null) {
+      commandDto.setPreviewUrl(asDto(command.getPreviewUrl()));
+    }
+    return commandDto;
+  }
+
+  private static PreviewUrlDto asDto(PreviewUrl previewUrl) {
+    final PreviewUrlDto previewUrlDto = newDto(PreviewUrlDto.class);
+    if (previewUrl != null) {
+      if (previewUrl.getPath() != null) {
+        previewUrlDto.setPath(previewUrl.getPath());
+      }
+      if (previewUrl.getPort() != 0) {
+        previewUrlDto.setPort(previewUrl.getPort());
+      }
+    }
+    return previewUrlDto;
   }
 
   private static DevfileActionDto asDto(Action action) {
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGenerator.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGenerator.java
new file mode 100644
index 0000000000..b79b8bd112
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGenerator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.workspace.server;
+
+import static org.eclipse.che.api.core.model.workspace.config.Command.PREVIEW_URL_ATTRIBUTE;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Singleton;
+import javax.ws.rs.core.UriBuilder;
+import org.eclipse.che.api.core.model.workspace.config.Command;
+import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
+
+/**
+ * Helps to generate links for Preview URLs and update commands with these links.
+ *
+ * <p>For preview URLs, we need to include following data in {@link
+ * org.eclipse.che.api.workspace.shared.dto.WorkspaceDto}:
+ * <li>Map with final preview url links where keys are composed from prefix {@link
+ *     PreviewUrlLinksVariableGenerator#PREVIEW_URL_VARIABLE_PREFIX}, modified command name and CRC
+ *     hash.
+ * <li>Each command that has defined Preview url must have attribute `previewUrl` referencing to
+ *     correct key in the map in format that IDE will understand.
+ */
+@Singleton
+class PreviewUrlLinksVariableGenerator {
+
+  private static final String PREVIEW_URL_VARIABLE_PREFIX = "previewurl/";
+
+  /**
+   * Takes commands from given {@code workspace}. For all commands that have defined previewUrl,
+   * creates variable name in defined format and final preview url link. It updates the command so
+   * it's `previewUrl` attribute will contain variable in proper format. Method then returns map of
+   * all preview url links with these variables as keys:
+   *
+   * <pre>
+   *   links:
+   *     "previewURl/run_123": http://your.domain
+   *   command:
+   *     attributes:
+   *       previewUrl: '${previewUrl/run_123}/some/path'
+   * </pre>
+   *
+   * @return map of all <commandPreviewUrlVariable, previewUrlFullLink>
+   */
+  Map<String, String> genLinksMapAndUpdateCommands(WorkspaceImpl workspace, UriBuilder uriBuilder) {
+    if (workspace == null
+        || workspace.getRuntime() == null
+        || workspace.getRuntime().getCommands() == null
+        || uriBuilder == null) {
+      return Collections.emptyMap();
+    }
+
+    Map<String, String> links = new HashMap<>();
+    for (Command command : workspace.getRuntime().getCommands()) {
+      Map<String, String> commandAttributes = command.getAttributes();
+
+      if (command.getPreviewUrl() != null
+          && commandAttributes != null
+          && commandAttributes.containsKey(PREVIEW_URL_ATTRIBUTE)) {
+        String previewUrlLinkValue = createPreviewUrlLinkValue(uriBuilder, command);
+        String previewUrlLinkKey = createPreviewUrlLinkKey(command);
+        links.put(previewUrlLinkKey, previewUrlLinkValue);
+
+        commandAttributes.replace(
+            PREVIEW_URL_ATTRIBUTE,
+            formatAttributeValue(previewUrlLinkKey, command.getPreviewUrl().getPath()));
+      }
+    }
+    return links;
+  }
+
+  private String createPreviewUrlLinkValue(UriBuilder uriBuilder, Command command) {
+    UriBuilder previewUriBuilder =
+        uriBuilder.clone().host(command.getAttributes().get(PREVIEW_URL_ATTRIBUTE));
+    previewUriBuilder.replacePath(null);
+    return previewUriBuilder.build().toString();
+  }
+
+  /**
+   * Creates link key for given command in format
+   * `previewUrl/<commandName_withoutSpaces>_<hash(command.name)>`
+   */
+  private String createPreviewUrlLinkKey(Command command) {
+    return PREVIEW_URL_VARIABLE_PREFIX
+        + command.getName().replaceAll(" ", "")
+        + "_"
+        + Math.abs(command.getName().hashCode());
+  }
+
+  private String formatAttributeValue(String var, String path) {
+    String previewUrlAttributeValue = "${" + var + "}";
+    if (path != null) {
+      previewUrlAttributeValue += path;
+    }
+    return previewUrlAttributeValue;
+  }
+}
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGenerator.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGenerator.java
index 0b6c9487ea..29b4000a4d 100644
--- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGenerator.java
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGenerator.java
@@ -41,13 +41,16 @@ import org.eclipse.che.api.workspace.server.spi.RuntimeContext;
 public class WorkspaceLinksGenerator {
 
   private final WorkspaceRuntimes workspaceRuntimes;
+  private final PreviewUrlLinksVariableGenerator previewUrlLinksVariableGenerator;
   private final String cheWebsocketEndpoint;
 
   @Inject
   public WorkspaceLinksGenerator(
       WorkspaceRuntimes workspaceRuntimes,
+      PreviewUrlLinksVariableGenerator previewUrlLinksVariableGenerator,
       @Named("che.websocket.endpoint") String cheWebsocketEndpoint) {
     this.workspaceRuntimes = workspaceRuntimes;
+    this.previewUrlLinksVariableGenerator = previewUrlLinksVariableGenerator;
     this.cheWebsocketEndpoint = cheWebsocketEndpoint;
   }
 
@@ -77,6 +80,10 @@ public class WorkspaceLinksGenerator {
       addRuntimeLinks(links, workspace.getId(), serviceContext);
     }
 
+    links.putAll(
+        previewUrlLinksVariableGenerator.genLinksMapAndUpdateCommands(
+            workspace, uriBuilder.clone()));
+
     return links;
   }
 
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/Constants.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/Constants.java
index be1b2d2813..486651f6bd 100644
--- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/Constants.java
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/Constants.java
@@ -11,7 +11,7 @@
  */
 package org.eclipse.che.api.workspace.server.devfile;
 
-import java.util.Collections;
+import java.util.Arrays;
 import java.util.List;
 
 public class Constants {
@@ -25,7 +25,7 @@ public class Constants {
   public static final String CURRENT_API_VERSION = "1.0.0";
 
   public static final List<String> SUPPORTED_VERSIONS =
-      Collections.singletonList(CURRENT_API_VERSION);
+      Arrays.asList(CURRENT_API_VERSION, "1.0.1-beta");
 
   public static final String EDITOR_COMPONENT_TYPE = "cheEditor";
 
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/CommandConverter.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/CommandConverter.java
index e74579fff8..c492e82f1f 100644
--- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/CommandConverter.java
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/devfile/convert/CommandConverter.java
@@ -95,6 +95,7 @@ public class CommandConverter {
     command.setName(devCommand.getName());
     command.setType(commandAction.getType());
     command.setCommandLine(commandAction.getCommand());
+    command.setPreviewUrl(devCommand.getPreviewUrl());
 
     if (commandAction.getWorkdir() != null) {
       command.getAttributes().put(WORKING_DIRECTORY_ATTRIBUTE, commandAction.getWorkdir());
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/CommandImpl.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/CommandImpl.java
index e9363bdba7..005791978a 100644
--- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/CommandImpl.java
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/CommandImpl.java
@@ -14,9 +14,11 @@ package org.eclipse.che.api.workspace.server.model.impl;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.StringJoiner;
 import javax.persistence.CollectionTable;
 import javax.persistence.Column;
 import javax.persistence.ElementCollection;
+import javax.persistence.Embedded;
 import javax.persistence.Entity;
 import javax.persistence.FetchType;
 import javax.persistence.GeneratedValue;
@@ -25,6 +27,8 @@ import javax.persistence.JoinColumn;
 import javax.persistence.MapKeyColumn;
 import javax.persistence.Table;
 import org.eclipse.che.api.core.model.workspace.config.Command;
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
 
 /**
  * Data object for {@link Command}.
@@ -49,6 +53,8 @@ public class CommandImpl implements Command {
   @Column(name = "type", nullable = false)
   private String type;
 
+  @Embedded private PreviewUrlImpl previewUrl;
+
   @ElementCollection(fetch = FetchType.EAGER)
   @CollectionTable(name = "command_attributes", joinColumns = @JoinColumn(name = "command_id"))
   @MapKeyColumn(name = "name")
@@ -63,10 +69,26 @@ public class CommandImpl implements Command {
     this.type = type;
   }
 
+  public CommandImpl(
+      String name,
+      String commandLine,
+      String type,
+      PreviewUrlImpl previewUrl,
+      Map<String, String> attributes) {
+    this.name = name;
+    this.commandLine = commandLine;
+    this.type = type;
+    this.previewUrl = previewUrl;
+    this.attributes = new HashMap<>(attributes);
+  }
+
   public CommandImpl(Command command) {
     this.name = command.getName();
     this.commandLine = command.getCommandLine();
     this.type = command.getType();
+    if (command.getPreviewUrl() != null) {
+      this.previewUrl = new PreviewUrlImpl(command.getPreviewUrl());
+    }
     this.attributes = new HashMap<>(command.getAttributes());
   }
 
@@ -88,6 +110,18 @@ public class CommandImpl implements Command {
     this.commandLine = commandLine;
   }
 
+  public PreviewUrlImpl getPreviewUrl() {
+    return previewUrl;
+  }
+
+  public void setPreviewUrl(PreviewUrl previewUrl) {
+    if (previewUrl != null) {
+      this.previewUrl = new PreviewUrlImpl(previewUrl);
+    } else {
+      this.previewUrl = null;
+    }
+  }
+
   @Override
   public String getType() {
     return type;
@@ -110,48 +144,36 @@ public class CommandImpl implements Command {
   }
 
   @Override
-  public boolean equals(Object obj) {
-    if (this == obj) {
+  public boolean equals(Object o) {
+    if (this == o) {
       return true;
     }
-    if (!(obj instanceof CommandImpl)) {
+    if (o == null || getClass() != o.getClass()) {
       return false;
     }
-    final CommandImpl that = (CommandImpl) obj;
-    return Objects.equals(id, that.id)
-        && Objects.equals(name, that.name)
-        && Objects.equals(commandLine, that.commandLine)
-        && Objects.equals(type, that.type)
-        && getAttributes().equals(that.getAttributes());
+    CommandImpl command = (CommandImpl) o;
+    return Objects.equals(id, command.id)
+        && Objects.equals(name, command.name)
+        && Objects.equals(commandLine, command.commandLine)
+        && Objects.equals(type, command.type)
+        && Objects.equals(previewUrl, command.previewUrl)
+        && Objects.equals(attributes, command.attributes);
   }
 
   @Override
   public int hashCode() {
-    int hash = 7;
-    hash = 31 * hash + Objects.hashCode(id);
-    hash = 31 * hash + Objects.hashCode(name);
-    hash = 31 * hash + Objects.hashCode(commandLine);
-    hash = 31 * hash + Objects.hashCode(type);
-    hash = 31 * hash + getAttributes().hashCode();
-    return hash;
+    return Objects.hash(id, name, commandLine, type, previewUrl, attributes);
   }
 
   @Override
   public String toString() {
-    return "CommandImpl{"
-        + "id="
-        + id
-        + ", name='"
-        + name
-        + '\''
-        + ", commandLine='"
-        + commandLine
-        + '\''
-        + ", type='"
-        + type
-        + '\''
-        + ", attributes="
-        + attributes
-        + '}';
+    return new StringJoiner(", ", CommandImpl.class.getSimpleName() + "[", "]")
+        .add("id=" + id)
+        .add("name='" + name + "'")
+        .add("commandLine='" + commandLine + "'")
+        .add("type='" + type + "'")
+        .add("previewUrl=" + previewUrl)
+        .add("attributes=" + attributes)
+        .toString();
   }
 }
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/CommandImpl.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/CommandImpl.java
index f037eb8c75..29a69a19a1 100644
--- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/CommandImpl.java
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/CommandImpl.java
@@ -18,10 +18,12 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.StringJoiner;
 import javax.persistence.CascadeType;
 import javax.persistence.CollectionTable;
 import javax.persistence.Column;
 import javax.persistence.ElementCollection;
+import javax.persistence.Embedded;
 import javax.persistence.Entity;
 import javax.persistence.FetchType;
 import javax.persistence.GeneratedValue;
@@ -32,6 +34,7 @@ import javax.persistence.OneToMany;
 import javax.persistence.Table;
 import org.eclipse.che.api.core.model.workspace.devfile.Action;
 import org.eclipse.che.api.core.model.workspace.devfile.Command;
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
 
 /** @author Sergii Leshchenko */
 @Entity(name = "DevfileCommand")
@@ -46,6 +49,8 @@ public class CommandImpl implements Command {
   @Column(name = "name", nullable = false)
   private String name;
 
+  @Embedded private PreviewUrlImpl previewUrl;
+
   @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
   @JoinColumn(name = "devfile_command_id")
   private List<ActionImpl> actions;
@@ -60,7 +65,11 @@ public class CommandImpl implements Command {
 
   public CommandImpl() {}
 
-  public CommandImpl(String name, List<? extends Action> actions, Map<String, String> attributes) {
+  public CommandImpl(
+      String name,
+      List<? extends Action> actions,
+      Map<String, String> attributes,
+      PreviewUrl previewUrl) {
     this.name = name;
     if (actions != null) {
       this.actions = actions.stream().map(ActionImpl::new).collect(toCollection(ArrayList::new));
@@ -68,10 +77,13 @@ public class CommandImpl implements Command {
     if (attributes != null) {
       this.attributes = new HashMap<>(attributes);
     }
+    if (previewUrl != null) {
+      this.previewUrl = new PreviewUrlImpl(previewUrl.getPort(), previewUrl.getPath());
+    }
   }
 
   public CommandImpl(Command command) {
-    this(command.getName(), command.getActions(), command.getAttributes());
+    this(command.getName(), command.getActions(), command.getAttributes(), command.getPreviewUrl());
   }
 
   @Override
@@ -83,6 +95,15 @@ public class CommandImpl implements Command {
     this.name = name;
   }
 
+  @Override
+  public PreviewUrlImpl getPreviewUrl() {
+    return previewUrl;
+  }
+
+  public void setPreviewUrl(PreviewUrlImpl previewUrl) {
+    this.previewUrl = previewUrl;
+  }
+
   @Override
   public List<ActionImpl> getActions() {
     if (actions == null) {
@@ -107,39 +128,35 @@ public class CommandImpl implements Command {
     this.attributes = attributes;
   }
 
+  @Override
+  public String toString() {
+    return new StringJoiner(", ", CommandImpl.class.getSimpleName() + "[", "]")
+        .add("id=" + id)
+        .add("name='" + name + "'")
+        .add("previewURL=" + previewUrl)
+        .add("actions=" + actions)
+        .add("attributes=" + attributes)
+        .toString();
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
       return true;
     }
-    if (!(o instanceof CommandImpl)) {
+    if (o == null || getClass() != o.getClass()) {
       return false;
     }
     CommandImpl command = (CommandImpl) o;
     return Objects.equals(id, command.id)
         && Objects.equals(name, command.name)
-        && Objects.equals(getActions(), command.getActions())
-        && Objects.equals(getAttributes(), command.getAttributes());
+        && Objects.equals(previewUrl, command.previewUrl)
+        && Objects.equals(actions, command.actions)
+        && Objects.equals(attributes, command.attributes);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(id, name, getActions(), getAttributes());
-  }
-
-  @Override
-  public String toString() {
-    return "CommandImpl{"
-        + "id='"
-        + id
-        + '\''
-        + ", name='"
-        + name
-        + '\''
-        + ", actions="
-        + actions
-        + ", attributes="
-        + attributes
-        + '}';
+    return Objects.hash(id, name, previewUrl, actions, attributes);
   }
 }
diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/PreviewUrlImpl.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/PreviewUrlImpl.java
new file mode 100644
index 0000000000..ca43cb3dc4
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/model/impl/devfile/PreviewUrlImpl.java
@@ -0,0 +1,74 @@
+/*
+ * 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.workspace.server.model.impl.devfile;
+
+import java.util.Objects;
+import java.util.StringJoiner;
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import org.eclipse.che.api.core.model.workspace.devfile.PreviewUrl;
+
+@Embeddable
+public class PreviewUrlImpl implements PreviewUrl {
+
+  @Column(name = "preview_url_port")
+  private int port;
+
+  @Column(name = "preview_url_path")
+  private String path;
+
+  public PreviewUrlImpl() {}
+
+  public PreviewUrlImpl(PreviewUrl previewUrl) {
+    this(previewUrl.getPort(), previewUrl.getPath());
+  }
+
+  public PreviewUrlImpl(int port, String path) {
+    this.port = port;
+    this.path = path;
+  }
+
+  @Override
+  public int getPort() {
+    return port;
+  }
+
+  @Override
+  public String getPath() {
+    return path;
+  }
+
+  @Override
+  public String toString() {
+    return new StringJoiner(", ", PreviewUrlImpl.class.getSimpleName() + "[", "]")
+        .add("port=" + port)
+        .add("path='" + path + "'")
+        .toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    PreviewUrlImpl that = (PreviewUrlImpl) o;
+    return port == that.port && Objects.equals(path, that.path);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(port, path);
+  }
+}
diff --git a/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json b/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json
index a5838df2e5..07da8e5e34 100644
--- a/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json
+++ b/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.0/devfile.json
@@ -617,6 +617,7 @@
       "description": "Description of the predefined commands to be available in workspace",
       "items": {
         "type": "object",
+        "additionalProperties": false,
         "required": [
           "name",
           "actions"
diff --git a/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.1-beta/devfile.json b/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.1-beta/devfile.json
new file mode 100644
index 0000000000..e80915ee0d
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/main/resources/schema/1.0.1-beta/devfile.json
@@ -0,0 +1,755 @@
+{
+  "meta:license": [
+    " Copyright (c) 2012-2019 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"
+  ],
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "type": "object",
+  "title": "Devfile object",
+  "description": "This schema describes the structure of the devfile object",
+  "definitions": {
+    "attributes": {
+      "type": "object",
+      "additionalProperties": {
+        "type": "string"
+      }
+    },
+    "selector": {
+      "type": "object",
+      "additionalProperties": {
+        "type": "string"
+      }
+    }
+  },
+  "required": [
+    "apiVersion",
+    "metadata"
+  ],
+  "additionalProperties": false,
+  "properties": {
+    "apiVersion": {
+      "const": "1.0.1-beta",
+      "title": "Devfile API Version"
+    },
+    "metadata": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string",
+          "minLength": 1,
+          "title": "Devfile Name",
+          "description": "The name of the devfile. Workspaces created from devfile, will inherit this name",
+          "examples": [
+            "petclinic-dev-environment"
+          ]
+        },
+        "generateName": {
+          "type": "string",
+          "minLength": 1,
+          "title": "Devfile Generate Name",
+          "description": "Workspaces created from devfile, will use it as base and append random suffix. It's used when name is not defined.",
+          "examples": [
+            "petclinic-"
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "anyOf": [
+        {
+          "required": ["name"]
+        },
+        {
+          "required": ["generateName"]
+        }
+      ]
+    },
+    "projects": {
+      "type": "array",
+      "title": "The Projects List",
+      "description": "Description of the projects, containing names and sources locations",
+      "items": {
+        "type": "object",
+        "required": [
+          "name",
+          "source"
+        ],
+        "additionalProperties": false,
+        "properties": {
+          "name": {
+            "type": "string",
+            "title": "The Project Name",
+            "examples": [
+              "petclinic"
+            ]
+          },
+          "source": {
+            "type": "object",
+            "title": "The Project Source object",
+            "description": "Describes the project's source - type and location",
+            "required": [
+              "type",
+              "location"
+            ],
+            "properties": {
+              "type": {
+                "type": "string",
+                "description": "Project's source type.",
+                "examples": [
+                  "git",
+                  "github",
+                  "zip"
+                ]
+              },
+              "location": {
+                "type": "string",
+                "description": "Project's source location address. Should be URL for git and github located projects, or file:// for zip.",
+                "examples": [
+                  "git@github.com:spring-projects/spring-petclinic.git"
+                ]
+              },
+              "branch": {
+                "type": "string",
+                "description": "The name of the of the branch to check out after obtaining the source from the location. The branch has to already exist in the source otherwise the default branch is used. In case of git, this is also the name of the remote branch to push to.",
+                "examples": [
+                  "master",
+                  "feature-42"
+                ]
+              },
+              "startPoint": {
+                "type": "string",
+                "description": "The tag or commit id to reset the checked out branch to.",
+                "examples": [
+                  "release/4.2",
+                  "349d3ad",
+                  "v4.2.0"
+                ]
+              },
+              "tag": {
+                "type": "string",
+                "description": "The name of the tag to reset the checked out branch to. Note that this is equivalent to 'startPoint' and provided for convenience.",
+                "examples": [
+                  "v4.2.0"
+                ]
+              },
+              "commitId": {
+                "type": "string",
+                "description": "The id of the commit to reset the checked out branch to. Note that this is equivalent to 'startPoint' and provided for convenience.",
+                "examples": [
+                  "349d3ad"
+                ]
+              }
+            }
+          },
+          "clonePath": {
+            "type": "string",
+            "description": "The path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name."
+          }
+        }
+      }
+    },
+    "components": {
+      "type": "array",
+      "title": "The Components List",
+      "description": "Description of the workspace components, such as editor and plugins",
+      "items": {
+        "type": "object",
+        "required": [
+          "type"
+        ],
+        "if": {
+          "properties": {
+            "type": {
+              "type": "string"
+            }
+          },
+          "required": ["type"]
+        },
+        "then": {
+          "allOf": [
+            {
+              "if": {
+                "properties": {
+                  "type": {
+                    "enum": [
+                      "cheEditor",
+                      "chePlugin"
+                    ]
+                  }
+                }
+              },
+              "then": {
+                "oneOf": [
+                  {
+                    "required": [
+                      "id"
+                    ],
+                    "not": {
+                      "required": [
+                        "reference"
+                      ]
+                    }
+                  },
+                  {
+                    "required": [
+                      "reference"
+                    ],
+                    "not": {
+                      "required": [
+                        "id"
+                      ]
+                    }
+                  }
+                ],
+                "properties": {
+                  "type": {},
+                  "alias": {},
+                  "id": {
+                    "type": "string",
+                    "description": "Describes the component id. It has the following format: {plugin/editor PUBLISHER}/{plugin/editor NAME}/{plugin/editor VERSION}",
+                    "pattern": "[a-z0-9_\\-.]+/[a-z0-9_\\-.]+/[a-z0-9_\\-.]+$",
+                    "examples": [
+                      "eclipse/maven-jdk8/1.0.0"
+                    ]
+                  },
+                  "reference": {
+                    "description": "Describes raw location of plugin yaml file.",
+                    "type": "string",
+                    "examples": [
+                      "https://pastebin.com/raw/kYprWiNB"
+                    ]
+                  },
+                  "registryUrl": {
+                    "description": "Describes URL of custom plugin registry.",
+                    "type": "string",
+                    "pattern": "^(https?://)[a-zA-Z0-9_\\-./]+",
+                    "examples": [
+                      "https://che-plugin-registry.openshift.io/v3/"
+                    ]
+                  },
+                  "memoryLimit": {
+                    "type": "string",
+                    "description": "Describes memory limit for the component. You can express memory as a plain integer or as a fixed-point integer using one of these suffixes: E, P, T, G, M, K. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki",
+                    "examples": [
+                      "128974848",
+                      "129e6",
+                      "129M",
+                      "123Mi"
+                    ]
+                  }
+                }
+              }
+            },
+            {
+              "if": {
+                "properties": {
+                  "type": {
+                    "enum": [
+                      "cheEditor"
+                    ]
+                  }
+                }
+              },
+              "then": {
+                "additionalProperties": false,
+                "properties": {
+                  "type": {},
+                  "alias": {},
+                  "id": {},
+                  "reference": {},
+                  "registryUrl": {},
+                  "memoryLimit": {}
+                }
+              }
+            },
+            {
+              "if": {
+                "properties": {
+                  "type": {
+                    "enum": [
+                      "chePlugin"
+                    ]
+                  }
+                }
+              },
+              "then": {
+                "additionalProperties": false,
+                "properties": {
+                  "type": {},
+                  "alias": {},
+                  "id": {},
+                  "memoryLimit": {},
+                  "reference": {},
+                  "registryUrl": {},
+                  "preferences": {
+                    "type": "object",
+                    "description": "Additional plugin preferences",
+                    "examples": [
+                      "{\"java.home\": \"/home/user/jdk11\", \"java.jdt.ls.vmargs\": \"-Xmx1G\"}"
+                    ],
+                    "additionalProperties": {
+                      "type": [
+                        "boolean",
+                        "string",
+                        "number"
+                      ]
+                    }
+                  }
+                }
+              }
+            },
+            {
+              "if": {
+                "properties": {
+                  "type": {
+                    "enum": [
+                      "kubernetes",
+                      "openshift"
+                    ]
+                  }
+                }
+              },
+              "then": {
+                "anyOf": [
+                  {
+                    "required": [
+                      "reference"
+                    ],
+                    "additionalProperties": true
+                  },
+                  {
+                    "required": [
+                      "referenceContent"
+                    ],
+                    "additionalProperties": true
+                  }
+                ],
+                "additionalProperties": false,
+                "properties": {
+                  "type": {},
+                  "alias": {},
+                  "mountSources": {},
+                  "reference": {
+                    "description": "Describes absolute or devfile-relative location of Kubernetes list yaml file. Applicable only for 'kubernetes' and 'openshift' type components",
+                    "type": "string",
+                    "examples": [
+                      "petclinic-app.yaml"
+                    ]
+                  },
+                  "referenceContent": {
+                    "description": "Inlined content of a file specified in field 'reference'",
+                    "type": "string",
+                    "examples": [
+                      "{\"kind\":\"List\",\"items\":[{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"name\":\"ws\"},\"spec\":{\"containers\":[{\"image\":\"eclipse/che-dev:nightly\"}]}}]}"
+                    ]
+                  },
+                  "selector": {
+                    "$ref": "#/definitions/selector",
+                    "description": "Describes the objects selector for the recipe type components. Allows to pick-up only selected items from k8s/openshift list",
+                    "examples": [
+                      "{\n   \"app.kubernetes.io/name\" : \"mysql\", \n   \"app.kubernetes.io/component\" : \"database\", \n   \"app.kubernetes.io/part-of\" : \"petclinic\" \n}"
+                    ]
+                  },
+                  "entrypoints": {
+                    "type": "array",
+                    "items": {
+                      "type": "object",
+                      "properties": {
+                        "parentName": {
+                          "type": "string",
+                          "description": "The name of the top level object in the referenced object list in which to search for containers. If not specified, the objects to search through can have any name."
+                        },
+                        "containerName": {
+                          "type": "string",
+                          "description": "The name of the container to apply the entrypoint to. If not specified, the entrypoint is modified on all matching containers."
+                        },
+                        "parentSelector": {
+                          "$ref": "#/definitions/selector",
+                          "description": "The selector on labels of the top level objects in the referenced list in which to search for containers. If not specified, the objects to search through can have any labels."
+                        },
+                        "command": {
+                          "type": "array",
+                          "items": {
+                            "type": "string"
+                          },
+                          "default": null,
+                          "description": "The command to run in the component instead of the default one provided in the image of the container. Defaults to null, meaning use whatever is defined in the image.",
+                          "examples": [
+                            "['/bin/sh', '-c']"
+                          ]
+                        },
+                        "args": {
+                          "type": "array",
+                          "items": {
+                            "type": "string"
+                          },
+                          "default": null,
+                          "description": "The arguments to supply to the command running the component. The arguments are supplied either to the default command provided in the image of the container or to the overridden command. Defaults to null, meaning use whatever is defined in the image.",
+                          "examples": [
+                            "['-R', '-f']"
+                          ]
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            {
+              "if": {
+                "properties": {
+                  "type": {
+                    "enum": [
+                      "dockerimage"
+                    ]
+                  }
+                }
+              },
+              "then": {
+                "required": [
+                  "image",
+                  "memoryLimit"
+                ],
+                "additionalProperties": false,
+                "properties": {
+                  "type": {},
+                  "alias": {},
+                  "mountSources": {},
+                  "image": {
+                    "type": "string",
+                    "description": "Specifies the docker image that should be used for component",
+                    "examples": [
+                      "eclipse/maven-jdk8:1.0.0"
+                    ]
+                  },
+                  "memoryLimit": {
+                    "type": "string",
+                    "description": "Describes memory limit for the component. You can express memory as a plain integer or as a fixed-point integer using one of these suffixes: E, P, T, G, M, K. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki",
+                    "examples": [
+                      "128974848",
+                      "129e6",
+                      "129M",
+                      "123Mi"
+                    ]
+                  },
+                  "command": {
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    },
+                    "default": null,
+                    "description": "The command to run in the dockerimage component instead of the default one provided in the image. Defaults to null, meaning use whatever is defined in the image.",
+                    "examples": [
+                      "['/bin/sh', '-c']"
+                    ]
+                  },
+                  "args": {
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    },
+                    "default": null,
+                    "description": "The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command. Defaults to null, meaning use whatever is defined in the image.",
+                    "examples": [
+                      "['-R', '-f']"
+                    ]
+                  },
+                  "volumes": {
+                    "type": "array",
+                    "description": "Describes volumes which should be mount to component",
+                    "items": {
+                      "type": "object",
+                      "description": "Describe volume that should be mount to component",
+                      "required": [
+                        "name",
+                        "containerPath"
+                      ],
+                      "properties": {
+                        "name": {
+                          "type": "string",
+                          "title": "The Volume Name",
+                          "description": "The volume name. If several components mount the same volume then they will reuse the volume and will be able to access to the same files",
+                          "examples": [
+                            "my-data"
+                          ]
+                        },
+                        "containerPath": {
+                          "type": "string",
+                          "title": "The path where volume should be mount to container",
+                          "examples": [
+                            "/home/user/data"
+                          ]
+                        }
+                      }
+                    }
+                  },
+                  "env": {
+                    "type": "array",
+                    "description": "The environment variables list that should be set to docker container",
+                    "items": {
+                      "type": "object",
+                      "description": "Describes environment variable",
+                      "required": [
+                        "name",
+                        "value"
+                      ],
+                      "properties": {
+                        "name": {
+                          "type": "string",
+                          "title": "The Environment Variable Name",
+                          "description": "The environment variable name"
+                        },
+                        "value": {
+                          "type": "string",
+                          "title": "The Environment Variable Value",
+                          "description": "The environment variable value"
+                        }
+                      }
+                    }
+                  },
+                  "endpoints": {
+                    "type": "array",
+                    "description": "Describes dockerimage component endpoints",
+                    "items": {
+                      "name": "object",
+                      "description": "Describes dockerimage component endpoint",
+                      "required": [
+                        "name",
+                        "port"
+                      ],
+                      "properties": {
+                        "name": {
+                          "type": "string",
+                          "title": "The Endpoint Name",
+                          "description": "The Endpoint name"
+                        },
+                        "port": {
+                          "type": "integer",
+                          "title": "The Endpoint Port",
+                          "description": "The container port that should be used as endpoint"
+                        },
+                        "attributes": {
+                          "type": "object",
+                          "public": {
+                            "type": "boolean",
+                            "description": "Identifies endpoint as workspace internally or externally accessible.",
+                            "default": "true"
+                          },
+                          "secure": {
+                            "type": "boolean",
+                            "description": "Identifies server as secure or non-secure. Requests to secure servers will be authenticated and must contain machine token",
+                            "default": "false"
+                          },
+                          "discoverable": {
+                            "type": "boolean",
+                            "description": "Identifies endpoint as accessible by its name.",
+                            "default": "false"
+                          },
+                          "protocol": {
+                            "type": "boolean",
+                            "description": "Defines protocol that should be used for communication with endpoint. Is used for endpoint URL evaluation"
+                          },
+                          "additionalProperties": {
+                            "type": "string"
+                          },
+                          "javaType": "java.util.Map<String, String>"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          ]
+        },
+        "properties": {
+          "alias": {
+            "description": "The name using which other places of this devfile (like commands) can refer to this component. This attribute is optional but must be unique in the devfile if specified.",
+            "type": "string",
+            "examples": [
+              "mvn-stack"
+            ]
+          },
+          "type": {
+            "description": "Describes type of the component, e.g. whether it is an plugin or editor or other type",
+            "enum": [
+              "cheEditor",
+              "chePlugin",
+              "kubernetes",
+              "openshift",
+              "dockerimage"
+            ],
+            "examples": [
+              "chePlugin",
+              "cheEditor",
+              "kubernetes",
+              "openshift",
+              "dockerimage"
+            ]
+          },
+          "mountSources": {
+            "type": "boolean",
+            "description": "Describes whether projects sources should be mount to the component. `CHE_PROJECTS_ROOT` environment variable should contains a path where projects sources are mount",
+            "default": "false"
+          }
+        },
+        "additionalProperties": true
+      }
+    },
+    "commands": {
+      "type": "array",
+      "title": "The Commands List",
+      "description": "Description of the predefined commands to be available in workspace",
+      "items": {
+        "type": "object",
+        "additionalProperties": false,
+        "required": [
+          "name",
+          "actions"
+        ],
+        "properties": {
+          "name": {
+            "description": "Describes the name of the command. Should be unique per commands set.",
+            "type": "string",
+            "examples": [
+              "build"
+            ]
+          },
+          "attributes": {
+            "description": "Additional command attributes",
+            "$ref": "#/definitions/attributes"
+          },
+          "actions": {
+            "type": "array",
+            "description": "List of the actions of given command. Now the only one command must be specified in list but there are plans to implement supporting multiple actions commands.",
+            "title": "The Command Actions List",
+            "minItems": 1,
+            "maxItems": 1,
+            "items": {
+              "oneOf": [
+                {
+                  "properties": {
+                    "type": {},
+                    "component": {},
+                    "command": {},
+                    "workdir": {}
+                  },
+                  "required": [
+                    "type",
+                    "component",
+                    "command"
+                  ],
+                  "additionalProperties": false
+                },
+                {
+                  "properties": {
+                    "type": {},
+                    "reference": {},
+                    "referenceContent": {}
+                  },
+                  "anyOf": [
+                    {
+                      "required": [
+                        "type",
+                        "reference"
+                      ],
+                      "additionalProperties": true
+                    },
+                    {
+                      "required": [
+                        "type",
+                        "referenceContent"
+                      ],
+                      "additionalProperties": true
+                    }
+                  ],
+                  "additionalProperties": false
+                }
+              ],
+              "type": "object",
+              "properties": {
+                "type": {
+                  "description": "Describes action type",
+                  "type": "string",
+                  "examples": [
+                    "exec"
+                  ]
+                },
+                "component": {
+                  "type": "string",
+                  "description": "Describes component to which given action relates",
+                  "examples": [
+                    "mvn-stack"
+                  ]
+                },
+                "command": {
+                  "type": "string",
+                  "description": "The actual action command-line string",
+                  "examples": [
+                    "mvn package"
+                  ]
+                },
+                "workdir": {
+                  "type": "string",
+                  "description": "Working directory where the command should be executed",
+                  "examples": [
+                    "/projects/spring-petclinic"
+                  ]
+                },
+                "reference": {
+                  "type": "string",
+                  "description": "the path relative to the location of the devfile to the configuration file defining one or more actions in the editor-specific format",
+                  "examples": [
+                    "../ide-config/launch.json"
+                  ]
+                },
+                "referenceContent": {
+                  "type": "string",
+                  "description": "The content of the referenced configuration file that defines one or more actions in the editor-specific format",
+                  "examples": [
+                    "{\"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"type\": \"typescript\",\n            \"tsconfig\": \"tsconfig.json\",\n            \"problemMatcher\": [\n                \"$tsc\"\n            ],\n            \"group\": {\n                \"kind\": \"build\",\n                \"isDefault\": true\n            }\n        }\n    ]}"
+                  ]
+                }
+              }
+            }
+          },
+          "previewUrl": {
+            "type": "object",
+            "required": ["port"],
+            "properties": {
+              "port": {
+                "type": "number",
+                "minimum": 0,
+                "maximum": 65535
+              },
+              "path":  {
+                "type": "string"
+              }
+            }
+          }
+        }
+      }
+    },
+    "attributes": {
+      "type": "object",
+      "editorFree": {
+        "type": "boolean",
+        "description": "Defines that no editor is needed and default one should not be provisioned. Defaults to `false`.",
+        "default": "false"
+      },
+      "persistVolumes": {
+        "type": "boolean",
+        "description": "Defines whether volumes should be stored or not. Defaults to `true`. In case of `false` workspace volumes will be created as `emptyDir`. The data in the `emptyDir` volume is deleted forever when a workspace Pod is removed for any reason(pod is crashed, workspace is restarted).",
+        "default": "true"
+      },
+      "additionalProperties": {
+        "type": "string"
+      },
+      "javaType": "java.util.Map<String, String>"
+    }
+  }
+}
diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGeneratorTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGeneratorTest.java
new file mode 100644
index 0000000000..38b1e178ef
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/PreviewUrlLinksVariableGeneratorTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.workspace.server;
+
+import static java.util.Collections.singletonList;
+import static org.testng.Assert.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.UriBuilder;
+import org.eclipse.che.api.core.model.workspace.config.Command;
+import org.eclipse.che.api.workspace.server.model.impl.CommandImpl;
+import org.eclipse.che.api.workspace.server.model.impl.RuntimeImpl;
+import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl;
+import org.eclipse.che.api.workspace.server.model.impl.devfile.PreviewUrlImpl;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class PreviewUrlLinksVariableGeneratorTest {
+
+  private PreviewUrlLinksVariableGenerator generator;
+  private UriBuilder uriBuilder;
+
+  @BeforeMethod
+  public void setUp() {
+    generator = new PreviewUrlLinksVariableGenerator();
+    uriBuilder = UriBuilder.fromUri("http://host/path");
+  }
+
+  @Test
+  public void shouldDoNothingWhenSomethingIsNull() {
+    assertTrue(generator.genLinksMapAndUpdateCommands(null, null).isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenRuntimeIsNull() {
+    WorkspaceImpl w = new WorkspaceImpl();
+    w.setRuntime(null);
+
+    assertTrue(generator.genLinksMapAndUpdateCommands(w, uriBuilder).isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenCommandsIsNull() {
+    WorkspaceImpl w = createWorkspaceWithCommands(null);
+
+    assertTrue(generator.genLinksMapAndUpdateCommands(w, uriBuilder).isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandWithPreviewUrl() {
+    WorkspaceImpl w = createWorkspaceWithCommands(singletonList(new CommandImpl("a", "a", "a")));
+
+    assertTrue(generator.genLinksMapAndUpdateCommands(w, uriBuilder).isEmpty());
+  }
+
+  @Test
+  public void shouldDoNothingWhenNoCommandWithPreviewurlAttribute() {
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(123, null), Collections.emptyMap());
+    WorkspaceImpl w = createWorkspaceWithCommands(singletonList(command));
+
+    assertTrue(generator.genLinksMapAndUpdateCommands(w, uriBuilder).isEmpty());
+  }
+
+  @Test
+  public void shouldUpdateCommandAndReturnLinkMapWhenPreviewUrlFound() {
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+    CommandImpl command =
+        new CommandImpl("a", "a", "a", new PreviewUrlImpl(123, null), commandAttrs);
+    WorkspaceImpl w =
+        createWorkspaceWithCommands(Arrays.asList(command, new CommandImpl("b", "b", "b")));
+
+    Map<String, String> linkMap = generator.genLinksMapAndUpdateCommands(w, uriBuilder);
+    assertEquals(linkMap.size(), 1);
+    assertEquals(linkMap.values().iterator().next(), "http://preview_url_host");
+    String varKey = linkMap.keySet().iterator().next();
+    assertTrue(varKey.startsWith("previewurl/"));
+
+    Command aCommand =
+        w.getRuntime()
+            .getCommands()
+            .stream()
+            .filter(c -> c.getName().equals("a"))
+            .findFirst()
+            .get();
+
+    assertTrue(aCommand.getAttributes().get(Command.PREVIEW_URL_ATTRIBUTE).contains(varKey));
+    assertEquals(aCommand.getAttributes().get(Command.PREVIEW_URL_ATTRIBUTE), "${" + varKey + "}");
+  }
+
+  @Test
+  public void variableNamesForTwoCommandsWithSimilarNameMustBeDifferent() {
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+
+    CommandImpl command =
+        new CommandImpl("run command", "a", "a", new PreviewUrlImpl(123, null), commandAttrs);
+
+    CommandImpl command2 = new CommandImpl(command);
+    command2.setName("runcommand");
+
+    WorkspaceImpl w = createWorkspaceWithCommands(Arrays.asList(command, command2));
+
+    Map<String, String> linkMap = generator.genLinksMapAndUpdateCommands(w, uriBuilder);
+    assertEquals(linkMap.size(), 2);
+
+    List<? extends Command> commandsAfter = w.getRuntime().getCommands();
+    assertNotEquals(
+        commandsAfter.get(1).getAttributes().get(Command.PREVIEW_URL_ATTRIBUTE),
+        commandsAfter.get(0).getAttributes().get(Command.PREVIEW_URL_ATTRIBUTE));
+  }
+
+  @Test
+  public void shouldGetHttpsWhenUriBuilderHasHttps() {
+    UriBuilder httpsUriBuilder = UriBuilder.fromUri("https://host/path");
+
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+
+    CommandImpl command =
+        new CommandImpl("run command", "a", "a", new PreviewUrlImpl(123, null), commandAttrs);
+
+    Map<String, String> linkMap =
+        generator.genLinksMapAndUpdateCommands(
+            createWorkspaceWithCommands(singletonList(command)), httpsUriBuilder);
+
+    assertTrue(linkMap.values().iterator().next().startsWith("https://"));
+  }
+
+  @Test
+  public void shouldAppendPathWhenDefinedInPreviewUrl() {
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+
+    CommandImpl command =
+        new CommandImpl("run command", "a", "a", new PreviewUrlImpl(123, "testpath"), commandAttrs);
+
+    WorkspaceImpl workspace = createWorkspaceWithCommands(singletonList(command));
+
+    Map<String, String> linkMap = generator.genLinksMapAndUpdateCommands(workspace, uriBuilder);
+
+    assertTrue(linkMap.values().iterator().next().endsWith("preview_url_host"));
+    String linkKey = linkMap.keySet().iterator().next();
+    assertEquals(
+        workspace
+            .getRuntime()
+            .getCommands()
+            .get(0)
+            .getAttributes()
+            .get(Command.PREVIEW_URL_ATTRIBUTE),
+        "${" + linkKey + "}testpath");
+  }
+
+  @Test
+  public void shouldAppendQueryParamsWhenDefinedInPreviewUrl() {
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+
+    CommandImpl command =
+        new CommandImpl("run command", "a", "a", new PreviewUrlImpl(123, "?a=b"), commandAttrs);
+
+    WorkspaceImpl workspace = createWorkspaceWithCommands(singletonList(command));
+
+    Map<String, String> linkMap = generator.genLinksMapAndUpdateCommands(workspace, uriBuilder);
+
+    assertTrue(linkMap.values().iterator().next().endsWith("preview_url_host"));
+    String linkKey = linkMap.keySet().iterator().next();
+    assertEquals(
+        workspace
+            .getRuntime()
+            .getCommands()
+            .get(0)
+            .getAttributes()
+            .get(Command.PREVIEW_URL_ATTRIBUTE),
+        "${" + linkKey + "}?a=b");
+  }
+
+  @Test
+  public void shouldAppendMultipleQueryParamsWhenDefinedInPreviewUrl() {
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+
+    CommandImpl command =
+        new CommandImpl("run command", "a", "a", new PreviewUrlImpl(123, "?a=b&c=d"), commandAttrs);
+
+    WorkspaceImpl workspace = createWorkspaceWithCommands(singletonList(command));
+    Map<String, String> linkMap = generator.genLinksMapAndUpdateCommands(workspace, uriBuilder);
+
+    assertTrue(linkMap.values().iterator().next().endsWith("preview_url_host"));
+    String linkKey = linkMap.keySet().iterator().next();
+    assertEquals(
+        workspace
+            .getRuntime()
+            .getCommands()
+            .get(0)
+            .getAttributes()
+            .get(Command.PREVIEW_URL_ATTRIBUTE),
+        "${" + linkKey + "}?a=b&c=d");
+  }
+
+  @Test
+  public void shouldAppendPathWithQueryParamsWhenDefinedInPreviewUrl() {
+    Map<String, String> commandAttrs = new HashMap<>();
+    commandAttrs.put(Command.PREVIEW_URL_ATTRIBUTE, "preview_url_host");
+
+    CommandImpl command =
+        new CommandImpl(
+            "run command", "a", "a", new PreviewUrlImpl(123, "/hello?a=b"), commandAttrs);
+
+    WorkspaceImpl workspace = createWorkspaceWithCommands(singletonList(command));
+
+    Map<String, String> linkMap = generator.genLinksMapAndUpdateCommands(workspace, uriBuilder);
+
+    assertTrue(linkMap.values().iterator().next().endsWith("preview_url_host"));
+    String linkKey = linkMap.keySet().iterator().next();
+    assertEquals(
+        workspace
+            .getRuntime()
+            .getCommands()
+            .get(0)
+            .getAttributes()
+            .get(Command.PREVIEW_URL_ATTRIBUTE),
+        "${" + linkKey + "}/hello?a=b");
+  }
+
+  private WorkspaceImpl createWorkspaceWithCommands(List<CommandImpl> commands) {
+    RuntimeImpl runtime =
+        new RuntimeImpl("", Collections.emptyMap(), "", commands, new ArrayList<>());
+    WorkspaceImpl w = new WorkspaceImpl();
+    w.setRuntime(runtime);
+
+    return w;
+  }
+}
diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGeneratorTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGeneratorTest.java
index dde5cd1e7a..0352eaf613 100644
--- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGeneratorTest.java
+++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceLinksGeneratorTest.java
@@ -50,6 +50,8 @@ public class WorkspaceLinksGeneratorTest {
 
   @Mock private RuntimeContext runtimeCtx;
 
+  @Mock private PreviewUrlLinksVariableGenerator previewUrlGenerator;
+
   private Map<String, String> expectedStoppedLinks;
   private Map<String, String> expectedRunningLinks;
   private WorkspaceLinksGenerator linksGenerator;
@@ -68,7 +70,7 @@ public class WorkspaceLinksGeneratorTest {
     lenient().when(serviceContextMock.getServiceUriBuilder()).thenReturn(uriBuilder);
     lenient().when(serviceContextMock.getBaseUriBuilder()).thenReturn(uriBuilder);
 
-    linksGenerator = new WorkspaceLinksGenerator(runtimes, "ws://localhost");
+    linksGenerator = new WorkspaceLinksGenerator(runtimes, previewUrlGenerator, "ws://localhost");
 
     expectedStoppedLinks = new HashMap<>();
     expectedStoppedLinks.put(LINK_REL_SELF, "http://localhost/api/workspace/wside-123877234580");
@@ -101,7 +103,7 @@ public class WorkspaceLinksGeneratorTest {
     uriBuilder.uri("https://mydomain:7345/api/workspace");
     doReturn(uriBuilder).when(serviceContextMock).getServiceUriBuilder();
 
-    linksGenerator = new WorkspaceLinksGenerator(runtimes, "ws://localhost");
+    linksGenerator = new WorkspaceLinksGenerator(runtimes, previewUrlGenerator, "ws://localhost");
     // when
     Map<String, String> actual = linksGenerator.genLinks(workspace, serviceContextMock);
     // then
diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java
index 64eab51294..d17f7ec193 100644
--- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java
+++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/devfile/validator/DevfileSchemaValidatorTest.java
@@ -68,7 +68,10 @@ public class DevfileSchemaValidatorTest {
       {"editor_plugin_component/devfile_plugin_component_with_reference.yaml"},
       {"devfile/devfile_just_generatename.yaml"},
       {"devfile/devfile_name_and_generatename.yaml"},
-      {"devfile/devfile_with_sparse_checkout_dir.yaml"}
+      {"devfile/devfile_with_sparse_checkout_dir.yaml"},
+      {"devfile/devfile_name_and_generatename.yaml"},
+      {"command/devfile_command_with_preview_url.yaml"},
+      {"command/devfile_command_with_preview_url_only_port.yaml"},
     };
   }
 
@@ -220,6 +223,26 @@ public class DevfileSchemaValidatorTest {
         "dockerimage_component/devfile_dockerimage_component_with_indistinctive_field_selector.yaml",
         "(/components/0/selector):The object must not have a property whose name is \"selector\"."
       },
+      {
+        "command/devfile_command_with_empty_preview_url.yaml",
+        "(/commands/0/previewUrl):The value must be of object type, but actual type is null."
+      },
+      {
+        "command/devfile_command_with_preview_url_port_is_string.yaml",
+        "(/commands/0/previewUrl/port):The value must be of number type, but actual type is string."
+      },
+      {
+        "command/devfile_command_with_preview_url_port_is_too_high.yaml",
+        "(/commands/0/previewUrl/port):The numeric value must be less than or equal to 65535."
+      },
+      {
+        "command/devfile_command_with_preview_url_port_is_negative.yaml",
+        "(/commands/0/previewUrl/port):The numeric value must be greater than or equal to 0."
+      },
+      {
+        "command/devfile_command_with_preview_url_only_path.yaml",
+        "(/commands/0/previewUrl):The object must have a property whose name is \"port\"."
+      },
     };
   }
 
diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java
index efe8541a88..0d42363e6c 100644
--- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java
+++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/spi/tck/WorkspaceDaoTest.java
@@ -566,7 +566,7 @@ public class WorkspaceDaoTest {
     // Add a new command
     final org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl newCmd =
         new org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl(
-            "command-3", singletonList(action3), singletonMap("attr3", "value3"));
+            "command-3", singletonList(action3), singletonMap("attr3", "value3"), null);
     workspace.getDevfile().getCommands().add(newCmd);
 
     // Update an existing command
@@ -890,10 +890,10 @@ public class WorkspaceDaoTest {
 
     org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl command1 =
         new org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl(
-            name + "-1", singletonList(action1), singletonMap("attr1", "value1"));
+            name + "-1", singletonList(action1), singletonMap("attr1", "value1"), null);
     org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl command2 =
         new org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl(
-            name + "-2", singletonList(action2), singletonMap("attr2", "value2"));
+            name + "-2", singletonList(action2), singletonMap("attr2", "value2"), null);
 
     EntrypointImpl entrypoint1 =
         new EntrypointImpl(
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_empty_preview_url.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_empty_preview_url.yaml
new file mode 100644
index 0000000000..76a8e2c9c8
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_empty_preview_url.yaml
@@ -0,0 +1,32 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url.yaml
new file mode 100644
index 0000000000..2b7bc12c46
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url.yaml
@@ -0,0 +1,34 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+      port: 65535
+      path: hello-world
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_path.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_path.yaml
new file mode 100644
index 0000000000..ebf105f41d
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_path.yaml
@@ -0,0 +1,33 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+      path: hello-world
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_port.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_port.yaml
new file mode 100644
index 0000000000..ba8b9485a7
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_only_port.yaml
@@ -0,0 +1,33 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+      port: 8080
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_negative.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_negative.yaml
new file mode 100644
index 0000000000..37f3104ff1
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_negative.yaml
@@ -0,0 +1,34 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+      port: -1
+      path: hello-world
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_string.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_string.yaml
new file mode 100644
index 0000000000..0dca894e24
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_string.yaml
@@ -0,0 +1,34 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+      port: abc
+      path: hello-world
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_too_high.yaml b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_too_high.yaml
new file mode 100644
index 0000000000..83f2d6e8e8
--- /dev/null
+++ b/wsmaster/che-core-api-workspace/src/test/resources/devfile/schema_test/command/devfile_command_with_preview_url_port_is_too_high.yaml
@@ -0,0 +1,34 @@
+#
+# 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
+#
+
+---
+apiVersion: 1.0.1-beta
+metadata:
+  name: petclinic-dev-environment
+components:
+  - alias: mysql
+    type: openshift
+    reference: petclinic.yaml
+    selector:
+      app.kubernetes.io/name: mysql
+      app.kubernetes.io/component: database
+      app.kubernetes.io/part-of: petclinic
+commands:
+  - name: build
+    previewUrl:
+      port: 123456
+      path: hello-world
+    actions:
+      - type: exec
+        component: mysql
+        command: mvn clean
+        workdir: /projects/spring-petclinic
diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.4.0/2__add_preview_url_to_devfile_command.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.4.0/2__add_preview_url_to_devfile_command.sql
new file mode 100644
index 0000000000..9a71835578
--- /dev/null
+++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/7.4.0/2__add_preview_url_to_devfile_command.sql
@@ -0,0 +1,21 @@
+--
+-- Copyright (c) 2012-2019 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
+--
+
+
+ALTER TABLE devfile_command ADD COLUMN preview_url_port INTEGER;
+ALTER TABLE devfile_command ADD COLUMN preview_url_path TEXT;
+
+ALTER TABLE k8s_runtime_command ADD COLUMN preview_url_port INTEGER;
+ALTER TABLE k8s_runtime_command ADD COLUMN preview_url_path TEXT;
+
+ALTER TABLE command ADD COLUMN preview_url_port INTEGER;
+ALTER TABLE command ADD COLUMN preview_url_path TEXT;
diff --git a/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java b/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java
index 0f9c5b489e..40ad52875d 100644
--- a/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java
+++ b/wsmaster/integration-tests/cascade-removal/src/test/java/org/eclipse/che/core/db/jpa/TestObjectsFactory.java
@@ -142,7 +142,7 @@ public final class TestObjectsFactory {
   private static org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl
       createDevfileCommand(String name) {
     return new org.eclipse.che.api.workspace.server.model.impl.devfile.CommandImpl(
-        name, singletonList(createAction()), singletonMap("attr1", "value1"));
+        name, singletonList(createAction()), singletonMap("attr1", "value1"), null);
   }
 
   private static ActionImpl createAction() {
-- 
GitLab