diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java index cc33e469780..8980fb64bf8 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java @@ -28,6 +28,7 @@ import org.eclipse.che.api.workspace.server.spi.provision.env.CheApiExternalEnvVarProvider; import org.eclipse.che.api.workspace.server.spi.provision.env.CheApiInternalEnvVarProvider; import org.eclipse.che.api.workspace.server.spi.provision.env.EnvVarProvider; +import org.eclipse.che.api.workspace.server.wsnext.WorkspaceNextApplier; import org.eclipse.che.workspace.infrastructure.docker.environment.dockerimage.DockerImageEnvironment; import org.eclipse.che.workspace.infrastructure.docker.environment.dockerimage.DockerImageEnvironmentFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.bootstrapper.KubernetesBootstrapperFactory; @@ -53,6 +54,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.server.IngressAnnotationsProvider; import org.eclipse.che.workspace.infrastructure.kubernetes.server.MultiHostIngressExternalServerExposer; import org.eclipse.che.workspace.infrastructure.kubernetes.server.SingleHostIngressExternalServerExposer; +import org.eclipse.che.workspace.infrastructure.kubernetes.wsnext.KubernetesWorkspaceNextApplier; /** @author Sergii Leshchenko */ public class KubernetesInfraModule extends AbstractModule { @@ -116,5 +118,9 @@ protected void configure() { bind(KubernetesRuntimeStateCache.class).to(JpaKubernetesRuntimeStateCache.class); bind(KubernetesMachineCache.class).to(JpaKubernetesMachineCache.class); + + MapBinder wsNext = + MapBinder.newMapBinder(binder(), String.class, WorkspaceNextApplier.class); + wsNext.addBinding(KubernetesEnvironment.TYPE).to(KubernetesWorkspaceNextApplier.class); } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsnext/KubernetesWorkspaceNextApplier.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsnext/KubernetesWorkspaceNextApplier.java new file mode 100644 index 00000000000..880b0f2f146 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsnext/KubernetesWorkspaceNextApplier.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.wsnext; + +import static java.util.Collections.singletonMap; +import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.MEMORY_LIMIT_ATTRIBUTE; + +import com.google.common.annotations.Beta; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.Quantity; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.wsnext.WorkspaceNextApplier; +import org.eclipse.che.api.workspace.server.wsnext.model.CheService; +import org.eclipse.che.api.workspace.server.wsnext.model.Container; +import org.eclipse.che.api.workspace.server.wsnext.model.EnvVar; +import org.eclipse.che.api.workspace.server.wsnext.model.ResourceRequirements; +import org.eclipse.che.api.workspace.server.wsnext.model.Server; +import org.eclipse.che.api.workspace.server.wsnext.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 Workspace.Next configuration to a kubernetes internal runtime object. + * + * @author Oleksander Garagatyi + */ +@Beta +public class KubernetesWorkspaceNextApplier implements WorkspaceNextApplier { + + private final String defaultMachineMemorySizeAttribute; + + @Inject + public KubernetesWorkspaceNextApplier( + @Named("che.workspace.default_memory_mb") long defaultMachineMemorySizeMB) { + this.defaultMachineMemorySizeAttribute = + String.valueOf(defaultMachineMemorySizeMB * 1024 * 1024); + } + + @Override + public void apply(InternalEnvironment internalEnvironment, Collection cheServices) + throws InfrastructureException { + if (cheServices.isEmpty()) { + return; + } + KubernetesEnvironment kubernetesEnvironment = (KubernetesEnvironment) internalEnvironment; + Map pods = kubernetesEnvironment.getPods(); + if (pods.size() != 1) { + throw new InfrastructureException( + "Workspace.Next configuration can be applied to a workspace with one pod only"); + } + Pod pod = pods.values().iterator().next(); + for (CheService cheService : cheServices) { + for (Container container : cheService.getSpec().getContainers()) { + io.fabric8.kubernetes.api.model.Container k8sContainer = + addContainer(pod, container.getImage(), container.getEnv(), container.getResources()); + + String machineName = Names.machineName(pod, k8sContainer); + + InternalMachineConfig machineConfig = + addMachine( + kubernetesEnvironment, machineName, container.getServers(), container.getVolumes()); + + normalizeMemory(k8sContainer, machineConfig); + } + } + } + + private io.fabric8.kubernetes.api.model.Container addContainer( + Pod toolingPod, String image, List env, ResourceRequirements resources) { + io.fabric8.kubernetes.api.model.Container container = + new ContainerBuilder() + .withImage(image) + .withName(Names.generateName("tooling")) + .withEnv(toK8sEnv(env)) + .withResources(toK8sResources(resources)) + .build(); + toolingPod.getSpec().getContainers().add(container); + return container; + } + + private io.fabric8.kubernetes.api.model.ResourceRequirements toK8sResources( + ResourceRequirements resources) { + io.fabric8.kubernetes.api.model.ResourceRequirements result = + new io.fabric8.kubernetes.api.model.ResourceRequirements(); + String memory = resources.getRequests().get("memory"); + if (memory != null) { + result.setRequests(singletonMap("memory", new Quantity(memory))); + } + return result; + } + + private InternalMachineConfig addMachine( + KubernetesEnvironment kubernetesEnvironment, + String machineName, + List servers, + List volumes) { + + InternalMachineConfig machineConfig = + new InternalMachineConfig( + null, toWorkspaceServers(servers), null, null, toWorkspaceVolumes(volumes)); + kubernetesEnvironment.getMachines().put(machineName, machineConfig); + + return machineConfig; + } + + 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, defaultMachineMemorySizeAttribute); + } + } + + 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 toWorkspaceServers(List servers) { + HashMap result = new HashMap<>(); + for (Server server : servers) { + result.put( + server.getName(), + normalizeServer( + new ServerConfigImpl( + server.getPort().toString(), + server.getProtocol(), + null, + server.getAttributes()))); + } + return result; + } + + private List toK8sEnv(List env) { + List result = new ArrayList<>(); + + 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; + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsnext/KubernetesWorkspaceNextApplierTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsnext/KubernetesWorkspaceNextApplierTest.java new file mode 100644 index 00000000000..0a378bd75db --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/wsnext/KubernetesWorkspaceNextApplierTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.wsnext; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.Quantity; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.api.workspace.server.wsnext.model.CheService; +import org.eclipse.che.api.workspace.server.wsnext.model.CheServiceSpec; +import org.eclipse.che.api.workspace.server.wsnext.model.EnvVar; +import org.eclipse.che.api.workspace.server.wsnext.model.ResourceRequirements; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** @author Alexander Garagatyi */ +@Listeners(MockitoTestNGListener.class) +public class KubernetesWorkspaceNextApplierTest { + private static final String TEST_IMAGE = "testImage/test:test"; + private static final String ENV_VAR = "PLUGINS_ENV_VAR"; + private static final String ENV_VAR_VALUE = "PLUGINS_ENV_VAR_VALUE"; + private static final String MEMORY_KEY = "memory"; + private static final String MEMORY_VALUE = "100Mi"; + private static final String POD_NAME = "pod12"; + private static final Map RESOURCES_REQUEST = + ImmutableMap.of(MEMORY_KEY, MEMORY_VALUE); + + @Mock Pod pod; + @Mock PodSpec podSpec; + @Mock ObjectMeta meta; + @Mock KubernetesEnvironment internalEnvironment; + + KubernetesWorkspaceNextApplier applier; + List containers; + Map machines; + + @BeforeMethod + public void setUp() { + applier = new KubernetesWorkspaceNextApplier(200); + machines = new HashMap<>(); + containers = new ArrayList<>(); + + when(internalEnvironment.getPods()).thenReturn(of(POD_NAME, pod)); + when(pod.getSpec()).thenReturn(podSpec); + when(podSpec.getContainers()).thenReturn(containers); + when(pod.getMetadata()).thenReturn(meta); + when(meta.getName()).thenReturn(POD_NAME); + when(internalEnvironment.getMachines()).thenReturn(machines); + } + + @Test + public void doesNothingIfServicesListIsEmpty() throws Exception { + applier.apply(internalEnvironment, emptyList()); + + verifyZeroInteractions(internalEnvironment); + } + + @Test( + expectedExceptions = InfrastructureException.class, + expectedExceptionsMessageRegExp = + "Workspace.Next configuration can be applied to a workspace with one pod only" + ) + public void throwsExceptionWhenTheNumberOfPodsIsNot1() throws Exception { + when(internalEnvironment.getPods()).thenReturn(of("pod1", pod, "pod2", pod)); + + applier.apply(internalEnvironment, singletonList(testService())); + } + + @Test + public void addToolingContainerToAPod() throws Exception { + applier.apply(internalEnvironment, singletonList(testService())); + + assertEquals(containers.size(), 1); + Container toolingContainer = containers.get(0); + verifyContainer(toolingContainer); + } + + @Test + public void canAddMultipleToolingContainersToAPodFromOneService() throws Exception { + applier.apply(internalEnvironment, singletonList(testServiceWith2Containers())); + + assertEquals(containers.size(), 2); + for (Container container : containers) { + verifyContainer(container); + } + } + + @Test + public void canAddMultipleToolingContainersToAPodFromSeveralServices() throws Exception { + applier.apply(internalEnvironment, ImmutableList.of(testService(), testService())); + + assertEquals(containers.size(), 2); + for (Container container : containers) { + verifyContainer(container); + } + } + + private CheService testService() { + CheService service = new CheService(); + CheServiceSpec cheServiceSpec = new CheServiceSpec(); + cheServiceSpec.setContainers(singletonList(testContainer())); + service.setSpec(cheServiceSpec); + return service; + } + + private CheService testServiceWith2Containers() { + CheService service = new CheService(); + CheServiceSpec cheServiceSpec = new CheServiceSpec(); + cheServiceSpec.setContainers(Arrays.asList(testContainer(), testContainer())); + service.setSpec(cheServiceSpec); + return service; + } + + private org.eclipse.che.api.workspace.server.wsnext.model.Container testContainer() { + org.eclipse.che.api.workspace.server.wsnext.model.Container cheContainer = + new org.eclipse.che.api.workspace.server.wsnext.model.Container(); + cheContainer.setImage(TEST_IMAGE); + cheContainer.setEnv(singletonList(new EnvVar().name(ENV_VAR).value(ENV_VAR_VALUE))); + cheContainer.setResources(new ResourceRequirements().requests(RESOURCES_REQUEST)); + return cheContainer; + } + + private void verifyContainer(Container toolingContainer) { + assertEquals(toolingContainer.getImage(), TEST_IMAGE); + assertEquals( + toolingContainer.getEnv(), + singletonList(new io.fabric8.kubernetes.api.model.EnvVar(ENV_VAR, ENV_VAR_VALUE, null))); + io.fabric8.kubernetes.api.model.ResourceRequirements resourceRequirements = + new io.fabric8.kubernetes.api.model.ResourceRequirements(); + resourceRequirements.setLimits(emptyMap()); + resourceRequirements.setRequests(singletonMap(MEMORY_KEY, new Quantity(MEMORY_VALUE))); + assertEquals(toolingContainer.getResources(), resourceRequirements); + } +}