/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.api.environment.server; import com.google.common.base.Joiner; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.workspace.Environment; import org.eclipse.che.api.environment.server.model.CheServiceBuildContextImpl; import org.eclipse.che.api.environment.server.model.CheServiceImpl; import org.eclipse.che.api.environment.server.model.CheServicesEnvironmentImpl; import org.eclipse.che.api.machine.server.MachineInstanceProviders; import org.eclipse.che.api.machine.shared.dto.MachineConfigDto; import org.eclipse.che.api.machine.shared.dto.MachineSourceDto; import org.eclipse.che.api.machine.shared.dto.ServerConfDto; import org.eclipse.che.api.workspace.server.DtoConverter; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; import org.eclipse.che.api.workspace.server.model.impl.EnvironmentRecipeImpl; import org.eclipse.che.api.workspace.server.model.impl.ExtendedMachineImpl; import org.eclipse.che.api.workspace.server.model.impl.ServerConf2Impl; import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; import org.mockito.InjectMocks; 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; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.eclipse.che.dto.server.DtoFactory.newDto; import static org.mockito.Matchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; /** * @author Alexander Garagatyi */ @Listeners(MockitoTestNGListener.class) public class CheEnvironmentValidatorTest { @Mock MachineInstanceProviders machineInstanceProviders; @Mock EnvironmentParser environmentParser; @Mock DefaultServicesStartStrategy startStrategy; @InjectMocks CheEnvironmentValidator environmentValidator; EnvironmentDto environment; CheServicesEnvironmentImpl cheServicesEnv; @BeforeMethod public void prepare() throws Exception { environment = spy(createEnv()); cheServicesEnv = spy(createServicesEnv()); when(machineInstanceProviders.hasProvider("docker")).thenReturn(true); when(machineInstanceProviders.getProviderTypes()).thenReturn(asList("docker", "ssh")); when(environmentParser.parse(any(Environment.class))).thenReturn(cheServicesEnv); when(environmentParser.getEnvironmentTypes()).thenReturn(singleton("compose")); } @Test public void shouldSucceedOnValidationOfValidEnvironment() throws Exception { environmentValidator.validate("env", environment); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Environment name should not be neither null nor empty") public void shouldFailValidationIfEnvNameIsNull() throws Exception { // when environmentValidator.validate(null, environment); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Environment name should not be neither null nor empty") public void shouldFailValidationIfEnvNameIsEmpty() throws Exception { // when environmentValidator.validate("", environment); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Parsing of recipe of environment '.*' failed. Error: test exception") public void shouldFailIfRecipeIsBroken() throws Exception { // given when(environmentParser.parse(any(Environment.class))) .thenThrow(new IllegalArgumentException("test exception")); // when environmentValidator.validate("env", environment); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Type 'compose' of environment 'env' is not supported. Supported types: otherType") public void shouldFailIfEnvTypeIsNotSupported() throws Exception { // given when(environmentParser.parse(any(Environment.class))) .thenThrow(new IllegalArgumentException("test exception")); when(environmentParser.getEnvironmentTypes()).thenReturn(singleton("otherType")); // when environmentValidator.validate("env", environment); } @Test(expectedExceptions = ServerException.class, expectedExceptionsMessageRegExp = "Parsing of recipe of environment '.*' failed. Error: test exception") public void shouldFailIfEnvironmentRecipeFetchingFails() throws Exception { // given when(environmentParser.parse(any(Environment.class))) .thenThrow(new ServerException("test exception")); // when environmentValidator.validate("env", environment); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Start order of machine in environment 'env' is not resolvable. Error: test exception") public void shouldFailIfServicesOrderingFails() throws Exception { when(startStrategy.order(any(CheServicesEnvironmentImpl.class))) .thenThrow(new IllegalArgumentException("test exception")); environmentValidator.validate("env", environment); } @Test(dataProvider = "invalidEnvironmentProvider") public void shouldFailValidationIfEnvironmentIsBroken(EnvironmentDto env, String expectedExceptionMessage) throws Exception { try { // when environmentValidator.validate("env", env); // then fail(format("Validation had to throw exception with message %s", expectedExceptionMessage)); } catch (IllegalArgumentException e) { // then assertEquals(e.getLocalizedMessage(), expectedExceptionMessage); } } @DataProvider public static Object[][] invalidEnvironmentProvider() { // InvalidEnvironmentObject | ExceptionMessage EnvironmentDto env; Map.Entry<String, ExtendedMachineDto> machineEntry; List<List<Object>> data = new ArrayList<>(); data.add(asList(createEnv().withRecipe(null), "Environment recipe should not be null")); env = createEnv(); env.getRecipe().setType("docker"); data.add(asList(env, "Type 'docker' of environment 'env' is not supported. Supported types: compose")); env = createEnv(); env.getRecipe().withLocation(null).withContent(null); data.add(asList(env, "Recipe of environment 'env' must contain location or content")); env = createEnv(); env.getRecipe().withLocation("location").withContent("content"); data.add(asList(env, "Recipe of environment 'env' contains mutually exclusive fields location and content")); env = createEnv(); env.setMachines(null); data.add(asList(env, "Environment 'env' doesn't contain machine with 'org.eclipse.che.ws-agent' agent")); env = createEnv(); env.setMachines(emptyMap()); data.add(asList(env, "Environment 'env' doesn't contain machine with 'org.eclipse.che.ws-agent' agent")); env = createEnv(); env.getMachines().put("missingInEnvMachine", newDto(ExtendedMachineDto.class).withAgents(singletonList("org.eclipse.che.ws-agent"))); data.add(asList(env, "Environment 'env' contains machines that are missing in environment recipe: missingInEnvMachine")); env = createEnv(); env.getMachines().entrySet().forEach(entry -> entry.getValue().getAgents().add("org.eclipse.che.ws-agent")); data.add(asList(env, "Environment 'env' should contain exactly 1 machine with agent 'org.eclipse.che.ws-agent', but contains '" + env.getMachines().size() + "'. " + "All machines with this agent: " + Joiner.on(", ").join(env.getMachines().keySet()))); env = createEnv(); env.getMachines().entrySet().forEach(entry -> entry.getValue().setAgents(null)); data.add(asList(env, "Environment 'env' should contain exactly 1 machine with agent 'org.eclipse.che.ws-agent', but contains '0'. All machines with this agent: ")); env = createEnv(); env.getMachines().entrySet().forEach(entry -> entry.getValue().getAgents().add(null)); data.add(asList(env, "Machine 'machine2' in environment 'env' contains invalid agent 'null'")); env = createEnv(); env.getMachines().entrySet().forEach(entry -> entry.getValue().getAgents().add("")); data.add(asList(env, "Machine 'machine2' in environment 'env' contains invalid agent ''")); env = createEnv(); machineEntry = env.getMachines().entrySet().iterator().next(); machineEntry.getValue().setAttributes(singletonMap("memoryLimitBytes", "0")); data.add(asList(env, format("Value of attribute 'memoryLimitBytes' of machine '%s' in environment 'env' is illegal", machineEntry.getKey()))); env = createEnv(); machineEntry = env.getMachines().entrySet().iterator().next(); machineEntry.getValue().setAttributes(singletonMap("memoryLimitBytes", "-1")); data.add(asList(env, format("Value of attribute 'memoryLimitBytes' of machine '%s' in environment 'env' is illegal", machineEntry.getKey()))); env = createEnv(); machineEntry = env.getMachines().entrySet().iterator().next(); machineEntry.getValue().setAttributes(singletonMap("memoryLimitBytes", "")); data.add(asList(env, format("Value of attribute 'memoryLimitBytes' of machine '%s' in environment 'env' is illegal", machineEntry.getKey()))); return data.stream() .map(list -> list.toArray(new Object[list.size()])) .toArray(value -> new Object[data.size()][]); } @Test public void shouldNotFailIfExtraMachineDoesNotHaveExtendedMachineEntry() throws Exception { // given CheServicesEnvironmentImpl cheServicesEnv = createServicesEnv(); cheServicesEnv.getServices().put("extra", createCheService("_extra", 1000000L, null, null, null)); when(environmentParser.parse(any(Environment.class))).thenReturn(cheServicesEnv); // when environmentValidator.validate("env", environment); } @Test(dataProvider = "invalidServicesEnvironmentProvider") public void shouldFailValidationIfCheServicesEnvironmentIsBroken(CheServicesEnvironmentImpl cheServicesEnv, String expectedExceptionMessage) throws Exception { // given when(environmentParser.parse(any(Environment.class))).thenReturn(cheServicesEnv); try { // when environmentValidator.validate("env", environment); // then fail(format("Validation had to throw exception with message %s", expectedExceptionMessage)); } catch (IllegalArgumentException e) { // then assertEquals(e.getLocalizedMessage(), expectedExceptionMessage); } } @DataProvider public static Object[][] invalidServicesEnvironmentProvider() { // Format of result array: // [ [InvalidCheServicesEnvironmentObject, ExceptionMessage], ... ] CheServicesEnvironmentImpl env; Map.Entry<String, CheServiceImpl> serviceEntry; CheServiceImpl service; List<List<Object>> data = new ArrayList<>(); env = createServicesEnv(); env.setServices(null); data.add(asList(env, "Environment 'env' should contain at least 1 machine")); env = createServicesEnv(); env.setServices(emptyMap()); data.add(asList(env, "Environment 'env' should contain at least 1 machine")); env = createServicesEnv(); serviceEntry = getAnyService(env); env.getServices().put("invalid service name", serviceEntry.getValue()); data.add(asList(env, "Name of machine 'invalid service name' in environment 'env' is invalid")); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(null); service.setBuild(null); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(""); service.setBuild(null); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(null); service.setBuild(new CheServiceBuildContextImpl()); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(""); service.setBuild(new CheServiceBuildContextImpl()); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setPorts(new ArrayList<>(singletonList("8080:8080"))); data.add(asList(env, format("Ports binding is forbidden but found in machine '%s' of environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setVolumes(new ArrayList<>(singletonList("volume"))); data.add(asList(env, format("Volumes binding is forbidden but found in machine '%s' of environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setNetworks(new ArrayList<>(singletonList("network1"))); data.add(asList(env, format("Networks configuration is forbidden but found in machine '%s' of environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(null); service.setBuild(new CheServiceBuildContextImpl(null, "dockerfile", null, null)); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(""); service.setBuild(new CheServiceBuildContextImpl("", "dockerfile", null, null)); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(""); service.setBuild(new CheServiceBuildContextImpl(null, null, null, null)); data.add(asList(env, format("Field 'image' or 'build.context' is required in machine '%s' in environment 'env'", serviceEntry.getKey()))); env = createServicesEnv(); serviceEntry = getAnyService(env); service = serviceEntry.getValue(); service.setImage(""); service.setBuild(new CheServiceBuildContextImpl("some url", null, "some content", new HashMap<String, String>() {{put("argkey","argvalue");}})); data.add(asList(env, format("Machine '%s' in environment 'env' contains mutually exclusive dockerfile content and build context.", serviceEntry.getKey()))); return data.stream() .map(list -> list.toArray(new Object[list.size()])) .toArray(value -> new Object[data.size()][]); } private static Map.Entry<String, CheServiceImpl> getAnyService(CheServicesEnvironmentImpl env) { return env.getServices() .entrySet() .iterator() .next(); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine name is null or empty") public void shouldFailValidationIfMachineNameIsNull() throws Exception { MachineConfigDto config = createMachineConfig(); config.withName(null); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine name is null or empty") public void shouldFailValidationIfMachineNameIsEmpty() throws Exception { MachineConfigDto config = createMachineConfig(); config.withName(""); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine '.*' doesn't have source") public void shouldFailValidationIfMachineSourceIsNull() throws Exception { MachineConfigDto config = createMachineConfig(); config.withSource(null); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Type 'null' of machine '.*' is not supported. Supported values are: docker, ssh.") public void shouldFailValidationIfMachineTypeIsNull() throws Exception { MachineConfigDto config = createMachineConfig(); config.withType(null); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Type 'compose' of machine '.*' is not supported. Supported values are: docker, ssh.") public void shouldFailValidationIfMachineTypeIsNotDocker() throws Exception { MachineConfigDto config = createMachineConfig(); config.withType("compose"); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine .* contains server conf with invalid port .*", dataProvider = "invalidPortProvider") public void shouldFailValidationIfServerConfPortIsInvalid(String invalidPort) throws Exception { MachineConfigDto config = createMachineConfig(); config.getServers() .add(newDto(ServerConfDto.class).withPort(invalidPort)); environmentValidator.validateMachine(config); } @DataProvider(name = "invalidPortProvider") public static Object[][] invalidPortProvider() { return new Object[][] { {"0"}, {"0123"}, {"012/tcp"}, {"8080/pct"}, {"8080/pdu"}, {"/tcp"}, {"tcp"}, {""}, {"8080/tcp1"}, {"8080/tcpp"}, {"8080tcp"}, {"8080/tc"}, {"8080/ud"}, {"8080/udpp"}, {"8080/udp/"}, {"8080/tcp/"}, {"8080/tcp/udp"}, {"8080/tcp/tcp"}, {"8080/tcp/8080"}, {null} }; } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine '.*' contains environment variable with null or empty name") public void shouldFailValidationIfEnvVarNameIsNull() throws Exception { MachineConfigDto config = createMachineConfig(); config.getEnvVariables() .put(null, "value"); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine '.*' contains environment variable with null or empty name") public void shouldFailValidationIfEnvVarNameIsEmpty() throws Exception { MachineConfigDto config = createMachineConfig(); config.getEnvVariables() .put("", "value"); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine '.*' contains environment variable 'key' with null value") public void shouldFailValidationIfEnvVarValueIsNull() throws Exception { MachineConfigDto config = createMachineConfig(); config.getEnvVariables() .put("key", null); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Source of machine '.*' must contain location or content") public void shouldFailValidationIfMissingSourceLocationAndContent() throws Exception { MachineConfigDto config = createMachineConfig(); config.withSource(newDto(MachineSourceDto.class).withType("dockerfile")); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine '.*' has invalid source location: 'localhost'") public void shouldFailValidationIfLocationIsInvalidUrl() throws Exception { MachineConfigDto config = createMachineConfig(); config.withSource(newDto(MachineSourceDto.class).withType("dockerfile").withLocation("localhost")); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine '.*' has invalid source location protocol: ftp://localhost") public void shouldFailValidationIfLocationHasInvalidProtocol() throws Exception { MachineConfigDto config = createMachineConfig(); config.withSource(newDto(MachineSourceDto.class).withType("dockerfile") .withLocation("ftp://localhost")); environmentValidator.validateMachine(config); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Machine .* contains server conf with invalid protocol .*", dataProvider = "invalidProtocolProvider") public void shouldFailValidationIfServerConfProtocolIsInvalid(String invalidProtocol) throws Exception { MachineConfigDto config = createMachineConfig(); config.getServers() .add(newDto(ServerConfDto.class).withPort("8080/tcp") .withProtocol(invalidProtocol)); environmentValidator.validateMachine(config); } @DataProvider(name = "invalidProtocolProvider") public static Object[][] invalidProtocolProvider() { return new Object[][] { {""}, {"http!"}, {"2http"}, {"http:"}, }; } private MachineConfigDto createMachineConfig() { List<ServerConfDto> serversConf = new ArrayList<>(asList(newDto(ServerConfDto.class).withRef("ref1") .withPort("8080/tcp") .withProtocol("https") .withPath("some/path"), newDto(ServerConfDto.class).withRef("ref2") .withPort("9090/udp") .withProtocol("protocol") .withPath("/some/path"))); return newDto(MachineConfigDto.class).withDev(true) .withName("machine1") .withType("docker") .withSource(newDto(MachineSourceDto.class) .withLocation("http://location") .withType("dockerfile")) .withServers(serversConf) .withEnvVariables(new HashMap<>(singletonMap("key1", "value1"))); } private static EnvironmentDto createEnv() { // singletonMap, asList are wrapped into modifiable collections to ease env modifying by tests EnvironmentImpl env = new EnvironmentImpl(); Map<String, ExtendedMachineImpl> machines = new HashMap<>(); Map<String, ServerConf2Impl> servers = new HashMap<>(); servers.put("ref1", new ServerConf2Impl("8080/tcp", "proto1", singletonMap("prop1", "propValue"))); servers.put("ref2", new ServerConf2Impl("8080/udp", "proto1", null)); servers.put("ref3", new ServerConf2Impl("9090", "proto1", null)); machines.put("dev-machine", new ExtendedMachineImpl(asList("org.eclipse.che.ws-agent", "someAgent"), servers, singletonMap("memoryLimitBytes", "10000"))); machines.put("machine2", new ExtendedMachineImpl(asList("someAgent2", "someAgent3"), null, singletonMap("memoryLimitBytes", "10000"))); env.setRecipe(new EnvironmentRecipeImpl("compose", "application/x-yaml", "content", null)); env.setMachines(machines); return DtoConverter.asDto(env); } private static CheServicesEnvironmentImpl createServicesEnv() { CheServicesEnvironmentImpl cheServicesEnvironment = new CheServicesEnvironmentImpl(); Map<String, CheServiceImpl> services = new HashMap<>(); Map<String, String> buildArgs = new HashMap<String, String>() {{put("argkey","argvalue");}}; cheServicesEnvironment.setServices(services); services.put("dev-machine", createCheService("_dev", 1024L * 1024L * 1024L, singletonList("machine2"), singletonList("machine2"), singletonList("machine2"))); CheServiceImpl service = createCheService("_machine2", 100L, null, emptyList(), null); service.setBuild(new CheServiceBuildContextImpl("context", "file", null, buildArgs)); services.put("machine2", service); return cheServicesEnvironment; } private static CheServiceImpl createCheService(String suffix, long memLimitBytes, List<String> links, List<String> dependsOn, List<String> volumesFrom) { CheServiceImpl service = new CheServiceImpl(); service.setMemLimit(memLimitBytes); service.setImage("image_repo/image" + suffix); service.setEnvironment(new HashMap<>(singletonMap("env" + suffix, "val" + suffix))); service.setCommand(new ArrayList<>(asList("this", "is", "command" + suffix))); service.setContainerName("containerName" + suffix); service.setEntrypoint(new ArrayList<>(asList("this", "is", "entrypoint" + suffix))); service.setExpose(new ArrayList<>(asList("8080", "9090/tcp", "7070/udp"))); service.setLabels(new HashMap<>(singletonMap("label" + suffix, "value" + suffix))); if (links != null) { service.setLinks(new ArrayList<>(links)); } if (dependsOn != null) { service.setDependsOn(new ArrayList<>(dependsOn)); } if (volumesFrom != null) { service.setVolumesFrom(new ArrayList<>(volumesFrom)); } // service.setPorts(new ArrayList<>(singletonList("8080:8080"))); Forbidden // service.setVolumes(new ArrayList<>(singletonList("volume"))); Forbidden return service; } }