diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/ServerConfig.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/ServerConfig.java index ca264acb7ea..d01f4ef36aa 100644 --- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/ServerConfig.java +++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/workspace/config/ServerConfig.java @@ -77,6 +77,20 @@ public interface ServerConfig { */ String SERVER_NAME_ATTRIBUTE = "serverName"; + /** + * This attribute is used to remember name of the service for single-host gateway configuration. + * It's used internally only, so the attribute is removed from {@link ServerConfig}'s attributes + * before going outside. + */ + String SERVICE_NAME_ATTRIBUTE = "serviceName"; + + /** + * This attribute is used to remember port of the service for single-host gateway configuration. + * It's used internally only, so the attribute is removed from {@link ServerConfig}'s attributes + * before going outside. + */ + String SERVICE_PORT_ATTRIBUTE = "servicePort"; + /** * Port used by server. * diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Annotations.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Annotations.java index 7977d0bd727..5a89947566c 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Annotations.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Annotations.java @@ -41,6 +41,14 @@ public class Annotations { public static final String MACHINE_NAME_ANNOTATION = ANNOTATION_PREFIX + "machine.name"; + /** + * Object annotated with this set to `true` should be created in Che installation namespace. It's + * used only internally so it may be removed before actually creating k8s object, so it's not + * exposed. + */ + public static final String CREATE_IN_CHE_INSTALLATION_NAMESPACE = + ANNOTATION_PREFIX + "installation.namespace"; + /** Pattern that matches server annotations e.g. "org.eclipse.che.server.exec-agent.port". */ private static final Pattern SERVER_ANNOTATION_PATTERN = Pattern.compile("org\\.eclipse\\.che\\.server\\.(?[\\w-/]+)\\..+"); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java new file mode 100644 index 00000000000..87171d29424 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import okhttp3.EventListener; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.annotation.Nullable; + +/** + * This {@link KubernetesClientFactory} is used to access Che installation namespace. It always + * provides client with default {@link Config}. + */ +@Singleton +public class CheServerKubernetesClientFactory extends KubernetesClientFactory { + + @Inject + public CheServerKubernetesClientFactory( + @Nullable @Named("che.infra.kubernetes.master_url") String masterUrl, + @Nullable @Named("che.infra.kubernetes.trust_certs") Boolean doTrustCerts, + @Named("che.infra.kubernetes.client.http.async_requests.max") int maxConcurrentRequests, + @Named("che.infra.kubernetes.client.http.async_requests.max_per_host") + int maxConcurrentRequestsPerHost, + @Named("che.infra.kubernetes.client.http.connection_pool.max_idle") int maxIdleConnections, + @Named("che.infra.kubernetes.client.http.connection_pool.keep_alive_min") + int connectionPoolKeepAlive, + EventListener eventListener) { + super( + masterUrl, + doTrustCerts, + maxConcurrentRequests, + maxConcurrentRequestsPerHost, + maxIdleConnections, + connectionPoolKeepAlive, + eventListener); + } + + /** @param workspaceId ignored */ + @Override + public KubernetesClient create(String workspaceId) throws InfrastructureException { + return create(); + } + + /** + * creates an instance of {@link KubernetesClient} that is meant to be used on Che installation + * namespace + */ + @Override + public KubernetesClient create() throws InfrastructureException { + return super.create(); + } + + @Override + protected Config buildConfig(Config config, String workspaceId) { + return config; + } +} 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 f06709ca60d..92822a1b841 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 @@ -23,6 +23,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStoragePodInterceptor; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStorageProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.CertificateProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GitConfigProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.ImagePullSecretProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.LogsVolumeMachineProvisioner; @@ -84,6 +85,7 @@ class KubernetesEnvironmentProvisionerImpl private final GitConfigProvisioner gitConfigProvisioner; private final PreviewUrlExposer previewUrlExposer; private final VcsSslCertificateProvisioner vcsSslCertificateProvisioner; + private final GatewayRouterProvisioner gatewayRouterProvisioner; @Inject public KubernetesEnvironmentProvisionerImpl( @@ -108,7 +110,8 @@ public KubernetesEnvironmentProvisionerImpl( SshKeysProvisioner sshKeysProvisioner, GitConfigProvisioner gitConfigProvisioner, PreviewUrlExposer previewUrlExposer, - VcsSslCertificateProvisioner vcsSslCertificateProvisioner) { + VcsSslCertificateProvisioner vcsSslCertificateProvisioner, + GatewayRouterProvisioner gatewayRouterProvisioner) { this.pvcEnabled = pvcEnabled; this.volumesStrategy = volumesStrategy; this.uniqueNamesProvisioner = uniqueNamesProvisioner; @@ -131,6 +134,7 @@ public KubernetesEnvironmentProvisionerImpl( this.vcsSslCertificateProvisioner = vcsSslCertificateProvisioner; this.gitConfigProvisioner = gitConfigProvisioner; this.previewUrlExposer = previewUrlExposer; + this.gatewayRouterProvisioner = gatewayRouterProvisioner; } @Traced @@ -173,6 +177,7 @@ public void provision(KubernetesEnvironment k8sEnv, RuntimeIdentity identity) sshKeysProvisioner.provision(k8sEnv, identity); vcsSslCertificateProvisioner.provision(k8sEnv, identity); gitConfigProvisioner.provision(k8sEnv, identity); + gatewayRouterProvisioner.provision(k8sEnv, identity); LOG.debug("Provisioning Kubernetes environment done for workspace '{}'", workspaceId); } } 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 c45a8efcc06..87900aa5069 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 @@ -14,6 +14,7 @@ import static java.lang.String.format; import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.toMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil.shouldCreateInCheNamespace; import static org.eclipse.che.workspace.infrastructure.kubernetes.util.TracingSpanConstants.CHECK_SERVERS; import static org.eclipse.che.workspace.infrastructure.kubernetes.util.TracingSpanConstants.WAIT_MACHINES_START; import static org.eclipse.che.workspace.infrastructure.kubernetes.util.TracingSpanConstants.WAIT_RUNNING_ASYNC; @@ -85,6 +86,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.PodMerger; import org.eclipse.che.workspace.infrastructure.kubernetes.model.KubernetesMachineImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.model.KubernetesRuntimeState; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.CheNamespace; 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.log.LogWatchTimeouts; @@ -131,6 +133,7 @@ public class KubernetesInternalRuntime private final PreviewUrlCommandProvisioner previewUrlCommandProvisioner; private final SecretAsContainerResourceProvisioner secretAsContainerResourceProvisioner; private final KubernetesServerResolverFactory serverResolverFactory; + protected final CheNamespace cheNamespace; protected final Tracer tracer; @Inject @@ -155,6 +158,7 @@ public KubernetesInternalRuntime( PreviewUrlCommandProvisioner previewUrlCommandProvisioner, SecretAsContainerResourceProvisioner secretAsContainerResourceProvisioner, KubernetesServerResolverFactory kubernetesServerResolverFactory, + CheNamespace cheNamespace, Tracer tracer, @Assisted KubernetesRuntimeContext context, @Assisted KubernetesNamespace namespace) { @@ -167,6 +171,7 @@ public KubernetesInternalRuntime( this.probeScheduler = probeScheduler; this.probesFactory = probesFactory; this.namespace = namespace; + this.cheNamespace = cheNamespace; this.eventPublisher = eventPublisher; this.executor = sharedPool.getExecutor(); this.runtimeStates = runtimeStates; @@ -190,7 +195,7 @@ protected void internalStart(Map startOptions) throws Infrastruc startSynchronizer.setStartThread(); startSynchronizer.start(); - namespace.cleanUp(); + cleanUp(workspaceId); provisionWorkspace(startOptions, context, workspaceId); volumesStrategy.prepare( @@ -255,7 +260,7 @@ protected void internalStart(Map startOptions) throws Infrastruc // stop watching before namespace cleaning up namespace.deployments().stopWatch(true); try { - namespace.cleanUp(); + cleanUp(workspaceId); } catch (InfrastructureException cleanUppingEx) { LOG.warn( "Failed to clean up namespace after workspace '{}' start failing. Cause: {}", @@ -272,6 +277,11 @@ protected void internalStart(Map startOptions) throws Infrastruc } } + private void cleanUp(String workspaceId) throws InfrastructureException { + namespace.cleanUp(); + cheNamespace.cleanUp(workspaceId); + } + protected void provisionWorkspace( Map startOptions, KubernetesRuntimeContext context, String workspaceId) throws InfrastructureException { @@ -584,7 +594,7 @@ protected void internalStop(Map stopOptions) throws Infrastructu // Che Server that is crashed so start is hung up in STOPPING phase. // Need to clean up runtime resources probeScheduler.cancel(identity.getWorkspaceId()); - namespace.cleanUp(); + cleanUp(identity.getWorkspaceId()); } } catch (InterruptedException e) { throw new InfrastructureException( @@ -594,7 +604,7 @@ protected void internalStop(Map stopOptions) throws Infrastructu // runtime is RUNNING. Clean up used resources // Cancels workspace servers probes if any probeScheduler.cancel(identity.getWorkspaceId()); - namespace.cleanUp(); + cleanUp(identity.getWorkspaceId()); } } @@ -614,7 +624,7 @@ protected void startMachines() throws InfrastructureException { String workspaceId = getContext().getIdentity().getWorkspaceId(); createSecrets(k8sEnv, workspaceId); - List createdConfigMaps = createConfigMaps(k8sEnv, workspaceId); + List createdConfigMaps = createConfigMaps(k8sEnv, getContext().getIdentity()); List createdServices = createServices(k8sEnv, workspaceId); // needed for resolution later on, even though n routes are actually created by ingress @@ -684,15 +694,25 @@ void createSecrets(KubernetesEnvironment env, String workspaceId) throws Infrast } @Traced - @SuppressWarnings("WeakerAccess") // package-private so that interception is possible - List createConfigMaps(KubernetesEnvironment env, String workspaceId) + protected List createConfigMaps(KubernetesEnvironment env, RuntimeIdentity identity) throws InfrastructureException { - TracingTags.WORKSPACE_ID.set(workspaceId); + TracingTags.WORKSPACE_ID.set(identity.getWorkspaceId()); List createdConfigMaps = new ArrayList<>(); + + List cheNamespaceConfigMaps = new ArrayList<>(); for (ConfigMap configMap : env.getConfigMaps().values()) { - createdConfigMaps.add(namespace.configMaps().create(configMap)); + if (shouldCreateInCheNamespace(configMap)) { + // we collect the che namespace configmaps into separate list + cheNamespaceConfigMaps.add(configMap); + } else { + createdConfigMaps.add(namespace.configMaps().create(configMap)); + } } + + // create che namespace configmaps in one batch, because we're doing some extra checks inside + createdConfigMaps.addAll(cheNamespace.createConfigMaps(cheNamespaceConfigMaps, identity)); + return createdConfigMaps; } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/CheInstallationLocation.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/CheInstallationLocation.java new file mode 100644 index 00000000000..339a49547e4 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/CheInstallationLocation.java @@ -0,0 +1,49 @@ +/* + * 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.environment; + +import com.google.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; + +/** + * This class checks the KUBERNETES_NAMESPACE and POD_NAMESPACE environment variables to determine + * what namespace Che is installed in. Users should use this class to retrieve the installation + * namespace name. + * + * @author Tom George + */ +@Singleton +public class CheInstallationLocation { + @Inject(optional = true) + @Named("env.KUBERNETES_NAMESPACE") + String kubernetesNamespace; + + @Inject(optional = true) + @Named("env.POD_NAMESPACE") + String podNamespace; + + /** + * @return The name of the namespace where Che is installed or null if both {@code + * KUBERNETES_NAMESPACE} and {@code POD_NAMESPACE} environment variables are not set + * @throws InfrastructureException when both {@code KUBERNETES_NAMESPACE} and {@code + * POD_NAMESPACE} are null + */ + public String getInstallationLocationNamespace() throws InfrastructureException { + if (kubernetesNamespace == null && podNamespace == null) { + throw new InfrastructureException( + "Neither KUBERNETES_NAMESPACE nor POD_NAMESPACE is defined. Unable to determine Che installation location"); + } + return kubernetesNamespace == null ? podNamespace : kubernetesNamespace; + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/CheNamespace.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/CheNamespace.java new file mode 100644 index 00000000000..ac889f8948d --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/CheNamespace.java @@ -0,0 +1,166 @@ +/* + * 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.namespace; + +import static org.eclipse.che.workspace.infrastructure.kubernetes.Annotations.CREATE_IN_CHE_INSTALLATION_NAMESPACE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_WORKSPACE_ID_LABEL; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil.putLabel; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil.shouldCreateInCheNamespace; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClientException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.WorkspaceRuntimes; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.InternalRuntime; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; +import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.CheInstallationLocation; + +/** + * This singleton bean can be used to create K8S object in Che installation namespaces. These + * objects are related to particular workspace, but for some reason has to be created in Che + * namespace. + */ +@Singleton +public class CheNamespace { + + private final String cheNamespaceName; + private final CheServerKubernetesClientFactory clientFactory; + private final WorkspaceRuntimes workspaceRuntimes; + + @Inject + public CheNamespace( + CheInstallationLocation installationLocation, + CheServerKubernetesClientFactory clientFactory, + WorkspaceRuntimes workspaceRuntimes) + throws InfrastructureException { + this.cheNamespaceName = installationLocation.getInstallationLocationNamespace(); + this.clientFactory = clientFactory; + this.workspaceRuntimes = workspaceRuntimes; + } + + /** + * Creates given {@link ConfigMap}s in Che installation namespace labeled with `workspaceId` from + * given `identity`. + * + *

`workspaceId` from given `identity` must be valid workspace ID, that is in {@link + * WorkspaceStatus#STARTING} state. Otherwise, {@link InfrastructureException} is thrown. + * + * @param configMap to create + * @param identity to validate and label configmaps + * @return created {@link ConfigMap}s + * @throws InfrastructureException when something goes wrong + */ + private ConfigMap createConfigMap(ConfigMap configMap, RuntimeIdentity identity) + throws InfrastructureException { + putLabel(configMap, CHE_WORKSPACE_ID_LABEL, identity.getWorkspaceId()); + // check that ConfigMap is properly annotated to be created in Che installation namespace + if (!shouldCreateInCheNamespace(configMap)) { + throw new InfrastructureException( + String.format( + "ConfigMap '%s' to be created in Che installation namespace is not properly annotated with '%s=true'. This is a bug, please report.", + configMap.getMetadata().getName(), CREATE_IN_CHE_INSTALLATION_NAMESPACE)); + } + // remove this annotation, so it's not exposed in actual k8s object + configMap.getMetadata().getAnnotations().remove(CREATE_IN_CHE_INSTALLATION_NAMESPACE); + return clientFactory.create().configMaps().inNamespace(cheNamespaceName).create(configMap); + } + + /** + * Creates given {@link ConfigMap}s in Che installation namespace labeled with `workspaceId` from + * given `identity`. + * + *

`workspaceId` from given `identity` must be valid workspace ID, that is in {@link + * WorkspaceStatus#STARTING} state. Otherwise, {@link InfrastructureException} is thrown. + * + *

all given {@code configMaps} must be annotated with {@link + * Annotations#CREATE_IN_CHE_INSTALLATION_NAMESPACE} set to 'true'. + * + * @param configMaps to create + * @param identity to validate and label configmaps + * @return created {@link ConfigMap}s + * @throws InfrastructureException when something goes wrong + */ + public List createConfigMaps(List configMaps, RuntimeIdentity identity) + throws InfrastructureException { + if (configMaps.isEmpty()) { + return Collections.emptyList(); + } + validate(identity, WorkspaceStatus.STARTING); + + List createdConfigMaps = new ArrayList<>(); + for (ConfigMap cm : configMaps) { + createdConfigMaps.add(createConfigMap(cm, identity)); + } + return createdConfigMaps; + } + + /** + * Cleanup all objects related to given `workspaceId` in Che installation namespace. + * + * @param workspaceId to delete objects + * @throws InfrastructureException when workspaceId is null or something bad happen during + * removing the objects + */ + public void cleanUp(String workspaceId) throws InfrastructureException { + if (workspaceId == null) { + throw new InfrastructureException("workspaceId to cleanup can't be null"); + } + cleanUpConfigMaps(workspaceId); + } + + /** + * Checks whether we have valid `workspaceId` and `owner` of existing workspace. + * + * @param identity to get `workspaceId` and `owner` to check + * @throws InfrastructureException when `workspaceId` is not valid workspace, is not in {@link + * WorkspaceStatus#STARTING} state, is `null`, or owner does not match. + */ + private void validate(RuntimeIdentity identity, WorkspaceStatus expectedStatus) + throws InfrastructureException { + try { + InternalRuntime runtime = workspaceRuntimes.getInternalRuntime(identity.getWorkspaceId()); + if (!identity.getOwnerId().equals(runtime.getOwner())) { + throw new InfrastructureException("Given owner does not match workspace's actual owner."); + } + + if (runtime.getStatus() != expectedStatus) { + throw new InfrastructureException("Can create objects only for starting workspaces."); + } + } catch (ServerException e) { + throw new InfrastructureException(e); + } + } + + private void cleanUpConfigMaps(String workspaceId) throws InfrastructureException { + try { + clientFactory + .create() + .configMaps() + .inNamespace(cheNamespaceName) + .withLabel(CHE_WORKSPACE_ID_LABEL, workspaceId) + .withPropagationPolicy("Background") + .delete(); + } catch (KubernetesClientException e) { + throw new KubernetesInfrastructureException(e); + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesObjectUtil.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesObjectUtil.java index 82d127ee51f..fb72a140f8c 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesObjectUtil.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesObjectUtil.java @@ -12,6 +12,9 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.namespace; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Annotations.CREATE_IN_CHE_INSTALLATION_NAMESPACE; import com.google.common.collect.ImmutableMap; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -33,6 +36,7 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; import java.util.HashMap; import java.util.Map; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; /** * Helps to work with Kubernetes objects. @@ -224,4 +228,29 @@ public static Volume newVolume(String name, String pvcName) { new PersistentVolumeClaimVolumeSourceBuilder().withClaimName(pvcName).build(); return new VolumeBuilder().withPersistentVolumeClaim(pvcs).withName(name).build(); } + + /** + * Checks the object if it is propetly annotated to be created in Che installation namespace. + * + *

Create in Che installation namespace only if there is {@link + * Annotations#CREATE_IN_CHE_INSTALLATION_NAMESPACE} annotation set exactly to `true`. In all + * other cases we create the object in Workspace's namespace. + * + * @param k8sObject object to check + * @return `true` if {@link Annotations#CREATE_IN_CHE_INSTALLATION_NAMESPACE} is set to `true`. + * `false` otherwise. + */ + public static boolean shouldCreateInCheNamespace(HasMetadata k8sObject) { + if (k8sObject.getMetadata() == null) { + return false; + } + Map annotations = k8sObject.getMetadata().getAnnotations(); + if (annotations == null || annotations.isEmpty()) { + return false; + } + + return annotations + .getOrDefault(CREATE_IN_CHE_INSTALLATION_NAMESPACE, FALSE.toString()) + .equals(TRUE.toString()); + } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayRouterProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayRouterProvisioner.java new file mode 100644 index 00000000000..e3c6b2fea0e --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayRouterProvisioner.java @@ -0,0 +1,117 @@ +/* + * 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.ServerConfig.SERVICE_NAME_ATTRIBUTE; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_PORT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil.isLabeled; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ConfigMap; +import java.util.Map; +import java.util.Map.Entry; +import javax.inject.Inject; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.GatewayRouteConfigGenerator; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.GatewayRouteConfigGeneratorFactory; + +/** + * This provisioner finds {@link ConfigMap}s, that configures the single-host Gateway, generates + * Gateway configuration and puts it into their data. + * + *

It uses {@link GatewayRouteConfigGenerator} to generate the gateway configuration. + */ +public class GatewayRouterProvisioner implements ConfigurationProvisioner { + + /** Configmap labeled with these holds the configuration of single-host gateway route */ + public static final Map GATEWAY_CONFIGMAP_LABELS = + ImmutableMap.builder() + .put("app", "che") + .put("role", "gateway-config") + .build(); + + private final GatewayRouteConfigGeneratorFactory configGeneratorFactory; + + @Inject + public GatewayRouterProvisioner(GatewayRouteConfigGeneratorFactory configGeneratorFactory) { + this.configGeneratorFactory = configGeneratorFactory; + } + + @Override + public void provision(KubernetesEnvironment k8sEnv, RuntimeIdentity identity) + throws InfrastructureException { + for (Entry configMapEntry : k8sEnv.getConfigMaps().entrySet()) { + if (isGatewayConfig(configMapEntry.getValue())) { + ConfigMap gatewayConfigMap = configMapEntry.getValue(); + + Map servers = + new Annotations.Deserializer(gatewayConfigMap.getMetadata().getAnnotations()).servers(); + if (servers.size() != 1) { + throw new InfrastructureException( + "Expected exactly 1 server in gateway config ConfigMap's '" + + gatewayConfigMap.getMetadata().getName() + + "' annotations. This is a bug, please report."); + } + Entry serverConfigEntry = servers.entrySet().iterator().next(); + ServerConfigImpl server = serverConfigEntry.getValue(); + + if (!server.getAttributes().containsKey(SERVICE_NAME_ATTRIBUTE) + || !server.getAttributes().containsKey(SERVICE_PORT_ATTRIBUTE)) { + throw new InfrastructureException( + "Expected `serviceName` and `servicePort` in gateway config ServerConfig attributes for gateway config Configmap '" + + gatewayConfigMap.getMetadata().getName() + + "'. This is a bug, please report."); + } + + // We're now creating only 1 gateway route configuration per ConfigMap, so we need to create + // generator in each loop iteration. + GatewayRouteConfigGenerator gatewayRouteConfigGenerator = configGeneratorFactory.create(); + gatewayRouteConfigGenerator.addRouteConfig(configMapEntry.getKey(), gatewayConfigMap); + + Map gatewayConfiguration = + gatewayRouteConfigGenerator.generate(identity.getInfrastructureNamespace()); + gatewayConfigMap.setData(gatewayConfiguration); + + // Configuration is now generated, so remove these internal attributes + server.getAttributes().remove(SERVICE_NAME_ATTRIBUTE); + server.getAttributes().remove(SERVICE_PORT_ATTRIBUTE); + gatewayConfigMap + .getMetadata() + .getAnnotations() + .putAll( + new Annotations.Serializer() + .server(serverConfigEntry.getKey(), server) + .annotations()); + } + } + } + + /** + * Check whether configmap is gateway route configuration. That is defined by {@link + * GatewayRouterProvisioner#GATEWAY_CONFIGMAP_LABELS} labels. + * + * @param configMap to check + * @return `true` if ConfigMap is gateway route configuration, `false` otherwise + */ + public static boolean isGatewayConfig(ConfigMap configMap) { + for (Entry labelEntry : GATEWAY_CONFIGMAP_LABELS.entrySet()) { + if (!isLabeled(configMap, labelEntry.getKey(), labelEntry.getValue())) { + return false; + } + } + return true; + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisioner.java index 483cd725242..882506f60a0 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisioner.java @@ -15,13 +15,14 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import java.util.Map; +import java.util.Map.Entry; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; /** @@ -39,30 +40,42 @@ public GatewayTlsProvisioner(@Named("che.infra.kubernetes.tls_enabled") boolean } @Override - public void provision(T k8sEnv, RuntimeIdentity identity) - throws KubernetesInfrastructureException { + public void provision(T k8sEnv, RuntimeIdentity identity) throws InfrastructureException { if (!isTlsEnabled) { return; } - for (ConfigMap cm : k8sEnv.getConfigMaps().values()) { - useSecureProtocolForGatewayServers(cm); + for (ConfigMap configMap : k8sEnv.getConfigMaps().values()) { + if (GatewayRouterProvisioner.isGatewayConfig(configMap)) { + useSecureProtocolForGatewayConfigMap(configMap); + } } } - private void useSecureProtocolForGatewayServers(ConfigMap cm) { + private void useSecureProtocolForGatewayConfigMap(ConfigMap configMap) + throws InfrastructureException { Map servers = - Annotations.newDeserializer(cm.getMetadata().getAnnotations()).servers(); + Annotations.newDeserializer(configMap.getMetadata().getAnnotations()).servers(); if (servers.isEmpty()) { return; } - - servers.values().forEach(s -> s.setProtocol(getSecureProtocol(s.getProtocol()))); - - Map annotations = Annotations.newSerializer().servers(servers).annotations(); - if (!annotations.isEmpty() && cm.getMetadata().getAnnotations() != null) { - cm.getMetadata().getAnnotations().putAll(annotations); + if (servers.size() != 1) { + throw new InfrastructureException( + "Expected exactly 1 server in Gateway configuration ConfigMap '" + + configMap.getMetadata().getName() + + "'. This is a bug, please report."); } + Entry serverConfigEntry = servers.entrySet().iterator().next(); + ServerConfigImpl serverConfig = serverConfigEntry.getValue(); + + serverConfig.setProtocol(getSecureProtocol(serverConfig.getProtocol())); + configMap + .getMetadata() + .getAnnotations() + .putAll( + Annotations.newSerializer() + .server(serverConfigEntry.getKey(), serverConfig) + .annotations()); } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/TlsProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/TlsProvisioner.java index d9449431e48..d0b3c77c284 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/TlsProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/TlsProvisioner.java @@ -12,7 +12,7 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.provision; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; /** @@ -27,9 +27,9 @@ public interface TlsProvisioner { * If TLS enabled, updates protocol to secure one and ensures that underlying exposure objects are * properly configured. * - * @throws KubernetesInfrastructureException in case of any infrastructure failure + * @throws InfrastructureException in case of any infrastructure failure */ - void provision(T k8sEnv, RuntimeIdentity identity) throws KubernetesInfrastructureException; + void provision(T k8sEnv, RuntimeIdentity identity) throws InfrastructureException; /** * Returns the secure version of the provided protocol or the same protocol if the conversion is diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayRouteConfigGenerator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayRouteConfigGenerator.java new file mode 100644 index 00000000000..5576cd7eafe --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayRouteConfigGenerator.java @@ -0,0 +1,57 @@ +/* + * 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 io.fabric8.kubernetes.api.model.ConfigMap; +import java.util.Map; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; + +/** + * Generates config for external servers that we want to expose in the Gateway. + * + *

Implementation provides configuration for specific Gateway technology (e.g., Traefik). + */ +public interface GatewayRouteConfigGenerator { + + /** + * Add prepared {@link ConfigMap},that will hold gateway route configuration, to the generator. So + * it can be generated later with {@link GatewayRouteConfigGenerator#generate(String)}. + * + *

Provided {@link ConfigMap} must be properly labeled and must be annotated with {@link + * org.eclipse.che.api.core.model.workspace.config.ServerConfig} annotations. + * + * @param routeConfig config to add + * @throws InfrastructureException when passed ConfigMap is not gateway configuration ConfigMap + */ + void addRouteConfig(String name, ConfigMap routeConfig) throws InfrastructureException; + + /** + * Generates content of configurations for services, defined earlier by added {@link + * GatewayRouteConfigGenerator#addRouteConfig(String, ConfigMap)}. Returned {@code Map} must be ready to be used as a {@link ConfigMap}'s data, which is further injected into + * Gateway pod. + * + *

Implementation must ensure that Gateway configured with returned content will route the + * requests on {@code path} into {@code serviceUrl}. Also it must strip {@code path} from request + * url. + * + *

Returned Map's Keys will be used as file names, Values as their content. e.g.: + * + *

+   *   service1.yml: {config-content-for-service-1}
+   *   service2.yml: {config-content-for-service-2}
+   * 
+ * + * @return full content of configuration for the services + */ + Map generate(String namespace) throws InfrastructureException; +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayRouteConfigGeneratorFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayRouteConfigGeneratorFactory.java new file mode 100644 index 00000000000..b68e3863c41 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayRouteConfigGeneratorFactory.java @@ -0,0 +1,26 @@ +/* + * 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 javax.inject.Singleton; + +/** + * This Factory provides {@link GatewayRouteConfigGenerator} instances, so implementation using + * these can stay Gateway technology agnostic. + */ +@Singleton +public class GatewayRouteConfigGeneratorFactory { + + public GatewayRouteConfigGenerator create() { + return new TraefikGatewayRouteConfigGenerator(); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposer.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposer.java index b50d6b37bc7..60b1387eb79 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposer.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposer.java @@ -11,29 +11,127 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.server.external; +import static java.lang.Boolean.TRUE; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_NAME_ATTRIBUTE; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_PORT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Annotations.CREATE_IN_CHE_INSTALLATION_NAMESPACE; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.ServicePort; import java.util.Map; +import javax.inject.Inject; import org.eclipse.che.api.core.model.workspace.config.ServerConfig; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer; /** - * Uses Traefik gateway configured with ConfigMaps to expose servers. - * - *

TODO: implement + * Uses gateway configured with ConfigMaps to expose servers. * * @param type of environment */ public class GatewayServerExposer implements ExternalServerExposer { + private final ExternalServiceExposureStrategy strategy; + + @Inject + public GatewayServerExposer(ExternalServiceExposureStrategy strategy) { + this.strategy = strategy; + } + + /** + * Exposes service port on given service externally (outside kubernetes cluster) using the Gateway + * specific configurations. + * + * @param k8sEnv Kubernetes environment + * @param machineName machine containing servers + * @param serviceName service associated with machine, mapping all machine server ports + * @param serverId non-null for a unique server, null for a compound set of servers that should be + * exposed together. + * @param servicePort specific service port to be exposed externally + * @param externalServers server configs of servers to be exposed externally + */ @Override public void expose( T k8sEnv, - String machineName, + @Nullable String machineName, String serviceName, String serverId, ServicePort servicePort, Map externalServers) { - throw new UnsupportedOperationException("Not implemented yet."); + + if (serverId == null) { + // this is the ID for non-unique servers + serverId = servicePort.getName(); + } + + for (String esKey : externalServers.keySet()) { + final String serverName = KubernetesServerExposer.makeServerNameValidForDns(serverId); + final String name = createName(serviceName, serverName); + k8sEnv + .getConfigMaps() + .put( + name, + createGatewayRouteConfigmap( + name, + machineName, + serviceName, + servicePort, + serverName, + esKey, + externalServers.get(esKey))); + } + } + + private ConfigMap createGatewayRouteConfigmap( + String name, + String machineName, + String serviceName, + ServicePort servicePort, + String serverName, + String scRef, + ServerConfig serverConfig) { + + final String path = ensureEndsWithSlash(strategy.getExternalPath(serviceName, serverName)); + serverConfig.getAttributes().put(SERVICE_NAME_ATTRIBUTE, serviceName); + serverConfig + .getAttributes() + .put(SERVICE_PORT_ATTRIBUTE, getTargetPort(servicePort.getTargetPort())); + + final Map annotations = + Annotations.newSerializer() + .server(scRef, new ServerConfigImpl(serverConfig).withPath(path)) + .machineName(machineName) + .annotations(); + annotations.put(CREATE_IN_CHE_INSTALLATION_NAMESPACE, TRUE.toString()); + + ConfigMapBuilder gatewayConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName(name) + .withLabels(GatewayRouterProvisioner.GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotations) + .endMetadata(); + return gatewayConfigMap.build(); + } + + private String ensureEndsWithSlash(String path) { + return path.endsWith("/") ? path : path + '/'; + } + + private String createName(String serviceName, String serverName) { + return serviceName + "-" + serverName; + } + + private String getTargetPort(IntOrString targetPort) { + return targetPort.getIntVal() != null + ? targetPort.getIntVal().toString() + : targetPort.getStrVal(); } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/TraefikGatewayRouteConfigGenerator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/TraefikGatewayRouteConfigGenerator.java new file mode 100644 index 00000000000..fd3e342ecbd --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/TraefikGatewayRouteConfigGenerator.java @@ -0,0 +1,265 @@ +/* + * 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 com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.WRITE_DOC_START_MARKER; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_NAME_ATTRIBUTE; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_PORT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner.isGatewayConfig; + +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import io.fabric8.kubernetes.api.model.ConfigMap; +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; + +/** + * Config generator for Traefik Gateway. + * + *

Content of single service configuration looks like this: + * + *

+ * http:
+ *   routers:
+ *     {name}:
+ *       rule: "PathPrefix(`{path}`)"
+ *       service: {name}
+ *       middlewares:
+ *       - "{name}"
+ *       - "{name}_headers"
+ *       priority: 100
+ *   services:
+ *     {name}:
+ *       loadBalancer:
+ *         servers:
+ *         - url: '{serviceUrl}'
+ *   middlewares:
+ *     {name}:
+ *       stripPrefix:
+ *         prefixes:
+ *         - "{GatewayRouteConfig#routePath}"
+ *     {name}_headers:
+ *       customRequestHeaders:
+ *         X-Forwarded-Proto: "{protocol}"
+ * 
+ */ +public class TraefikGatewayRouteConfigGenerator implements GatewayRouteConfigGenerator { + + private static final String SERVICE_URL_FORMAT = "http://%s.%s.svc.cluster.local:%s"; + + private final Map routeConfigs = new HashMap<>(); + + @Override + public void addRouteConfig(String name, ConfigMap routeConfig) throws InfrastructureException { + if (isGatewayConfig(routeConfig)) { + this.routeConfigs.put(name, routeConfig); + } else { + throw new InfrastructureException( + "Not a gateway configuration ConfigMap '" + + routeConfig.getMetadata().getName() + + "'. This is a bug, please report."); + } + } + + /** + * Generates configuration for all configs added by {@link + * TraefikGatewayRouteConfigGenerator#addRouteConfig(String, ConfigMap)} so far. It does not + * change them, so this method can be used repeatedly. + * + *

Returned {@code Map} has keys created from {@code name} parameter of {@link + * TraefikGatewayRouteConfigGenerator#addRouteConfig(String, ConfigMap)} + '.yml' suffix. Values + * are full configuration for single gateway route. This map is suppose to be directly used as + * {@link ConfigMap}'s data. + * + * @return map with added routes configurations + */ + @Override + public Map generate(String namespace) throws InfrastructureException { + Map cmData = new HashMap<>(); + for (Entry routeConfig : routeConfigs.entrySet()) { + Map servers = + new Annotations.Deserializer(routeConfig.getValue().getMetadata().getAnnotations()) + .servers(); + if (servers.size() != 1) { + throw new InfrastructureException( + "Expected exactly 1 server [" + routeConfig.getValue().toString() + "]"); + } + ServerConfigImpl server = servers.get(servers.keySet().iterator().next()); + String serviceName = server.getAttributes().get(SERVICE_NAME_ATTRIBUTE); + String servicePort = server.getAttributes().get(SERVICE_PORT_ATTRIBUTE); + + String traefikRouteConfig = + generate( + routeConfig.getKey(), + createServiceUrl(serviceName, servicePort, namespace), + server.getPath(), + server.getProtocol()); + cmData.put(routeConfig.getKey() + ".yml", traefikRouteConfig); + } + return cmData; + } + + /** + * Generates Traefik specific configuration for single service. + * + * @param name name of the service + * @param serviceUrl url of service we want to route to + * @param path path to route and strip + * @param protocol protocol of the service to properly set the headers + * @return traefik service route config + */ + private String generate(String name, String serviceUrl, String path, String protocol) + throws InfrastructureException { + StringWriter sw = new StringWriter(); + try { + YAMLGenerator generator = + YAMLFactory.builder().disable(WRITE_DOC_START_MARKER).build().createGenerator(sw); + + generator.writeStartObject(); + generator.writeFieldName("http"); + generator.writeStartObject(); + + generator.writeFieldName("routers"); + generateRouters(generator, name, path); + + generator.writeFieldName("services"); + generateServices(generator, name, serviceUrl); + + generator.writeFieldName("middlewares"); + generateMiddlewares(generator, name, path, protocol); + + generator.writeEndObject(); + generator.writeEndObject(); + + generator.flush(); + + return sw.toString(); + } catch (IOException e) { + throw new InfrastructureException(e); + } + } + + /** + * generates Routers part of Traefik config + * + *

+   * {name}:
+   *   rule: "PathPrefix(`{path}`)"
+   *   service: "{name}"
+   *   middlewares:
+   *   - "{name}"
+   *   - "{name}_headers
+   *   priority: 100
+   * 
+ */ + private void generateRouters(YAMLGenerator generator, String name, String path) + throws IOException { + generator.writeStartObject(); + generator.writeFieldName(name); + generator.writeStartObject(); + generator.writeFieldName("rule"); + generator.writeString("PathPrefix(`" + path + "`)"); + generator.writeFieldName("service"); + generator.writeString(name); + generator.writeFieldName("middlewares"); + generator.writeStartArray(); + generator.writeString(name); + generator.writeString(name + "_headers"); + generator.writeEndArray(); + generator.writeFieldName("priority"); + generator.writeNumber(100); + generator.writeEndObject(); + generator.writeEndObject(); + } + + /** + * generates Services part of Traefik config + * + *
+   * {name}:
+   *   loadBalancer:
+   *     servers:
+   *     - url: "{serviceUrl}"
+   * 
+ */ + private void generateServices(YAMLGenerator generator, String name, String serviceUrl) + throws IOException { + generator.writeStartObject(); + generator.writeFieldName(name); + generator.writeStartObject(); + generator.writeFieldName("loadBalancer"); + generator.writeStartObject(); + generator.writeFieldName("servers"); + generator.writeStartArray(); + generator.writeStartObject(); + generator.writeFieldName("url"); + generator.writeString(serviceUrl); + generator.writeEndObject(); + generator.writeEndArray(); + generator.writeEndObject(); + generator.writeEndObject(); + generator.writeEndObject(); + } + + /** + * generates Middlewares part of Traefik config + * + *
+   * {name}:
+   *   stripPrefix:
+   *     prefixes:
+   *     - "{path}"
+   * {name}_headers:
+   *   headers:
+   *     customRequestHeaders:
+   *       X-Forwarded-Proto: "{protocol}"
+   * 
+ */ + private void generateMiddlewares( + YAMLGenerator generator, String name, String path, String protocol) throws IOException { + generator.writeStartObject(); + generator.writeFieldName(name); + generator.writeStartObject(); + generator.writeFieldName("stripPrefix"); + generator.writeStartObject(); + generator.writeFieldName("prefixes"); + generator.writeStartArray(); + generator.writeString(path); + generator.writeEndArray(); + generator.writeEndObject(); + generator.writeEndObject(); + + generator.writeFieldName(name + "_headers"); + generator.writeStartObject(); + generator.writeFieldName("headers"); + generator.writeStartObject(); + generator.writeFieldName("customRequestHeaders"); + generator.writeStartObject(); + generator.writeFieldName("X-Forwarded-Proto"); + generator.writeString(protocol); + generator.writeEndObject(); + generator.writeEndObject(); + generator.writeEndObject(); + + generator.writeEndObject(); + } + + private String createServiceUrl(String serviceName, String servicePort, String serviceNamespace) { + return String.format(SERVICE_URL_FORMAT, serviceName, serviceNamespace, servicePort); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/resolver/ConfigMapServerResolver.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/resolver/ConfigMapServerResolver.java index f662d63e74d..dc75e024d7e 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/resolver/ConfigMapServerResolver.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/resolver/ConfigMapServerResolver.java @@ -37,7 +37,9 @@ public ConfigMapServerResolver( for (ConfigMap configMap : configMaps) { String machineName = Annotations.newDeserializer(configMap.getMetadata().getAnnotations()).machineName(); - this.configMaps.put(machineName, configMap); + if (machineName != null) { + this.configMaps.put(machineName, configMap); + } } } 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 40c53cfab0f..dd5e52a47b4 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 @@ -22,6 +22,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStoragePodInterceptor; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.AsyncStorageProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.CertificateProvisioner; +import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.GitConfigProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.ImagePullSecretProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.LogsVolumeMachineProvisioner; @@ -82,6 +83,7 @@ public class KubernetesEnvironmentProvisionerTest { @Mock private PreviewUrlExposer previewUrlExposer; @Mock private VcsSslCertificateProvisioner vcsSslCertificateProvisioner; @Mock private NodeSelectorProvisioner nodeSelectorProvisioner; + @Mock private GatewayRouterProvisioner gatewayRouterProvisioner; private KubernetesEnvironmentProvisioner k8sInfraProvisioner; @@ -114,7 +116,8 @@ public void setUp() { sshKeysProvisioner, gitConfigProvisioner, previewUrlExposer, - vcsSslCertificateProvisioner); + vcsSslCertificateProvisioner, + gatewayRouterProvisioner); provisionOrder = inOrder( logsVolumeMachineProvisioner, @@ -133,7 +136,8 @@ public void setUp() { serviceAccountProvisioner, certificateProvisioner, gitConfigProvisioner, - previewUrlExposer); + previewUrlExposer, + gatewayRouterProvisioner); } @Test @@ -160,6 +164,7 @@ public void performsOrderedProvisioning() throws Exception { provisionOrder.verify(serviceAccountProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); provisionOrder.verify(certificateProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); provisionOrder.verify(gitConfigProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); + provisionOrder.verify(gatewayRouterProvisioner).provision(eq(k8sEnv), eq(runtimeIdentity)); provisionOrder.verifyNoMoreInteractions(); } } 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 f53cf07efcc..d02b19692c3 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 @@ -24,7 +24,9 @@ import static org.eclipse.che.api.workspace.shared.Constants.DEBUG_WORKSPACE_START; import static org.eclipse.che.api.workspace.shared.Constants.DEBUG_WORKSPACE_START_LOG_LIMIT_BYTES; import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Annotations.CREATE_IN_CHE_INSTALLATION_NAMESPACE; import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner.GATEWAY_CONFIGMAP_LABELS; import static org.eclipse.che.workspace.infrastructure.kubernetes.server.external.MultiHostExternalServiceExposureStrategy.MULTI_HOST_STRATEGY; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -52,6 +54,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerPort; import io.fabric8.kubernetes.api.model.ContainerPortBuilder; @@ -75,6 +78,7 @@ import io.opentracing.Tracer; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -121,6 +125,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.model.KubernetesRuntimeCommandImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.model.KubernetesRuntimeState; import org.eclipse.che.workspace.infrastructure.kubernetes.model.KubernetesServerImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.CheNamespace; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesConfigsMaps; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesDeployments; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesIngresses; @@ -196,6 +201,7 @@ public class KubernetesInternalRuntimeTest { @Mock private UnrecoverablePodEventListenerFactory unrecoverablePodEventListenerFactory; @Mock private KubernetesEnvironment k8sEnv; @Mock private KubernetesNamespace namespace; + @Mock private CheNamespace cheNamespace; @Mock private KubernetesServices services; @Mock private KubernetesIngresses ingresses; @Mock private KubernetesSecrets secrets; @@ -264,7 +270,7 @@ public void setup() throws Exception { when(startSynchronizerFactory.create(any())).thenReturn(startSynchronizer); internalRuntime = - new KubernetesInternalRuntime( + new KubernetesInternalRuntime<>( 13, 5, new URLRewriter.NoOpURLRewriter(), @@ -285,6 +291,7 @@ public void setup() throws Exception { previewUrlCommandProvisioner, secretAsContainerResourceProvisioner, serverResolverFactory, + cheNamespace, tracer, context, namespace); @@ -426,6 +433,7 @@ public void startsKubernetesEnvironment() throws Exception { verify(secrets).create(any()); verify(configMaps).create(any()); verify(namespace).cleanUp(); + verify(cheNamespace).cleanUp(WORKSPACE_ID); verify(namespace.deployments(), times(1)).watchEvents(any()); verify(eventService, times(4)).publish(any()); verifyOrderedEventsChains( @@ -442,8 +450,9 @@ public void testCleanupHappensFirst() throws InfrastructureException { internalRuntime.start(emptyMap()); InOrder cleanupInOrderExecutionVerification = - Mockito.inOrder(namespace, deployments, toolingProvisioner); + Mockito.inOrder(namespace, cheNamespace, deployments, toolingProvisioner); cleanupInOrderExecutionVerification.verify(namespace).cleanUp(); + cleanupInOrderExecutionVerification.verify(cheNamespace).cleanUp(WORKSPACE_ID); cleanupInOrderExecutionVerification .verify(toolingProvisioner) .provision(any(), any(), any(), any()); @@ -647,6 +656,7 @@ public void throwsInternalInfrastructureExceptionWhenRuntimeErrorOccurs() throws internalRuntime.start(emptyMap()); } catch (Exception rethrow) { verify(namespace, times(2)).cleanUp(); + verify(cheNamespace, times(2)).cleanUp(WORKSPACE_ID); verify(namespace, never()).services(); verify(namespace, never()).ingresses(); throw rethrow; @@ -726,6 +736,7 @@ public void stopsKubernetesEnvironment() throws Exception { verify(runtimeHangingDetector).stopTracking(IDENTITY); verify(namespace).cleanUp(); + verify(cheNamespace).cleanUp(WORKSPACE_ID); } @Test(expectedExceptions = InfrastructureException.class) @@ -735,6 +746,14 @@ public void throwsInfrastructureExceptionWhenKubernetesNamespaceCleanupFailed() internalRuntime.internalStop(emptyMap()); } + @Test(expectedExceptions = InfrastructureException.class) + public void throwsInfrastructureExceptionWhenKubernetesCheNamespaceCleanupFailed() + throws Exception { + doThrow(InfrastructureException.class).when(cheNamespace).cleanUp(WORKSPACE_ID); + + internalRuntime.internalStop(emptyMap()); + } + @Test public void testRepublishContainerOutputAsMachineLogEvents() throws Exception { final MachineLogsPublisher logsPublisher = @@ -1109,6 +1128,45 @@ WORKSPACE_POD_NAME, mockDeployment(singletonList(mockContainer(CONTAINER_NAME_1) new HashSet<>(asList(CONTAINER_NAME_1, "injectedContainer"))); } + @Test + public void testGatewayRouteConfigsAreCreatedAsConfigmapsInCheNamespace() + throws InfrastructureException { + // given + ConfigMap cmRoute1 = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route1") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(ImmutableMap.of(CREATE_IN_CHE_INSTALLATION_NAMESPACE, "true")) + .endMetadata() + .build(); + ConfigMap cmRoute2 = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route2") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(ImmutableMap.of(CREATE_IN_CHE_INSTALLATION_NAMESPACE, "true")) + .endMetadata() + .build(); + ConfigMap cm3 = + new ConfigMapBuilder().withNewMetadata().withName("route2").endMetadata().build(); + + Map configMaps = + ImmutableMap.of("route1", cmRoute1, "route2", cmRoute2, "cm3", cm3); + when(k8sEnv.getConfigMaps()).thenReturn(configMaps); + + List expectedCreatedCheNamespaceConfigmaps = Arrays.asList(cmRoute1, cmRoute2); + + // when + internalRuntime.start(emptyMap()); + + // then + verify(cheNamespace) + .createConfigMaps( + expectedCreatedCheNamespaceConfigmaps, internalRuntime.getContext().getIdentity()); + verify(cheNamespace).cleanUp(WORKSPACE_ID); + } + private static MachineStatusEvent newEvent(String machineName, MachineStatus status) { return newDto(MachineStatusEvent.class) .withIdentity(DtoConverter.asDto(IDENTITY)) diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/CheInstallationLocationTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/CheInstallationLocationTest.java new file mode 100644 index 00000000000..479b8a89617 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/CheInstallationLocationTest.java @@ -0,0 +1,51 @@ +/* + * 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.environment; + +import static org.testng.Assert.*; + +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.testng.annotations.Test; + +public class CheInstallationLocationTest { + @Test + public void returnKubernetesNamespaceWhenBothSet() throws InfrastructureException { + CheInstallationLocation cheInstallationLocation = new CheInstallationLocation(); + cheInstallationLocation.kubernetesNamespace = "kube"; + cheInstallationLocation.podNamespace = "pod"; + assertEquals(cheInstallationLocation.getInstallationLocationNamespace(), "kube"); + } + + @Test + public void returnKubernetesNamespaceWhenItsOnlySet() throws InfrastructureException { + CheInstallationLocation cheInstallationLocation = new CheInstallationLocation(); + cheInstallationLocation.kubernetesNamespace = "kube"; + cheInstallationLocation.podNamespace = null; + assertEquals(cheInstallationLocation.getInstallationLocationNamespace(), "kube"); + } + + @Test + public void returnPodNamespaceWhenKubernetesNamespaceNotSet() throws InfrastructureException { + CheInstallationLocation cheInstallationLocation = new CheInstallationLocation(); + cheInstallationLocation.kubernetesNamespace = null; + cheInstallationLocation.podNamespace = "pod"; + assertEquals(cheInstallationLocation.getInstallationLocationNamespace(), "pod"); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void throwExceptionWhenNoneSet() throws InfrastructureException { + CheInstallationLocation cheInstallationLocation = new CheInstallationLocation(); + cheInstallationLocation.kubernetesNamespace = null; + cheInstallationLocation.podNamespace = null; + cheInstallationLocation.getInstallationLocationNamespace(); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/CheNamespaceTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/CheNamespaceTest.java new file mode 100644 index 00000000000..7622ad4c6f1 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/CheNamespaceTest.java @@ -0,0 +1,256 @@ +/* + * 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.namespace; + +import static org.eclipse.che.workspace.infrastructure.kubernetes.Annotations.CREATE_IN_CHE_INSTALLATION_NAMESPACE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_WORKSPACE_ID_LABEL; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.*; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapList; +import io.fabric8.kubernetes.api.model.DoneableConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.WorkspaceRuntimes; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.InternalRuntime; +import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.CheInstallationLocation; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class CheNamespaceTest { + + private static final String WORKSPACE_ID = "ws-id"; + private static final String OWNER_ID = "owner-id"; + private static final String CHE_NAMESPACE = "che"; + + private CheNamespace cheNamespace; + + @Mock private CheInstallationLocation cheInstallationLocation; + @Mock private CheServerKubernetesClientFactory clientFactory; + @Mock private WorkspaceRuntimes workspaceRuntimes; + @Mock private RuntimeIdentity identity; + @Mock private KubernetesClient kubeClient; + + @Mock + private MixedOperation< + ConfigMap, ConfigMapList, DoneableConfigMap, Resource> + kubeConfigMaps; + + @Mock + private MixedOperation< + ConfigMap, ConfigMapList, DoneableConfigMap, Resource> + kubeConfigMapsInNamespace; + + @Mock + private MixedOperation< + ConfigMap, ConfigMapList, DoneableConfigMap, Resource> + kubeConfigMapsWithLabel; + + @Mock + private MixedOperation< + ConfigMap, ConfigMapList, DoneableConfigMap, Resource> + kubeConfigMapsWithPropagationPolicy; + + @Mock private InternalRuntime internalRuntime; + + @BeforeMethod + public void setUp() throws InfrastructureException, ServerException { + when(cheInstallationLocation.getInstallationLocationNamespace()).thenReturn(CHE_NAMESPACE); + cheNamespace = new CheNamespace(cheInstallationLocation, clientFactory, workspaceRuntimes); + lenient().when(identity.getWorkspaceId()).thenReturn(WORKSPACE_ID); + lenient().when(identity.getOwnerId()).thenReturn(OWNER_ID); + lenient().when(workspaceRuntimes.getInternalRuntime(WORKSPACE_ID)).thenReturn(internalRuntime); + } + + @Test + public void testCreateConfigMaps() throws InfrastructureException { + // given + when(internalRuntime.getOwner()).thenReturn(OWNER_ID); + when(internalRuntime.getStatus()).thenReturn(WorkspaceStatus.STARTING); + Map cheNamespaceAnnotations = + ImmutableMap.of(CREATE_IN_CHE_INSTALLATION_NAMESPACE, "true"); + + ConfigMap cm1 = + new ConfigMapBuilder() + .withNewMetadata() + .withName("cm1") + .withAnnotations(cheNamespaceAnnotations) + .endMetadata() + .build(); + ConfigMap cm2 = + new ConfigMapBuilder() + .withNewMetadata() + .withName("cm2") + .withAnnotations(cheNamespaceAnnotations) + .endMetadata() + .build(); + + when(clientFactory.create()).thenReturn(kubeClient); + when(kubeClient.configMaps()).thenReturn(kubeConfigMaps); + when(kubeConfigMaps.inNamespace(CHE_NAMESPACE)).thenReturn(kubeConfigMapsInNamespace); + when(kubeConfigMapsInNamespace.create(any(ConfigMap.class))).thenReturn(cm1).thenReturn(cm2); + + List configMapsToCreate = Arrays.asList(cm1, cm2); + + // when + List createdConfigMaps = cheNamespace.createConfigMaps(configMapsToCreate, identity); + + // then + assertEquals(createdConfigMaps.size(), 2); + createdConfigMaps.forEach( + cm -> assertEquals(cm.getMetadata().getLabels().get(CHE_WORKSPACE_ID_LABEL), WORKSPACE_ID)); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void failWhenTryToCreateCmWithoutAnnotation() throws InfrastructureException { + // given + when(internalRuntime.getOwner()).thenReturn(OWNER_ID); + when(internalRuntime.getStatus()).thenReturn(WorkspaceStatus.STARTING); + + ConfigMap cm1 = new ConfigMapBuilder().withNewMetadata().withName("cm1").endMetadata().build(); + + when(clientFactory.create()).thenReturn(kubeClient); + when(kubeClient.configMaps()).thenReturn(kubeConfigMaps); + when(kubeConfigMaps.inNamespace(CHE_NAMESPACE)).thenReturn(kubeConfigMapsInNamespace); + when(kubeConfigMapsInNamespace.create(any(ConfigMap.class))).thenReturn(cm1); + + List configMapsToCreate = Collections.singletonList(cm1); + + // when + cheNamespace.createConfigMaps(configMapsToCreate, identity); + + // then exception + } + + @Test(expectedExceptions = InfrastructureException.class) + public void failWhenTryToCreateCmWithWronglySetAnnotation() throws InfrastructureException { + // given + when(internalRuntime.getOwner()).thenReturn(OWNER_ID); + when(internalRuntime.getStatus()).thenReturn(WorkspaceStatus.STARTING); + + ConfigMap cm1 = + new ConfigMapBuilder() + .withNewMetadata() + .withName("cm1") + .withAnnotations( + Collections.singletonMap(CREATE_IN_CHE_INSTALLATION_NAMESPACE, "blabol")) + .endMetadata() + .build(); + + when(clientFactory.create()).thenReturn(kubeClient); + when(kubeClient.configMaps()).thenReturn(kubeConfigMaps); + when(kubeConfigMaps.inNamespace(CHE_NAMESPACE)).thenReturn(kubeConfigMapsInNamespace); + when(kubeConfigMapsInNamespace.create(any(ConfigMap.class))).thenReturn(cm1); + + List configMapsToCreate = Collections.singletonList(cm1); + + // when + cheNamespace.createConfigMaps(configMapsToCreate, identity); + + // then exception + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testCreateConfigmapFailsWhenNoWorkspaceIdFoundInRuntimes() + throws InfrastructureException, ServerException { + // given + when(workspaceRuntimes.getInternalRuntime(WORKSPACE_ID)).thenThrow(ServerException.class); + when(internalRuntime.getOwner()).thenReturn(OWNER_ID); + when(internalRuntime.getStatus()).thenReturn(WorkspaceStatus.STARTING); + + ConfigMap cm1 = new ConfigMapBuilder().withNewMetadata().withName("cm1").endMetadata().build(); + + // when + cheNamespace.createConfigMaps(Collections.singletonList(cm1), identity); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testCreateConfigmapFailsWhenOwnerDontMatch() throws InfrastructureException { + // given + when(internalRuntime.getOwner()).thenReturn("nope"); + when(internalRuntime.getStatus()).thenReturn(WorkspaceStatus.STARTING); + + ConfigMap cm1 = new ConfigMapBuilder().withNewMetadata().withName("cm1").endMetadata().build(); + + // when + cheNamespace.createConfigMaps(Collections.singletonList(cm1), identity); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testCreateConfigmapFailsWhenWorkspaceStatusIsNotStarting() + throws InfrastructureException { + // given + when(internalRuntime.getOwner()).thenReturn(OWNER_ID); + when(internalRuntime.getStatus()).thenReturn(WorkspaceStatus.RUNNING); + + ConfigMap cm1 = new ConfigMapBuilder().withNewMetadata().withName("cm1").endMetadata().build(); + + // when + cheNamespace.createConfigMaps(Collections.singletonList(cm1), identity); + } + + @Test + public void testCreateEmptyReturnEmpty() throws InfrastructureException { + assertTrue(cheNamespace.createConfigMaps(Collections.emptyList(), identity).isEmpty()); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void throwExceptionWhenCheInstallationLocationFails() throws InfrastructureException { + when(cheInstallationLocation.getInstallationLocationNamespace()) + .thenThrow(InfrastructureException.class); + + new CheNamespace(cheInstallationLocation, clientFactory, workspaceRuntimes); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void cleanupThrowExceptionWhenWorkspaceIdIsNull() throws InfrastructureException { + cheNamespace.cleanUp(null); + } + + @Test + public void testCleanup() throws InfrastructureException { + // given + when(clientFactory.create()).thenReturn(kubeClient); + when(kubeClient.configMaps()).thenReturn(kubeConfigMaps); + when(kubeConfigMaps.inNamespace(CHE_NAMESPACE)).thenReturn(kubeConfigMapsInNamespace); + when(kubeConfigMapsInNamespace.withLabel(CHE_WORKSPACE_ID_LABEL, WORKSPACE_ID)) + .thenReturn(kubeConfigMapsWithLabel); + when(kubeConfigMapsWithLabel.withPropagationPolicy("Background")) + .thenReturn(kubeConfigMapsWithPropagationPolicy); + + // when + cheNamespace.cleanUp(WORKSPACE_ID); + + // then + verify(kubeConfigMapsWithPropagationPolicy).delete(); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayRouterProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayRouterProvisionerTest.java new file mode 100644 index 00000000000..c2200b0641c --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayRouterProvisionerTest.java @@ -0,0 +1,200 @@ +/* + * 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 java.util.Collections.emptyMap; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_NAME_ATTRIBUTE; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_PORT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner.GATEWAY_CONFIGMAP_LABELS; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import java.util.Collections; +import java.util.Map; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.GatewayRouteConfigGenerator; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.GatewayRouteConfigGeneratorFactory; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class GatewayRouterProvisionerTest { + + private final String NAMESPACE = "nejmspejs"; + + @Mock private GatewayRouteConfigGeneratorFactory configGeneratorFactory; + @Mock private GatewayRouteConfigGenerator gatewayRouteConfigGenerator; + @Mock private KubernetesEnvironment env; + @Mock private RuntimeIdentity identity; + + private GatewayRouterProvisioner gatewayRouterProvisioner; + private final ServerConfigImpl serverConfigWithoutAttributes = + new ServerConfigImpl("1234", "http", "/hello/there", emptyMap()); + private final ServerConfigImpl serverConfig = + new ServerConfigImpl( + "1234", + "http", + "/hello/there", + ImmutableMap.of(SERVICE_NAME_ATTRIBUTE, "serviceName", SERVICE_PORT_ATTRIBUTE, "1111")); + + @BeforeMethod + public void setUp() { + lenient().when(configGeneratorFactory.create()).thenReturn(gatewayRouteConfigGenerator); + lenient().when(identity.getInfrastructureNamespace()).thenReturn(NAMESPACE); + + gatewayRouterProvisioner = new GatewayRouterProvisioner(configGeneratorFactory); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testFailWhenNoServersInConfigmapAnnotations() throws InfrastructureException { + // given + ConfigMap gatewayRouteConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .endMetadata() + .build(); + when(env.getConfigMaps()).thenReturn(Collections.singletonMap("route", gatewayRouteConfigMap)); + + // when + gatewayRouterProvisioner.provision(env, identity); + + // then exception + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testFailWhenMoreThanOneServerInConfigmapAnnotations() throws InfrastructureException { + // given + Map annotationsWith2Servers = + new Annotations.Serializer() + .server("s1", serverConfigWithoutAttributes) + .server("s2", serverConfigWithoutAttributes) + .annotations(); + + ConfigMap gatewayRouteConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotationsWith2Servers) + .endMetadata() + .build(); + when(env.getConfigMaps()).thenReturn(Collections.singletonMap("route", gatewayRouteConfigMap)); + + // when + gatewayRouterProvisioner.provision(env, identity); + + // then exception + } + + @Test(expectedExceptions = InfrastructureException.class) + public void testFailWhenServerHasNotAllNeededAttributes() throws InfrastructureException { + // given + Map annotationsWith2Servers = + new Annotations.Serializer().server("s1", serverConfigWithoutAttributes).annotations(); + + ConfigMap gatewayRouteConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotationsWith2Servers) + .endMetadata() + .build(); + when(env.getConfigMaps()).thenReturn(Collections.singletonMap("route", gatewayRouteConfigMap)); + + // when + gatewayRouterProvisioner.provision(env, identity); + + // then exception + } + + @Test + public void testProvision() throws InfrastructureException { + // given + Map annotationsWith2Servers = + new Annotations.Serializer().server("s1", serverConfig).annotations(); + + ConfigMap gatewayRouteConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotationsWith2Servers) + .endMetadata() + .build(); + when(env.getConfigMaps()).thenReturn(Collections.singletonMap("route", gatewayRouteConfigMap)); + Map expectedData = + Collections.singletonMap("data.yml", "this is for sure generated configuration"); + when(gatewayRouteConfigGenerator.generate(NAMESPACE)).thenReturn(expectedData); + + // when + gatewayRouterProvisioner.provision(env, identity); + + // then + verify(configGeneratorFactory).create(); + verify(gatewayRouteConfigGenerator).addRouteConfig("route", gatewayRouteConfigMap); + verify(gatewayRouteConfigGenerator).generate(NAMESPACE); + + Map serverConfigsAfterProvisioning = + new Annotations.Deserializer(gatewayRouteConfigMap.getMetadata().getAnnotations()) + .servers(); + assertEquals(serverConfigsAfterProvisioning.size(), 1); + ServerConfigImpl server = + serverConfigsAfterProvisioning.get( + serverConfigsAfterProvisioning.keySet().iterator().next()); + + // verify that provisioner removes the internal attributes + assertFalse(server.getAttributes().containsKey(SERVICE_NAME_ATTRIBUTE)); + assertFalse(server.getAttributes().containsKey(SERVICE_PORT_ATTRIBUTE)); + + // verify that provisioner included the data info configmap + Map actualData = gatewayRouteConfigMap.getData(); + assertEquals(actualData, expectedData); + } + + @Test(dataProvider = "isGatewayConfigData") + public void testIsGatewayConfig(Map labels, boolean isGatewayConfigExpected) { + ConfigMap cm = + new ConfigMapBuilder().withNewMetadata().withLabels(labels).endMetadata().build(); + assertEquals(GatewayRouterProvisioner.isGatewayConfig(cm), isGatewayConfigExpected); + } + + @DataProvider + public Object[][] isGatewayConfigData() { + return new Object[][] { + {GATEWAY_CONFIGMAP_LABELS, true}, + {ImmutableMap.builder().putAll(GATEWAY_CONFIGMAP_LABELS).put("other", "value").build(), true}, + {emptyMap(), false}, + {ImmutableMap.of("one", "two"), false}, + {ImmutableMap.of(), false}, + {ImmutableMap.of("app", "yes", "role", "no"), false}, + {ImmutableMap.of("app", GATEWAY_CONFIGMAP_LABELS.get("app"), "role", "no"), false}, + {ImmutableMap.of("app", "no", "role", GATEWAY_CONFIGMAP_LABELS.get("role")), false}, + }; + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisionerTest.java index 0eabcf0e9db..302301ebb70 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisionerTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/GatewayTlsProvisionerTest.java @@ -13,27 +13,28 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner.GATEWAY_CONFIGMAP_LABELS; import static org.mockito.Mockito.when; -import static org.testng.Assert.*; +import static org.testng.Assert.assertEquals; -import com.google.common.collect.ImmutableMap; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import java.util.HashMap; import java.util.Map; -import org.eclipse.che.api.core.model.workspace.config.ServerConfig; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructureException; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.DataProvider; import org.testng.annotations.Listeners; import org.testng.annotations.Test; @Listeners(MockitoTestNGListener.class) public class GatewayTlsProvisionerTest { + public static final String WORKSPACE_ID = "workspace123"; @Mock private KubernetesEnvironment k8sEnv; @Mock private RuntimeIdentity runtimeIdentity; @@ -42,67 +43,77 @@ public class GatewayTlsProvisionerTest { new ServerConfigImpl("8080/tpc", "http", "/api", emptyMap()); private final ServerConfigImpl wsServer = new ServerConfigImpl("8080/tpc", "ws", "/ws", emptyMap()); - private final Map servers = - ImmutableMap.of("http-server", httpServer, "ws-server", wsServer); private final Map annotations = singletonMap("annotation-key", "annotation-value"); private final String machine = "machine"; - private final ConfigMap configMap = - new ConfigMapBuilder() - .withNewMetadata() - .withName("gatewayConfig") - .withAnnotations(annotations) - .endMetadata() - .build(); - - @Test - public void provisionTlsForConfigMap() throws Exception { + @Test(dataProvider = "tlsProvisionData") + public void provisionTlsForGatewayRouteConfigmaps( + ServerConfigImpl server, boolean tlsEnabled, String expectedProtocol) throws Exception { // given - GatewayTlsProvisioner ingressTlsProvisioner = - new GatewayTlsProvisioner<>(true); + Map composedAnnotations = new HashMap<>(annotations); + composedAnnotations.putAll( + Annotations.newSerializer().server("server", server).machineName(machine).annotations()); + ConfigMap routeConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(composedAnnotations) + .endMetadata() + .build(); - configMap - .getMetadata() - .getAnnotations() - .putAll(Annotations.newSerializer().servers(servers).machineName(machine).annotations()); + GatewayTlsProvisioner gatewayTlsProvisioner = + new GatewayTlsProvisioner<>(tlsEnabled); - Map configMaps = new HashMap<>(); - configMaps.put("cm", configMap); - when(k8sEnv.getConfigMaps()).thenReturn(configMaps); + when(k8sEnv.getConfigMaps()).thenReturn(singletonMap("route", routeConfigMap)); // when - ingressTlsProvisioner.provision(k8sEnv, runtimeIdentity); + gatewayTlsProvisioner.provision(k8sEnv, runtimeIdentity); // then Map servers = - Annotations.newDeserializer(configMap.getMetadata().getAnnotations()).servers(); - assertEquals(servers.get("http-server").getProtocol(), "https"); - assertEquals(servers.get("ws-server").getProtocol(), "wss"); + Annotations.newDeserializer(routeConfigMap.getMetadata().getAnnotations()).servers(); + assertEquals(servers.get("server").getProtocol(), expectedProtocol); } - @Test - public void shouldNotChangeProtocolWhenTlsDisabled() throws KubernetesInfrastructureException { - // given - GatewayTlsProvisioner ingressTlsProvisioner = - new GatewayTlsProvisioner<>(false); + @DataProvider + public Object[][] tlsProvisionData() { + return new Object[][] { + {httpServer, true, "https"}, + {httpServer, false, "http"}, + {wsServer, true, "wss"}, + {wsServer, false, "ws"}, + }; + } - configMap - .getMetadata() - .getAnnotations() - .putAll(Annotations.newSerializer().servers(servers).machineName(machine).annotations()); + @Test(expectedExceptions = InfrastructureException.class) + public void throwExceptionWhenMultipleServersInGatewayRouteConfigAnnotations() + throws InfrastructureException { + // given + Map composedAnnotations = new HashMap<>(annotations); + composedAnnotations.putAll( + Annotations.newSerializer() + .server("server1", httpServer) + .server("server2", wsServer) + .machineName(machine) + .annotations()); + ConfigMap routeConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(composedAnnotations) + .endMetadata() + .build(); - Map configMaps = new HashMap<>(); - configMaps.put("cm", configMap); - when(k8sEnv.getConfigMaps()).thenReturn(configMaps); + when(k8sEnv.getConfigMaps()).thenReturn(singletonMap("route", routeConfigMap)); + GatewayTlsProvisioner gatewayTlsProvisioner = + new GatewayTlsProvisioner<>(true); // when - ingressTlsProvisioner.provision(k8sEnv, runtimeIdentity); + gatewayTlsProvisioner.provision(k8sEnv, runtimeIdentity); - // then - Map servers = - Annotations.newDeserializer(configMap.getMetadata().getAnnotations()).servers(); - assertEquals(servers.get("http-server").getProtocol(), "http"); - assertEquals(servers.get("ws-server").getProtocol(), "ws"); + // then exception } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposerTest.java new file mode 100644 index 00000000000..c50b08106a8 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/GatewayServerExposerTest.java @@ -0,0 +1,80 @@ +/* + * 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 org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner.GATEWAY_CONFIGMAP_LABELS; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.ServicePort; +import java.util.Collections; +import java.util.Map; +import org.eclipse.che.api.core.model.workspace.config.ServerConfig; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.testng.annotations.Test; + +public class GatewayServerExposerTest { + + private final String machineName = "machine"; + private final String serviceName = "service"; + private final String serverId = "server"; + private final ServicePort servicePort = + new ServicePort("portName", 1, 1, "http", new IntOrString(1234)); + + private final Map s1attrs = Collections.singletonMap("s1attr", "s1val"); + + private final Map servers = + Collections.singletonMap("serverOne", new ServerConfigImpl("1111", "ws", null, s1attrs)); + + private final ExternalServerExposer serverExposer = + new GatewayServerExposer<>(new SingleHostExternalServiceExposureStrategy("che-host")); + + @Test + public void testExposeServiceWithGatewayConfigmap() { + // given + KubernetesEnvironment k8sEnv = KubernetesEnvironment.builder().build(); + + // when + serverExposer.expose(k8sEnv, machineName, serviceName, serverId, servicePort, servers); + + // then + Map configMaps = k8sEnv.getConfigMaps(); + assertTrue(configMaps.containsKey(serviceName + "-" + serverId)); + ConfigMap serverConfigMap = configMaps.get("service-server"); + + // data should be empty at this point + assertTrue(serverConfigMap.getData() == null || serverConfigMap.getData().isEmpty()); + + assertEquals(serverConfigMap.getMetadata().getLabels(), GATEWAY_CONFIGMAP_LABELS); + + Map annotations = serverConfigMap.getMetadata().getAnnotations(); + Annotations.Deserializer deserializer = Annotations.newDeserializer(annotations); + assertEquals(deserializer.machineName(), machineName); + + Map exposedServers = deserializer.servers(); + assertTrue(exposedServers.containsKey("serverOne")); + + ServerConfig s1 = exposedServers.get("serverOne"); + assertEquals( + s1.getAttributes().get(s1attrs.keySet().iterator().next()), + s1attrs.values().iterator().next()); + assertEquals(s1.getAttributes().get(ServerConfigImpl.SERVICE_NAME_ATTRIBUTE), "service"); + assertEquals(s1.getAttributes().get(ServerConfigImpl.SERVICE_PORT_ATTRIBUTE), "1234"); + assertEquals(s1.getPort(), "1111"); + assertEquals(s1.getProtocol(), "ws"); + assertEquals(s1.getPath(), "/service/server/"); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/TraefikGatewayRouteConfigGeneratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/TraefikGatewayRouteConfigGeneratorTest.java new file mode 100644 index 00000000000..7158a10ff1e --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/external/TraefikGatewayRouteConfigGeneratorTest.java @@ -0,0 +1,149 @@ +/* + * 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 org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_NAME_ATTRIBUTE; +import static org.eclipse.che.api.core.model.workspace.config.ServerConfig.SERVICE_PORT_ATTRIBUTE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.provision.GatewayRouterProvisioner.GATEWAY_CONFIGMAP_LABELS; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import java.util.Map; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class TraefikGatewayRouteConfigGeneratorTest { + + private GatewayRouteConfigGenerator gatewayConfigGenerator; + + @BeforeMethod + public void setUp() { + gatewayConfigGenerator = new TraefikGatewayRouteConfigGenerator(); + } + + @Test + public void testGenerateGatewayConfig() throws InfrastructureException { + String expectedConfig = + "http:\n" + + " routers:\n" + + " external-server-1:\n" + + " rule: \"PathPrefix(`/blabol-cesta`)\"\n" + + " service: \"external-server-1\"\n" + + " middlewares:\n" + + " - \"external-server-1\"\n" + + " - \"external-server-1_headers\"\n" + + " priority: 100\n" + + " services:\n" + + " external-server-1:\n" + + " loadBalancer:\n" + + " servers:\n" + + " - url: \"http://service-url.che-namespace.svc.cluster.local:1234\"\n" + + " middlewares:\n" + + " external-server-1:\n" + + " stripPrefix:\n" + + " prefixes:\n" + + " - \"/blabol-cesta\"\n" + + " external-server-1_headers:\n" + + " headers:\n" + + " customRequestHeaders:\n" + + " X-Forwarded-Proto: \"https\""; + + ServerConfigImpl serverConfig = + new ServerConfigImpl( + "123", + "https", + "/blabol-cesta", + ImmutableMap.of(SERVICE_NAME_ATTRIBUTE, "service-url", SERVICE_PORT_ATTRIBUTE, "1234")); + Map annotations = + new Annotations.Serializer().server("s1", serverConfig).annotations(); + ConfigMap routeConfig = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotations) + .endMetadata() + .build(); + + gatewayConfigGenerator.addRouteConfig("external-server-1", routeConfig); + Map generatedConfig = gatewayConfigGenerator.generate("che-namespace"); + + assertTrue(generatedConfig.containsKey("external-server-1.yml")); + assertEquals(generatedConfig.get("external-server-1.yml"), expectedConfig); + } + + @Test + public void testMultipleRouteConfigsAreGeneratedAsMultipleMapEntries() + throws InfrastructureException { + ServerConfigImpl serverConfig = + new ServerConfigImpl( + "123", + "https", + "/blabol-cesta", + ImmutableMap.of(SERVICE_NAME_ATTRIBUTE, "service-url", SERVICE_PORT_ATTRIBUTE, "1234")); + Map annotations = + new Annotations.Serializer().server("s1", serverConfig).annotations(); + ConfigMap routeConfig = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotations) + .endMetadata() + .build(); + gatewayConfigGenerator.addRouteConfig("c1", routeConfig); + gatewayConfigGenerator.addRouteConfig("c2", routeConfig); + Map generatedConfig = gatewayConfigGenerator.generate("che-namespace"); + + assertTrue(generatedConfig.containsKey("c1.yml")); + assertTrue(generatedConfig.containsKey("c2.yml")); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void failWhenMultipleServersInConfigmapAnnotations() throws InfrastructureException { + ServerConfigImpl serverConfig = + new ServerConfigImpl( + "123", + "https", + "/blabol-cesta", + ImmutableMap.of(SERVICE_NAME_ATTRIBUTE, "service-url", SERVICE_PORT_ATTRIBUTE, "1234")); + Map annotations = + new Annotations.Serializer() + .server("s1", serverConfig) + .server("s2", serverConfig) + .annotations(); + ConfigMap routeConfig = + new ConfigMapBuilder() + .withNewMetadata() + .withName("route") + .withLabels(GATEWAY_CONFIGMAP_LABELS) + .withAnnotations(annotations) + .endMetadata() + .build(); + gatewayConfigGenerator.addRouteConfig("c1", routeConfig); + + gatewayConfigGenerator.generate("che-namespace"); + } + + @Test(expectedExceptions = InfrastructureException.class) + public void failWhenAddConfigmapWithoutLabels() throws InfrastructureException { + ConfigMap routeConfig = + new ConfigMapBuilder().withNewMetadata().withName("route").endMetadata().build(); + gatewayConfigGenerator.addRouteConfig("c1", routeConfig); + } +} 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 e4d8520fb9b..883b7ac145b 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 @@ -38,6 +38,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.StartSynchronizerFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesMachineCache; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.CheNamespace; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.WorkspaceVolumesStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.secret.SecretAsContainerResourceProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; @@ -83,6 +84,7 @@ public OpenShiftInternalRuntime( SecretAsContainerResourceProvisioner secretAsContainerResourceProvisioner, OpenShiftServerResolverFactory serverResolverFactory, + CheNamespace cheNamespace, Tracer tracer, Openshift4TrustedCAProvisioner trustedCAProvisioner, @Assisted OpenShiftRuntimeContext context, @@ -108,6 +110,7 @@ public OpenShiftInternalRuntime( previewUrlCommandProvisioner, secretAsContainerResourceProvisioner, null, + cheNamespace, tracer, context, project); @@ -133,7 +136,7 @@ protected void startMachines() throws InfrastructureException { String workspaceId = getContext().getIdentity().getWorkspaceId(); createSecrets(osEnv, workspaceId); - List createdConfigMaps = createConfigMaps(osEnv, workspaceId); + List createdConfigMaps = createConfigMaps(osEnv, getContext().getIdentity()); List createdServices = createServices(osEnv, workspaceId); List createdRoutes = createRoutes(osEnv, workspaceId); @@ -151,18 +154,6 @@ void createSecrets(OpenShiftEnvironment env, String workspaceId) throws Infrastr } } - @Traced - @SuppressWarnings("WeakerAccess") // package-private so that interception is possible - List createConfigMaps(OpenShiftEnvironment env, String workspaceId) - throws InfrastructureException { - TracingTags.WORKSPACE_ID.set(workspaceId); - List createdConfigMaps = new ArrayList<>(); - for (ConfigMap configMap : env.getConfigMaps().values()) { - createdConfigMaps.add(project.configMaps().create(configMap)); - } - return createdConfigMaps; - } - @Traced @SuppressWarnings("WeakerAccess") // package-private so that interception is possible List createServices(OpenShiftEnvironment env, String workspaceId) diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftCheInstallationLocation.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftCheInstallationLocation.java deleted file mode 100644 index 8b292f10b1b..00000000000 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftCheInstallationLocation.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.environment; - -import com.google.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * OpenShiftCheInstallationLocation checks the KUBERNETES_NAMESPACE and POD_NAMESPACE environment - * variables to determine what namespace Che is installed in. Users should use this class to - * retrieve the installation namespace name. - * - * @author Tom George - */ -@Singleton -public class OpenShiftCheInstallationLocation { - - private static final Logger LOG = LoggerFactory.getLogger(OpenShiftCheInstallationLocation.class); - - @Inject(optional = true) - @Named("env.KUBERNETES_NAMESPACE") - private String kubernetesNamespace = null; - - @Inject(optional = true) - @Named("env.POD_NAMESPACE") - private String podNamespace = null; - - /** @return The name of the namespace where Che is installed */ - public String getInstallationLocationNamespace() { - if (kubernetesNamespace == null && podNamespace == null) { - LOG.warn( - "Neither KUBERNETES_NAMESPACE nor POD_NAMESPACE is defined. Unable to determine Che installation location"); - } - return kubernetesNamespace == null ? podNamespace : kubernetesNamespace; - } -} diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java index 28e0f255f95..2b55aa3a988 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java @@ -21,8 +21,8 @@ import javax.inject.Inject; import javax.inject.Named; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.CheInstallationLocation; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; -import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftCheInstallationLocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,8 +44,9 @@ public class OpenShiftStopWorkspaceRoleProvisioner { @Inject public OpenShiftStopWorkspaceRoleProvisioner( OpenShiftClientFactory clientFactory, - OpenShiftCheInstallationLocation installationLocation, - @Named("che.workspace.stop.role.enabled") boolean stopWorkspaceRoleEnabled) { + CheInstallationLocation installationLocation, + @Named("che.workspace.stop.role.enabled") boolean stopWorkspaceRoleEnabled) + throws InfrastructureException { this.clientFactory = clientFactory; this.installationLocation = installationLocation.getInstallationLocationNamespace(); this.stopWorkspaceRoleEnabled = stopWorkspaceRoleEnabled; 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 c0724b5319c..8603eeb31ca 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 @@ -71,6 +71,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.StartSynchronizerFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesMachineCache; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.KubernetesRuntimeStateCache; +import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.CheNamespace; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesConfigsMaps; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesDeployments; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; @@ -148,6 +149,7 @@ public class OpenShiftInternalRuntimeTest { @Mock private OpenShiftPreviewUrlCommandProvisioner previewUrlCommandProvisioner; @Mock private SecretAsContainerResourceProvisioner secretAsContainerResourceProvisioner; @Mock private Openshift4TrustedCAProvisioner trustedCAProvisioner; + @Mock private CheNamespace cheNamespace; private OpenShiftServerResolverFactory serverResolverFactory; @Mock(answer = Answers.RETURNS_MOCKS) @@ -192,6 +194,7 @@ public void setup() throws Exception { previewUrlCommandProvisioner, secretAsContainerResourceProvisioner, serverResolverFactory, + cheNamespace, tracer, trustedCAProvisioner, context, diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java index 99d82964123..02d35e79920 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java @@ -36,8 +36,8 @@ import io.fabric8.openshift.api.model.PolicyRuleBuilder; import io.fabric8.openshift.client.OpenShiftClient; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.CheInstallationLocation; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; -import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftCheInstallationLocation; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; @@ -52,7 +52,7 @@ @Listeners(MockitoTestNGListener.class) public class OpenShiftStopWorkspaceRoleProvisionerTest { - @Mock private OpenShiftCheInstallationLocation cheInstallationLocation; + @Mock private CheInstallationLocation cheInstallationLocation; private OpenShiftStopWorkspaceRoleProvisioner stopWorkspaceRoleProvisioner; @Mock private OpenShiftClientFactory clientFactory; @@ -174,7 +174,7 @@ public void shouldCreateRole() { } @Test - public void shouldCreateRoleBinding() { + public void shouldCreateRoleBinding() throws InfrastructureException { when(cheInstallationLocation.getInstallationLocationNamespace()).thenReturn("che"); assertEquals( stopWorkspaceRoleProvisioner.createStopWorkspacesRoleBinding("developer-che"), diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/Openshift4TrustedCAProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/Openshift4TrustedCAProvisionerTest.java index 28bae4e93f3..919eb9d3b4b 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/Openshift4TrustedCAProvisionerTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/Openshift4TrustedCAProvisionerTest.java @@ -32,7 +32,6 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesConfigsMaps; import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; -import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftCheInstallationLocation; import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProject; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; @@ -50,7 +49,6 @@ public class Openshift4TrustedCAProvisionerTest { private static final String CONFIGMAP_KEY = "testConfigMapKey"; private static final String CONFIGMAP_VALUE = "testConfigMapValue"; - @Mock OpenShiftCheInstallationLocation cheInstallationLocation; @Mock OpenShiftClientFactory clientFactory; @Mock private KubernetesEnvironment k8sEnv;