diff --git a/backend/pom.xml b/backend/pom.xml index 9a01cdd495..c9fa81eaca 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -18,9 +18,16 @@ 1.5.1 1.8 5.2.1 + 4.9.0 + + + io.fabric8 + kubernetes-client + ${kubernetes-client.version} + org.springframework.boot spring-boot-starter-web @@ -57,7 +64,7 @@ org.mybatis.spring.boot mybatis-spring-boot-starter - 1.3.2 + 2.1.2 diff --git a/backend/src/main/java/io/metersphere/base/domain/FileMetadata.java b/backend/src/main/java/io/metersphere/base/domain/FileMetadata.java index 047b2db62a..60da25a128 100644 --- a/backend/src/main/java/io/metersphere/base/domain/FileMetadata.java +++ b/backend/src/main/java/io/metersphere/base/domain/FileMetadata.java @@ -9,12 +9,14 @@ public class FileMetadata implements Serializable { private String type; - private Long size; + private String engine; private Long createTime; private Long updateTime; + private Long size; + private static final long serialVersionUID = 1L; public String getId() { @@ -41,12 +43,12 @@ public class FileMetadata implements Serializable { this.type = type == null ? null : type.trim(); } - public Long getSize() { - return size; + public String getEngine() { + return engine; } - public void setSize(Long size) { - this.size = size; + public void setEngine(String engine) { + this.engine = engine == null ? null : engine.trim(); } public Long getCreateTime() { @@ -64,4 +66,12 @@ public class FileMetadata implements Serializable { public void setUpdateTime(Long updateTime) { this.updateTime = updateTime; } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } } \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/FileMetadataExample.java b/backend/src/main/java/io/metersphere/base/domain/FileMetadataExample.java index 653beb3de9..7c82bf430d 100644 --- a/backend/src/main/java/io/metersphere/base/domain/FileMetadataExample.java +++ b/backend/src/main/java/io/metersphere/base/domain/FileMetadataExample.java @@ -314,63 +314,73 @@ public class FileMetadataExample { return (Criteria) this; } - public Criteria andSizeIsNull() { - addCriterion("size is null"); + public Criteria andEngineIsNull() { + addCriterion("engine is null"); return (Criteria) this; } - public Criteria andSizeIsNotNull() { - addCriterion("size is not null"); + public Criteria andEngineIsNotNull() { + addCriterion("engine is not null"); return (Criteria) this; } - public Criteria andSizeEqualTo(Long value) { - addCriterion("size =", value, "size"); + public Criteria andEngineEqualTo(String value) { + addCriterion("engine =", value, "engine"); return (Criteria) this; } - public Criteria andSizeNotEqualTo(Long value) { - addCriterion("size <>", value, "size"); + public Criteria andEngineNotEqualTo(String value) { + addCriterion("engine <>", value, "engine"); return (Criteria) this; } - public Criteria andSizeGreaterThan(Long value) { - addCriterion("size >", value, "size"); + public Criteria andEngineGreaterThan(String value) { + addCriterion("engine >", value, "engine"); return (Criteria) this; } - public Criteria andSizeGreaterThanOrEqualTo(Long value) { - addCriterion("size >=", value, "size"); + public Criteria andEngineGreaterThanOrEqualTo(String value) { + addCriterion("engine >=", value, "engine"); return (Criteria) this; } - public Criteria andSizeLessThan(Long value) { - addCriterion("size <", value, "size"); + public Criteria andEngineLessThan(String value) { + addCriterion("engine <", value, "engine"); return (Criteria) this; } - public Criteria andSizeLessThanOrEqualTo(Long value) { - addCriterion("size <=", value, "size"); + public Criteria andEngineLessThanOrEqualTo(String value) { + addCriterion("engine <=", value, "engine"); return (Criteria) this; } - public Criteria andSizeIn(List values) { - addCriterion("size in", values, "size"); + public Criteria andEngineLike(String value) { + addCriterion("engine like", value, "engine"); return (Criteria) this; } - public Criteria andSizeNotIn(List values) { - addCriterion("size not in", values, "size"); + public Criteria andEngineNotLike(String value) { + addCriterion("engine not like", value, "engine"); return (Criteria) this; } - public Criteria andSizeBetween(Long value1, Long value2) { - addCriterion("size between", value1, value2, "size"); + public Criteria andEngineIn(List values) { + addCriterion("engine in", values, "engine"); return (Criteria) this; } - public Criteria andSizeNotBetween(Long value1, Long value2) { - addCriterion("size not between", value1, value2, "size"); + public Criteria andEngineNotIn(List values) { + addCriterion("engine not in", values, "engine"); + return (Criteria) this; + } + + public Criteria andEngineBetween(String value1, String value2) { + addCriterion("engine between", value1, value2, "engine"); + return (Criteria) this; + } + + public Criteria andEngineNotBetween(String value1, String value2) { + addCriterion("engine not between", value1, value2, "engine"); return (Criteria) this; } @@ -493,6 +503,66 @@ public class FileMetadataExample { addCriterion("update_time not between", value1, value2, "updateTime"); return (Criteria) this; } + + public Criteria andSizeIsNull() { + addCriterion("size is null"); + return (Criteria) this; + } + + public Criteria andSizeIsNotNull() { + addCriterion("size is not null"); + return (Criteria) this; + } + + public Criteria andSizeEqualTo(Long value) { + addCriterion("size =", value, "size"); + return (Criteria) this; + } + + public Criteria andSizeNotEqualTo(Long value) { + addCriterion("size <>", value, "size"); + return (Criteria) this; + } + + public Criteria andSizeGreaterThan(Long value) { + addCriterion("size >", value, "size"); + return (Criteria) this; + } + + public Criteria andSizeGreaterThanOrEqualTo(Long value) { + addCriterion("size >=", value, "size"); + return (Criteria) this; + } + + public Criteria andSizeLessThan(Long value) { + addCriterion("size <", value, "size"); + return (Criteria) this; + } + + public Criteria andSizeLessThanOrEqualTo(Long value) { + addCriterion("size <=", value, "size"); + return (Criteria) this; + } + + public Criteria andSizeIn(List values) { + addCriterion("size in", values, "size"); + return (Criteria) this; + } + + public Criteria andSizeNotIn(List values) { + addCriterion("size not in", values, "size"); + return (Criteria) this; + } + + public Criteria andSizeBetween(Long value1, Long value2) { + addCriterion("size between", value1, value2, "size"); + return (Criteria) this; + } + + public Criteria andSizeNotBetween(Long value1, Long value2) { + addCriterion("size not between", value1, value2, "size"); + return (Criteria) this; + } } public static class Criteria extends GeneratedCriteria { diff --git a/backend/src/main/java/io/metersphere/base/mapper/FileMetadataMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/FileMetadataMapper.xml index b10df4fe3b..7ae6826a04 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/FileMetadataMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/FileMetadataMapper.xml @@ -5,9 +5,10 @@ - + + @@ -68,7 +69,7 @@ - id, name, type, size, create_time, update_time + id, name, type, engine, create_time, update_time, size @@ -169,8 +176,8 @@ type = #{record.type,jdbcType=VARCHAR}, - - size = #{record.size,jdbcType=BIGINT}, + + engine = #{record.engine,jdbcType=VARCHAR}, create_time = #{record.createTime,jdbcType=BIGINT}, @@ -178,6 +185,9 @@ update_time = #{record.updateTime,jdbcType=BIGINT}, + + size = #{record.size,jdbcType=BIGINT}, + @@ -188,9 +198,10 @@ set id = #{record.id,jdbcType=VARCHAR}, name = #{record.name,jdbcType=VARCHAR}, type = #{record.type,jdbcType=VARCHAR}, - size = #{record.size,jdbcType=BIGINT}, + engine = #{record.engine,jdbcType=VARCHAR}, create_time = #{record.createTime,jdbcType=BIGINT}, - update_time = #{record.updateTime,jdbcType=BIGINT} + update_time = #{record.updateTime,jdbcType=BIGINT}, + size = #{record.size,jdbcType=BIGINT} @@ -204,8 +215,8 @@ type = #{type,jdbcType=VARCHAR}, - - size = #{size,jdbcType=BIGINT}, + + engine = #{engine,jdbcType=VARCHAR}, create_time = #{createTime,jdbcType=BIGINT}, @@ -213,6 +224,9 @@ update_time = #{updateTime,jdbcType=BIGINT}, + + size = #{size,jdbcType=BIGINT}, + where id = #{id,jdbcType=VARCHAR} @@ -220,9 +234,10 @@ update file_metadata set name = #{name,jdbcType=VARCHAR}, type = #{type,jdbcType=VARCHAR}, - size = #{size,jdbcType=BIGINT}, + engine = #{engine,jdbcType=VARCHAR}, create_time = #{createTime,jdbcType=BIGINT}, - update_time = #{updateTime,jdbcType=BIGINT} + update_time = #{updateTime,jdbcType=BIGINT}, + size = #{size,jdbcType=BIGINT} where id = #{id,jdbcType=VARCHAR} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtUserRoleMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtUserRoleMapper.xml index 80606210b0..b7d42f3e7b 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtUserRoleMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtUserRoleMapper.xml @@ -11,7 +11,7 @@ - + diff --git a/backend/src/main/java/io/metersphere/commons/constants/EngineType.java b/backend/src/main/java/io/metersphere/commons/constants/EngineType.java index 5277f90647..0199c1e6e8 100644 --- a/backend/src/main/java/io/metersphere/commons/constants/EngineType.java +++ b/backend/src/main/java/io/metersphere/commons/constants/EngineType.java @@ -1,5 +1,5 @@ package io.metersphere.commons.constants; public enum EngineType { - JMX + DOCKER, KUBERNETES } diff --git a/backend/src/main/java/io/metersphere/commons/constants/FileType.java b/backend/src/main/java/io/metersphere/commons/constants/FileType.java new file mode 100644 index 0000000000..6236b13cba --- /dev/null +++ b/backend/src/main/java/io/metersphere/commons/constants/FileType.java @@ -0,0 +1,5 @@ +package io.metersphere.commons.constants; + +public enum FileType { + JMX +} diff --git a/backend/src/main/java/io/metersphere/controller/request/TestRequest.java b/backend/src/main/java/io/metersphere/controller/request/TestRequest.java new file mode 100644 index 0000000000..bb4cdcbbf0 --- /dev/null +++ b/backend/src/main/java/io/metersphere/controller/request/TestRequest.java @@ -0,0 +1,32 @@ +package io.metersphere.controller.request; + +public class TestRequest { + + int size; + String fileString; + String testId; + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public String getFileString() { + return fileString; + } + + public void setFileString(String fileString) { + this.fileString = fileString; + } + + public String getTestId() { + return testId; + } + + public void setTestId(String testId) { + this.testId = testId; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/EngineContext.java b/backend/src/main/java/io/metersphere/engine/EngineContext.java index a6eaf79851..c88ab8cfad 100644 --- a/backend/src/main/java/io/metersphere/engine/EngineContext.java +++ b/backend/src/main/java/io/metersphere/engine/EngineContext.java @@ -1,21 +1,39 @@ package io.metersphere.engine; -import java.io.InputStream; import java.util.HashMap; import java.util.Map; public class EngineContext { - private String engineId; + private String testId; + private String testName; + private String namespace; private String engineType; - private InputStream inputStream; + private String fileType; + private String content; private Map properties = new HashMap<>(); - public String getEngineId() { - return engineId; + public String getTestId() { + return testId; } - public void setEngineId(String engineId) { - this.engineId = engineId; + public void setTestId(String testId) { + this.testId = testId; + } + + public String getTestName() { + return testName; + } + + public void setTestName(String testName) { + this.testName = testName; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; } public String getEngineType() { @@ -26,14 +44,6 @@ public class EngineContext { this.engineType = engineType; } - public InputStream getInputStream() { - return inputStream; - } - - public void setInputStream(InputStream inputStream) { - this.inputStream = inputStream; - } - public void addProperty(String key, Object value) { this.properties.put(key, value); } @@ -41,4 +51,20 @@ public class EngineContext { public Object getProperty(String key) { return this.properties.get(key); } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getFileType() { + return fileType; + } + + public void setFileType(String fileType) { + this.fileType = fileType; + } } diff --git a/backend/src/main/java/io/metersphere/engine/EngineFactory.java b/backend/src/main/java/io/metersphere/engine/EngineFactory.java index ae90a1cd89..e89df864e7 100644 --- a/backend/src/main/java/io/metersphere/engine/EngineFactory.java +++ b/backend/src/main/java/io/metersphere/engine/EngineFactory.java @@ -7,29 +7,34 @@ import io.metersphere.base.domain.FileMetadata; import io.metersphere.base.domain.LoadTestWithBLOBs; import io.metersphere.commons.constants.EngineType; import io.metersphere.commons.exception.MSException; -import io.metersphere.engine.jmx.JmxEngine; +import io.metersphere.engine.docker.DockerTestEngine; +import io.metersphere.engine.kubernetes.KubernetesTestEngine; import io.metersphere.parse.EngineSourceParser; import io.metersphere.parse.EngineSourceParserFactory; import org.apache.commons.lang3.StringUtils; import java.io.ByteArrayInputStream; -import java.io.InputStream; public class EngineFactory { public static Engine createEngine(String engineType) { final EngineType type = EngineType.valueOf(engineType); - if (type == EngineType.JMX) { - return new JmxEngine(); + switch (type) { + case DOCKER: + return new DockerTestEngine(); + case KUBERNETES: + return new KubernetesTestEngine(); } return null; } public static EngineContext createContext(LoadTestWithBLOBs loadTest, FileMetadata fileMetadata, FileContent fileContent) throws Exception { final EngineContext engineContext = new EngineContext(); - engineContext.setEngineId(loadTest.getId()); - engineContext.setInputStream(new ByteArrayInputStream(fileContent.getFile())); - engineContext.setEngineType(fileMetadata.getType()); + engineContext.setTestId(loadTest.getId()); + engineContext.setTestName(loadTest.getName()); + engineContext.setNamespace(loadTest.getProjectId()); + engineContext.setEngineType(fileMetadata.getEngine()); + engineContext.setFileType(fileMetadata.getType()); if (!StringUtils.isEmpty(loadTest.getLoadConfiguration())) { final JSONArray jsonArray = JSONObject.parseArray(loadTest.getLoadConfiguration()); @@ -40,14 +45,14 @@ public class EngineFactory { } } - final EngineSourceParser engineSourceParser = EngineSourceParserFactory.createEngineSourceParser(engineContext.getEngineType()); + final EngineSourceParser engineSourceParser = EngineSourceParserFactory.createEngineSourceParser(engineContext.getFileType()); if (engineSourceParser == null) { MSException.throwException("未知的文件类型!"); } - final InputStream inputStream = engineSourceParser.parse(engineContext, engineContext.getInputStream()); - engineContext.setInputStream(inputStream); + String content = engineSourceParser.parse(engineContext, new ByteArrayInputStream(fileContent.getFile())); + engineContext.setContent(content); return engineContext; } diff --git a/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java b/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java new file mode 100644 index 0000000000..6b1861b8bb --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java @@ -0,0 +1,59 @@ +package io.metersphere.engine.docker; + +import io.metersphere.commons.exception.MSException; +import io.metersphere.controller.request.TestRequest; +import io.metersphere.engine.Engine; +import io.metersphere.engine.EngineContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; + +public class DockerTestEngine implements Engine { + private EngineContext context; + + @Override + public boolean init(EngineContext context) { + // todo 初始化操作 + this.context = context; + return true; + } + + @Override + public void start() { + RestTemplate restTemplate = new RestTemplate(); + + String testId = context.getTestId(); + String content = context.getContent(); + + String uri = "http://localhost:8082/jmeter/container/start"; + + TestRequest testRequest = new TestRequest(); + testRequest.setSize(1); + testRequest.setTestId(testId); + testRequest.setFileString(content); + + String taskStatusUri = "http://localhost:8082/jmeter/task/status/" + testId; + List containerList = restTemplate.getForObject(taskStatusUri, List.class); + for (int i = 0; i < containerList.size(); i++) { + HashMap h = (HashMap) containerList.get(i); + if (StringUtils.equals((String)h.get("State"), "running")) { + MSException.throwException("the test is running!"); + } + } + + restTemplate.postForObject(uri, testRequest, String.class); + } + + @Override + public void stop() { + RestTemplate restTemplate = new RestTemplate(); + + String testId = context.getTestId(); + + String uri = "http://localhost:8082/jmeter/container/stop" + testId; + restTemplate.getForObject(uri, String.class); + + } +} diff --git a/backend/src/main/java/io/metersphere/engine/jmx/JmxEngine.java b/backend/src/main/java/io/metersphere/engine/jmx/JmxEngine.java deleted file mode 100644 index 19c92c1b14..0000000000 --- a/backend/src/main/java/io/metersphere/engine/jmx/JmxEngine.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.metersphere.engine.jmx; - -import io.metersphere.engine.Engine; -import io.metersphere.engine.EngineContext; -import io.metersphere.engine.EngineThread; -import io.metersphere.engine.jmx.client.DistributedRunner; -import io.metersphere.engine.jmx.client.JmeterProperties; -import org.apache.jmeter.JMeter; -import org.apache.jmeter.save.SaveService; -import org.apache.jmeter.services.FileServer; -import org.apache.jmeter.threads.ThreadGroup; -import org.apache.jorphan.collections.HashTree; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.InputStream; -import java.lang.reflect.Method; -import java.util.Set; - -public class JmxEngine extends EngineThread implements Engine { - private static final Logger log = LoggerFactory.getLogger(JmxEngine.class); - /// todo:从测试属性中读取 - private final static Integer MAX_DURATION = 60; - /// todo:从测试属性中读取 - private final static String REMOTE_HOSTS = "127.0.0.1"; - /// todo:jmeter home如何确定 - private final static String jmeterHome = "/opt/fit2cloud/apache-jmeter-5.2.1"; - private static Method readTreeMethod; - - static { - try { - readTreeMethod = SaveService.class.getDeclaredMethod("readTree", InputStream.class, File.class); - readTreeMethod.setAccessible(true); - } catch (NoSuchMethodException e) { - // ignore - } - } - - private EngineContext context; - private DistributedRunner runner; - - private static void setMaxTestDuration(HashTree jmxTree) { - for (HashTree item : jmxTree.values()) { - Set treeKeys = item.keySet(); - for (Object key : treeKeys) { - if (key instanceof ThreadGroup) { - ((ThreadGroup) key).setProperty(ThreadGroup.SCHEDULER, true); - ((ThreadGroup) key).setProperty(ThreadGroup.DURATION, MAX_DURATION); - } - } - } - } - - @Override - public String getEngineName() { - return "JMX"; - } - - @Override - public boolean init(EngineContext context) { - this.context = context; - - new JmeterProperties(JmxEngine.jmeterHome).initJmeterProperties(); - FileServer.getFileServer().setBaseForScript(new File(JmxEngine.jmeterHome + File.separator + "nothing")); - - final HashTree jmxTree = loadTree(this.context.getInputStream()); - if (jmxTree == null) { - return false; - } - - JMeter.convertSubTree(jmxTree, true); - - setMaxTestDuration(jmxTree); - - this.runner = new DistributedRunner(jmxTree, REMOTE_HOSTS); - - return true; - } - - @Override - public void run() { - try { - this.runner.run(); - } catch (Throwable e) { - log.error("run test error, id: " + this.context.getEngineId(), e); - } - } - - @Override - public void stop() { - super.stop(false); - this.runner.stop(); - } - - private HashTree loadTree(InputStream inputStream) { - try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)) { - return (HashTree) readTreeMethod.invoke(null, bufferedInputStream, null); - } catch (Exception e) { - log.error("Failed to load tree", e); - } - - return null; - } -} diff --git a/backend/src/main/java/io/metersphere/engine/jmx/client/DistributedRunner.java b/backend/src/main/java/io/metersphere/engine/jmx/client/DistributedRunner.java deleted file mode 100644 index ffd02c3e74..0000000000 --- a/backend/src/main/java/io/metersphere/engine/jmx/client/DistributedRunner.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.metersphere.engine.jmx.client; - -import org.apache.jmeter.engine.ClientJMeterEngine; -import org.apache.jmeter.engine.JMeterEngine; -import org.apache.jmeter.report.dashboard.ReportGenerator; -import org.apache.jmeter.samplers.Remoteable; -import org.apache.jmeter.testelement.TestStateListener; -import org.apache.jmeter.util.JMeterUtils; -import org.apache.jorphan.collections.HashTree; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -public class DistributedRunner extends org.apache.jmeter.engine.DistributedRunner { - private static final String HOSTS_SEPARATOR = ","; - private HashTree jmxTree; - private String hosts; - // 脚本运行完成后是否停止jmeter-server - private boolean remoteStop = false; - - public DistributedRunner(HashTree jmxTree, String hosts) { - this.jmxTree = jmxTree; - this.hosts = hosts; - } - - public void run() { - final List hosts = getRemoteHosts(); - final ListenToTest listener = new ListenToTest(remoteStop, null); - jmxTree.add(jmxTree.getArray()[0], listener); - init(hosts, jmxTree); - listener.setStartedRemoteEngines(new ArrayList<>(getEngines())); - start(); - } - - private List getRemoteHosts() { - StringTokenizer st = new StringTokenizer(hosts, HOSTS_SEPARATOR); - List list = new LinkedList<>(); - while (st.hasMoreElements()) { - list.add((String) st.nextElement()); - } - return list; - } - - private static class ListenToTest implements TestStateListener, Remoteable { - private final Logger log = LoggerFactory.getLogger(ListenToTest.class); - private final ReportGenerator reportGenerator; - private AtomicInteger startedRemoteEngines = new AtomicInteger(0); - private ConcurrentLinkedQueue remoteEngines = new ConcurrentLinkedQueue<>(); - private boolean remoteStop; - - ListenToTest(boolean remoteStop, ReportGenerator reportGenerator) { - this.remoteStop = remoteStop; - this.reportGenerator = reportGenerator; - } - - void setStartedRemoteEngines(List engines) { - this.remoteEngines.clear(); - this.remoteEngines.addAll(engines); - this.startedRemoteEngines = new AtomicInteger(remoteEngines.size()); - } - - @Override - // N.B. this is called by a daemon RMI thread from the remote host - public void testEnded(String host) { - final long now = System.currentTimeMillis(); - log.info("Finished remote host: {} ({})", host, now); - if (startedRemoteEngines.decrementAndGet() <= 0) { - log.info("All remote engines have ended test, starting RemoteTestStopper thread"); - Thread stopSoon = new Thread(() -> endTest(true), "RemoteTestStopper"); - // the calling thread is a daemon; this thread must not be - // see Bug 59391 - stopSoon.setDaemon(false); - stopSoon.start(); - } - } - - @Override - public void testEnded() { - endTest(false); - } - - @Override - public void testStarted(String host) { - final long now = System.currentTimeMillis(); - log.info("Started remote host: {} ({})", host, now); - } - - @Override - public void testStarted() { - if (log.isInfoEnabled()) { - final long now = System.currentTimeMillis(); - log.info("{} ({})", JMeterUtils.getResString("running_test"), now);//$NON-NLS-1$ - } - } - - private void endTest(boolean isDistributed) { - long now = System.currentTimeMillis(); - if (isDistributed) { - log.info("Tidying up remote @ " + new Date(now) + " (" + now + ")"); - } else { - log.info("Tidying up ... @ " + new Date(now) + " (" + now + ")"); - } - - if (isDistributed) { - if (remoteStop) { - log.info("Exiting remote servers:" + remoteEngines); - for (JMeterEngine engine : remoteEngines) { - log.info("Exiting remote server:" + engine); - engine.exit(); - } - } - try { - TimeUnit.SECONDS.sleep(5); // Allow listeners to close files - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - ClientJMeterEngine.tidyRMI(log); - } - - if (reportGenerator != null) { - try { - log.info("Generating Dashboard"); - reportGenerator.generate(); - log.info("Dashboard generated"); - } catch (Exception ex) { - System.err.println("Error generating the report: " + ex); - log.error("Error generating the report: {}", ex.getMessage(), ex); - } - } - log.info("... end of run"); - } - } -} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/engine/jmx/client/JmeterProperties.java b/backend/src/main/java/io/metersphere/engine/jmx/client/JmeterProperties.java deleted file mode 100644 index 169f584402..0000000000 --- a/backend/src/main/java/io/metersphere/engine/jmx/client/JmeterProperties.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.metersphere.engine.jmx.client; - -import org.apache.jmeter.util.JMeterUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Map; -import java.util.Properties; - -public class JmeterProperties extends Properties { - private final static Logger logger = LoggerFactory.getLogger(JmeterProperties.class); - - private final String jmeterHome; - - public JmeterProperties(String jmeterHome) { - this.jmeterHome = jmeterHome; - } - - public void initJmeterProperties() { - JMeterUtils.loadJMeterProperties(getJmeterHomeBin() + File.separator + "jmeter.properties"); - JMeterUtils.setJMeterHome(getJmeterHome()); - JMeterUtils.initLocale(); - - Properties jmeterProps = JMeterUtils.getJMeterProperties(); - - // Add local JMeter properties, if the file is found - String userProp = JMeterUtils.getPropDefault("user.properties", ""); - if (userProp.length() > 0) { - File file = JMeterUtils.findFile(userProp); - if (file.canRead()) { - try (FileInputStream fis = new FileInputStream(file)) { - Properties tmp = new Properties(); - tmp.load(fis); - jmeterProps.putAll(tmp); - } catch (IOException e) { - logger.error("Failed to init jmeter properties", e); - } - } - } - - // Add local system properties, if the file is found - String sysProp = JMeterUtils.getPropDefault("system.properties", ""); - if (sysProp.length() > 0) { - File file = JMeterUtils.findFile(sysProp); - if (file.canRead()) { - try (FileInputStream fis = new FileInputStream(file)) { - System.getProperties().load(fis); - } catch (IOException e) { - logger.error("Failed to init jmeter properties", e); - } - } - } - - jmeterProps.put("jmeter.version", JMeterUtils.getJMeterVersion()); - for (Map.Entry entry : jmeterProps.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } - - private String getJmeterHome() { - return jmeterHome; - } - - private String getJmeterHomeBin() { - return getJmeterHome() + File.separator + "bin"; - } -} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java b/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java new file mode 100644 index 0000000000..f803ad032d --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java @@ -0,0 +1,77 @@ +package io.metersphere.engine.kubernetes; + +import com.alibaba.fastjson.JSON; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.engine.Engine; +import io.metersphere.engine.EngineContext; +import io.metersphere.engine.kubernetes.crds.jmeter.Jmeter; +import io.metersphere.engine.kubernetes.crds.jmeter.JmeterSpec; +import io.metersphere.engine.kubernetes.provider.ClientCredential; +import io.metersphere.engine.kubernetes.provider.KubernetesProvider; + +import java.util.HashMap; + +public class KubernetesTestEngine implements Engine { + private EngineContext context; + + @Override + public boolean init(EngineContext context) { + // todo 初始化操作 + this.context = context; + return true; + } + + + @Override + public void start() { + if (context == null) { + LogUtil.warn("Please initial the engine."); + return; + } + // todo 运行测试 + ClientCredential credential = new ClientCredential(); + credential.setMasterUrl("https://172.16.10.93:6443"); + KubernetesProvider kubernetesProvider = new KubernetesProvider(JSON.toJSONString(credential)); + // create namespace + kubernetesProvider.confirmNamespace(context.getNamespace()); + // create cm + try (KubernetesClient client = kubernetesProvider.getKubernetesClient()) { + String configMapName = context.getTestId() + "-files"; + ConfigMap configMap = client.configMaps().inNamespace(context.getNamespace()).withName(configMapName).get(); + if (configMap == null) { + ConfigMap item = new ConfigMap(); + item.setMetadata(new ObjectMeta() {{ + setName(configMapName); + }}); + item.setData(new HashMap() {{ + put(context.getTestId() + ".jmx", context.getContent()); + }}); + client.configMaps().inNamespace(context.getNamespace()).create(item); + } + } + // create jmeter + try { + Jmeter jmeter = new Jmeter(); + jmeter.setMetadata(new ObjectMeta() {{ + setNamespace(context.getNamespace()); + setName(context.getTestId()); + }}); + jmeter.setSpec(new JmeterSpec() {{ + setReplicas(1); + setImage("registry.fit2cloud.com/metersphere/jmeter-master:0.0.2"); + }}); + LogUtil.info("Load test started. " + context.getTestId()); + kubernetesProvider.applyCustomResource(jmeter); + } catch (Exception e) { + LogUtil.error(e); + } + } + + @Override + public void stop() { + + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResource.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResource.java new file mode 100644 index 0000000000..91a0dce6a2 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResource.java @@ -0,0 +1,36 @@ +package io.metersphere.engine.kubernetes.crds; + +import io.fabric8.kubernetes.client.CustomResource; + +public class MeterSphereCustomResource extends CustomResource { + + private String crd; + + private Object spec; + + private Object status; + + public String getCrd() { + return crd; + } + + public void setCrd(String crd) { + this.crd = crd; + } + + public Object getSpec() { + return spec; + } + + public void setSpec(Object spec) { + this.spec = spec; + } + + public Object getStatus() { + return status; + } + + public void setStatus(Object status) { + this.status = status; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResourceDoneable.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResourceDoneable.java new file mode 100644 index 0000000000..f3075fa695 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResourceDoneable.java @@ -0,0 +1,10 @@ +package io.metersphere.engine.kubernetes.crds; + +import io.fabric8.kubernetes.api.builder.Function; +import io.fabric8.kubernetes.client.CustomResourceDoneable; + +public class MeterSphereCustomResourceDoneable extends CustomResourceDoneable { + public MeterSphereCustomResourceDoneable(T resource, Function function) { + super(resource, function); + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResourceList.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResourceList.java new file mode 100644 index 0000000000..086cf9e1b6 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/MeterSphereCustomResourceList.java @@ -0,0 +1,6 @@ +package io.metersphere.engine.kubernetes.crds; + +import io.fabric8.kubernetes.client.CustomResourceList; + +public class MeterSphereCustomResourceList extends CustomResourceList { +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/Jmeter.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/Jmeter.java new file mode 100644 index 0000000000..0727157ecc --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/Jmeter.java @@ -0,0 +1,33 @@ +package io.metersphere.engine.kubernetes.crds.jmeter; + +import io.metersphere.engine.kubernetes.crds.MeterSphereCustomResource; + +public class Jmeter extends MeterSphereCustomResource { + public static final String CRD = "jmeters.metersphere.io"; + public static final String KIND = "Jmeter"; + private JmeterSpec spec; + private JmeterStatus status; + + public Jmeter() { + this.setCrd(CRD); + this.setKind(KIND); + } + + @Override + public JmeterSpec getSpec() { + return spec; + } + + public void setSpec(JmeterSpec spec) { + this.spec = spec; + } + + @Override + public JmeterStatus getStatus() { + return status; + } + + public void setStatus(JmeterStatus status) { + this.status = status; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterDoneable.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterDoneable.java new file mode 100644 index 0000000000..42acb26aef --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterDoneable.java @@ -0,0 +1,10 @@ +package io.metersphere.engine.kubernetes.crds.jmeter; + +import io.fabric8.kubernetes.api.builder.Function; +import io.fabric8.kubernetes.client.CustomResourceDoneable; + +public class JmeterDoneable extends CustomResourceDoneable { + public JmeterDoneable(Jmeter resource, Function function) { + super(resource, function); + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterSpec.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterSpec.java new file mode 100644 index 0000000000..d2baaac44d --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterSpec.java @@ -0,0 +1,28 @@ +package io.metersphere.engine.kubernetes.crds.jmeter; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.fabric8.kubernetes.api.model.KubernetesResource; + +@JsonDeserialize +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class JmeterSpec implements KubernetesResource { + private int replicas = 1; + private String image; + + public int getReplicas() { + return replicas; + } + + public void setReplicas(int replicas) { + this.replicas = replicas; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterStatus.java b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterStatus.java new file mode 100644 index 0000000000..9b24290bf7 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/crds/jmeter/JmeterStatus.java @@ -0,0 +1,36 @@ +package io.metersphere.engine.kubernetes.crds.jmeter; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.fabric8.kubernetes.api.model.KubernetesResource; + +@JsonDeserialize +@JsonInclude(JsonInclude.Include.NON_NULL) +public class JmeterStatus implements KubernetesResource { + private String phase; + private String reason; + + public String getPhase() { + return phase; + } + + public void setPhase(String phase) { + this.phase = phase; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + @Override + public String toString() { + return "JmeterStatus{" + + "phase='" + phase + '\'' + + ", reason='" + reason + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/provider/AbstractClientProvider.java b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/AbstractClientProvider.java new file mode 100644 index 0000000000..ba65cd2cb5 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/AbstractClientProvider.java @@ -0,0 +1,267 @@ +package io.metersphere.engine.kubernetes.provider; + +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.*; +import io.metersphere.engine.kubernetes.crds.MeterSphereCustomResource; +import io.metersphere.engine.kubernetes.crds.MeterSphereCustomResourceDoneable; +import io.metersphere.engine.kubernetes.crds.MeterSphereCustomResourceList; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.regex.Pattern; + +public abstract class AbstractClientProvider { + private static ObjectMapper objectMapper = new ObjectMapper(); + + private String credential; + + public AbstractClientProvider(String credential) { + setCredential(credential); + } + + public String getCredential() { + return credential; + } + + public void setCredential(String credential) { + this.credential = credential; + } + + /** + * OpenShiftClient继承自KubernetesClient,OpenShiftClient对OpenShift和kubernetes的共有资源也是用KubernetesClient代理的, + * 所以可以在把client提取到抽象类 + */ + public KubernetesClient getKubernetesClient() { + ClientCredential providerCredential = JSONObject.parseObject(getCredential(), ClientCredential.class); + io.fabric8.kubernetes.client.ConfigBuilder configBuilder = new ConfigBuilder(); + configBuilder.withMasterUrl(providerCredential.getMasterUrl()); + configBuilder.withOauthToken(providerCredential.getToken()); + configBuilder.withTrustCerts(true); + //设置默认的 namespace 为 null, + configBuilder.withNamespace(null); + return new DefaultKubernetesClient(configBuilder.build()); + } + + + public void validateCredential() { + try (KubernetesClient client = getKubernetesClient()) { + client.namespaces().list(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + /** + * 确保指定的namespace存在,不存在直接创建一个 + * + * @param namespace namespace标识 + * @return + */ + public synchronized String confirmNamespace(String namespace) { + KubernetesClient kubernetesClient = getKubernetesClient(); + Namespace currentNamespace = kubernetesClient.namespaces().withName(namespace).get(); + if (currentNamespace == null) { + Map annotations = new HashMap<>(); + Namespace newNamespace = new NamespaceBuilder() + .withNewMetadata() + .withName(namespace) + .withAnnotations(annotations) + .endMetadata() + .build(); + currentNamespace = kubernetesClient.namespaces().createOrReplace(newNamespace); + } + return currentNamespace.getMetadata().getName(); + } + + + /** + * 获取集群UUID,当前集群UUID等于default namespace的UID + * + * @return + */ + public String getClusterUUID() { + KubernetesClient kubernetesClient = getKubernetesClient(); + Namespace defaultNamespace = kubernetesClient.namespaces().withName("kube-system").get(); + if (defaultNamespace == null) { + throw new RuntimeException("无法获取集群的kube-system namespace"); + } else { + return defaultNamespace.getMetadata().getUid(); + } + } + + + /** + * 确保docker registry存在, + * 不存在 创建一个harbor-secret,并修改serviceaccount default + * + * @param registry + */ + public void dockerRegistry(DockerRegistry registry) { + + if (StringUtils.isEmpty(registry.getUsername()) || + StringUtils.isEmpty(registry.getPassword()) || + StringUtils.isEmpty(registry.getUrl()) + ) { + throw new RuntimeException("Please set the docker registry information"); + } + + String secretName = "docker-registry-ms-secret"; + KubernetesClient kubernetesClient = getKubernetesClient(); + + Secret secretRegistry = new SecretBuilder() + .withNewMetadata().withName(secretName).endMetadata() + .addToData(".dockerconfigjson", DockerRegistryUtil.getDockerConfig(registry)) + .withType("kubernetes.io/dockerconfigjson") + .build(); + kubernetesClient.secrets().inNamespace(registry.getNamespace()).createOrReplace(secretRegistry); + + //sa + ServiceAccount serviceAccount = kubernetesClient.serviceAccounts().inNamespace(registry.getNamespace()) + .withName("default").get(); + List imagePullSecrets = serviceAccount.getImagePullSecrets(); + for (LocalObjectReference pullSecret : imagePullSecrets) { + if (secretName.equals(pullSecret.getName())) { + return; + } + } + + LocalObjectReference localObjectReference = new LocalObjectReference(secretName); + imagePullSecrets.add(localObjectReference); + serviceAccount.setImagePullSecrets(imagePullSecrets); + + kubernetesClient.serviceAccounts().inNamespace(registry.getNamespace()) + .createOrReplace(serviceAccount); + + } + + + public T applyCustomResource(MeterSphereCustomResource customResource) throws Exception { + try (KubernetesClient kubernetesClient = getKubernetesClient()) { + CustomResourceDefinition crd = kubernetesClient.customResourceDefinitions().withName(customResource.getCrd()).get(); + if (crd == null) { + throw new Exception("CRD does not exists."); + } + MixedOperation> + operation = kubernetesClient.customResources(crd, MeterSphereCustomResource.class, MeterSphereCustomResourceList.class, MeterSphereCustomResourceDoneable.class); + MeterSphereCustomResource replace = operation.inNamespace(customResource.getMetadata().getNamespace()).createOrReplace(customResource); + return (T) objectMapper.readValue(objectMapper.writeValueAsString(replace), customResource.getClass()); + } + } + + + public boolean deleteCustomResource(MeterSphereCustomResource customResource) { + try (KubernetesClient kubernetesClient = getKubernetesClient()) { + CustomResourceDefinition crd = kubernetesClient.customResourceDefinitions().withName(customResource.getCrd()).get(); + MixedOperation> + operation = kubernetesClient.customResources(crd, MeterSphereCustomResource.class, MeterSphereCustomResourceList.class, MeterSphereCustomResourceDoneable.class); + Boolean result = operation.inNamespace(customResource.getMetadata().getNamespace()).withName(customResource.getMetadata().getName()).cascading(true).delete(); + return result == null ? false : result; + } + } + + + public T getCustomResource(MeterSphereCustomResource customResource) throws Exception { + try (KubernetesClient kubernetesClient = getKubernetesClient()) { + CustomResourceDefinition crd = kubernetesClient.customResourceDefinitions().withName(customResource.getCrd()).get(); + MixedOperation> + operation = kubernetesClient.customResources(crd, MeterSphereCustomResource.class, MeterSphereCustomResourceList.class, MeterSphereCustomResourceDoneable.class); + MeterSphereCustomResource meterSphereCustomResource = operation.inNamespace(customResource.getMetadata().getNamespace()).withName(customResource.getMetadata().getName()).get(); + if (meterSphereCustomResource == null) { + return null; + } + return (T) objectMapper.readValue(objectMapper.writeValueAsString(meterSphereCustomResource), customResource.getClass()); + } + } + + + public List listCustomResource(MeterSphereCustomResource customResource) throws Exception { + try (KubernetesClient kubernetesClient = getKubernetesClient()) { + CustomResourceDefinition crd = kubernetesClient.customResourceDefinitions().withName(customResource.getCrd()).get(); + MixedOperation> + operation = kubernetesClient.customResources(crd, MeterSphereCustomResource.class, MeterSphereCustomResourceList.class, MeterSphereCustomResourceDoneable.class); + MeterSphereCustomResourceList list; + if (StringUtils.isNotEmpty(customResource.getMetadata().getNamespace())) { + list = operation.inNamespace(customResource.getMetadata().getNamespace()).list(); + } else { + list = operation.inAnyNamespace().list(); + } + + List resultList = new ArrayList<>(); + for (Object cr : list.getItems()) { + resultList.add((T) objectMapper.readValue(objectMapper.writeValueAsString(cr), customResource.getClass())); + } + return resultList; + } + } + + + public boolean checkPVCNotExists(String namespace, String statefulsetName) { + KubernetesClient kubernetesClient = getKubernetesClient(); + NonNamespaceOperation> operation = kubernetesClient.persistentVolumeClaims() + .inNamespace(namespace); + + PersistentVolumeClaimList pvcList = operation.list(100, null); + Pattern compile = Pattern.compile(statefulsetName + "-\\d+"); + return checkPVCNotExists(pvcList, compile, operation); + } + + private boolean checkPVCNotExists(PersistentVolumeClaimList pvcList, Pattern compile, NonNamespaceOperation> operation) { + if (pvcList == null || CollectionUtils.isEmpty(pvcList.getItems())) { + return true; + } + Optional claimOptional = pvcList.getItems().stream().filter(pvc -> compile.matcher(pvc.getMetadata().getName()).matches()).findAny(); + if (claimOptional.isPresent()) { + return false; + } else if (StringUtils.isNotEmpty(pvcList.getMetadata().getContinue())) { + return checkPVCNotExists(operation.list(100, pvcList.getMetadata().getContinue()), compile, operation); + } else { + return true; + } + } + + public boolean deletePVC(HasMetadata hasMetadata) { + KubernetesClient kubernetesClient = getKubernetesClient(); + NonNamespaceOperation> operation = kubernetesClient.persistentVolumeClaims() + .inNamespace(hasMetadata.getMetadata().getNamespace()); + + if (MapUtils.isNotEmpty(hasMetadata.getMetadata().getLabels())) { + operation.withLabelSelector(new LabelSelector(null, hasMetadata.getMetadata().getLabels())); + } + if (StringUtils.isNotEmpty(hasMetadata.getMetadata().getName())) { + operation.withName(hasMetadata.getMetadata().getName()); + } + Boolean delete = operation.delete(); + return delete == null ? false : delete; + } + + + public boolean deleteNamespace(String namespace) { + KubernetesClient kubernetesClient = getKubernetesClient(); + Boolean delete = kubernetesClient.namespaces().withName(namespace).delete(); + + return delete == null ? false : delete; + } + + + public String getLog(String namespace, String pod, String container, int tailingLines) { + try (KubernetesClient client = getKubernetesClient()) { + PrettyLoggable loggable; + if (tailingLines > 0) { + loggable = client.pods().inNamespace(namespace).withName(pod).inContainer(container).tailingLines(tailingLines); + } else { + loggable = client.pods().inNamespace(namespace).withName(pod).inContainer(container); + } + + return loggable.getLog(); + } + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/provider/ClientCredential.java b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/ClientCredential.java new file mode 100644 index 0000000000..1a73a4d442 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/ClientCredential.java @@ -0,0 +1,23 @@ +package io.metersphere.engine.kubernetes.provider; + +public class ClientCredential { + + private String masterUrl; + private String token; + + public String getMasterUrl() { + return masterUrl; + } + + public void setMasterUrl(String masterUrl) { + this.masterUrl = masterUrl; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/provider/DockerRegistry.java b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/DockerRegistry.java new file mode 100644 index 0000000000..344afdbbac --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/DockerRegistry.java @@ -0,0 +1,50 @@ +package io.metersphere.engine.kubernetes.provider; + +public class DockerRegistry { + + private String url; + private String username; + private String password; + private String email; + private String namespace; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/provider/DockerRegistryUtil.java b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/DockerRegistryUtil.java new file mode 100644 index 0000000000..11340e7f2f --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/DockerRegistryUtil.java @@ -0,0 +1,24 @@ +package io.metersphere.engine.kubernetes.provider; + +import com.alibaba.fastjson.JSONObject; + +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +public class DockerRegistryUtil { + + + public static String getDockerConfig(DockerRegistry registry) { + Map config = new HashMap<>(); + config.put("username", registry.getUsername()); + config.put("password", registry.getPassword()); + config.put("auth", Base64.getEncoder().encodeToString((registry.getUsername() + ":" + registry.getPassword()).getBytes())); + + JSONObject jb = new JSONObject(); + jb.put(registry.getUrl(), config); + JSONObject result = new JSONObject(); + result.put("auths", jb); + return Base64.getEncoder().encodeToString(result.toJSONString().getBytes()); + } +} diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/provider/KubernetesProvider.java b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/KubernetesProvider.java new file mode 100644 index 0000000000..26a41c29a4 --- /dev/null +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/provider/KubernetesProvider.java @@ -0,0 +1,7 @@ +package io.metersphere.engine.kubernetes.provider; + +public class KubernetesProvider extends AbstractClientProvider { + public KubernetesProvider(String credential) { + super(credential); + } +} diff --git a/backend/src/main/java/io/metersphere/parse/EngineSourceParser.java b/backend/src/main/java/io/metersphere/parse/EngineSourceParser.java index ef3a8d8c0e..25de3624d2 100644 --- a/backend/src/main/java/io/metersphere/parse/EngineSourceParser.java +++ b/backend/src/main/java/io/metersphere/parse/EngineSourceParser.java @@ -5,5 +5,5 @@ import io.metersphere.engine.EngineContext; import java.io.InputStream; public interface EngineSourceParser { - InputStream parse(EngineContext context, InputStream source) throws Exception; + String parse(EngineContext context, InputStream source) throws Exception; } diff --git a/backend/src/main/java/io/metersphere/parse/EngineSourceParserFactory.java b/backend/src/main/java/io/metersphere/parse/EngineSourceParserFactory.java index 5fd673fdbc..6659d8d17d 100644 --- a/backend/src/main/java/io/metersphere/parse/EngineSourceParserFactory.java +++ b/backend/src/main/java/io/metersphere/parse/EngineSourceParserFactory.java @@ -1,13 +1,13 @@ package io.metersphere.parse; -import io.metersphere.commons.constants.EngineType; +import io.metersphere.commons.constants.FileType; import io.metersphere.parse.xml.XmlEngineSourceParse; public class EngineSourceParserFactory { public static EngineSourceParser createEngineSourceParser(String type) { - final EngineType engineType = EngineType.valueOf(type); + final FileType engineType = FileType.valueOf(type); - if (EngineType.JMX.equals(engineType)) { + if (FileType.JMX.equals(engineType)) { return new XmlEngineSourceParse(); } diff --git a/backend/src/main/java/io/metersphere/parse/xml/XmlEngineSourceParse.java b/backend/src/main/java/io/metersphere/parse/xml/XmlEngineSourceParse.java index 42648601bb..db8dfe02ae 100644 --- a/backend/src/main/java/io/metersphere/parse/xml/XmlEngineSourceParse.java +++ b/backend/src/main/java/io/metersphere/parse/xml/XmlEngineSourceParse.java @@ -13,7 +13,7 @@ import java.io.InputStream; public class XmlEngineSourceParse implements EngineSourceParser { @Override - public InputStream parse(EngineContext context, InputStream source) throws Exception { + public String parse(EngineContext context, InputStream source) throws Exception { final InputSource inputSource = new InputSource(source); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -21,7 +21,7 @@ public class XmlEngineSourceParse implements EngineSourceParser { DocumentBuilder docBuilder = factory.newDocumentBuilder(); final Document document = docBuilder.parse(inputSource); - final DocumentParser documentParser = createDocumentParser(context.getEngineType()); + final DocumentParser documentParser = createDocumentParser(context.getFileType()); return documentParser.parse(context, document); } diff --git a/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParser.java b/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParser.java index 3e4813760d..2ab6f67a72 100644 --- a/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParser.java +++ b/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParser.java @@ -6,5 +6,5 @@ import org.w3c.dom.Document; import java.io.InputStream; public interface DocumentParser { - InputStream parse(EngineContext context, Document document) throws Exception; + String parse(EngineContext context, Document document) throws Exception; } diff --git a/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParserFactory.java b/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParserFactory.java index 218499951b..d5772d174a 100644 --- a/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParserFactory.java +++ b/backend/src/main/java/io/metersphere/parse/xml/reader/DocumentParserFactory.java @@ -1,16 +1,18 @@ package io.metersphere.parse.xml.reader; -import io.metersphere.commons.constants.EngineType; +import io.metersphere.commons.constants.FileType; import io.metersphere.parse.xml.reader.jmx.JmeterDocumentParser; public class DocumentParserFactory { public static DocumentParser createDocumentParser(String type) { - final EngineType engineType = EngineType.valueOf(type); + final FileType fileType = FileType.valueOf(type); - if (EngineType.JMX.equals(engineType)) { - return new JmeterDocumentParser(); + switch (fileType) { + case JMX: + return new JmeterDocumentParser(); + default: + break; } - return null; } } diff --git a/backend/src/main/java/io/metersphere/parse/xml/reader/jmx/JmeterDocumentParser.java b/backend/src/main/java/io/metersphere/parse/xml/reader/jmx/JmeterDocumentParser.java index 4575d34f5e..5437a58d3c 100644 --- a/backend/src/main/java/io/metersphere/parse/xml/reader/jmx/JmeterDocumentParser.java +++ b/backend/src/main/java/io/metersphere/parse/xml/reader/jmx/JmeterDocumentParser.java @@ -15,13 +15,11 @@ import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.io.StringWriter; -import java.nio.charset.StandardCharsets; public class JmeterDocumentParser implements DocumentParser { private final static String HASH_TREE_ELEMENT = "hashTree"; + private final static String TEST_PLAN = "TestPlan"; private final static String STRING_PROP = "stringProp"; private final static String COLLECTION_PROP = "collectionProp"; private final static String CONCURRENCY_THREAD_GROUP = "com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup"; @@ -32,7 +30,7 @@ public class JmeterDocumentParser implements DocumentParser { private EngineContext context; @Override - public InputStream parse(EngineContext context, Document document) throws Exception { + public String parse(EngineContext context, Document document) throws Exception { this.context = context; final Element jmeterTestPlan = document.getDocumentElement(); @@ -49,18 +47,17 @@ public class JmeterDocumentParser implements DocumentParser { } } - return documentToInputStream(document); + return documentToString(document); } - private InputStream documentToInputStream(Document document) throws TransformerException { + private String documentToString(Document document) throws TransformerException { DOMSource domSource = new DOMSource(document); StringWriter writer = new StringWriter(); StreamResult result = new StreamResult(writer); TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.transform(domSource, result); - final String resultStr = writer.toString(); - return new ByteArrayInputStream(resultStr.getBytes(StandardCharsets.UTF_8)); + return writer.toString(); } private void parseHashTree(Element hashTree) { @@ -80,6 +77,8 @@ public class JmeterDocumentParser implements DocumentParser { if (nodeNameEquals(ele, HASH_TREE_ELEMENT)) { parseHashTree(ele); + } else if (nodeNameEquals(ele, TEST_PLAN)) { + processTearDownTestPlan(ele); } else if (nodeNameEquals(ele, CONCURRENCY_THREAD_GROUP)) { processConcurrencyThreadGroup(ele); processCheckoutTimer(ele); @@ -100,12 +99,107 @@ public class JmeterDocumentParser implements DocumentParser { } } + private void processTearDownTestPlan(Element ele) { + /*true*/ + Document document = ele.getOwnerDocument(); + Element tearDownSwitch = createBoolProp(document, "TestPlan.tearDown_on_shutdown", true); + ele.appendChild(tearDownSwitch); + + Node hashTree = ele.getNextSibling(); + while (!(hashTree instanceof Element)) { + hashTree = hashTree.getNextSibling(); + } + /* + + continue + + false + 1 + + 1 + 1 + false + + + true + + */ + Element tearDownElement = document.createElement("PostThreadGroup"); + tearDownElement.setAttribute("guiclass", "PostThreadGroupGui"); + tearDownElement.setAttribute("testclass", "PostThreadGroup"); + tearDownElement.setAttribute("testname", "tearDown Thread Group"); + tearDownElement.setAttribute("enabled", "true"); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "continue")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.num_threads", "1")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.ramp_time", "1")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.duration", "")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.delay", "")); + tearDownElement.appendChild(createBoolProp(document, "ThreadGroup.scheduler", false)); + tearDownElement.appendChild(createBoolProp(document, "ThreadGroup.same_user_on_next_iteration", true)); + Element elementProp = document.createElement("elementProp"); + elementProp.setAttribute("name", "ThreadGroup.main_controller"); + elementProp.setAttribute("elementType", "LoopController"); + elementProp.setAttribute("guiclass", "LoopControlPanel"); + elementProp.setAttribute("testclass", "LoopController"); + elementProp.setAttribute("testname", "Loop Controller"); + elementProp.setAttribute("enabled", "true"); + elementProp.appendChild(createBoolProp(document, "LoopController.continue_forever", false)); + elementProp.appendChild(createStringProp(document, "LoopController.loops", "1")); + tearDownElement.appendChild(elementProp); + hashTree.appendChild(tearDownElement); + + Element tearDownHashTree = document.createElement(HASH_TREE_ELEMENT); + /* + + */ + Element onceOnlyController = document.createElement("OnceOnlyController"); + onceOnlyController.setAttribute("guiclass", "OnceOnlyControllerGui"); + onceOnlyController.setAttribute("testclass", "OnceOnlyController"); + onceOnlyController.setAttribute("testname", "Once Only Controller"); + onceOnlyController.setAttribute("enabled", "true"); + tearDownHashTree.appendChild(onceOnlyController); + /* + + + false + true + false + + + + */ + Element onceOnlyHashTree = document.createElement(HASH_TREE_ELEMENT); + Element debugSampler = document.createElement("DebugSampler"); + debugSampler.setAttribute("guiclass", "TestBeanGUI"); + debugSampler.setAttribute("testclass", "DebugSampler"); + debugSampler.setAttribute("testname", "Debug Sampler"); + debugSampler.setAttribute("enabled", "true"); + debugSampler.appendChild(createBoolProp(document, "displayJMeterProperties", false)); + debugSampler.appendChild(createBoolProp(document, "displayJMeterVariables", true)); + debugSampler.appendChild(createBoolProp(document, "displaySystemProperties", false)); + onceOnlyHashTree.appendChild(debugSampler); + // 添加空的 hashTree + onceOnlyHashTree.appendChild(document.createElement(HASH_TREE_ELEMENT)); + tearDownHashTree.appendChild(onceOnlyHashTree); + hashTree.appendChild(tearDownHashTree); + // 添加backend listener + processCheckoutBackendListener(tearDownElement); + } + + private Element createBoolProp(Document document, String name, boolean value) { + Element tearDownSwitch = document.createElement("boolProp"); + tearDownSwitch.setAttribute("name", name); + tearDownSwitch.appendChild(document.createTextNode(String.valueOf(value))); + return tearDownSwitch; + } + private void processBackendListener(Element backendListener) { KafkaProperties kafkaProperties = CommonBeanFactory.getBean(KafkaProperties.class); Document document = backendListener.getOwnerDocument(); // 清空child removeChildren(backendListener); backendListener.appendChild(createStringProp(document, "classname", "io.github.rahulsinghai.jmeter.backendlistener.kafka.KafkaBackendClient")); + backendListener.appendChild(createStringProp(document, "QUEUE_SIZE", "5000")); // elementProp Element elementProp = document.createElement("elementProp"); elementProp.setAttribute("name", "arguments"); @@ -139,6 +233,9 @@ public class JmeterDocumentParser implements DocumentParser { collectionProp.appendChild(createKafkaProp(document, "kafka.batch.size", kafkaProperties.getBatchSize())); collectionProp.appendChild(createKafkaProp(document, "kafka.client.id", kafkaProperties.getClientId())); collectionProp.appendChild(createKafkaProp(document, "kafka.connections.max.idle.ms", kafkaProperties.getConnectionsMaxIdleMs())); + // 添加关联关系 test.id test.name + collectionProp.appendChild(createKafkaProp(document, "test.id", context.getTestId())); + collectionProp.appendChild(createKafkaProp(document, "test.name", context.getTestName())); elementProp.appendChild(collectionProp); // set elementProp @@ -263,6 +360,8 @@ public class JmeterDocumentParser implements DocumentParser { collectionProp.appendChild(childCollectionProp); timer.appendChild(collectionProp); timerParent.appendChild(timer); + // 添加一个空的hashTree + timerParent.appendChild(document.createElement(HASH_TREE_ELEMENT)); } private Element createStringProp(Document document, String name, String value) { diff --git a/backend/src/main/java/io/metersphere/service/FuctionalTestService.java b/backend/src/main/java/io/metersphere/service/FuctionalTestService.java index ca87cdbb59..aaf1f5e736 100644 --- a/backend/src/main/java/io/metersphere/service/FuctionalTestService.java +++ b/backend/src/main/java/io/metersphere/service/FuctionalTestService.java @@ -93,7 +93,9 @@ public class FuctionalTestService { fileMetadata.setSize(file.getSize()); fileMetadata.setCreateTime(System.currentTimeMillis()); fileMetadata.setUpdateTime(System.currentTimeMillis()); - fileMetadata.setType(EngineType.JMX.name()); + fileMetadata.setType("jmx"); + // TODO engine 选择 + fileMetadata.setEngine(EngineType.DOCKER.name()); fileMetadataMapper.insert(fileMetadata); FileContent fileContent = new FileContent(); diff --git a/backend/src/main/java/io/metersphere/service/LoadTestService.java b/backend/src/main/java/io/metersphere/service/LoadTestService.java index f85cad86f5..eacc35d222 100644 --- a/backend/src/main/java/io/metersphere/service/LoadTestService.java +++ b/backend/src/main/java/io/metersphere/service/LoadTestService.java @@ -1,10 +1,14 @@ package io.metersphere.service; import io.metersphere.base.domain.*; -import io.metersphere.base.mapper.*; +import io.metersphere.base.mapper.FileContentMapper; +import io.metersphere.base.mapper.FileMetadataMapper; +import io.metersphere.base.mapper.LoadTestFileMapper; +import io.metersphere.base.mapper.LoadTestMapper; import io.metersphere.base.mapper.ext.ExtLoadTestMapper; import io.metersphere.commons.constants.EngineType; import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.utils.LogUtil; import io.metersphere.controller.request.testplan.*; import io.metersphere.dto.LoadTestDTO; import io.metersphere.engine.Engine; @@ -29,8 +33,6 @@ public class LoadTestService { @Resource private ExtLoadTestMapper extLoadTestMapper; @Resource - private ProjectMapper projectMapper; - @Resource private FileMetadataMapper fileMetadataMapper; @Resource private FileContentMapper fileContentMapper; @@ -51,7 +53,7 @@ public class LoadTestService { public String save(SaveTestPlanRequest request, MultipartFile file) { if (file == null) { - throw new IllegalArgumentException("文件不能为空!"); + throw new IllegalArgumentException(Translator.get("file_cannot_be_null")); } final FileMetadata fileMetadata = saveFile(file); @@ -95,7 +97,9 @@ public class LoadTestService { fileMetadata.setSize(file.getSize()); fileMetadata.setCreateTime(System.currentTimeMillis()); fileMetadata.setUpdateTime(System.currentTimeMillis()); - fileMetadata.setType(EngineType.JMX.name()); + fileMetadata.setType("JMX"); + // TODO engine 选择 + fileMetadata.setEngine(EngineType.DOCKER.name()); fileMetadataMapper.insert(fileMetadata); FileContent fileContent = new FileContent(); @@ -123,7 +127,7 @@ public class LoadTestService { final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(request.getId()); if (loadTest == null) { - MSException.throwException("无法编辑测试,未找到测试:" + request.getId()); + MSException.throwException(Translator.get("edit_load_test_not_found") + request.getId()); } else { loadTest.setName(request.getName()); loadTest.setProjectId(request.getProjectId()); @@ -141,23 +145,25 @@ public class LoadTestService { public void run(RunTestPlanRequest request) { final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(request.getId()); if (loadTest == null) { - MSException.throwException("无法运行测试,未找到测试:" + request.getId()); + MSException.throwException(Translator.get("run_load_test_not_found") + request.getId()); } final FileMetadata fileMetadata = fileService.getFileMetadataByTestId(request.getId()); if (fileMetadata == null) { - MSException.throwException("无法运行测试,无法获取测试文件元信息,测试ID:" + request.getId()); + MSException.throwException(Translator.get("run_load_test_file_not_found") + request.getId()); } final FileContent fileContent = fileService.getFileContent(fileMetadata.getId()); if (fileContent == null) { - MSException.throwException("无法运行测试,无法获取测试文件内容,测试ID:" + request.getId()); + MSException.throwException(Translator.get("run_load_test_file_content_not_found") + request.getId()); } - System.out.println("开始运行:" + loadTest.getName()); - final Engine engine = EngineFactory.createEngine(fileMetadata.getType()); + LogUtil.info("Load test started " + loadTest.getName()); + // engine type (DOCKER|KUBERNETES) + // todo set type + final Engine engine = EngineFactory.createEngine(fileMetadata.getEngine()); if (engine == null) { - MSException.throwException(String.format("无法运行测试,未识别测试文件类型,测试ID:%s,文件类型:%s", + MSException.throwException(String.format("Test cannot be run,test ID:%s,file type:%s", request.getId(), fileMetadata.getType())); } @@ -169,12 +175,12 @@ public class LoadTestService { MSException.throwException(e); } if (!init) { - MSException.throwException(String.format("无法运行测试,初始化运行环境失败,测试ID:%s", request.getId())); + MSException.throwException(Translator.get("run_load_test_file_init_error") + request.getId()); } engine.start(); - /// todo:通过调用stop方法能够停止正在运行的engine,但是如果部署了多个backend实例,页面发送的停止请求如何定位到具体的engine + // todo:通过调用stop方法能够停止正在运行的engine,但是如果部署了多个backend实例,页面发送的停止请求如何定位到具体的engine } public List recentTestPlans(QueryTestPlanRequest request) { diff --git a/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql b/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql index 3b402a3855..5e3491e7e0 100644 --- a/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql +++ b/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql @@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS `file_metadata` ( `id` varchar(64) NOT NULL COMMENT 'File ID', `name` varchar(64) NOT NULL COMMENT 'File name', `type` varchar(64) DEFAULT NULL COMMENT 'File type', + `engine` varchar(64) DEFAULT 'DOCKER' COMMENT 'engine type', `size` bigint(13) NOT NULL COMMENT 'File size', `create_time` bigint(13) NOT NULL COMMENT 'Create timestamp', `update_time` bigint(13) NOT NULL COMMENT 'Update timestamp', diff --git a/backend/src/main/resources/i18n/en-US.json b/backend/src/main/resources/i18n/en-US.json index 8b66b212cf..4d1a6251c1 100644 --- a/backend/src/main/resources/i18n/en-US.json +++ b/backend/src/main/resources/i18n/en-US.json @@ -6,5 +6,11 @@ "workspace_name_is_null": "Workspace name cannot be null", "workspace_name_already_exists": "The workspace name already exists", "workspace_does_not_belong_to_user": "The current workspace does not belong to the current user", - "organization_does_not_belong_to_user": "The current organization does not belong to the current user" + "organization_does_not_belong_to_user": "The current organization does not belong to the current user", + "file_cannot_be_null": "File cannot be empty!", + "edit_load_test_not_found": "Cannot edit test, test not found:", + "run_load_test_not_found": "Cannot run test, test not found:", + "run_load_test_file_not_found": "Unable to run test, unable to get test file meta information, test ID:", + "run_load_test_file_content_not_found": "Cannot run test, cannot get test file content, test ID:", + "run_load_test_file_init_error": "Failed to run test, failed to initialize run environment, test ID:" } \ No newline at end of file diff --git a/backend/src/main/resources/i18n/zh-CN.json b/backend/src/main/resources/i18n/zh-CN.json index 58a2502874..85ca41c996 100644 --- a/backend/src/main/resources/i18n/zh-CN.json +++ b/backend/src/main/resources/i18n/zh-CN.json @@ -6,5 +6,11 @@ "workspace_name_is_null": "工作空间名不能为空", "workspace_name_already_exists": "工作空间名已存在", "workspace_does_not_belong_to_user": "当前工作空间不属于当前用户", - "organization_does_not_belong_to_user": "当前组织不属于当前用户" + "organization_does_not_belong_to_user": "当前组织不属于当前用户", + "file_cannot_be_null": "文件不能为空!", + "edit_load_test_not_found": "无法编辑测试,未找到测试:", + "run_load_test_not_found": "无法运行测试,未找到测试:", + "run_load_test_file_not_found": "无法运行测试,无法获取测试文件元信息,测试ID:", + "run_load_test_file_content_not_found": "无法运行测试,无法获取测试文件内容,测试ID:", + "run_load_test_file_init_error": "无法运行测试,初始化运行环境失败,测试ID:" } \ No newline at end of file diff --git a/backend/src/test/java/io/metersphere/BaseTest.java b/backend/src/test/java/io/metersphere/BaseTest.java new file mode 100644 index 0000000000..8c56d027ab --- /dev/null +++ b/backend/src/test/java/io/metersphere/BaseTest.java @@ -0,0 +1,34 @@ +package io.metersphere; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.Before; +import org.junit.Test; + +public class BaseTest { + protected KubernetesClient kubernetesClient; + + @Before + public void before() { + + try { + ConfigBuilder configBuilder = new ConfigBuilder(); + configBuilder.withMasterUrl("https://172.16.10.93:6443"); + kubernetesClient = new DefaultKubernetesClient(configBuilder.build()); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + + } + + @Test + public void test1() { + PodList list = kubernetesClient.pods().list(); + for (Pod item : list.getItems()) { + System.out.println(item); + } + } +} diff --git a/backend/src/test/java/io/metersphere/JmxFileParseTest.java b/backend/src/test/java/io/metersphere/JmxFileParseTest.java index b7ff2fb536..1d2eaaded8 100644 --- a/backend/src/test/java/io/metersphere/JmxFileParseTest.java +++ b/backend/src/test/java/io/metersphere/JmxFileParseTest.java @@ -27,6 +27,7 @@ import java.io.StringWriter; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class JmxFileParseTest { private final static String HASH_TREE_ELEMENT = "hashTree"; + private final static String TEST_PLAN = "TestPlan"; private final static String STRING_PROP = "stringProp"; private final static String CONCURRENCY_THREAD_GROUP = "com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup"; private final static String VARIABLE_THROUGHPUT_TIMER = "kg.apc.jmeter.timers.VariableThroughputTimer"; @@ -94,6 +95,8 @@ public class JmxFileParseTest { if (nodeNameEquals(ele, HASH_TREE_ELEMENT)) { parseHashTree(ele); + } else if (nodeNameEquals(ele, TEST_PLAN)) { + processTearDownTestPlan(ele); } else if (nodeNameEquals(ele, CONCURRENCY_THREAD_GROUP)) { processConcurrencyThreadGroup(ele); processCheckoutTimer(ele); @@ -113,11 +116,106 @@ public class JmxFileParseTest { } } + private void processTearDownTestPlan(Element ele) { + /*true*/ + Document document = ele.getOwnerDocument(); + Element tearDownSwitch = createBoolProp(document, "TestPlan.tearDown_on_shutdown", true); + ele.appendChild(tearDownSwitch); + + Node hashTree = ele.getNextSibling(); + while (!(hashTree instanceof Element)) { + hashTree = hashTree.getNextSibling(); + } + /* + + continue + + false + 1 + + 1 + 1 + false + + + true + + */ + Element tearDownElement = document.createElement("PostThreadGroup"); + tearDownElement.setAttribute("guiclass", "PostThreadGroupGui"); + tearDownElement.setAttribute("testclass", "PostThreadGroup"); + tearDownElement.setAttribute("testname", "tearDown Thread Group"); + tearDownElement.setAttribute("enabled", "true"); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "continue")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.num_threads", "1")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.ramp_time", "1")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.duration", "")); + tearDownElement.appendChild(createStringProp(document, "ThreadGroup.delay", "")); + tearDownElement.appendChild(createBoolProp(document, "ThreadGroup.scheduler", false)); + tearDownElement.appendChild(createBoolProp(document, "ThreadGroup.same_user_on_next_iteration", true)); + Element elementProp = document.createElement("elementProp"); + elementProp.setAttribute("name", "ThreadGroup.main_controller"); + elementProp.setAttribute("elementType", "LoopController"); + elementProp.setAttribute("guiclass", "LoopControlPanel"); + elementProp.setAttribute("testclass", "LoopController"); + elementProp.setAttribute("testname", "Loop Controller"); + elementProp.setAttribute("enabled", "true"); + elementProp.appendChild(createBoolProp(document, "LoopController.continue_forever", false)); + elementProp.appendChild(createStringProp(document, "LoopController.loops", "1")); + tearDownElement.appendChild(elementProp); + hashTree.appendChild(tearDownElement); + + Element tearDownHashTree = document.createElement(HASH_TREE_ELEMENT); + /* + + */ + Element onceOnlyController = document.createElement("OnceOnlyController"); + onceOnlyController.setAttribute("guiclass", "OnceOnlyControllerGui"); + onceOnlyController.setAttribute("testclass", "OnceOnlyController"); + onceOnlyController.setAttribute("testname", "Once Only Controller"); + onceOnlyController.setAttribute("enabled", "true"); + tearDownHashTree.appendChild(onceOnlyController); + /* + + + false + true + false + + + + */ + Element onceOnlyHashTree = document.createElement(HASH_TREE_ELEMENT); + Element debugSampler = document.createElement("DebugSampler"); + debugSampler.setAttribute("guiclass", "TestBeanGUI"); + debugSampler.setAttribute("testclass", "DebugSampler"); + debugSampler.setAttribute("testname", "Debug Sampler"); + debugSampler.setAttribute("enabled", "true"); + debugSampler.appendChild(createBoolProp(document, "displayJMeterProperties", false)); + debugSampler.appendChild(createBoolProp(document, "displayJMeterVariables", true)); + debugSampler.appendChild(createBoolProp(document, "displaySystemProperties", false)); + onceOnlyHashTree.appendChild(debugSampler); + // 添加空的 hashTree + onceOnlyHashTree.appendChild(document.createElement(HASH_TREE_ELEMENT)); + tearDownHashTree.appendChild(onceOnlyHashTree); + hashTree.appendChild(tearDownHashTree); + // 添加backend listener + processCheckoutBackendListener(tearDownElement); + } + + private Element createBoolProp(Document document, String name, boolean value) { + Element tearDownSwitch = document.createElement("boolProp"); + tearDownSwitch.setAttribute("name", name); + tearDownSwitch.appendChild(document.createTextNode(String.valueOf(value))); + return tearDownSwitch; + } + private void processBackendListener(Element backendListener) { Document document = backendListener.getOwnerDocument(); // 清空child removeChildren(backendListener); backendListener.appendChild(createStringProp(document, "classname", "io.github.rahulsinghai.jmeter.backendlistener.kafka.KafkaBackendClient")); + backendListener.appendChild(createStringProp(document, "QUEUE_SIZE", "5000")); // elementProp Element elementProp = document.createElement("elementProp"); elementProp.setAttribute("name", "arguments"); @@ -151,6 +249,9 @@ public class JmxFileParseTest { collectionProp.appendChild(createKafkaProp(document, "kafka.batch.size", kafkaProperties.getBatchSize())); collectionProp.appendChild(createKafkaProp(document, "kafka.client.id", kafkaProperties.getClientId())); collectionProp.appendChild(createKafkaProp(document, "kafka.connections.max.idle.ms", kafkaProperties.getConnectionsMaxIdleMs())); + // 添加关联关系 test.id test.name + collectionProp.appendChild(createKafkaProp(document, "test.id", "")); + collectionProp.appendChild(createKafkaProp(document, "test.name", "")); elementProp.appendChild(collectionProp); // set elementProp