| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.dubbo.scenario.builder; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.dubbo.scenario.builder.exception.ConfigureFileNotFoundException; |
| import org.apache.dubbo.scenario.builder.vo.CaseConfiguration; |
| import org.apache.dubbo.scenario.builder.vo.DockerService; |
| import org.apache.dubbo.scenario.builder.vo.ServiceComponent; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.yaml.snakeyaml.Yaml; |
| |
| import java.io.DataInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| public class ConfigurationImpl implements IConfiguration { |
| public static final String SAMPLE_TEST_IMAGE = "dubbo/sample-test"; |
| public static final String DUBBO_APP_DIR = "/usr/local/dubbo/app"; |
| public static final String DUBBO_LOG_DIR = "/usr/local/dubbo/logs"; |
| public static final String ENV_SERVICE_NAME = "SERVICE_NAME"; |
| public static final String ENV_SERVICE_TYPE = "SERVICE_TYPE"; |
| public static final String ENV_APP_MAIN_CLASS = "APP_MAIN_CLASS"; |
| public static final String ENV_WAIT_PORTS_BEFORE_RUN = "WAIT_PORTS_BEFORE_RUN"; |
| public static final String ENV_CHECK_PORTS_AFTER_RUN = "CHECK_PORTS_AFTER_RUN"; |
| public static final String ENV_CHECK_LOG = "CHECK_LOG"; |
| public static final String ENV_CHECK_TIMEOUT = "CHECK_TIMEOUT"; |
| public static final String ENV_TEST_PATTERNS = "TEST_PATTERNS"; |
| public static final String ENV_JAVA_OPTS = "JAVA_OPTS"; |
| public static final String ENV_DEBUG_OPTS = "DEBUG_OPTS"; |
| public static final String ENV_SCENARIO_HOME = "SCENARIO_HOME"; |
| |
| private static final Logger logger = LoggerFactory.getLogger(ConfigurationImpl.class); |
| private final CaseConfiguration configuration; |
| private String scenarioHome; |
| private String configBasedir; |
| private String scenarioName; |
| private final String scenarioLogDir; |
| private int scenarioTimeout = 90; |
| private int javaDebugPort = 20660; |
| private int debugTimeout = 36000; |
| private Set<String> debugServices = new HashSet<>(); |
| |
| public ConfigurationImpl() throws IOException, ConfigureFileNotFoundException { |
| String configureFile = System.getProperty("configure.file"); |
| if (StringUtils.isBlank(configureFile)) { |
| throw new ConfigureFileNotFoundException(); |
| } |
| this.configBasedir = new File(configureFile).getParentFile().getCanonicalPath(); |
| |
| //set default scenarioHome dir to ${configBasedir}/target |
| this.scenarioHome = System.getProperty("scenario.home"); |
| if (StringUtils.isBlank(scenarioHome)) { |
| scenarioHome = configBasedir + "/target"; |
| } |
| scenarioLogDir = new File(scenarioHome, "logs").getCanonicalPath(); |
| |
| //set default scenarioName |
| scenarioName = System.getProperty("scenario.name"); |
| if (StringUtils.isBlank(scenarioName)) { |
| scenarioName = new File(configBasedir).getName(); |
| } |
| |
| String debugService = System.getProperty("debug.service"); |
| if (StringUtils.isNotBlank(debugService)) { |
| String[] strs = debugService.split(","); |
| for (String str : strs) { |
| str = str.trim(); |
| if (StringUtils.isNotBlank(str)) { |
| debugServices.add(str); |
| } |
| } |
| } |
| |
| this.configuration = loadCaseConfiguration(configureFile); |
| |
| //set scenario timeout |
| if (this.configuration.getTimeout() > 0) { |
| scenarioTimeout = this.configuration.getTimeout(); |
| } |
| String timeout = System.getProperty("timeout"); |
| if (StringUtils.isNotBlank(timeout)) { |
| scenarioTimeout = Integer.parseInt(timeout); |
| } |
| if (isDebug()) { |
| scenarioTimeout = debugTimeout; |
| } |
| |
| logger.info("scenarioName:{}, timeout: {}, debugServices:{}, config: {}", |
| scenarioName, scenarioTimeout, debugServices, configuration); |
| |
| for (String service : debugServices) { |
| if (!configuration.getServices().containsKey(service)) { |
| logger.warn("debug service not found: {}", service); |
| } |
| } |
| } |
| |
| private boolean isDebug() { |
| return debugServices != null && debugServices.size() > 0; |
| } |
| |
| private CaseConfiguration loadCaseConfiguration(String configureFile) throws IOException { |
| // read 'props' |
| String configYaml = readFully(configureFile); |
| CaseConfiguration tmpConfiguration = parseConfiguration(configYaml); |
| Map<String, String> props = tmpConfiguration.getProps(); |
| |
| // process 'from', load parent config |
| CaseConfiguration parentConfiguration = null; |
| if (StringUtils.isNotBlank(tmpConfiguration.getFrom())) { |
| String parentConfigYaml = loadParentConfigYaml(tmpConfiguration); |
| CaseConfiguration tmpParentConfiguration = parseConfiguration(parentConfigYaml); |
| |
| //merge props, overwrite parent props |
| Map<String, String> newProps = new HashMap<>(tmpParentConfiguration.getProps()); |
| newProps.putAll(props); |
| props = newProps; |
| |
| // replace variables '${...}' |
| String newParentConfigYaml = replaceHolders(parentConfigYaml, props); |
| parentConfiguration = parseConfiguration(newParentConfigYaml); |
| } |
| |
| // replace variables '${...}' |
| String newConfigYaml = replaceHolders(configYaml, props); |
| CaseConfiguration caseConfiguration = parseConfiguration(newConfigYaml); |
| |
| //merge globalSystemProps, overwrite parent |
| if (parentConfiguration != null) { |
| List<String> systemProps = mergeSystemProps(parentConfiguration.getSystemProps(), caseConfiguration.getSystemProps()); |
| caseConfiguration.setSystemProps(systemProps); |
| } |
| |
| //merge services |
| if (parentConfiguration != null && parentConfiguration.getServices() != null) { |
| Map<String, ServiceComponent> newServices = new LinkedHashMap<>(parentConfiguration.getServices()); |
| if (caseConfiguration.getServices() != null) { |
| newServices.putAll(caseConfiguration.getServices()); |
| } |
| caseConfiguration.setServices(newServices); |
| } |
| |
| fillupServices(caseConfiguration); |
| return caseConfiguration; |
| } |
| |
| private List<String> mergeSystemProps(List<String> parentSystemProps, List<String> childSystemProps) { |
| List<String> newSystemProps = new ArrayList<>(parentSystemProps != null ? parentSystemProps : Collections.emptyList()); |
| if (childSystemProps != null) { |
| childSystemProps.forEach(entry -> { |
| String[] strs = entry.split("="); |
| addOrReplaceKVEntry(newSystemProps, strs[0].trim(), strs.length > 1 ? strs[1].trim() : ""); |
| }); |
| } |
| return newSystemProps; |
| } |
| |
| private String loadParentConfigYaml(CaseConfiguration caseConfiguration) throws IOException { |
| try { |
| String file = "configs/" + caseConfiguration.getFrom(); |
| InputStream inputStream = CaseConfiguration.class.getClassLoader().getResourceAsStream(file); |
| return readFully(inputStream); |
| } catch (Exception e) { |
| logger.error("load parent config failed: " + caseConfiguration.getFrom(), e); |
| throw new IOException("load parent config failed: " + caseConfiguration.getFrom(), e); |
| } |
| } |
| |
| private CaseConfiguration parseConfiguration(String configYaml) { |
| return new Yaml().loadAs(configYaml, CaseConfiguration.class); |
| } |
| |
| private void fillupServices(CaseConfiguration caseConfiguration) throws IOException { |
| List<String> caseSystemProps = caseConfiguration.getSystemProps(); |
| for (Map.Entry<String, ServiceComponent> entry : caseConfiguration.getServices().entrySet()) { |
| String serviceName = entry.getKey(); |
| ServiceComponent service = entry.getValue(); |
| String type = service.getType(); |
| if (isAppService(type)) { |
| service.setImage(SAMPLE_TEST_IMAGE); |
| service.setBasedir(toAbsolutePath(service.getBasedir())); |
| if (service.getVolumes() == null) { |
| service.setVolumes(new ArrayList<>()); |
| } |
| //mount ${project.basedir}/target : DUBBO_APP_DIR |
| String targetPath = new File(service.getBasedir(), "target").getCanonicalPath(); |
| service.getVolumes().add(targetPath + ":" + DUBBO_APP_DIR); |
| |
| //mount ${scenario_home}/logs : DUBBO_LOG_DIR |
| service.getVolumes().add(scenarioLogDir + ":" + DUBBO_LOG_DIR); |
| |
| if (service.getEnvironment() == null) { |
| service.setEnvironment(new ArrayList<>()); |
| } |
| // set service name |
| setEnv(service, ENV_SERVICE_NAME, serviceName); |
| |
| //set wait ports |
| if (isNotEmpty(service.getWaitPortsBeforeRun())) { |
| String str = convertAddrPortsToString(service.getWaitPortsBeforeRun()); |
| setEnv(service, ENV_WAIT_PORTS_BEFORE_RUN, str); |
| } |
| |
| //set check timeout |
| if (isDebug()) { |
| service.setCheckTimeout(debugTimeout); |
| |
| if (debugServices.contains(serviceName)) { |
| //set java remote debug opts |
| //-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 |
| int debugPort = nextDebugPort(); |
| String debugOpts = String.format("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=%s", debugPort); |
| appendEnv(service, ENV_DEBUG_OPTS, debugOpts); |
| |
| //mapping debug port |
| if (service.getPorts() == null) { |
| service.setPorts(new ArrayList<>()); |
| } |
| service.getPorts().add(debugPort + ":" + debugPort); |
| } |
| } |
| if (service.getCheckTimeout() > 0) { |
| setEnv(service, ENV_CHECK_TIMEOUT, service.getCheckTimeout() + ""); |
| } |
| |
| if ("app".equals(type)) { |
| String mainClass = service.getMainClass(); |
| if (StringUtils.isBlank(mainClass)) { |
| throw new RuntimeException("Missing 'mainClass' for app service [" + serviceName + "]"); |
| } |
| //set SERVICE_TYPE |
| setEnv(service, ENV_SERVICE_TYPE, type); |
| //set mainClass env |
| setEnv(service, ENV_APP_MAIN_CLASS, mainClass); |
| //set SCENARIO_HOME |
| setEnv(service, ENV_SCENARIO_HOME, scenarioHome); |
| |
| //set check_log env |
| if (StringUtils.isNotBlank(service.getCheckLog())) { |
| setEnv(service, ENV_CHECK_LOG, service.getCheckLog()); |
| } |
| |
| //set check ports |
| if (isNotEmpty(service.getCheckPortsAfterRun())) { |
| String str = convertAddrPortsToString(service.getCheckPortsAfterRun()); |
| setEnv(service, ENV_CHECK_PORTS_AFTER_RUN, str); |
| } |
| } else if ("test".equals(type)) { |
| String mainClass = service.getMainClass(); |
| if (StringUtils.isNotBlank(mainClass)) { |
| throw new RuntimeException("Illegal attribute 'mainClass' for test service [" + serviceName + "]"); |
| } |
| //set SERVICE_TYPE |
| setEnv(service, ENV_SERVICE_TYPE, type); |
| |
| //set TEST_PATTERNS |
| if (isNotEmpty(service.getTests())) { |
| String str = StringUtils.join(service.getTests(), ';'); |
| setEnv(service, ENV_TEST_PATTERNS, str); |
| } |
| } else { |
| throw new RuntimeException("Illegal service type: " + type); |
| } |
| } |
| |
| // set hostname to serviceId if absent |
| if (StringUtils.isBlank(service.getHostname())) { |
| service.setHostname(serviceName); |
| } |
| |
| //set jvmFlags |
| if (isNotEmpty(service.getJvmFlags())) { |
| String str = StringUtils.join(service.getJvmFlags(), ' '); |
| appendEnv(service, ENV_JAVA_OPTS, str); |
| } |
| |
| //set systemProps |
| List<String> systemProps = mergeSystemProps(caseSystemProps, service.getSystemProps()); |
| if (isNotEmpty(systemProps)) { |
| String str = convertSystemPropsToJvmFlags(systemProps); |
| appendEnv(service, ENV_JAVA_OPTS, str); |
| } |
| |
| } |
| } |
| |
| private void appendEnv(ServiceComponent service, String name, String value) { |
| String prefix = name + "="; |
| List<String> environments = service.getEnvironment(); |
| if (environments == null) { |
| environments = new ArrayList<>(); |
| service.setEnvironment(environments); |
| } |
| for (int i = 0; i < environments.size(); i++) { |
| String env = environments.get(i); |
| if (env.startsWith(prefix)) { |
| // append to exist env |
| env += " " + value; |
| environments.set(i, env); |
| return; |
| } |
| } |
| environments.add(name + "=" + value); |
| } |
| |
| private void setEnv(ServiceComponent service, String name, String value) { |
| List<String> environments = service.getEnvironment(); |
| addOrReplaceKVEntry(environments, name, value); |
| } |
| |
| private void addOrReplaceKVEntry(List<String> map, String name, String value) { |
| String prefix = name + "="; |
| for (int i = 0; i < map.size(); i++) { |
| String env = map.get(i); |
| if (env.startsWith(prefix)) { |
| //replace old env |
| env = name + "=" + value; |
| map.set(i, env); |
| return; |
| } |
| } |
| map.add(name + "=" + value); |
| } |
| |
| //convert systemProp key=value to -Dkey=value |
| private String convertSystemPropsToJvmFlags(List<String> systemProps) { |
| StringBuilder sb = new StringBuilder(); |
| for (String propkv : systemProps) { |
| sb.append("-D").append(propkv).append(' '); |
| } |
| return sb.toString(); |
| } |
| |
| private boolean isNotEmpty(List<String> list) { |
| return list != null && list.size() > 0; |
| } |
| |
| private String convertAddrPortsToString(List<String> addrPorts) { |
| StringBuilder sb = new StringBuilder(); |
| for (String addrPort : addrPorts) { |
| if (!addrPort.contains(":")) { |
| addrPort = "127.0.0.1" + ":" + addrPort; |
| } |
| sb.append(addrPort).append(";"); |
| } |
| return sb.toString(); |
| } |
| |
| private String toAbsolutePath(String path) throws IOException { |
| File file = new File(path); |
| if (file.isAbsolute()) { |
| return file.getCanonicalPath(); |
| } |
| //relative path to basedir of configuration file |
| return new File(configBasedir, path).getCanonicalPath(); |
| } |
| |
| private boolean isAppService(String type) { |
| if (type == null) { |
| return false; |
| } |
| switch (type) { |
| case "app": |
| case "test": |
| return true; |
| } |
| throw new RuntimeException("Illegal service type: " + type); |
| } |
| |
| private String readFully(String file) throws IOException { |
| try (FileInputStream fis = new FileInputStream(file)) { |
| return readFully(fis); |
| } |
| } |
| |
| private String readFully(InputStream input) throws IOException { |
| DataInputStream dis = new DataInputStream(input); |
| byte[] bytes = new byte[input.available()]; |
| dis.readFully(bytes); |
| return new String(bytes, StandardCharsets.UTF_8); |
| } |
| |
| private Pattern pattern = Pattern.compile("\\$\\{(.+?)}"); |
| |
| private String replaceHolders(String str, Map<String, String> props) { |
| StringBuffer buf = new StringBuffer(str.length()); |
| Matcher matcher = pattern.matcher(str); |
| while (matcher.find()) { |
| String var = matcher.group(1); |
| String value = props.get(var); |
| matcher.appendReplacement(buf, value != null ? value : matcher.group()); |
| } |
| matcher.appendTail(buf); |
| return buf.toString(); |
| } |
| |
| @Override |
| public ScenarioRunningScriptGenerator scenarioGenerator() { |
| return new DockerComposeRunningGenerator(); |
| } |
| |
| @Override |
| public CaseConfiguration caseConfiguration() { |
| return this.configuration; |
| } |
| |
| @Override |
| public String scenarioName() { |
| return scenarioName; |
| } |
| |
| @Override |
| public String scenarioVersion() { |
| return System.getProperty("scenario.version"); |
| } |
| |
| @Override |
| public String dockerImageVersion() { |
| return System.getProperty("docker.image.version", "latest"); |
| } |
| |
| @Override |
| public String dockerNetworkName() { |
| return (scenarioName() + "-" + dockerImageVersion()).toLowerCase(); |
| } |
| |
| @Override |
| public String dockerContainerName() { |
| return (scenarioName() + "-" + scenarioVersion() + "-" + dockerImageVersion()).toLowerCase(); |
| } |
| |
| @Override |
| public String scenarioHome() { |
| return this.scenarioHome; |
| } |
| |
| @Override |
| public String outputDir() { |
| return scenarioHome; |
| } |
| |
| @Override |
| public String jacocoHome() { |
| return System.getProperty("jacoco.home"); |
| } |
| |
| @Override |
| public String debugMode() { |
| return isDebug() ? "1" : "0"; |
| } |
| |
| private int nextDebugPort() { |
| return javaDebugPort++; |
| } |
| |
| @Override |
| public Map<String, Object> toMap() { |
| CaseConfiguration caseConfiguration = caseConfiguration(); |
| final Map<String, Object> root = new HashMap<>(); |
| |
| root.put("scenario_home", scenarioHome()); |
| root.put("scenario_name", scenarioName()); |
| root.put("scenario_version", scenarioVersion()); |
| root.put("docker_container_name", dockerContainerName()); |
| root.put("jacoco_home", jacocoHome()); |
| root.put("debug_mode", debugMode()); |
| root.put("docker_compose_file", outputDir() + File.separator + "docker-compose.yml"); |
| root.put("network_name", dockerNetworkName()); |
| root.put("timeout", scenarioTimeout); |
| |
| final StringBuilder removeImagesScript = new StringBuilder(); |
| List<String> links = new ArrayList<>(); |
| if (caseConfiguration.getServices() != null) { |
| caseConfiguration.getServices().forEach((name, service) -> { |
| links.add(service.getHostname()); |
| if (service.isRemoveOnExit()) { |
| removeImagesScript.append("docker rmi ") |
| .append(service.getImage()) |
| .append(System.lineSeparator()); |
| } |
| }); |
| } |
| root.put("removeImagesScript", removeImagesScript.toString()); |
| |
| // add links to test service |
| // caseConfiguration.getServices().forEach((name, service) -> { |
| // if ("test".equals(service.getType())) { |
| // if (service.getLinks() == null) { |
| // service.setLinks(new ArrayList<>()); |
| // } |
| // for (String link : links) { |
| // if (!StringUtils.equals(link, service.getHostname())) { |
| // service.getLinks().add(link); |
| // } |
| // } |
| // } |
| // }); |
| |
| root.put("services", convertDockerServices(scenarioVersion(), caseConfiguration.getServices())); |
| List<String> testServiceNames = findTestServiceNames(caseConfiguration); |
| root.put("test_service_name", testServiceNames.size() > 0 ? testServiceNames.get(0) : ""); |
| |
| return root; |
| } |
| |
| private List<String> findTestServiceNames(CaseConfiguration caseConfiguration) { |
| List<String> serviceNames = new ArrayList<>(); |
| for (Map.Entry<String, ServiceComponent> entry : caseConfiguration.getServices().entrySet()) { |
| ServiceComponent service = entry.getValue(); |
| if ("test".equals(service.getType())) { |
| serviceNames.add(entry.getKey()); |
| } |
| } |
| return serviceNames; |
| } |
| |
| protected List<DockerService> convertDockerServices(final String version, |
| Map<String, ServiceComponent> componentMap) { |
| final ArrayList<DockerService> services = new ArrayList<>(); |
| if (componentMap == null) { |
| return services; |
| } |
| componentMap.forEach((name, dependency) -> { |
| DockerService service = new DockerService(); |
| |
| String imageName = dependency.getImage(); |
| service.setName(name); |
| service.setImageName(imageName); |
| service.setHostname(dependency.getHostname()); |
| service.setExpose(dependency.getExpose()); |
| service.setPorts(dependency.getPorts()); |
| service.setDepends_on(dependency.getDepends_on()); |
| service.setLinks(dependency.getDepends_on()); |
| service.setEntrypoint(dependency.getEntrypoint()); |
| service.setHealthcheck(dependency.getHealthcheck()); |
| service.setEnvironment(dependency.getEnvironment()); |
| service.setVolumes(dependency.getVolumes()); |
| service.setRemoveOnExit(dependency.isRemoveOnExit()); |
| services.add(service); |
| }); |
| return services; |
| } |
| } |