Skip to content

Commit

Permalink
CHE-8265: Add Workspace.Next objects applier for k8s recipe
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Garagatyi <ogaragat@redhat.com>
  • Loading branch information
Oleksandr Garagatyi committed Jun 5, 2018
1 parent f6b639d commit b6104fb
Show file tree
Hide file tree
Showing 3 changed files with 353 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -116,5 +118,9 @@ protected void configure() {

bind(KubernetesRuntimeStateCache.class).to(JpaKubernetesRuntimeStateCache.class);
bind(KubernetesMachineCache.class).to(JpaKubernetesMachineCache.class);

MapBinder<String, WorkspaceNextApplier> wsNext =
MapBinder.newMapBinder(binder(), String.class, WorkspaceNextApplier.class);
wsNext.addBinding(KubernetesEnvironment.TYPE).to(KubernetesWorkspaceNextApplier.class);
}
}
Original file line number Diff line number Diff line change
@@ -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<CheService> cheServices)
throws InfrastructureException {
if (cheServices.isEmpty()) {
return;
}
KubernetesEnvironment kubernetesEnvironment = (KubernetesEnvironment) internalEnvironment;
Map<String, Pod> 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<EnvVar> 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<Server> servers,
List<Volume> 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<String, String> attributes = machineConfig.getAttributes();
if (ramLimit > 0) {
attributes.put(MEMORY_LIMIT_ATTRIBUTE, String.valueOf(ramLimit));
} else {
attributes.put(MEMORY_LIMIT_ATTRIBUTE, defaultMachineMemorySizeAttribute);
}
}

private Map<String, ? extends org.eclipse.che.api.core.model.workspace.config.Volume>
toWorkspaceVolumes(List<Volume> volumes) {
Map<String, VolumeImpl> result = new HashMap<>();

for (Volume volume : volumes) {
result.put(volume.getName(), new VolumeImpl().withPath(volume.getMountPath()));
}
return result;
}

private Map<String, ? extends ServerConfig> toWorkspaceServers(List<Server> servers) {
HashMap<String, ServerConfigImpl> 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<io.fabric8.kubernetes.api.model.EnvVar> toK8sEnv(List<EnvVar> env) {
List<io.fabric8.kubernetes.api.model.EnvVar> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> RESOURCES_REQUEST =
ImmutableMap.of(MEMORY_KEY, MEMORY_VALUE);

@Mock Pod pod;
@Mock PodSpec podSpec;
@Mock ObjectMeta meta;
@Mock KubernetesEnvironment internalEnvironment;

KubernetesWorkspaceNextApplier applier;
List<Container> containers;
Map<String, InternalMachineConfig> 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);
}
}

0 comments on commit b6104fb

Please sign in to comment.