diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactory.java index 1924067186..651ce492e7 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactory.java @@ -23,8 +23,6 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.extensions.Ingress; -import io.fabric8.kubernetes.client.KubernetesClientException; -import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -36,11 +34,17 @@ import org.eclipse.che.api.core.model.workspace.Warning; import org.eclipse.che.api.installer.server.InstallerRegistry; import org.eclipse.che.api.workspace.server.model.impl.WarningImpl; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; -import org.eclipse.che.api.workspace.server.spi.environment.*; +import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; +import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironmentFactory; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.api.workspace.server.spi.environment.InternalRecipe; +import org.eclipse.che.api.workspace.server.spi.environment.MachineConfigsValidator; +import org.eclipse.che.api.workspace.server.spi.environment.MemoryAttributeProvisioner; +import org.eclipse.che.api.workspace.server.spi.environment.RecipeRetriever; import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.Names; import org.eclipse.che.workspace.infrastructure.kubernetes.Warnings; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; /** @@ -51,7 +55,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; public class KubernetesEnvironmentFactory extends InternalEnvironmentFactory { - private final KubernetesClientFactory clientFactory; + private final KubernetesRecipeParser recipeParser; private final KubernetesEnvironmentValidator envValidator; private final MemoryAttributeProvisioner memoryProvisioner; @@ -60,11 +64,11 @@ public class KubernetesEnvironmentFactory InstallerRegistry installerRegistry, RecipeRetriever recipeRetriever, MachineConfigsValidator machinesValidator, - KubernetesClientFactory clientFactory, + KubernetesRecipeParser recipeParser, KubernetesEnvironmentValidator envValidator, MemoryAttributeProvisioner memoryProvisioner) { super(installerRegistry, recipeRetriever, machinesValidator); - this.clientFactory = clientFactory; + this.recipeParser = recipeParser; this.envValidator = envValidator; this.memoryProvisioner = memoryProvisioner; } @@ -80,37 +84,8 @@ public class KubernetesEnvironmentFactory if (sourceWarnings != null) { warnings.addAll(sourceWarnings); } - String content = recipe.getContent(); - String contentType = recipe.getContentType(); - checkNotNull(contentType, "Kubernetes Recipe content type should not be null"); - switch (contentType) { - case "application/x-yaml": - case "text/yaml": - case "text/x-yaml": - break; - default: - throw new ValidationException( - "Provided environment recipe content type '" - + contentType - + "' is unsupported. Supported values are: " - + "application/x-yaml, text/yaml, text/x-yaml"); - } - - final List list; - try { - // Load list of Kubernetes objects into java List. - list = clientFactory.create().load(new ByteArrayInputStream(content.getBytes())).get(); - } catch (KubernetesClientException e) { - // KubernetesClient wraps the error when a JsonMappingException occurs so we need the cause - String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); - if (message.contains("\n")) { - // Clean up message if it comes from JsonMappingException. Format is e.g. - // `No resource type found for:v1#Route1\n at [...]` - message = message.split("\\n", 2)[0]; - } - throw new ValidationException(format("Could not parse Kubernetes recipe: %s", message)); - } + final List recipeObjects = recipeParser.parse(recipe); Map pods = new HashMap<>(); Map deployments = new HashMap<>(); @@ -119,14 +94,11 @@ public class KubernetesEnvironmentFactory Map pvcs = new HashMap<>(); Map secrets = new HashMap<>(); boolean isAnyIngressPresent = false; - for (HasMetadata object : list) { + for (HasMetadata object : recipeObjects) { checkNotNull(object.getKind(), "Environment contains object without specified kind field"); checkNotNull(object.getMetadata(), "%s metadata must not be null", object.getKind()); checkNotNull(object.getMetadata().getName(), "%s name must not be null", object.getKind()); - // needed because Che master namespace is set by K8s API during list loading - object.getMetadata().setNamespace(null); - if (object instanceof Pod) { Pod pod = (Pod) object; pods.put(pod.getMetadata().getName(), pod); @@ -161,8 +133,6 @@ public class KubernetesEnvironmentFactory Warnings.INGRESSES_IGNORED_WARNING_CODE, Warnings.INGRESSES_IGNORED_WARNING_MESSAGE)); } - addRamAttributes(machines, pods.values()); - KubernetesEnvironment k8sEnv = KubernetesEnvironment.builder() .setInternalRecipe(recipe) @@ -178,14 +148,16 @@ public class KubernetesEnvironmentFactory .setConfigMaps(configMaps) .build(); + addRamAttributes(k8sEnv.getMachines(), k8sEnv.getPodsData().values()); + envValidator.validate(k8sEnv); return k8sEnv; } @VisibleForTesting - void addRamAttributes(Map machines, Collection pods) { - for (Pod pod : pods) { + void addRamAttributes(Map machines, Collection pods) { + for (PodData pod : pods) { for (Container container : pod.getSpec().getContainers()) { final String machineName = Names.machineName(pod, container); InternalMachineConfig machineConfig; diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesRecipeParser.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesRecipeParser.java new file mode 100644 index 0000000000..f23eb86d9c --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesRecipeParser.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.environment; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +import com.google.common.collect.ImmutableSet; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClientException; +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import org.eclipse.che.api.core.ValidationException; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.environment.InternalRecipe; +import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; + +/** + * Parses Kubernetes objects from recipe. + * + *

Note that this class can also parse OpenShift specific objects. + * + * @author Sergii Leshchenko + */ +public class KubernetesRecipeParser { + + private static final Set SUPPORTED_CONTENT_TYPES = + ImmutableSet.of("application/x-yaml", "text/yaml", "text/x-yaml"); + + private final KubernetesClientFactory clientFactory; + + @Inject + public KubernetesRecipeParser(KubernetesClientFactory clientFactory) { + this.clientFactory = clientFactory; + } + + /** + * Parses Kubernetes objects from recipe. + * + * @param recipe that contains objects to parse + * @return parsed objects + * @throws IllegalArgumentException if recipe content is null + * @throws IllegalArgumentException if recipe content type is null + * @throws ValidationException if recipe content has broken format + * @throws ValidationException if recipe content has unrecognized objects + * @throws InfrastructureException when exception occurred during kubernetes client creation + */ + public List parse(InternalRecipe recipe) + throws ValidationException, InfrastructureException { + String content = recipe.getContent(); + String contentType = recipe.getContentType(); + checkNotNull(contentType, "Recipe content type must not be null"); + + if (!SUPPORTED_CONTENT_TYPES.contains(contentType)) { + throw new ValidationException( + format( + "Provided environment recipe content type '%s' is unsupported. Supported values are: %s", + contentType, String.join(", ", SUPPORTED_CONTENT_TYPES))); + } + + return parse(content); + } + + /** + * Parses Kubernetes objects from recipe content. + * + * @param recipeContent recipe content that should be parsed + * @return parsed objects + * @throws IllegalArgumentException if recipe content is null + * @throws ValidationException if recipe content has broken format + * @throws ValidationException if recipe content has unrecognized objects + * @throws InfrastructureException when exception occurred during kubernetes client creation + */ + public List parse(String recipeContent) + throws ValidationException, InfrastructureException { + checkNotNull(recipeContent, "Recipe content type must not be null"); + + try { + // Behavior: + // - If `content` is a single object like Deployment, load().get() will get the object in that + // list + // - If `content` is a Kubernetes List, load().get() will get the objects in that list + // - If `content` is an OpenShift template, load().get() will get the objects in the template + // with parameters substituted (e.g. with default values). + List parsed = + clientFactory.create().load(new ByteArrayInputStream(recipeContent.getBytes())).get(); + + // needed because Che master namespace is set by K8s API during list loading + parsed + .stream() + .filter(o -> o.getMetadata() != null) + .forEach(o -> o.getMetadata().setNamespace(null)); + + return parsed; + } catch (KubernetesClientException e) { + // KubernetesClient wraps the error when a JsonMappingException occurs so we need the cause + String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); + if (message.contains("\n")) { + // Clean up message if it comes from JsonMappingException. Format is e.g. + // `No resource type found for:v1#Route1\n at [...]` + message = message.split("\\n", 2)[0]; + } + throw new ValidationException(format("Could not parse Kubernetes recipe: %s", message)); + } + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactoryTest.java index f921b040e4..2c8513a0ac 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/environment/KubernetesEnvironmentFactoryTest.java @@ -53,10 +53,6 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.extensions.IngressBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; -import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -66,7 +62,7 @@ import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; import org.eclipse.che.api.workspace.server.spi.environment.InternalRecipe; import org.eclipse.che.api.workspace.server.spi.environment.MemoryAttributeProvisioner; -import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; @@ -81,37 +77,27 @@ import org.testng.annotations.Test; @Listeners(MockitoTestNGListener.class) public class KubernetesEnvironmentFactoryTest { - private static final String YAML_RECIPE = "application/x-yaml"; public static final String MACHINE_NAME_1 = "machine1"; public static final String MACHINE_NAME_2 = "machine2"; private KubernetesEnvironmentFactory k8sEnvFactory; - @Mock private KubernetesClientFactory clientFactory; @Mock private KubernetesEnvironmentValidator k8sEnvValidator; - @Mock private KubernetesClient client; @Mock private InternalEnvironment internalEnvironment; @Mock private InternalRecipe internalRecipe; @Mock private InternalMachineConfig machineConfig1; @Mock private InternalMachineConfig machineConfig2; @Mock private MemoryAttributeProvisioner memoryProvisioner; + @Mock private KubernetesRecipeParser k8sRecipeParser; private Map machines; - @Mock - private ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable - loadedRecipe; - @BeforeMethod public void setup() throws Exception { k8sEnvFactory = new KubernetesEnvironmentFactory( - null, null, null, clientFactory, k8sEnvValidator, memoryProvisioner); - when(clientFactory.create()).thenReturn(client); - when(client.load(any(InputStream.class))).thenReturn(loadedRecipe); + null, null, null, k8sRecipeParser, k8sEnvValidator, memoryProvisioner); lenient().when(internalEnvironment.getRecipe()).thenReturn(internalRecipe); - when(internalRecipe.getContentType()).thenReturn(YAML_RECIPE); - when(internalRecipe.getContent()).thenReturn("recipe content"); machines = ImmutableMap.of(MACHINE_NAME_1, machineConfig1, MACHINE_NAME_2, machineConfig2); } @@ -122,7 +108,7 @@ public class KubernetesEnvironmentFactoryTest { new ServiceBuilder().withNewMetadata().withName("service1").endMetadata().build(); Service service2 = new ServiceBuilder().withNewMetadata().withName("service2").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(asList(service1, service2)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(asList(service1, service2)); // when KubernetesEnvironment k8sEnv = k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -140,7 +126,7 @@ public class KubernetesEnvironmentFactoryTest { new PersistentVolumeClaimBuilder().withNewMetadata().withName("pvc1").endMetadata().build(); PersistentVolumeClaim pvc2 = new PersistentVolumeClaimBuilder().withNewMetadata().withName("pvc2").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(asList(pvc1, pvc2)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(asList(pvc1, pvc2)); // when KubernetesEnvironment k8sEnv = k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -153,7 +139,7 @@ public class KubernetesEnvironmentFactoryTest { @Test public void ignoreIgressesWhenRecipeContainsThem() throws Exception { - when(loadedRecipe.get()) + when(k8sRecipeParser.parse(any(InternalRecipe.class))) .thenReturn( asList( new IngressBuilder().withNewMetadata().withName("ingress1").endMetadata().build(), @@ -173,7 +159,7 @@ public class KubernetesEnvironmentFactoryTest { public void addSecretsWhenRecipeContainsThem() throws Exception { Secret secret = new SecretBuilder().withNewMetadata().withName("test-secret").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(singletonList(secret)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(secret)); final KubernetesEnvironment parsed = k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -188,7 +174,7 @@ public class KubernetesEnvironmentFactoryTest { public void addConfigMapsWhenRecipeContainsThem() throws Exception { ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("test-configmap").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(singletonList(configMap)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(configMap)); final KubernetesEnvironment parsed = k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -207,7 +193,7 @@ public class KubernetesEnvironmentFactoryTest { .endMetadata() .withSpec(new PodSpec()) .build(); - when(loadedRecipe.get()).thenReturn(singletonList(pod)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(pod)); // when KubernetesEnvironment k8sEnv = k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -242,7 +228,7 @@ public class KubernetesEnvironmentFactoryTest { .endSpec() .build(); - when(loadedRecipe.get()).thenReturn(singletonList(deployment)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(deployment)); // when final KubernetesEnvironment k8sEnv = @@ -286,7 +272,7 @@ public class KubernetesEnvironmentFactoryTest { .withNewSpec() .endSpec() .build(); - when(loadedRecipe.get()).thenReturn(asList(deployment, pod)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(asList(deployment, pod)); // when final KubernetesEnvironment k8sEnv = @@ -305,7 +291,8 @@ public class KubernetesEnvironmentFactoryTest { @Test(expectedExceptions = ValidationException.class) public void exceptionOnRecipeLoadError() throws Exception { - when(loadedRecipe.get()).thenThrow(new KubernetesClientException("Could not parse recipe")); + when(k8sRecipeParser.parse(any(InternalRecipe.class))) + .thenThrow(new ValidationException("Could not parse recipe")); k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -316,7 +303,7 @@ public class KubernetesEnvironmentFactoryTest { public void exceptionOnObjectWithNoKindSpecified() throws Exception { HasMetadata object = mock(HasMetadata.class); when(object.getKind()).thenReturn(null); - when(loadedRecipe.get()).thenReturn(singletonList(object)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(object)); k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -328,7 +315,7 @@ public class KubernetesEnvironmentFactoryTest { HasMetadata object = mock(HasMetadata.class); when(object.getKind()).thenReturn("MyObject"); when(object.getMetadata()).thenReturn(null); - when(loadedRecipe.get()).thenReturn(singletonList(object)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(object)); k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -340,7 +327,7 @@ public class KubernetesEnvironmentFactoryTest { HasMetadata object = mock(HasMetadata.class); when(object.getKind()).thenReturn("MyObject"); when(object.getMetadata()).thenReturn(new ObjectMetaBuilder().withName(null).build()); - when(loadedRecipe.get()).thenReturn(singletonList(object)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(object)); k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -353,10 +340,10 @@ public class KubernetesEnvironmentFactoryTest { final long secondMachineRamRequest = 512; when(machineConfig1.getAttributes()).thenReturn(new HashMap<>()); when(machineConfig2.getAttributes()).thenReturn(new HashMap<>()); - final Set pods = + final Set pods = ImmutableSet.of( - mockPod(MACHINE_NAME_1, firstMachineRamLimit, firstMachineRamRequest), - mockPod(MACHINE_NAME_2, secondMachineRamLimit, secondMachineRamRequest)); + createPodData(MACHINE_NAME_1, firstMachineRamLimit, firstMachineRamRequest), + createPodData(MACHINE_NAME_2, secondMachineRamLimit, secondMachineRamRequest)); k8sEnvFactory.addRamAttributes(machines, pods); @@ -366,17 +353,14 @@ public class KubernetesEnvironmentFactoryTest { .provision(eq(machineConfig2), eq(secondMachineRamLimit), eq(secondMachineRamRequest)); } - private static Pod mockPod(String machineName, long ramLimit, long ramRequest) { + private static PodData createPodData(String machineName, long ramLimit, long ramRequest) { final String containerName = "container_" + machineName; final Container containerMock = mock(Container.class); final ResourceRequirements resourcesMock = mock(ResourceRequirements.class); final Quantity limitQuantityMock = mock(Quantity.class); final Quantity requestQuantityMock = mock(Quantity.class); - final Pod podMock = mock(Pod.class); final PodSpec specMock = mock(PodSpec.class); final ObjectMeta metadataMock = mock(ObjectMeta.class); - when(podMock.getSpec()).thenReturn(specMock); - when(podMock.getMetadata()).thenReturn(metadataMock); when(limitQuantityMock.getAmount()).thenReturn(String.valueOf(ramLimit)); when(requestQuantityMock.getAmount()).thenReturn(String.valueOf(ramRequest)); when(resourcesMock.getLimits()).thenReturn(ImmutableMap.of("memory", limitQuantityMock)); @@ -387,6 +371,6 @@ public class KubernetesEnvironmentFactoryTest { .thenReturn( ImmutableMap.of(format(MACHINE_NAME_ANNOTATION_FMT, containerName), machineName)); when(specMock.getContainers()).thenReturn(ImmutableList.of(containerMock)); - return podMock; + return new PodData(specMock, metadataMock); } } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactory.java index 0669919932..c71929cffc 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactory.java @@ -22,10 +22,8 @@ import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.openshift.api.model.DeploymentConfig; import io.fabric8.openshift.api.model.Route; -import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -36,11 +34,18 @@ import org.eclipse.che.api.core.ValidationException; import org.eclipse.che.api.core.model.workspace.Warning; import org.eclipse.che.api.installer.server.InstallerRegistry; import org.eclipse.che.api.workspace.server.spi.InfrastructureException; -import org.eclipse.che.api.workspace.server.spi.environment.*; +import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironment; +import org.eclipse.che.api.workspace.server.spi.environment.InternalEnvironmentFactory; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.api.workspace.server.spi.environment.InternalRecipe; +import org.eclipse.che.api.workspace.server.spi.environment.MachineConfigsValidator; +import org.eclipse.che.api.workspace.server.spi.environment.MemoryAttributeProvisioner; +import org.eclipse.che.api.workspace.server.spi.environment.RecipeRetriever; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.workspace.infrastructure.kubernetes.Names; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesRecipeParser; import org.eclipse.che.workspace.infrastructure.kubernetes.util.Containers; -import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; /** * Parses {@link InternalEnvironment} into {@link OpenShiftEnvironment}. @@ -49,8 +54,8 @@ import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory */ public class OpenShiftEnvironmentFactory extends InternalEnvironmentFactory { - private final OpenShiftClientFactory clientFactory; private final OpenShiftEnvironmentValidator envValidator; + private final KubernetesRecipeParser k8sObjectsParser; private final MemoryAttributeProvisioner memoryProvisioner; @Inject @@ -58,12 +63,12 @@ public class OpenShiftEnvironmentFactory extends InternalEnvironmentFactory list; - try { - // Behavior: - // - If `content` is a Kubernetes List, load().get() will get the objects in that list - // - If `content` is an OpenShift template, load().get() will get the objects in the template - // with parameters substituted (e.g. with default values). - list = clientFactory.create().load(new ByteArrayInputStream(content.getBytes())).get(); - } catch (KubernetesClientException e) { - // KubernetesClient wraps the error when a JsonMappingException occurs so we need the cause - String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); - if (message.contains("\n")) { - // Clean up message if it comes from JsonMappingException. Format is e.g. - // `No resource type found for:v1#Route1\n at [...]` - message = message.split("\\n", 2)[0]; - } - throw new ValidationException(format("Could not parse OpenShift recipe: %s", message)); - } + final List list = k8sObjectsParser.parse(recipe); Map pods = new HashMap<>(); Map deployments = new HashMap<>(); @@ -125,9 +98,6 @@ public class OpenShiftEnvironmentFactory extends InternalEnvironmentFactory machines, Collection pods) { - for (Pod pod : pods) { + void addRamAttributes(Map machines, Collection pods) { + for (PodData pod : pods) { for (Container container : pod.getSpec().getContainers()) { final String machineName = Names.machineName(pod, container); InternalMachineConfig machineConfig; diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactoryTest.java index 0e8416418a..fbfca3123a 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/environment/OpenShiftEnvironmentFactoryTest.java @@ -48,12 +48,8 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; import io.fabric8.openshift.api.model.Route; import io.fabric8.openshift.api.model.RouteBuilder; -import io.fabric8.openshift.client.OpenShiftClient; -import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -62,7 +58,8 @@ import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfi import org.eclipse.che.api.workspace.server.spi.environment.InternalRecipe; import org.eclipse.che.api.workspace.server.spi.environment.MemoryAttributeProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; -import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesRecipeParser; import org.mockito.Mock; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; @@ -77,36 +74,26 @@ import org.testng.annotations.Test; @Listeners(MockitoTestNGListener.class) public class OpenShiftEnvironmentFactoryTest { - private static final String YAML_RECIPE = "application/x-yaml"; private static final long BYTES_IN_MB = 1024 * 1024; private static final String MACHINE_NAME_1 = "machine1"; private static final String MACHINE_NAME_2 = "machine2"; private OpenShiftEnvironmentFactory osEnvFactory; - @Mock private OpenShiftClientFactory clientFactory; @Mock private OpenShiftEnvironmentValidator openShiftEnvValidator; - @Mock private OpenShiftClient client; @Mock private InternalRecipe internalRecipe; @Mock private InternalMachineConfig machineConfig1; @Mock private InternalMachineConfig machineConfig2; @Mock private MemoryAttributeProvisioner memoryProvisioner; + @Mock private KubernetesRecipeParser k8sRecipeParser; private Map machines; - @Mock - private ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable - loadedRecipe; - @BeforeMethod public void setup() throws Exception { osEnvFactory = new OpenShiftEnvironmentFactory( - null, null, null, clientFactory, openShiftEnvValidator, memoryProvisioner); - when(clientFactory.create()).thenReturn(client); - when(client.load(any(InputStream.class))).thenReturn(loadedRecipe); - when(internalRecipe.getContentType()).thenReturn(YAML_RECIPE); - when(internalRecipe.getContent()).thenReturn("recipe content"); + null, null, null, openShiftEnvValidator, k8sRecipeParser, memoryProvisioner); machines = ImmutableMap.of(MACHINE_NAME_1, machineConfig1, MACHINE_NAME_2, machineConfig2); } @@ -117,7 +104,7 @@ public class OpenShiftEnvironmentFactoryTest { new ServiceBuilder().withNewMetadata().withName("service1").endMetadata().build(); Service service2 = new ServiceBuilder().withNewMetadata().withName("service2").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(asList(service1, service2)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(asList(service1, service2)); // when KubernetesEnvironment osEnv = osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -135,7 +122,7 @@ public class OpenShiftEnvironmentFactoryTest { new PersistentVolumeClaimBuilder().withNewMetadata().withName("pvc1").endMetadata().build(); PersistentVolumeClaim pvc2 = new PersistentVolumeClaimBuilder().withNewMetadata().withName("pvc2").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(asList(pvc1, pvc2)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(asList(pvc1, pvc2)); // when OpenShiftEnvironment osEnv = osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -149,7 +136,7 @@ public class OpenShiftEnvironmentFactoryTest { @Test public void addRoutesWhenRecipeContainsThem() throws Exception { Route route = new RouteBuilder().withNewMetadata().withName("test-route").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(singletonList(route)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(route)); final OpenShiftEnvironment parsed = osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -164,7 +151,7 @@ public class OpenShiftEnvironmentFactoryTest { public void addSecretsWhenRecipeContainsThem() throws Exception { Secret secret = new SecretBuilder().withNewMetadata().withName("test-secret").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(singletonList(secret)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(secret)); final OpenShiftEnvironment parsed = osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -179,7 +166,7 @@ public class OpenShiftEnvironmentFactoryTest { public void addConfigMapsWhenRecipeContainsThem() throws Exception { ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("test-configmap").endMetadata().build(); - when(loadedRecipe.get()).thenReturn(singletonList(configMap)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(configMap)); final KubernetesEnvironment parsed = osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -200,7 +187,7 @@ public class OpenShiftEnvironmentFactoryTest { .endMetadata() .withSpec(new PodSpec()) .build(); - when(loadedRecipe.get()).thenReturn(singletonList(pod)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(pod)); // when KubernetesEnvironment osEnv = osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); @@ -235,7 +222,7 @@ public class OpenShiftEnvironmentFactoryTest { .endSpec() .build(); - when(loadedRecipe.get()).thenReturn(singletonList(deployment)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(deployment)); // when final KubernetesEnvironment osEnv = @@ -279,7 +266,7 @@ public class OpenShiftEnvironmentFactoryTest { .withNewSpec() .endSpec() .build(); - when(loadedRecipe.get()).thenReturn(asList(deployment, pod)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(asList(deployment, pod)); // when final KubernetesEnvironment osEnv = @@ -298,7 +285,8 @@ public class OpenShiftEnvironmentFactoryTest { @Test(expectedExceptions = ValidationException.class) public void exceptionOnRecipeLoadError() throws Exception { - when(loadedRecipe.get()).thenThrow(new KubernetesClientException("Could not parse recipe")); + when(k8sRecipeParser.parse(any(InternalRecipe.class))) + .thenThrow(new ValidationException("Could not parse recipe")); osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -309,7 +297,7 @@ public class OpenShiftEnvironmentFactoryTest { public void exceptionOnObjectWithNoKindSpecified() throws Exception { HasMetadata object = mock(HasMetadata.class); when(object.getKind()).thenReturn(null); - when(loadedRecipe.get()).thenReturn(singletonList(object)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(object)); osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -321,7 +309,7 @@ public class OpenShiftEnvironmentFactoryTest { HasMetadata object = mock(HasMetadata.class); when(object.getKind()).thenReturn("MyObject"); when(object.getMetadata()).thenReturn(null); - when(loadedRecipe.get()).thenReturn(singletonList(object)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(object)); osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -333,7 +321,7 @@ public class OpenShiftEnvironmentFactoryTest { HasMetadata object = mock(HasMetadata.class); when(object.getKind()).thenReturn("MyObject"); when(object.getMetadata()).thenReturn(new ObjectMetaBuilder().withName(null).build()); - when(loadedRecipe.get()).thenReturn(singletonList(object)); + when(k8sRecipeParser.parse(any(InternalRecipe.class))).thenReturn(singletonList(object)); osEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList()); } @@ -346,10 +334,10 @@ public class OpenShiftEnvironmentFactoryTest { final long secondMachineRamRequest = 512 * BYTES_IN_MB; when(machineConfig1.getAttributes()).thenReturn(new HashMap<>()); when(machineConfig2.getAttributes()).thenReturn(new HashMap<>()); - final Set pods = + final Set pods = ImmutableSet.of( - mockPod(MACHINE_NAME_1, firstMachineRamLimit, firstMachineRamRequest), - mockPod(MACHINE_NAME_2, secondMachineRamLimit, secondMachineRamRequest)); + createPodData(MACHINE_NAME_1, firstMachineRamLimit, firstMachineRamRequest), + createPodData(MACHINE_NAME_2, secondMachineRamLimit, secondMachineRamRequest)); osEnvFactory.addRamAttributes(machines, pods); @@ -360,7 +348,7 @@ public class OpenShiftEnvironmentFactoryTest { } /** If provided {@code ramLimit} is {@code null} ram limit won't be set in POD */ - private static Pod mockPod(String machineName, Long ramLimit, Long ramRequest) { + private static PodData createPodData(String machineName, Long ramLimit, Long ramRequest) { final String containerName = "container_" + machineName; final Container containerMock = mock(Container.class); final Pod podMock = mock(Pod.class); @@ -385,6 +373,6 @@ public class OpenShiftEnvironmentFactoryTest { .thenReturn( ImmutableMap.of(format(MACHINE_NAME_ANNOTATION_FMT, containerName), machineName)); when(specMock.getContainers()).thenReturn(ImmutableList.of(containerMock)); - return podMock; + return new PodData(specMock, metadataMock); } } diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileRecipeFormatException.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileRecipeFormatException.java index 1f8c6c6c8e..4a8ed4ad44 100644 --- a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileRecipeFormatException.java +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/DevfileRecipeFormatException.java @@ -19,4 +19,8 @@ public class DevfileRecipeFormatException extends DevfileException { public DevfileRecipeFormatException(String message) { super(message); } + + public DevfileRecipeFormatException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplier.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplier.java index f04e682287..23e4969dd5 100644 --- a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplier.java +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplier.java @@ -15,6 +15,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.String.format; import static java.util.Collections.emptyMap; +import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toList; import static org.eclipse.che.api.core.model.workspace.config.Command.MACHINE_NAME_ATTRIBUTE; import static org.eclipse.che.api.devfile.server.Constants.KUBERNETES_TOOL_TYPE; @@ -22,14 +23,16 @@ import static org.eclipse.che.api.devfile.server.Constants.OPENSHIFT_TOOL_TYPE; import com.google.common.annotations.VisibleForTesting; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.utils.Serialization; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import javax.inject.Inject; import org.eclipse.che.api.core.model.workspace.WorkspaceConfig; import org.eclipse.che.api.core.model.workspace.config.Command; import org.eclipse.che.api.devfile.model.Tool; @@ -43,6 +46,7 @@ import org.eclipse.che.api.workspace.server.model.impl.RecipeImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.workspace.infrastructure.kubernetes.Names; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesRecipeParser; /** * Applies changes on workspace config according to the specified kubernetes/openshift tool. @@ -53,6 +57,13 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier @VisibleForTesting static final String YAML_CONTENT_TYPE = "application/x-yaml"; + private final KubernetesRecipeParser objectsParser; + + @Inject + public KubernetesToolToWorkspaceApplier(KubernetesRecipeParser objectsParser) { + this.objectsParser = objectsParser; + } + /** * Applies changes on workspace config according to the specified kubernetes/openshift tool. * @@ -79,10 +90,10 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier String recipeFileContent = retrieveContent(k8sTool, contentProvider); - final KubernetesList list = unmarshal(k8sTool, recipeFileContent); + List list = unmarshal(k8sTool, recipeFileContent); if (!k8sTool.getSelector().isEmpty()) { - list.setItems(filter(list, k8sTool.getSelector())); + list = filter(list, k8sTool.getSelector()); } estimateCommandsMachineName(workspaceConfig, k8sTool, list); @@ -128,8 +139,16 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier return recipeFileContent; } - private List filter(KubernetesList list, Map selector) { - return list.getItems().stream().filter(item -> matchLabels(item, selector)).collect(toList()); + /** + * Returns a lists consisting of the elements of the specified list that match the given selector. + * + * @param list list to filter + * @param selector selector that should be matched with objects' labels + */ + private List filter(List list, Map selector) { + return list.stream() + .filter(item -> matchLabels(item, selector)) + .collect(toCollection(ArrayList::new)); } /** @@ -160,7 +179,7 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier *

Machine name will be set only if the specified recipe objects has the only one container. */ private void estimateCommandsMachineName( - WorkspaceConfig workspaceConfig, Tool tool, KubernetesList recipeObjects) { + WorkspaceConfig workspaceConfig, Tool tool, List recipeObjects) { List toolCommands = workspaceConfig .getCommands() @@ -175,7 +194,6 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier } List pods = recipeObjects - .getItems() .stream() .filter(hasMetadata -> hasMetadata instanceof Pod) .map(hasMetadata -> (Pod) hasMetadata) @@ -192,11 +210,11 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier toolCommands.forEach(c -> c.getAttributes().put(MACHINE_NAME_ATTRIBUTE, machineName)); } - private KubernetesList unmarshal(Tool tool, String recipeContent) + private List unmarshal(Tool tool, String recipeContent) throws DevfileRecipeFormatException { try { - return Serialization.unmarshal(recipeContent, KubernetesList.class); - } catch (KubernetesClientException e) { + return objectsParser.parse(recipeContent); + } catch (Exception e) { throw new DevfileRecipeFormatException( format( "Error occurred during parsing list from file %s for tool '%s': %s", @@ -204,14 +222,15 @@ public class KubernetesToolToWorkspaceApplier implements ToolToWorkspaceApplier } } - private String asYaml(Tool tool, KubernetesList list) throws DevfileRecipeFormatException { + private String asYaml(Tool tool, List list) throws DevfileRecipeFormatException { try { - return Serialization.asYaml(list); + return Serialization.asYaml(new KubernetesListBuilder().withItems(list).build()); } catch (KubernetesClientException e) { throw new DevfileRecipeFormatException( format( "Unable to deserialize specified local file content for tool '%s'. Error: %s", - tool.getName(), e.getMessage())); + tool.getName(), e.getMessage()), + e); } } } diff --git a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/exception/DevfileException.java b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/exception/DevfileException.java index 91e8ed2d21..a7f66c127f 100644 --- a/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/exception/DevfileException.java +++ b/wsmaster/che-core-api-devfile/src/main/java/org/eclipse/che/api/devfile/server/exception/DevfileException.java @@ -14,7 +14,7 @@ package org.eclipse.che.api.devfile.server.exception; /** Describes general devfile exception. */ public class DevfileException extends Exception { - public DevfileException(String message, Exception cause) { + public DevfileException(String message, Throwable cause) { super(message, cause); } diff --git a/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplierTest.java b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplierTest.java index b72cb5bc56..0819f018d4 100644 --- a/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplierTest.java +++ b/wsmaster/che-core-api-devfile/src/test/java/org/eclipse/che/api/devfile/server/convert/tool/kubernetes/KubernetesToolToWorkspaceApplierTest.java @@ -18,16 +18,21 @@ import static org.eclipse.che.api.devfile.server.Constants.KUBERNETES_TOOL_TYPE; import static org.eclipse.che.api.devfile.server.Constants.OPENSHIFT_TOOL_TYPE; import static org.eclipse.che.api.devfile.server.Constants.TOOL_NAME_COMMAND_ATTRIBUTE; import static org.eclipse.che.api.devfile.server.convert.tool.kubernetes.KubernetesToolToWorkspaceApplier.YAML_CONTENT_TYPE; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.che.api.core.ValidationException; import org.eclipse.che.api.devfile.model.Tool; import org.eclipse.che.api.devfile.server.FileContentProvider.FetchNotSupportedProvider; import org.eclipse.che.api.devfile.server.exception.DevfileException; @@ -35,11 +40,16 @@ import org.eclipse.che.api.workspace.server.model.impl.CommandImpl; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; import org.eclipse.che.api.workspace.server.model.impl.RecipeImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesRecipeParser; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; import org.testng.annotations.Test; import org.testng.reporters.Files; /** @author Sergii Leshchenko */ +@Listeners(MockitoTestNGListener.class) public class KubernetesToolToWorkspaceApplierTest { public static final String LOCAL_FILENAME = "local.yaml"; @@ -48,10 +58,11 @@ public class KubernetesToolToWorkspaceApplierTest { private WorkspaceConfigImpl workspaceConfig; private KubernetesToolToWorkspaceApplier applier; + @Mock private KubernetesRecipeParser k8sRecipeParser; @BeforeMethod public void setUp() { - applier = new KubernetesToolToWorkspaceApplier(); + applier = new KubernetesToolToWorkspaceApplier(k8sRecipeParser); workspaceConfig = new WorkspaceConfigImpl(); } @@ -86,6 +97,7 @@ public class KubernetesToolToWorkspaceApplierTest { + "': .*") public void shouldThrowExceptionWhenRecipeContentIsNotAValidYaml() throws Exception { // given + doThrow(new ValidationException("non valid")).when(k8sRecipeParser).parse(anyString()); Tool tool = new Tool().withType(KUBERNETES_TOOL_TYPE).withLocal(LOCAL_FILENAME).withName(TOOL_NAME); @@ -116,6 +128,7 @@ public class KubernetesToolToWorkspaceApplierTest { throws Exception { // given String yamlRecipeContent = getResource("petclinic.yaml"); + doReturn(toK8SList(yamlRecipeContent).getItems()).when(k8sRecipeParser).parse(anyString()); Tool tool = new Tool() .withType(KUBERNETES_TOOL_TYPE) @@ -135,12 +148,17 @@ public class KubernetesToolToWorkspaceApplierTest { assertNotNull(recipe); assertEquals(recipe.getType(), KUBERNETES_TOOL_TYPE); assertEquals(recipe.getContentType(), YAML_CONTENT_TYPE); - assertEquals(toK8SList(recipe.getContent()), toK8SList(yamlRecipeContent)); + + // it is expected that applier wrap original recipes objects in new Kubernetes list + KubernetesList expectedKubernetesList = + new KubernetesListBuilder().withItems(toK8SList(yamlRecipeContent).getItems()).build(); + assertEquals(toK8SList(recipe.getContent()).getItems(), expectedKubernetesList.getItems()); } @Test public void shouldUseLocalContentAsRecipeIfPresent() throws Exception { String yamlRecipeContent = getResource("petclinic.yaml"); + doReturn(toK8SList(yamlRecipeContent).getItems()).when(k8sRecipeParser).parse(anyString()); Tool tool = new Tool() .withType(KUBERNETES_TOOL_TYPE) @@ -159,7 +177,11 @@ public class KubernetesToolToWorkspaceApplierTest { assertNotNull(recipe); assertEquals(recipe.getType(), KUBERNETES_TOOL_TYPE); assertEquals(recipe.getContentType(), YAML_CONTENT_TYPE); - assertEquals(toK8SList(recipe.getContent()), toK8SList(yamlRecipeContent)); + + // it is expected that applier wrap original recipes objects in new Kubernetes list + KubernetesList expectedKubernetesList = + new KubernetesListBuilder().withItems(toK8SList(yamlRecipeContent).getItems()).build(); + assertEquals(toK8SList(recipe.getContent()).getItems(), expectedKubernetesList.getItems()); } @Test @@ -167,6 +189,7 @@ public class KubernetesToolToWorkspaceApplierTest { throws Exception { // given String yamlRecipeContent = getResource("petclinic.yaml"); + doReturn(toK8SList(yamlRecipeContent).getItems()).when(k8sRecipeParser).parse(anyString()); Tool tool = new Tool() .withType(OPENSHIFT_TOOL_TYPE) @@ -186,7 +209,11 @@ public class KubernetesToolToWorkspaceApplierTest { assertNotNull(recipe); assertEquals(recipe.getType(), OPENSHIFT_TOOL_TYPE); assertEquals(recipe.getContentType(), YAML_CONTENT_TYPE); - assertEquals(toK8SList(recipe.getContent()), toK8SList(yamlRecipeContent)); + + // it is expected that applier wrap original recipes objects in new Kubernetes list + KubernetesList expectedKubernetesList = + new KubernetesListBuilder().withItems(toK8SList(yamlRecipeContent).getItems()).build(); + assertEquals(toK8SList(recipe.getContent()).getItems(), expectedKubernetesList.getItems()); } @Test @@ -201,6 +228,7 @@ public class KubernetesToolToWorkspaceApplierTest { .withLocal(LOCAL_FILENAME) .withName(TOOL_NAME) .withSelector(selector); + doReturn(toK8SList(yamlRecipeContent).getItems()).when(k8sRecipeParser).parse(anyString()); // when applier.apply(workspaceConfig, tool, s -> yamlRecipeContent); @@ -224,6 +252,7 @@ public class KubernetesToolToWorkspaceApplierTest { throws Exception { // given String yamlRecipeContent = getResource("petclinic.yaml"); + doReturn(toK8SList(yamlRecipeContent).getItems()).when(k8sRecipeParser).parse(anyString()); final Map selector = singletonMap("app.kubernetes.io/component", "webapp"); Tool tool = @@ -250,6 +279,7 @@ public class KubernetesToolToWorkspaceApplierTest { throws Exception { // given String yamlRecipeContent = getResource("petclinic.yaml"); + doReturn(toK8SList(yamlRecipeContent).getItems()).when(k8sRecipeParser).parse(anyString()); Tool tool = new Tool()