/******************************************************************************* * 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.machine.MachineConfig; import org.eclipse.che.api.core.model.machine.ServerConf; import org.eclipse.che.api.core.model.workspace.Environment; import org.eclipse.che.api.core.model.workspace.ExtendedMachine; import org.eclipse.che.api.core.model.workspace.ServerConf2; 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.commons.annotation.Nullable; import javax.inject.Inject; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; 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.stream.Collectors.toList; /** * Validates description of environment of workspace. * * @author Alexander Garagatyi */ public class CheEnvironmentValidator { /* machine name must contain only {a-zA-Z0-9_-} characters and it's needed for validation machine names */ private static final String MACHINE_NAME_REGEXP = "[a-zA-Z0-9_-]+"; private static final Pattern MACHINE_NAME_PATTERN = Pattern.compile("^" + MACHINE_NAME_REGEXP + "$"); private static final Pattern SERVER_PORT = Pattern.compile("^[1-9]+[0-9]*(/(tcp|udp))?$"); private static final Pattern SERVER_PROTOCOL = Pattern.compile("^[a-z][a-z0-9-+.]*$"); // CheService syntax patterns /** * Examples: * <ul> * <li>8080/tcp</li> * <li>8080/udp</li> * <li>8080</li> * <li>8/tcp</li> * <li>8</li> * </ul> */ private static final Pattern EXPOSE_PATTERN = Pattern.compile("^[1-9]+[0-9]*(/(tcp|udp))?$"); /** * Examples: * <ul> * <li>service1</li> * <li>service1:alias1</li> * </ul> */ private static final Pattern LINK_PATTERN = Pattern.compile("^(?<serviceName>" + MACHINE_NAME_REGEXP + ")(:" + MACHINE_NAME_REGEXP + ")?$"); private static final Pattern VOLUME_FROM_PATTERN = Pattern.compile("^(?<serviceName>" + MACHINE_NAME_REGEXP + ")(:(ro|rw))?$"); private final MachineInstanceProviders machineInstanceProviders; private final EnvironmentParser environmentParser; private final DefaultServicesStartStrategy startStrategy; @Inject public CheEnvironmentValidator(MachineInstanceProviders machineInstanceProviders, EnvironmentParser environmentParser, DefaultServicesStartStrategy startStrategy) { this.machineInstanceProviders = machineInstanceProviders; this.environmentParser = environmentParser; this.startStrategy = startStrategy; } // TODO fix error messages: fields mentioning, usage of service term public void validate(String envName, Environment env) throws IllegalArgumentException, ServerException { checkArgument(!isNullOrEmpty(envName), "Environment name should not be neither null nor empty"); checkNotNull(env.getRecipe(), "Environment recipe should not be null"); checkArgument(environmentParser.getEnvironmentTypes().contains(env.getRecipe().getType()), "Type '%s' of environment '%s' is not supported. Supported types: %s", env.getRecipe().getType(), envName, Joiner.on(',').join(environmentParser.getEnvironmentTypes())); checkArgument(env.getRecipe().getContent() != null || env.getRecipe().getLocation() != null, "Recipe of environment '%s' must contain location or content", envName); checkArgument(env.getRecipe().getContent() == null || env.getRecipe().getLocation() == null, "Recipe of environment '%s' contains mutually exclusive fields location and content", envName); CheServicesEnvironmentImpl cheServicesEnvironment; try { cheServicesEnvironment = environmentParser.parse(env); } catch (ServerException e) { throw new ServerException(format("Parsing of recipe of environment '%s' failed. Error: %s", envName, e.getLocalizedMessage())); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(format("Parsing of recipe of environment '%s' failed. Error: %s", envName, e.getLocalizedMessage())); } checkArgument(cheServicesEnvironment.getServices() != null && !cheServicesEnvironment.getServices().isEmpty(), "Environment '%s' should contain at least 1 machine", envName); checkArgument(env.getMachines() != null && !env.getMachines().isEmpty(), "Environment '%s' doesn't contain machine with 'org.eclipse.che.ws-agent' agent", envName); List<String> missingServices = env.getMachines() .keySet() .stream() .filter(machineName -> !cheServicesEnvironment.getServices() .containsKey(machineName)) .collect(toList()); checkArgument(missingServices.isEmpty(), "Environment '%s' contains machines that are missing in environment recipe: %s", envName, Joiner.on(", ").join(missingServices)); List<String> devMachines = env.getMachines() .entrySet() .stream() .filter(entry -> entry.getValue().getAgents() != null && entry.getValue().getAgents().contains("org.eclipse.che.ws-agent")) .map(Map.Entry::getKey) .collect(toList()); checkArgument(devMachines.size() == 1, "Environment '%s' should contain exactly 1 machine with agent 'org.eclipse.che.ws-agent', but contains '%s'. " + "All machines with this agent: %s", envName, devMachines.size(), Joiner.on(", ").join(devMachines)); // needed to validate different kinds of dependencies in services to other services Set<String> servicesNames = cheServicesEnvironment.getServices().keySet(); cheServicesEnvironment.getServices() .forEach((serviceName, service) -> validateMachine(serviceName, env.getMachines().get(serviceName), service, envName, servicesNames)); // check that order can be resolved try { startStrategy.order(cheServicesEnvironment); } catch (IllegalArgumentException e) { throw new IllegalArgumentException( format("Start order of machine in environment '%s' is not resolvable. Error: %s", envName, e.getLocalizedMessage())); } } protected void validateMachine(String machineName, @Nullable ExtendedMachine extendedMachine, CheServiceImpl service, String envName, Set<String> servicesNames) throws IllegalArgumentException { checkArgument(MACHINE_NAME_PATTERN.matcher(machineName).matches(), "Name of machine '%s' in environment '%s' is invalid", machineName, envName); checkArgument(!isNullOrEmpty(service.getImage()) || (service.getBuild() != null && (!isNullOrEmpty(service.getBuild().getContext()) || !isNullOrEmpty(service.getBuild().getDockerfileContent()))), "Field 'image' or 'build.context' is required in machine '%s' in environment '%s'", machineName, envName); checkArgument(service.getBuild() == null || (isNullOrEmpty(service.getBuild().getContext()) != isNullOrEmpty(service.getBuild().getDockerfileContent())), "Machine '%s' in environment '%s' contains mutually exclusive dockerfile content and build context.", machineName, envName); if (extendedMachine != null) { validateExtendedMachine(extendedMachine, envName, machineName); } service.getExpose() .forEach(expose -> checkArgument(EXPOSE_PATTERN.matcher(expose).matches(), "Exposed port '%s' in machine '%s' in environment '%s' is invalid", expose, machineName, envName)); service.getLinks() .forEach(link -> { Matcher matcher = LINK_PATTERN.matcher(link); checkArgument(matcher.matches(), "Link '%s' in machine '%s' in environment '%s' is invalid", link, machineName, envName); String serviceFromLink = matcher.group("serviceName"); checkArgument(servicesNames.contains(serviceFromLink), "Machine '%s' in environment '%s' contains link to non existing machine '%s'", machineName, envName, serviceFromLink); }); service.getDependsOn() .forEach(depends -> { checkArgument(MACHINE_NAME_PATTERN.matcher(depends).matches(), "Dependency '%s' in machine '%s' in environment '%s' is invalid", depends, machineName, envName); checkArgument(servicesNames.contains(depends), "Machine '%s' in environment '%s' contains dependency to non existing machine '%s'", machineName, envName, depends); }); service.getVolumesFrom() .forEach(volumesFrom -> { Matcher matcher = VOLUME_FROM_PATTERN.matcher(volumesFrom); checkArgument(matcher.matches(), "Machine name '%s' in field 'volumes_from' of machine '%s' in environment '%s' is invalid", volumesFrom, machineName, envName); String serviceFromVolumesFrom = matcher.group("serviceName"); checkArgument(servicesNames.contains(serviceFromVolumesFrom), "Machine '%s' in environment '%s' contains non existing machine '%s' in 'volumes_from' field", machineName, envName, serviceFromVolumesFrom); }); checkArgument(service.getPorts() == null || service.getPorts().isEmpty(), "Ports binding is forbidden but found in machine '%s' of environment '%s'", machineName, envName); checkArgument(service.getVolumes() == null || service.getVolumes().isEmpty(), "Volumes binding is forbidden but found in machine '%s' of environment '%s'", machineName, envName); checkArgument(service.getNetworks() == null || service.getNetworks().isEmpty(), "Networks configuration is forbidden but found in machine '%s' of environment '%s'", machineName, envName); } private void validateExtendedMachine(ExtendedMachine extendedMachine, String envName, String machineName) { if (extendedMachine.getAttributes() != null && extendedMachine.getAttributes().get("memoryLimitBytes") != null) { try { long memoryLimitBytes = Long.parseLong(extendedMachine.getAttributes().get("memoryLimitBytes")); checkArgument(memoryLimitBytes > 0, "Value of attribute 'memoryLimitBytes' of machine '%s' in environment '%s' is illegal", machineName, envName); } catch (NumberFormatException e) { throw new IllegalArgumentException( format("Value of attribute 'memoryLimitBytes' of machine '%s' in environment '%s' is illegal", machineName, envName)); } } if (extendedMachine.getServers() != null) { extendedMachine.getServers() .entrySet() .forEach(serverEntry -> { String serverName = serverEntry.getKey(); ServerConf2 server = serverEntry.getValue(); checkArgument(server.getPort() != null && SERVER_PORT.matcher(server.getPort()).matches(), "Machine '%s' in environment '%s' contains server conf '%s' with invalid port '%s'", machineName, envName, serverName, server.getPort()); checkArgument(server.getProtocol() == null || SERVER_PROTOCOL.matcher(server.getProtocol()).matches(), "Machine '%s' in environment '%s' contains server conf '%s' with invalid protocol '%s'", machineName, envName, serverName, server.getProtocol()); }); } if (extendedMachine.getAgents() != null) { for (String agent : extendedMachine.getAgents()) { checkArgument(!isNullOrEmpty(agent), "Machine '%s' in environment '%s' contains invalid agent '%s'", machineName, envName, agent); } } } public void validateMachine(MachineConfig machineCfg) throws IllegalArgumentException { String machineName = machineCfg.getName(); checkArgument(!isNullOrEmpty(machineName), "Machine name is null or empty"); checkArgument(MACHINE_NAME_PATTERN.matcher(machineName).matches(), "Machine name '%s' is invalid", machineName); checkNotNull(machineCfg.getSource(), "Machine '%s' doesn't have source", machineName); checkArgument(machineCfg.getSource().getContent() != null || machineCfg.getSource().getLocation() != null, "Source of machine '%s' must contain location or content", machineName); checkArgument(machineCfg.getSource().getContent() == null || machineCfg.getSource().getLocation() == null, "Source of machine '%s' contains mutually exclusive fields location and content", machineName); checkArgument(machineInstanceProviders.hasProvider(machineCfg.getType()), "Type '%s' of machine '%s' is not supported. Supported values are: %s.", machineCfg.getType(), machineName, Joiner.on(", ").join(machineInstanceProviders.getProviderTypes())); if (machineCfg.getSource().getType().equals("dockerfile") && machineCfg.getSource().getLocation() != null) { try { final String protocol = new URL(machineCfg.getSource().getLocation()).getProtocol(); checkArgument(protocol.equals("http") || protocol.equals("https"), "Machine '%s' has invalid source location protocol: %s", machineName, machineCfg.getSource().getLocation()); } catch (MalformedURLException e) { throw new IllegalArgumentException(format("Machine '%s' has invalid source location: '%s'", machineName, machineCfg.getSource().getLocation())); } } for (ServerConf serverConf : machineCfg.getServers()) { checkArgument(serverConf.getPort() != null && SERVER_PORT.matcher(serverConf.getPort()).matches(), "Machine '%s' contains server conf with invalid port '%s'", machineName, serverConf.getPort()); checkArgument(serverConf.getProtocol() == null || SERVER_PROTOCOL.matcher(serverConf.getProtocol()).matches(), "Machine '%s' contains server conf with invalid protocol '%s'", machineName, serverConf.getProtocol()); } for (Map.Entry<String, String> envVariable : machineCfg.getEnvVariables().entrySet()) { checkArgument(!isNullOrEmpty(envVariable.getKey()), "Machine '%s' contains environment variable with null or empty name", machineName); checkNotNull(envVariable.getValue(), "Machine '%s' contains environment variable '%s' with null value", machineName, envVariable.getKey()); } } /** * Checks that object reference is not null, throws {@link IllegalArgumentException} otherwise. * * <p>Exception uses error message built from error message template and error message parameters. */ private static void checkNotNull(Object object, String errorMessageTemplate, Object... errorMessageParams) { if (object == null) { throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageParams)); } } }