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 238a6d368dae91aeb8e784df05068c86d7cf7cfe..197d80ffe7863c916d7b009e131febe9e952ee14 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 daeba1958560475c04f97332e0c13885fa0622da..d07b2506fdeeaca42ea08d1e8d2b0c73bdf35ddd 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 0000000000000000000000000000000000000000..35476cef93ba271e300b7908b638e8c24e5bb125 --- /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 5759e086cb60ea5595ecee07fa7170875ee2b4d1..2c8725d610cd7766f1155bcc67125232a7a3c6b5 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 62a227b261e9185a376cc9e223ff05e2c1ddc453..444914a66c2de6fd875dd13e4dcb6a8ecf915825 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 67ddb32a08877e8d6ed529d5568f8dcbe03c45b2..090eb740777766535a9b53da00f71e5189d1e2b7 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 7f831b4ef17ed810b5127f9c7d9500578a3d6af7..8a6ab9b3669f4025e0b89f5e2f0b1bdda1e5fd5f 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 5ca13b4ba844b006fa71c83bc44e6ac6beefb326..fd8ad5e12a3b3998e7cf95b123ac35bfb4b614bc 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 422bb194bfe8c7e032788522cb1ff99937ad0877..080e6b0a129445e0e9eb4580cc31d22703f24c93 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 0000000000000000000000000000000000000000..2746bd8160028ca20d411f609915218552a32844 --- /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 0000000000000000000000000000000000000000..286475c902565273e061356597b4022e8004f472 --- /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 0000000000000000000000000000000000000000..1c7fe2066948b081b8cfff625617444c4917e4b1 --- /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 0000000000000000000000000000000000000000..133ba3ac790ea6086e269703110515feb0e981e6 --- /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 0000000000000000000000000000000000000000..f7c99955db29c66099d59a28ab1088564a987e9d --- /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 7a466bfa1e1c32ca100349b3c4f443eca6dd5ec9..970ce43c3a2ca98575da1e75b5822e6068d17d98 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 66db6b05ff7896c37ea183336ac4fd083f37cc1b..fe41dd62bc14a235a561bdc17638b5a37a2da60a 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 0000000000000000000000000000000000000000..64ab0fd8c750dd0c8b41c41a6778a99ae5075f16 --- /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 0000000000000000000000000000000000000000..c6dc47b30fe192cf013bc963ee329306c28aa274 --- /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 0000000000000000000000000000000000000000..39e3ab4b75b04f493b7566028d8ecfc1cfb037be --- /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 0000000000000000000000000000000000000000..f7ff7465d274c63087f6056a133d2b6117ffdd1c --- /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 251898f71b9110a83bf649ba10cd844b1c68229a..505eef323291a351974e079192435b722fb71332 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 4c8f27ce8300f741db76f9802c27df6292d2547b..82c692a319982548fed0084a09a2977425c108ab 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 1d431479f67cebe48a8c84cfc225fed4691006f8..58679758e23eab2bc137eaf0bddcc3fa18d2338a 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 d78b45976772ed2e21042dce99087e6989514516..a3a365427d052ec8b5c1eea943e2b16acf2506d5 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 0000000000000000000000000000000000000000..0867c03257b4706739a702c04e61c2331223ac9e --- /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 0000000000000000000000000000000000000000..8310c6bce8ff30450d72facd03167fca458fd95c --- /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 0000000000000000000000000000000000000000..286d65a6e2fba3ed8807d5b6d002e51c7e9b0758 --- /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 42ef2a69cc2e532a3ad75847c69c215859fc5fb3..53b7f01c381e2c3094547f3ac9b589da853b3dec 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 b317cc0d7dec226638ce502cb879a676ed6665d4..9d14dc0cfe747619e0f38affa445b5d661b74496 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 0000000000000000000000000000000000000000..47aa2a04da331f54c396dc5d237276aab1a01172 --- /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 0000000000000000000000000000000000000000..20b8bc09f6fc45330739f39b2a809e61fd13693f --- /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 0000000000000000000000000000000000000000..e503f16329c5c6416c67c611d45dec1bc4da15fd --- /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 25949c990b034228843cd4a1acf828398584b223..2a8d77a115069604d4f9f38971c9aaa0277557a3 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 0000000000000000000000000000000000000000..e35aca4d88d412b9e124a43d0a76a8fe5b8b7c1b --- /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 0f5cb5c0187bf89634dfbbba1ed77dd3122a5ea2..16a6d26161da0d82082ecfa7860532447cb19817 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 0000000000000000000000000000000000000000..b79b8bd112536f2e0f1920577e38ece32ffdbc67 --- /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 0b6c9487eacfd6fc42d1d1d512b8b98159e7b096..29b4000a4d2ed191c062083a3e6200e00d2d741e 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 be1b2d28132cc7b055ab57687b7afc31a768f046..486651f6bd96d77a1238a1592502c0becf99be44 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 e74579fff8fe74642d021f0c74f0baf6bcc405b1..c492e82f1f2dacbac56ab9929a8e91202fa7c449 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 e9363bdba75bd2563f07078f4fed99fa5999abae..005791978ae4a05a8bf5755654683568e502d1fc 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 f037eb8c756fff117810c023643f18dc3c9dd463..29a69a19a1b520a141b538953eba7c987c449ef3 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 0000000000000000000000000000000000000000..ca43cb3dc47a219605677a8aa12c3a41007b3859 --- /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 a5838df2e5bb55ee96eab1daecbd33ea9978533d..07da8e5e3438350b9405735f94448a4d32a0bdf0 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 0000000000000000000000000000000000000000..e80915ee0d9b086259d12b192da0d932da59a8e7 --- /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 0000000000000000000000000000000000000000..38b1e178ef672ea2007d3f788566cbdd5db8f6ae --- /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 dde5cd1e7a2173353e66ef76f0cf9adfaa3dc431..0352eaf61307d6d8019061d657f31b3da740eec7 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 64eab51294d7a14c5f89a4e271a69d9fc7d76e78..d17f7ec193df759be8e033f11335c404dff1d034 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 efe8541a88dedb2ddaf73b3b2651dcbede0f4b5a..0d42363e6c08c702743d7acccf877dd6559560b1 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 0000000000000000000000000000000000000000..76a8e2c9c8e2ac871e424a3859a838488e86dbb5 --- /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 0000000000000000000000000000000000000000..2b7bc12c4686f942a57bba0499952d078b130279 --- /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 0000000000000000000000000000000000000000..ebf105f41dfdd0878c2de1348f2c55ff0f6d4a08 --- /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 0000000000000000000000000000000000000000..ba8b9485a70a66ba7ae17a1d196f75815b4fdb2f --- /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 0000000000000000000000000000000000000000..37f3104ff1f921f55b487cf488f607aaa281c11d --- /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 0000000000000000000000000000000000000000..0dca894e248b612170a47d01f4ab805c51a1e15a --- /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 0000000000000000000000000000000000000000..83f2d6e8e838257f8b9838c318fea00d4ae1d0d4 --- /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 0000000000000000000000000000000000000000..9a71835578eda488bb5520810eb383423e1ebc97 --- /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 0f9c5b489e2fc570ff952e69a2101fcc11638229..40ad52875d011bc12fc5bbd0b9e4fb44b4de5ee4 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() {