diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Containers.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Containers.java index 7f27182134..817ae2e492 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Containers.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/Containers.java @@ -56,6 +56,21 @@ public class Containers { resourceBuilder.addToLimits("memory", new Quantity(String.valueOf(ramLimit))).build()); } + /** + * Sets given RAM limit in kubernetes notion to specified container. Note if the container already + * contains a RAM limit, it will be overridden, other resources won't be affected. + */ + public static void addRamLimit(Container container, String limitInK8sNotion) { + final ResourceRequirementsBuilder resourceBuilder; + if (container.getResources() != null) { + resourceBuilder = new ResourceRequirementsBuilder(container.getResources()); + } else { + resourceBuilder = new ResourceRequirementsBuilder(); + } + container.setResources( + resourceBuilder.addToLimits("memory", new Quantity(limitInK8sNotion)).build()); + } + /** * Returns the RAM request in bytes, if it is present in given container otherwise 0 will be * returned. diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolver.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolver.java new file mode 100644 index 0000000000..e09bc6ce32 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolver.java @@ -0,0 +1,103 @@ +/* + * 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.wsplugins; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; +import static java.util.Collections.emptyList; + +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPort; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import java.util.List; +import java.util.stream.Collectors; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.eclipse.che.api.workspace.server.wsplugins.model.EnvVar; +import org.eclipse.che.workspace.infrastructure.kubernetes.Names; +import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; +import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSize; + +/** + * Resolves Kubernetes container {@link Container} configuration from Che workspace sidecar and Che + * plugin endpoints. + * + * @author Oleksandr Garagatyi + */ +public class K8sContainerResolver { + + private final CheContainer cheContainer; + private final List containerEndpoints; + + public K8sContainerResolver(CheContainer container, List containerEndpoints) { + this.cheContainer = container; + this.containerEndpoints = containerEndpoints; + } + + public List getEndpoints() { + return containerEndpoints; + } + + public Container resolve() throws InfrastructureException { + Container container = + new ContainerBuilder() + .withImage(cheContainer.getImage()) + .withName(Names.generateName("tooling")) + .withEnv(toK8sEnv(cheContainer.getEnv())) + .withPorts(getContainerPorts()) + .build(); + + provisionMemoryLimit(container, cheContainer); + + return container; + } + + private void provisionMemoryLimit(Container container, CheContainer cheContainer) + throws InfrastructureException { + String memoryLimit = cheContainer.getMemoryLimit(); + if (isNullOrEmpty(memoryLimit)) { + return; + } + try { + KubernetesSize.toBytes(memoryLimit); + } catch (IllegalArgumentException e) { + throw new InfrastructureException( + format( + "Sidecar memory limit field contains illegal value '%s'. Error: '%s'", + memoryLimit, e.getMessage())); + } + Containers.addRamLimit(container, memoryLimit); + } + + private List getContainerPorts() { + return containerEndpoints + .stream() + .map( + endpoint -> + new ContainerPortBuilder() + .withContainerPort(endpoint.getTargetPort()) + .withProtocol("TCP") + .build()) + .collect(Collectors.toList()); + } + + private List toK8sEnv(List env) { + if (env == null) { + return emptyList(); + } + return env.stream() + .map(e -> new io.fabric8.kubernetes.api.model.EnvVar(e.getName(), e.getValue(), null)) + .collect(Collectors.toList()); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverBuilder.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverBuilder.java new file mode 100644 index 0000000000..a63fb99d37 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverBuilder.java @@ -0,0 +1,58 @@ +/* + * 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.wsplugins; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainerPort; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; + +/** @author Oleksandr Garagatyi */ +public class K8sContainerResolverBuilder { + + private CheContainer container; + private List pluginEndpoints; + + public K8sContainerResolverBuilder setContainer(CheContainer container) { + this.container = container; + return this; + } + + public K8sContainerResolverBuilder setPluginEndpoints(List pluginEndpoints) { + this.pluginEndpoints = pluginEndpoints; + return this; + } + + public K8sContainerResolver build() { + if (container == null || pluginEndpoints == null) { + throw new IllegalStateException(); + } + List containerEndpoints = + getContainerEndpoints(container.getPorts(), pluginEndpoints); + return new K8sContainerResolver(container, containerEndpoints); + } + + private List getContainerEndpoints( + List ports, List endpoints) { + + if (ports == null || ports.isEmpty()) { + return Collections.emptyList(); + } + return ports + .stream() + .map(CheContainerPort::getExposedPort) + .flatMap(port -> endpoints.stream().filter(e -> e.getTargetPort() == port)) + .collect(Collectors.toList()); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java index 07aed34269..ac593bb5ca 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/KubernetesPluginsToolingApplier.java @@ -11,46 +11,24 @@ */ package org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static java.util.stream.Collectors.toMap; -import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.MEMORY_LIMIT_ATTRIBUTE; -import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL; - import com.google.common.annotations.Beta; -import io.fabric8.kubernetes.api.model.ContainerBuilder; -import io.fabric8.kubernetes.api.model.ContainerPort; -import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.fabric8.kubernetes.api.model.ServicePort; -import io.fabric8.kubernetes.api.model.ServicePortBuilder; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; -import org.eclipse.che.api.core.model.workspace.config.ServerConfig; -import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; -import org.eclipse.che.api.workspace.server.model.impl.VolumeImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; import org.eclipse.che.api.workspace.server.wsplugins.ChePluginsApplier; import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; -import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainerPort; import org.eclipse.che.api.workspace.server.wsplugins.model.ChePlugin; import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; -import org.eclipse.che.api.workspace.server.wsplugins.model.EnvVar; -import org.eclipse.che.api.workspace.server.wsplugins.model.Volume; import org.eclipse.che.workspace.infrastructure.kubernetes.Names; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; -import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; /** * Applies Che plugins tooling configuration to a kubernetes internal runtime object. @@ -60,13 +38,12 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; @Beta public class KubernetesPluginsToolingApplier implements ChePluginsApplier { - private final String defaultSidecarMemorySizeAttribute; + private final String defaultSidecarMemoryLimitBytes; @Inject public KubernetesPluginsToolingApplier( - @Named("che.workspace.sidecar.default_memory_limit_mb") long defaultSidecarMemorySizeMB) { - this.defaultSidecarMemorySizeAttribute = - String.valueOf(defaultSidecarMemorySizeMB * 1024 * 1024); + @Named("che.workspace.sidecar.default_memory_limit_mb") long defaultSidecarMemoryLimitMB) { + this.defaultSidecarMemoryLimitBytes = String.valueOf(defaultSidecarMemoryLimitMB * 1024 * 1024); } @Override @@ -86,207 +63,52 @@ public class KubernetesPluginsToolingApplier implements ChePluginsApplier { Pod pod = pods.values().iterator().next(); for (ChePlugin chePlugin : chePlugins) { - if (chePlugin.getContainers() == null) { - continue; - } for (CheContainer container : chePlugin.getContainers()) { - addMachine(pod, container, chePlugin, kubernetesEnvironment); + addSidecar(pod, container, chePlugin, kubernetesEnvironment); } } } - private void addMachine( + /** + * Adds k8s and Che specific configuration of a sidecar into the environment. For example: + *
  • k8s container configuration {@link Container} + *
  • k8s service configuration {@link Service} + *
  • Che machine config {@link InternalMachineConfig} + * + * @throws InfrastructureException when any error occurs + */ + private void addSidecar( Pod pod, CheContainer container, ChePlugin chePlugin, KubernetesEnvironment kubernetesEnvironment) throws InfrastructureException { - List containerEndpoints = - getContainerEndpoints(container.getPorts(), chePlugin.getEndpoints()); - io.fabric8.kubernetes.api.model.Container k8sContainer = - addContainer(pod, container.getImage(), container.getEnv(), containerEndpoints); + K8sContainerResolver k8sContainerResolver = + new K8sContainerResolverBuilder() + .setContainer(container) + .setPluginEndpoints(chePlugin.getEndpoints()) + .build(); + List containerEndpoints = k8sContainerResolver.getEndpoints(); + + Container k8sContainer = k8sContainerResolver.resolve(); String machineName = Names.machineName(pod, k8sContainer); + pod.getSpec().getContainers().add(k8sContainer); - InternalMachineConfig machineConfig = - addMachineConfig( - kubernetesEnvironment, machineName, containerEndpoints, container.getVolumes()); - - addEndpointsServices(kubernetesEnvironment, containerEndpoints, pod.getMetadata().getName()); - - normalizeMemory(k8sContainer, machineConfig); - } - - /** - * Add k8s Service objects to environment to provide service discovery in sidecar based - * workspaces. - */ - private void addEndpointsServices( - KubernetesEnvironment kubernetesEnvironment, - List endpoints, - String podName) - throws InfrastructureException { - - for (ChePluginEndpoint endpoint : endpoints) { - String serviceName = endpoint.getName(); - Service service = createService(serviceName, podName, endpoint.getTargetPort()); - - Map services = kubernetesEnvironment.getServices(); - if (!services.containsKey(serviceName)) { - services.put(serviceName, service); - } else { - throw new InfrastructureException( - "Applying of sidecar tooling failed. Kubernetes service with name '" - + serviceName - + "' already exists in the workspace environment."); - } - } - } - - private Service createService(String name, String podName, int port) { - ServicePort servicePort = - new ServicePortBuilder().withPort(port).withProtocol("TCP").withNewTargetPort(port).build(); - return new ServiceBuilder() - .withNewMetadata() - .withName(name) - .endMetadata() - .withNewSpec() - .withSelector(singletonMap(CHE_ORIGINAL_NAME_LABEL, podName)) - .withPorts(singletonList(servicePort)) - .endSpec() - .build(); - } - - private io.fabric8.kubernetes.api.model.Container addContainer( - Pod toolingPod, String image, List env, List containerEndpoints) { - - List containerPorts = - containerEndpoints - .stream() - .map( - endpoint -> - new ContainerPortBuilder() - .withContainerPort(endpoint.getTargetPort()) - .withProtocol("TCP") - .build()) - .collect(Collectors.toList()); - - io.fabric8.kubernetes.api.model.Container container = - new ContainerBuilder() - .withImage(image) - .withName(Names.generateName("tooling")) - .withEnv(toK8sEnv(env)) - .withPorts(containerPorts) + MachineResolver machineResolver = + new MachineResolverBuilder() + .setCheContainer(container) + .setContainer(k8sContainer) + .setContainerEndpoints(containerEndpoints) + .setDefaultSidecarMemorySizeAttribute(defaultSidecarMemoryLimitBytes) .build(); - toolingPod.getSpec().getContainers().add(container); - return container; - } - - private InternalMachineConfig addMachineConfig( - KubernetesEnvironment kubernetesEnvironment, - String machineName, - List endpoints, - List volumes) { - - InternalMachineConfig machineConfig = - new InternalMachineConfig( - null, toWorkspaceServers(endpoints), null, null, toWorkspaceVolumes(volumes)); + InternalMachineConfig machineConfig = machineResolver.resolve(); kubernetesEnvironment.getMachines().put(machineName, machineConfig); - return machineConfig; - } - - private List getContainerEndpoints( - List ports, List endpoints) { - - if (ports != null) { - return ports - .stream() - .flatMap( - cheContainerPort -> - endpoints - .stream() - .filter( - chePluginEndpoint -> - chePluginEndpoint.getTargetPort() - == cheContainerPort.getExposedPort())) - .collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - private void normalizeMemory( - io.fabric8.kubernetes.api.model.Container container, InternalMachineConfig machineConfig) { - long ramLimit = Containers.getRamLimit(container); - Map attributes = machineConfig.getAttributes(); - if (ramLimit > 0) { - attributes.put(MEMORY_LIMIT_ATTRIBUTE, String.valueOf(ramLimit)); - } else { - attributes.put(MEMORY_LIMIT_ATTRIBUTE, defaultSidecarMemorySizeAttribute); - } - } - - private Map - toWorkspaceVolumes(List volumes) { - - Map result = new HashMap<>(); - - if (volumes == null) { - return result; - } - - for (Volume volume : volumes) { - result.put(volume.getName(), new VolumeImpl().withPath(volume.getMountPath())); - } - return result; - } - - private Map toWorkspaceServers( - List endpoints) { - return endpoints - .stream() - .collect( - toMap(ChePluginEndpoint::getName, endpoint -> normalizeServer(toServer(endpoint)))); - } - - private ServerConfigImpl toServer(ChePluginEndpoint endpoint) { - Map attributes = new HashMap<>(); - attributes.put("internal", Boolean.toString(!endpoint.isPublic())); - endpoint - .getAttributes() - .forEach( - (k, v) -> { - if (!"protocol".equals(k) && !"path".equals(k)) { - attributes.put(k, v); - } - }); - return new ServerConfigImpl( - Integer.toString(endpoint.getTargetPort()), - endpoint.getAttributes().get("protocol"), - endpoint.getAttributes().get("path"), - attributes); - } - - private List toK8sEnv(List env) { - List result = new ArrayList<>(); - - if (env != null) { - for (EnvVar envVar : env) { - result.add( - new io.fabric8.kubernetes.api.model.EnvVar(envVar.getName(), envVar.getValue(), null)); - } - } - - return result; - } - - private ServerConfigImpl normalizeServer(ServerConfigImpl serverConfig) { - String port = serverConfig.getPort(); - if (port != null && !port.contains("/")) { - serverConfig.setPort(port + "/tcp"); - } - return serverConfig; + SidecarServicesProvisioner sidecarServicesProvisioner = + new SidecarServicesProvisioner(containerEndpoints, pod.getMetadata().getName()); + sidecarServicesProvisioner.provision(kubernetesEnvironment); } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolver.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolver.java new file mode 100644 index 0000000000..a341374103 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolver.java @@ -0,0 +1,103 @@ +/* + * 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.wsplugins; + +import static java.util.stream.Collectors.toMap; +import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.MEMORY_LIMIT_ATTRIBUTE; + +import io.fabric8.kubernetes.api.model.Container; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.eclipse.che.api.core.model.workspace.config.ServerConfig; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.VolumeImpl; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.eclipse.che.api.workspace.server.wsplugins.model.Volume; +import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; + +/** @author Oleksandr Garagatyi */ +public class MachineResolver { + + private final Container container; + private final CheContainer cheContainer; + private final String defaultSidecarMemoryLimitBytes; + private final List containerEndpoints; + + public MachineResolver( + Container container, + CheContainer cheContainer, + String defaultSidecarMemoryLimitBytes, + List containerEndpoints) { + this.container = container; + this.cheContainer = cheContainer; + this.defaultSidecarMemoryLimitBytes = defaultSidecarMemoryLimitBytes; + this.containerEndpoints = containerEndpoints; + } + + public InternalMachineConfig resolve() { + InternalMachineConfig machineConfig = + new InternalMachineConfig( + null, + toServers(containerEndpoints), + null, + null, + toWorkspaceVolumes(cheContainer.getVolumes())); + + normalizeMemory(container, machineConfig); + return machineConfig; + } + + private void normalizeMemory(Container container, InternalMachineConfig machineConfig) { + long ramLimit = Containers.getRamLimit(container); + if (ramLimit == 0) { + machineConfig.getAttributes().put(MEMORY_LIMIT_ATTRIBUTE, defaultSidecarMemoryLimitBytes); + } + } + + private Map + toWorkspaceVolumes(List volumes) { + + Map result = new HashMap<>(); + + for (Volume volume : volumes) { + result.put(volume.getName(), new VolumeImpl().withPath(volume.getMountPath())); + } + return result; + } + + private Map toServers(List endpoints) { + return endpoints.stream().collect(toMap(ChePluginEndpoint::getName, this::toServer)); + } + + private ServerConfigImpl toServer(ChePluginEndpoint endpoint) { + ServerConfigImpl serverConfig = + new ServerConfigImpl().withPort(Integer.toString(endpoint.getTargetPort()) + "/tcp"); + serverConfig.getAttributes().put("internal", Boolean.toString(!endpoint.isPublic())); + for (Entry attribute : endpoint.getAttributes().entrySet()) { + switch (attribute.getKey()) { + case "protocol": + serverConfig.setProtocol(attribute.getValue()); + break; + case "path": + serverConfig.setPath(attribute.getValue()); + break; + default: + serverConfig.getAttributes().put(attribute.getKey(), attribute.getValue()); + } + } + return serverConfig; + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolverBuilder.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolverBuilder.java new file mode 100644 index 0000000000..743e1d8831 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolverBuilder.java @@ -0,0 +1,59 @@ +/* + * 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.wsplugins; + +import io.fabric8.kubernetes.api.model.Container; +import java.util.List; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; + +/** @author Alexander Garagatyi */ +public class MachineResolverBuilder { + + private Container container; + private CheContainer cheContainer; + private String defaultSidecarMemorySizeAttribute; + private List containerEndpoints; + + public MachineResolver build() { + if (container == null + || cheContainer == null + || defaultSidecarMemorySizeAttribute == null + || containerEndpoints == null) { + throw new IllegalStateException(); + } + + return new MachineResolver( + container, cheContainer, defaultSidecarMemorySizeAttribute, containerEndpoints); + } + + public MachineResolverBuilder setContainer(Container container) { + this.container = container; + return this; + } + + public MachineResolverBuilder setCheContainer(CheContainer cheContainer) { + this.cheContainer = cheContainer; + return this; + } + + public MachineResolverBuilder setDefaultSidecarMemorySizeAttribute( + String defaultSidecarMemorySizeAttribute) { + this.defaultSidecarMemorySizeAttribute = defaultSidecarMemorySizeAttribute; + return this; + } + + public MachineResolverBuilder setContainerEndpoints(List containerEndpoints) { + this.containerEndpoints = containerEndpoints; + return this; + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/SidecarServicesProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/SidecarServicesProvisioner.java new file mode 100644 index 0000000000..f02bdcd106 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/SidecarServicesProvisioner.java @@ -0,0 +1,83 @@ +/* + * 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.wsplugins; + +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import java.util.List; +import java.util.Map; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; + +/** + * Resolves Kubernetes {@link Service}s needed for a proper accessibility of a Che workspace + * sidecar. + * + *

    Proper accessibility here means that sidecar endpoint should be discoverable by an endpoint + * name inside of a workspace. + * + * @author Oleksandr Garagatyi + */ +public class SidecarServicesProvisioner { + + private final List endpoints; + private final String podName; + + public SidecarServicesProvisioner(List containerEndpoints, String podName) { + this.endpoints = containerEndpoints; + this.podName = podName; + } + + /** + * Add k8s Service objects to environment to provide service discovery in sidecar based + * workspaces. + */ + public void provision(KubernetesEnvironment kubernetesEnvironment) + throws InfrastructureException { + for (ChePluginEndpoint endpoint : endpoints) { + String serviceName = endpoint.getName(); + Service service = createService(serviceName, podName, endpoint.getTargetPort()); + + Map services = kubernetesEnvironment.getServices(); + if (!services.containsKey(serviceName)) { + services.put(serviceName, service); + } else { + throw new InfrastructureException( + format( + "Applying of sidecar tooling failed. Kubernetes service with name '%s' already exists in the workspace environment.", + serviceName)); + } + } + } + + private Service createService(String name, String podName, int port) { + ServicePort servicePort = + new ServicePortBuilder().withPort(port).withProtocol("TCP").withNewTargetPort(port).build(); + return new ServiceBuilder() + .withNewMetadata() + .withName(name) + .endMetadata() + .withNewSpec() + .withSelector(singletonMap(CHE_ORIGINAL_NAME_LABEL, podName)) + .withPorts(singletonList(servicePort)) + .endSpec() + .build(); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ContainersTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ContainersTest.java index 5277f5a8a0..bd7aa2b75e 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ContainersTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/util/ContainersTest.java @@ -29,6 +29,7 @@ import org.mockito.Captor; 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; @@ -60,14 +61,14 @@ public class ContainersTest { } @Test - public void testReturnContainerRamLimit() throws Exception { + public void testReturnContainerRamLimit() { long actual = Containers.getRamLimit(container); assertEquals(actual, RAM_LIMIT); } @Test - public void testReturnsZeroContainerRamLimitWhenResourceIsNull() throws Exception { + public void testReturnsZeroContainerRamLimitWhenResourceIsNull() { when(container.getResources()).thenReturn(null); final long actual = Containers.getRamLimit(container); @@ -76,7 +77,7 @@ public class ContainersTest { } @Test - public void testReturnsZeroContainerRamLimitWhenResourceDoesNotContainIt() throws Exception { + public void testReturnsZeroContainerRamLimitWhenResourceDoesNotContainIt() { when(resource.getLimits()).thenReturn(Collections.emptyMap()); final long actual = Containers.getRamLimit(container); @@ -85,7 +86,7 @@ public class ContainersTest { } @Test - public void testReturnsZeroContainerRamLimitWhenActualValueIsNull() throws Exception { + public void testReturnsZeroContainerRamLimitWhenActualValueIsNull() { when(resource.getLimits()).thenReturn(ImmutableMap.of("memory", new Quantity())); final long actual = Containers.getRamLimit(container); @@ -94,7 +95,7 @@ public class ContainersTest { } @Test - public void testOverridesContainerRamLimit() throws Exception { + public void testOverridesContainerRamLimit() { Containers.addRamLimit(container, 3221225472L); assertTrue(limits.containsKey("cpu")); @@ -102,7 +103,7 @@ public class ContainersTest { } @Test - public void testAddContainerRamLimitWhenItNotPresent() throws Exception { + public void testAddContainerRamLimitWhenItNotPresent() { final Map limits = new HashMap<>(); when(resource.getLimits()).thenReturn(limits); @@ -112,7 +113,7 @@ public class ContainersTest { } @Test - public void testAddContainerRamLimitWhenResourceIsNull() throws Exception { + public void testAddContainerRamLimitWhenResourceIsNull() { when(container.getResources()).thenReturn(null); Containers.addRamLimit(container, RAM_LIMIT); @@ -123,7 +124,7 @@ public class ContainersTest { } @Test - public void testAddContainerRamLimitWhenResourceDoesNotContainAnyLimits() throws Exception { + public void testAddContainerRamLimitWhenResourceDoesNotContainAnyLimits() { when(resource.getLimits()).thenReturn(null); Containers.addRamLimit(container, RAM_LIMIT); @@ -132,4 +133,25 @@ public class ContainersTest { final ResourceRequirements captured = resourceCaptor.getValue(); assertEquals(captured.getLimits().get("memory").getAmount(), String.valueOf(RAM_LIMIT)); } + + @Test(dataProvider = "k8sNotionRamLimitProvider") + public void testAddContainerRamLimitInK8sNotion(String ramLimit, ResourceRequirements resources) { + when(container.getResources()).thenReturn(resources); + + Containers.addRamLimit(container, ramLimit); + + verify(container).setResources(resourceCaptor.capture()); + ResourceRequirements captured = resourceCaptor.getValue(); + assertEquals(captured.getLimits().get("memory").getAmount(), ramLimit); + } + + @DataProvider + public static Object[][] k8sNotionRamLimitProvider() { + return new Object[][] { + {"123456789", new ResourceRequirements()}, + {"1M", new ResourceRequirements()}, + {"10Ki", null}, + {"10G", null}, + }; + } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverBuilderTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverBuilderTest.java new file mode 100644 index 0000000000..84684c1d29 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverBuilderTest.java @@ -0,0 +1,68 @@ +/* + * 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.wsplugins; + +import static java.util.Arrays.asList; +import static org.testng.Assert.assertEqualsNoOrder; + +import java.util.ArrayList; +import java.util.List; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainerPort; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** @author Alexander Garagatyi */ +public class K8sContainerResolverBuilderTest { + @Test(dataProvider = "allFieldsSetProvider", expectedExceptions = IllegalStateException.class) + public void shouldCheckThatAllFieldsAreSet(K8sContainerResolverBuilder builder) { + builder.build(); + } + + @DataProvider + public static Object[][] allFieldsSetProvider() { + return new Object[][] { + {new K8sContainerResolverBuilder()}, + {new K8sContainerResolverBuilder().setContainer(new CheContainer())}, + {new K8sContainerResolverBuilder().setPluginEndpoints(new ArrayList<>())}, + }; + } + + @Test + public void shouldPassOnlyParticularContainerEndpoints() { + // given + K8sContainerResolverBuilder builder = new K8sContainerResolverBuilder(); + builder.setContainer( + new CheContainer() + .ports( + asList( + new CheContainerPort().exposedPort(9014), + new CheContainerPort().exposedPort(4040)))); + builder.setPluginEndpoints( + asList( + new ChePluginEndpoint().targetPort(9014), + new ChePluginEndpoint().targetPort(9013), + new ChePluginEndpoint().targetPort(8080), + new ChePluginEndpoint().targetPort(4040))); + K8sContainerResolver resolver = builder.build(); + ArrayList expected = new ArrayList<>(); + expected.add(new ChePluginEndpoint().targetPort(9014)); + expected.add(new ChePluginEndpoint().targetPort(4040)); + + // when + List actualEndpoints = resolver.getEndpoints(); + + // then + assertEqualsNoOrder(actualEndpoints.toArray(), expected.toArray()); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverTest.java new file mode 100644 index 0000000000..a261385c92 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/K8sContainerResolverTest.java @@ -0,0 +1,156 @@ +/* + * 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.wsplugins; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertEqualsNoOrder; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerPort; +import io.fabric8.kubernetes.api.model.Quantity; +import io.fabric8.kubernetes.api.model.ResourceRequirements; +import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.eclipse.che.api.workspace.server.wsplugins.model.EnvVar; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** @author Alexander Garagatyi */ +public class K8sContainerResolverTest { + private static final String IMAGE = "testImage:tag"; + + private CheContainer cheContainer; + private K8sContainerResolver resolver; + private List endpoints; + + @BeforeMethod + public void setUp() { + cheContainer = new CheContainer(); + endpoints = new ArrayList<>(); + resolver = new K8sContainerResolver(cheContainer, endpoints); + } + + @Test + public void shouldSetImageFromSidecar() throws Exception { + cheContainer.setImage(IMAGE); + + Container container = resolver.resolve(); + + assertEquals(container.getImage(), IMAGE); + } + + @Test + public void shouldSetName() throws Exception { + Container container = resolver.resolve(); + + assertTrue(container.getName().startsWith("tooling")); + } + + @Test + public void shouldSetEnvVarsFromSidecar() throws Exception { + Map env = ImmutableMap.of("name1", "value1", "name2", "value2"); + cheContainer.setEnv(toSidecarEnvVars(env)); + + Container container = resolver.resolve(); + + assertEqualsNoOrder(container.getEnv().toArray(), toK8sEnvVars(env).toArray()); + } + + @Test + public void shouldSetPortsFromContainerEndpoints() throws Exception { + Integer[] ports = new Integer[] {3030, 10000}; + endpoints.addAll(toEndpoints(ports)); + + Container container = resolver.resolve(); + + assertEqualsNoOrder(container.getPorts().toArray(), toK8sPorts(ports).toArray()); + } + + @Test(dataProvider = "memLimitResourcesProvider") + public void shouldProvisionSidecarMemoryLimit( + String sidecarMemLimit, ResourceRequirements resources) throws Exception { + cheContainer.setMemoryLimit(sidecarMemLimit); + + Container container = resolver.resolve(); + + assertEquals(container.getResources(), resources); + } + + @DataProvider + public static Object[][] memLimitResourcesProvider() { + return new Object[][] { + {"", null}, + {null, null}, + {"123456789", toK8sResources("123456789")}, + {"1Ki", toK8sResources("1Ki")}, + {"100M", toK8sResources("100M")}, + }; + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = "Sidecar memory limit field contains illegal value .*") + public void shouldThrowExceptionIfMemoryLimitIsInIllegalFormat() throws Exception { + cheContainer.setMemoryLimit("IllegalValue"); + + resolver.resolve(); + } + + private static ResourceRequirements toK8sResources(String memLimit) { + return new ResourceRequirementsBuilder().addToLimits("memory", new Quantity(memLimit)).build(); + } + + private List toSidecarEnvVars(Map envVars) { + return envVars + .entrySet() + .stream() + .map(entry -> new EnvVar().name(entry.getKey()).value(entry.getValue())) + .collect(Collectors.toList()); + } + + private List toK8sEnvVars(Map envVars) { + return envVars + .entrySet() + .stream() + .map( + entry -> + new io.fabric8.kubernetes.api.model.EnvVar(entry.getKey(), entry.getValue(), null)) + .collect(Collectors.toList()); + } + + private List toK8sPorts(Integer[] ports) { + return Arrays.stream(ports).map(this::k8sPort).collect(Collectors.toList()); + } + + private ContainerPort k8sPort(Integer port) { + ContainerPort containerPort = new ContainerPort(); + containerPort.setContainerPort(port); + containerPort.setProtocol("TCP"); + return containerPort; + } + + private List toEndpoints(Integer[] ports) { + return Arrays.stream(ports) + .map(p -> new ChePluginEndpoint().targetPort(p)) + .collect(Collectors.toList()); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolverTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolverTest.java new file mode 100644 index 0000000000..c86f837ff6 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/MachineResolverTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Arrays.asList; +import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.MEMORY_LIMIT_ATTRIBUTE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import io.fabric8.kubernetes.api.model.Container; +import java.util.ArrayList; +import java.util.List; +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.api.workspace.server.model.impl.VolumeImpl; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.api.workspace.server.wsplugins.model.CheContainer; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.eclipse.che.api.workspace.server.wsplugins.model.Volume; +import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** @author Oleksandr Garagatyi */ +public class MachineResolverTest { + + private static final String DEFAULT_MEM_LIMIT = "100001"; + + private List endpoints; + private CheContainer cheContainer; + private Container container; + private MachineResolver resolver; + + @BeforeMethod + public void setUp() { + endpoints = new ArrayList<>(); + cheContainer = new CheContainer(); + container = new Container(); + resolver = new MachineResolver(container, cheContainer, DEFAULT_MEM_LIMIT, endpoints); + } + + @Test + public void shouldSetVolumesInMachineConfig() { + List sidecarVolumes = + asList( + new Volume().name("vol1").mountPath("/path1"), + new Volume().name("vol2").mountPath("/path2")); + cheContainer.setVolumes(sidecarVolumes); + Map expected = of("vol1", volume("/path1"), "vol2", volume("/path2")); + + InternalMachineConfig machineConfig = resolver.resolve(); + + assertEquals(machineConfig.getVolumes(), expected); + } + + @Test(dataProvider = "serverProvider") + public void shouldSetServersInMachineConfig( + List containerEndpoints, Map expected) { + endpoints.addAll(containerEndpoints); + + InternalMachineConfig machineConfig = resolver.resolve(); + + assertEquals(machineConfig.getServers(), expected); + } + + @DataProvider + public static Object[][] serverProvider() { + return new Object[][] { + // default minimal case + { + asList(endpt("endp1", 8080), endpt("endp2", 10000)), + of("endp1", server(8080), "endp2", server(10000)) + }, + // case with publicity setting + { + asList(endpt("endp1", 8080, false), endpt("endp2", 10000, true)), + of("endp1", server(8080, false), "endp2", server(10000, true)) + }, + // case with protocol attribute + { + asList(endptPrtc("endp1", 8080, "http"), endptPrtc("endp2", 10000, "ws")), + of("endp1", serverPrtc(8080, "http"), "endp2", serverPrtc(10000, "ws")) + }, + // case with path attribute + { + asList(endptPath("endp1", 8080, "/"), endptPath("endp2", 10000, "/some/thing")), + of("endp1", serverPath(8080, "/"), "endp2", serverPath(10000, "/some/thing")) + }, + // case with other attributes + { + asList( + endpt("endp1", 8080, of("a1", "v1")), + endpt("endp2", 10000, of("a2", "v1", "a3", "v3"))), + of( + "endp1", + server(8080, of("a1", "v1")), + "endp2", + server(10000, of("a2", "v1", "a3", "v3"))) + }, + }; + } + + @Test + public void shouldSetDefaultMemLimitIfSidecarDoesNotHaveOne() { + InternalMachineConfig machineConfig = resolver.resolve(); + + assertEquals(machineConfig.getAttributes().get(MEMORY_LIMIT_ATTRIBUTE), DEFAULT_MEM_LIMIT); + } + + @Test + public void shouldNotSetMemLimitAttributeIfLimitIsInContainer() { + Containers.addRamLimit(container, 123456789); + + InternalMachineConfig machineConfig = resolver.resolve(); + + assertNull(machineConfig.getAttributes().get(MEMORY_LIMIT_ATTRIBUTE)); + } + + private static ChePluginEndpoint endptPath(String name, int port, String path) { + return new ChePluginEndpoint().name(name).targetPort(port).attributes(of("path", path)); + } + + private static ChePluginEndpoint endptPrtc(String name, int port, String protocol) { + return new ChePluginEndpoint().name(name).targetPort(port).attributes(of("protocol", protocol)); + } + + private static ChePluginEndpoint endpt(String name, int port, boolean isPublic) { + return new ChePluginEndpoint().name(name).targetPort(port).setPublic(isPublic); + } + + private static ChePluginEndpoint endpt(String name, int port, Map attributes) { + return new ChePluginEndpoint().name(name).targetPort(port).attributes(attributes); + } + + private static ChePluginEndpoint endpt(String name, int port) { + return new ChePluginEndpoint().name(name).targetPort(port); + } + + private static ServerConfigImpl server(int port) { + return server(port, false); + } + + private static ServerConfig server(int port, Map attributes) { + ServerConfigImpl server = server(port); + server.getAttributes().putAll(attributes); + return server; + } + + private static ServerConfigImpl serverPath(int port, String path) { + return server(port).withPath(path); + } + + private static ServerConfigImpl serverPrtc(int port, String protocol) { + return server(port).withProtocol(protocol); + } + + private static ServerConfigImpl server(int port, boolean external) { + return new ServerConfigImpl() + .withPort(port + "/tcp") + .withAttributes(of("internal", Boolean.toString(!external))); + } + + private org.eclipse.che.api.core.model.workspace.config.Volume volume(String mountPath) { + return new VolumeImpl().withPath(mountPath); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/SidecarServicesProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/SidecarServicesProvisionerTest.java new file mode 100644 index 0000000000..a60f5ec426 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsplugins/SidecarServicesProvisionerTest.java @@ -0,0 +1,122 @@ +/* + * 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.wsplugins; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toMap; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL; +import static org.testng.Assert.assertEquals; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.wsplugins.model.ChePluginEndpoint; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** @author Oleksandr Garagatyi */ +public class SidecarServicesProvisionerTest { + private static final String POD_NAME = "testPod"; + private static final String CONFLICTING_SERVICE_NAME = "testService"; + + private SidecarServicesProvisioner provisioner; + private List endpoints; + + @BeforeMethod + public void setUp() { + endpoints = new ArrayList<>(); + provisioner = new SidecarServicesProvisioner(endpoints, POD_NAME); + } + + @Test + public void shouldAddServiceForEachEndpoint() throws Exception { + List actualEndpoints = + asList( + new ChePluginEndpoint().name("testE1").targetPort(8080), + new ChePluginEndpoint().name("testE2").targetPort(10000)); + endpoints.addAll(actualEndpoints); + KubernetesEnvironment kubernetesEnvironment = KubernetesEnvironment.builder().build(); + + provisioner.provision(kubernetesEnvironment); + + assertEquals(kubernetesEnvironment.getServices(), toK8sServices(actualEndpoints)); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Applying of sidecar tooling failed. Kubernetes service with name '" + + CONFLICTING_SERVICE_NAME + + "' already exists in the workspace environment.") + public void shouldNotDuplicateServicesWhenThereAreConflictingEndpoints() throws Exception { + List actualEndpoints = + asList( + new ChePluginEndpoint().name(CONFLICTING_SERVICE_NAME).targetPort(8080), + new ChePluginEndpoint().name(CONFLICTING_SERVICE_NAME).targetPort(10000)); + endpoints.addAll(actualEndpoints); + KubernetesEnvironment kubernetesEnvironment = KubernetesEnvironment.builder().build(); + + provisioner.provision(kubernetesEnvironment); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Applying of sidecar tooling failed. Kubernetes service with name '" + + CONFLICTING_SERVICE_NAME + + "' already exists in the workspace environment.") + public void shouldNotDuplicateServicesWhenThereIsConflictingServiceInK8sEnv() throws Exception { + List actualEndpoints = + singletonList(new ChePluginEndpoint().name(CONFLICTING_SERVICE_NAME).targetPort(8080)); + endpoints.addAll(actualEndpoints); + KubernetesEnvironment kubernetesEnvironment = + KubernetesEnvironment.builder() + .setServices(singletonMap(CONFLICTING_SERVICE_NAME, new Service())) + .build(); + + provisioner.provision(kubernetesEnvironment); + } + + private Map toK8sServices(List endpoints) { + return endpoints + .stream() + .map(this::createService) + .collect(toMap(s -> s.getMetadata().getName(), Function.identity())); + } + + private Service createService(ChePluginEndpoint endpoint) { + ServicePort servicePort = + new ServicePortBuilder() + .withPort(endpoint.getTargetPort()) + .withProtocol("TCP") + .withNewTargetPort(endpoint.getTargetPort()) + .build(); + return new ServiceBuilder() + .withNewMetadata() + .withName(endpoint.getName()) + .endMetadata() + .withNewSpec() + .withSelector(singletonMap(CHE_ORIGINAL_NAME_LABEL, POD_NAME)) + .withPorts(singletonList(servicePort)) + .endSpec() + .build(); + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/CheContainer.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/CheContainer.java index f5f09d17a7..4d6667abcb 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/CheContainer.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/CheContainer.java @@ -51,6 +51,9 @@ public class CheContainer { } public List getEnv() { + if (env == null) { + env = new ArrayList<>(); + } return env; } @@ -65,6 +68,9 @@ public class CheContainer { } public List getCommands() { + if (commands == null) { + commands = new ArrayList<>(); + } return commands; } @@ -79,6 +85,9 @@ public class CheContainer { } public List getVolumes() { + if (volumes == null) { + volumes = new ArrayList<>(); + } return volumes; } @@ -92,6 +101,9 @@ public class CheContainer { } public List getPorts() { + if (ports == null) { + ports = new ArrayList<>(); + } return ports; } @@ -126,7 +138,6 @@ public class CheContainer { && Objects.equals(getCommands(), that.getCommands()) && Objects.equals(getVolumes(), that.getVolumes()) && Objects.equals(getPorts(), that.getPorts()) - && Objects.equals(getPorts(), that.getPorts()) && Objects.equals(getMemoryLimit(), that.getMemoryLimit()); } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePlugin.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePlugin.java index a5a5fba74c..1006af217e 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePlugin.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePlugin.java @@ -25,6 +25,9 @@ public class ChePlugin extends PluginBase { } public List getEditors() { + if (editors == null) { + editors = new ArrayList<>(); + } return editors; } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePluginEndpoint.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePluginEndpoint.java index b2ce140913..c2b7b76070 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePluginEndpoint.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/ChePluginEndpoint.java @@ -16,7 +16,14 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +/** + * Represents a network endpoint that can be accessed by clients inside or outside of workspace. + * + *

    Whether an endpoint is accessible from the outside of a workspace is defined by {@link + * #isPublic()} method. + */ public class ChePluginEndpoint { + private String name = null; @JsonProperty("public") @@ -66,6 +73,9 @@ public class ChePluginEndpoint { } public Map getAttributes() { + if (attributes == null) { + attributes = new HashMap<>(); + } return attributes; } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/Command.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/Command.java index 82f3513957..35a5b66540 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/Command.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/Command.java @@ -60,6 +60,9 @@ public class Command { } public List getCommand() { + if (command == null) { + command = new ArrayList<>(); + } return command; } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/EditorCompatibility.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/EditorCompatibility.java index d05b7ce725..57da122c0f 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/EditorCompatibility.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/EditorCompatibility.java @@ -11,6 +11,7 @@ */ package org.eclipse.che.api.workspace.server.wsplugins.model; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -38,6 +39,9 @@ public class EditorCompatibility { } public List getPlugins() { + if (plugins == null) { + plugins = new ArrayList<>(); + } return plugins; } diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/PluginBase.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/PluginBase.java index 8800fda87c..0173348a88 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/PluginBase.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/wsplugins/model/PluginBase.java @@ -69,6 +69,9 @@ public class PluginBase { } public List getContainers() { + if (containers == null) { + containers = new ArrayList<>(); + } return containers; } @@ -82,6 +85,9 @@ public class PluginBase { } public List getEndpoints() { + if (endpoints == null) { + endpoints = new ArrayList<>(); + } return endpoints; }