/*******************************************************************************
* 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));
}
}
}