# 原子任务开发说明
# 数据库脚本定义实现
DPS_ENGINE_STAGE_TEMPLATE 表: 原子任务模板定义表,用于定义任务的名称、类型和图标等。
CREATE TABLE DPS_ENGINE_STAGE_TEMPLATE ( STAGE_TP_ID VARCHAR(64) NOT NULL COMMENT '唯一标识', STAGE_TP_NAME VARCHAR(64) COMMENT '任务名称', STAGE_TP_LABEL VARCHAR(64) COMMENT '任务标签名称', STAGE_TP_TYPE VARCHAR(64) COMMENT '任务类型', STAGE_TP_TYPE_LABEL VARCHAR(64) COMMENT '任务类型标签名称', STAGE_TAGS VARCHAR(512) COMMENT '任务标签', STAGE_TP_ICON VARCHAR(512) COMMENT '任务ICON路径', COMMON_STAGE_TPS VARCHAR(1024) COMMENT '公共任务模板列表,逗号分隔', STAGE_HANDLER TEXT COMMENT '任务拦截器类名', ESTIMATED_DURATION INTEGER COMMENT '预估持续时间', DESCRIPTION TEXT COMMENT '任务描述', IS_HIDDEN BOOLEAN COMMENT '是否隐藏', PRIMARY KEY (STAGE_TP_ID) ) COMMENT='任务模板表'
注意事项
STAGE_TP_NAME(任务名称):需要保证唯一性,不能出现冲突。
STAGE_HANDLER(任务拦截器类):实现了
IStageHandler
接口的类才可用作原子任务的拦截器(可参考com.primeton.devops.ci.helper.CiResourceStageHandler
)。
IStageHandler
接口中有afterScriptAttributes
和afterExecute
两个方法:afterScriptAttributes
主要用于在流水线任务执行前做一些数据准备;afterExecute
主要用于在流水线任务执行完后回调到 DevOps 中做一些入库操作。
DPS_ENGINE_STAGE_ATTR_DEF 表: 原子任务属性定义表,用于定义任务属性字段的名称、标签、类别和控件类型等。
CREATE TABLE DPS_ENGINE_STAGE_ATTR_DEF ( ATTR_DEF_ID VARCHAR(64) NOT NULL COMMENT '唯一标识', ATTR_DEF_NAME VARCHAR(64) COMMENT '任务属性定义名称', ATTR_DEF_LABEL VARCHAR(64) COMMENT '任务属性定义标签名称', STAGE_TP_ID VARCHAR(64) COMMENT '任务模板ID', SORT FLOAT COMMENT '任务属性顺序', CATEGORY VARCHAR(64) COMMENT '任务属性类别', IS_REQUIRED BOOLEAN COMMENT '是否为必填项', TIP TEXT COMMENT '提示信息', DEFAULT_VALUE TEXT COMMENT '默认值', CONTROL_TYPE VARCHAR(64) COMMENT '控件类型', VALUE_PROVIDER TEXT COMMENT '值提供者', OPTIONS TEXT COMMENT '控件其它相关信息', CHECK_POLICY VARCHAR(64) COMMENT '校验策略', PRIMARY KEY (ATTR_DEF_ID) ) COMMENT='任务属性定义表';
CONTROL_TYPE(控件类型):用于定义属性在前端页面所显示的控件类型。
控件类型列表如下:
控件类型 说明 textbox 文本输入框 combobox 下拉选框。需配合 VALUE_PROVIDER 字段使用 dict 下拉选框,用于获取业务字典的值。需配合 VALUE_PROVIDER 字段使用 checkbox 复选框 editor 文本编辑器 large-select 需配合 VALUE_PROVIDER 字段使用 VALUE_PROVIDER(值提供者):用于定义属性的值来源。属性值来源包括
接口响应值
、业务字典项值
、枚举
等。配置规则如下:
CONTROL_TYPE VALUE_PROVIDER 字段配置说明 combobox 1)示例: url
中可以使用:varName
进行变量引用拼串,现在默认的变量有projectId
、envType
、当前控件名字
、childrenRequestValueField对应字段名字
以及valueField对应字段的名字
等。
{"url":"/api/pcm/templates?templateType=thymeleaf&withTemplateContent=true&scopeTargetId=:projectId&templateScope=PROJECT","textField":"templateName","valueField":"templateContent","value":"", "multiSelect":false}
2)联动示例:childName
是联动的控件的名字
{"url":"api/vcs/repos?projectId=:projectId&repoTypes=Github,Gitlab,Bitbucket,Tfs","textField":"repoUrl","valueField":"repoUrl","value": "","multiSelect":false,"childName":"branch","childrenRequestValueField":"repoId","childUrl":"api/vcs/repos/:repoId/branches"}
联动的控件的 VALUE_PROVIDER 值:{"textField":"branchName","valueField":"branchName","value":"","multiSelect":false,"allowAnyData":true}
3)枚举示例:enumData
用于定义枚举
{"multiSelect":false,"textField":"label","value":"","valueField":"value","enumData":[{"value":"1","label":"选项一"},{"value":"2","label":"选项二"}]}dict 模板:{"type":"dictcombobox","dictTypeId":"业务字典名称"}
示例:
{"type":"dictcombobox","dictTypeId":"DPS_CD_PIPELINE_DATABASE_TYPE"}large-select 示例:
{"url":"api/cd/project-resources?projectId=:projectId&envType=:envType&resourceType=host","textField":"resourceName,hostAddr","valueField":"resourceId","value":"","multiSelect":true,"filters":[{"label":"名称","field":"resourceName"},{"label":"标签","field":"labelName"}],"tableColumnFields":[{"label":"名称","field":"resourceName"},{"label":"IP地址","field":"hostAddr"},{"label":"标签","field":"labelNames"}],"valueDetailSearchField":"resourceId"}
# Groovy 执行脚本实现
Groovy 脚本文件定义
- Groovy 执行脚本文件存放于
devops-engine
工程的src/META-INF/pipeline/stages
目录下 - 脚本文件需放在与任务类型(STAGE_TP_TYPE)同名的目录下。比如
STAGE_TP_TYPE='build'
,则脚本文件就放在build
目录下 - 脚本文件名称需与任务名称(STAGE_TP_NAME)相同。比如
STAGE_TP_NAME='git-pull'
,则脚本文件的名称需取为git-pull.groovy
- 脚本入口方法名需与任务名称(STAGE_TP_NAME)相同,但如果任务名称中有中横杠"-",则需将其转化为下划线"_"。比如
STAGE_TP_NAME='git-pull'
,则脚本入口方法定义为def git_pull(...)
- Groovy 执行脚本文件存放于
公共实现
- 可以直接使用 Jenkins 提供的 Groovy 方法,也可以直接使用系统提供的 common.groovy 脚本中的方法。
- 可以使用
//import_files("stages/tool/methods/upload.groovy")
引用其他 Groovy 脚本中的方法,需从stages
目录开始。
# 示例
实现一个拉取 Git 代码库的流水线任务,且需满足如下条件:
- 代码库从项目中已关联的代码库列表中获取
- 拉取完代码后自动切换分支
- 分支需与代码库联动(即切换代码库后,分支列表需为切换后的代码库下的)
- 支持自定义目录进行拉取代码
- 支持在目标机器或 Jenkins 节点上进行拉取代码
# 数据库脚本定义
-- 任务模板定义
INSERT INTO DPS_ENGINE_STAGE_TEMPLATE (STAGE_TP_ID, STAGE_TP_NAME, STAGE_TP_LABEL, STAGE_TP_TYPE, STAGE_TP_TYPE_LABEL, STAGE_HANDLER, ESTIMATED_DURATION, DESCRIPTION, STAGE_TAGS, STAGE_TP_ICON, COMMON_STAGE_TPS)
VALUES ('10', 'git-pull', '拉取Git代码', 'code', '代码', 'com.primeton.devops.ci.helper.CiResourceStageHandler', 5000, '从代码库中拉取源代码', 'build,deploy', '/static/images/component_logo/git.svg', 'all-common');
-- 任务属性定义
INSERT INTO DPS_ENGINE_STAGE_ATTR_DEF (ATTR_DEF_ID, ATTR_DEF_NAME, ATTR_DEF_LABEL, STAGE_TP_ID, SORT, CATEGORY, IS_REQUIRED, TIP, DEFAULT_VALUE, CONTROL_TYPE, VALUE_PROVIDER, OPTIONS, CHECK_POLICY)
VALUES ('1000', 'url', '代码库', '10', 1, '1:基本信息', 1, NULL, NULL, 'combobox' , '{"url":"api/vcs/repos?projectId=:projectId&repoTypes=Github,Gitlab,Bitbucket,Tfs","textField":"repoUrl","valueField":"repoUrl","value": "","multiSelect":false,"childName":"branch","childrenRequestValueField":"repoId","childUrl":"api/vcs/repos/:repoId/branches"}', '{"needClean": "true"}', NULL);
INSERT INTO DPS_ENGINE_STAGE_ATTR_DEF (ATTR_DEF_ID, ATTR_DEF_NAME, ATTR_DEF_LABEL, STAGE_TP_ID, SORT, CATEGORY, IS_REQUIRED, TIP, DEFAULT_VALUE, CONTROL_TYPE, VALUE_PROVIDER, OPTIONS, CHECK_POLICY)
VALUES ('1001', 'branch', 'branch/tag/commitId', '10', 2, '1:基本信息', 1, '分支(branch) 标签(tag) 或 Git提交唯一标识(commitId),默认值master', 'master', 'combobox', '{"textField":"branchName","valueField":"branchName","value":"","multiSelect":false,"allowAnyData":true}', '{"needClean": "true"}', NULL);
INSERT INTO DPS_ENGINE_STAGE_ATTR_DEF (ATTR_DEF_ID, ATTR_DEF_NAME, ATTR_DEF_LABEL, STAGE_TP_ID, SORT, CATEGORY, IS_REQUIRED, TIP, DEFAULT_VALUE, CONTROL_TYPE, VALUE_PROVIDER, OPTIONS, CHECK_POLICY)
VALUES ('1004', 'checkoutDir', 'checkout目录', '10', 3,'1:基本信息', NULL, '默认为空,即克隆仓库文件到工作空间根目录,否则克隆到指定的目录下。(该参数多用于多代码库联合构建)<br>【注意】在其他构建任务中配置文件路径需要考虑所设置的checkout目录', NULL, 'textbox', NULL, NULL, NULL);
INSERT INTO DPS_ENGINE_STAGE_ATTR_DEF (ATTR_DEF_ID, ATTR_DEF_NAME, ATTR_DEF_LABEL, STAGE_TP_ID, SORT, CATEGORY, IS_REQUIRED, TIP, DEFAULT_VALUE, CONTROL_TYPE, VALUE_PROVIDER, OPTIONS, CHECK_POLICY)
VALUES ('1005', 'resources', '目标机器', '10', 1, '2:目标机器信息', NULL, '如果为空,则为当前jenkins节点;否则为所选机器', NULL, 'large-select', '{"url":"api/cd/project-resources?projectId=:projectId&envType=:envType&resourceType=host","textField":"resourceName,hostAddr","valueField":"resourceId","value":"","multiSelect":false,"filters":[{"label":"名称","field":"resourceName"},{"label":"标签","field":"labelName"}],"tableColumnFields":[{"label":"名称","field":"resourceName"},{"label":"IP地址","field":"hostAddr"},{"label":"标签","field":"labelNames"}],"valueDetailSearchField":"resourceId"}', '{"needClean": "true"}', NULL);
INSERT INTO DPS_ENGINE_STAGE_ATTR_DEF (ATTR_DEF_ID, ATTR_DEF_NAME, ATTR_DEF_LABEL, STAGE_TP_ID, SORT, CATEGORY, IS_REQUIRED, TIP, DEFAULT_VALUE, CONTROL_TYPE, VALUE_PROVIDER, OPTIONS, CHECK_POLICY)
VALUES ('1006', 'userDir', '用户权限目录', '10', 2, '2:目标机器信息', NULL, '目标机器执行命令的用户权限目录', NULL, 'textbox', NULL, NULL, NULL);
# 拦截器实现
package com.primeton.devops.ci.helper;
import com.primeton.devops.specs.api.engine.service.IStageHandler;
// 省略其他引入
@org.springframework.stereotype.Component
public class CiResourceStageHandler implements IStageHandler {
@Override
public void afterScriptAttributes(Map<String, Object> pipelineContext, EngineStage engineStage, Map<String, Object> stageAttrs, Map<String, Object> params) throws Exception {
String stageTpName = engineStage.getEngineStageTemplate().getStageTpName();
if (CiConstants.GIT_PULL_STAGE_TP_NAME.equals(stageTpName)) {// 代码拉取构建任务
String url = (String) stageAttrs.get("url");
ValidateUtil.assertNotNullOrEmpty(url, "codeRepoUrl");
CodeRepository codeRepository = baseDao.getEntityByProperty(CodeRepository.QNAME, "projectId", engineStage.getProjectId(), "repoUrl", url);
if (codeRepository == null) {
throw new DevOpsException(DevOps.VCS.VCS_REPOSITORY_NOT_EXISTED, url);
}
stageAttrs.put("repoId", codeRepository.getRepoId());
stageAttrs.put("userName", codeRepository.getUserName());
stageAttrs.put("password", codeRepository.getPassword() == null ? codeRepository.getApiPrivateToken() : passwordService.getPlaintext(codeRepository.getPassword(), false));
String systemBranch = (String) params.get(CiConstants.SYSTEM_PARAM_BRANCH + codeRepository.getRepoId());
if (StringUtils.isNotBlank(systemBranch)) {
stageAttrs.put("branch", systemBranch);
}
}
}
@Override
public void afterExecute(EnginePipeline enginePipeline, EnginePipelineInstance enginePipelineInstance, EngineStage engineStage, EngineStageInstance engineStageInstance) throws Exception {
}
}
# Groovy 脚本定义
因当前示例的任务模板定义中 STAGE_TP_NAME='git-pull'
以及 STAGE_TP_TYPE='code'
,则在devops-engine
工程的 src/META-INF/pipeline/stages/code
目录下新建一个名为 git-pull.groovy
的脚本文件
# Groovy 执行脚本实现
git-pull.groovy
脚本内容
/**
* 引入groovy脚本或ansible脚本
*/
//import_files("stages/code/basic-git-pull.groovy")
/**
* 脚本入口方法
*/
def git_pull(pipelineContext, pipelineResult, stageAttrs, stageResult) {
basic_git_pull(pipelineContext, pipelineResult, stageAttrs, stageResult)
pipelineContext.global.codeRepoId = "${stageAttrs.repoId}"
}
git-pull.groovy
脚本中引入的 basic-git-pull.groovy
脚本内容
//import_files("ansible/git/git-clone-exec.yml")
def basic_git_pull(pipelineContext, pipelineResult, stageAttrs, stageResult) {
// Git拉取代码基础任务
def targetIps = stageAttrs.targetIps
if (isNullOrBlank(targetIps)) {
def workspaceDir = getContext hudson.FilePath
if (isNullOrBlank(stageAttrs.checkoutDir)) {
stageAttrs.checkoutDir = "."
}
checkCodeCheckoutDir(stageAttrs.checkoutDir)
def checkoutDir = workspaceDir.child("${stageAttrs.checkoutDir}")
if (!checkoutDir.exists()) {
checkoutDir.mkdirs()
}
dir("${stageAttrs.checkoutDir}") {
if (stageAttrs.url.startsWith('git@')) {
checkout([$class: 'GitSCM', branches: [[name: "${stageAttrs.branch}"]],
extensions: [[$class: 'CheckoutOption', timeout: 60],[$class: 'CloneOption', timeout: 60]],
userRemoteConfigs: [[url: "${stageAttrs.url}", credentialsId: getSshCredentialsId("${stageAttrs.sshPrivateKey}", "${stageAttrs.userName}", null)]]])
} else {
def codeRepoUrl = stageAttrs.url
if (codeRepoUrl.startsWith('http://') || codeRepoUrl.startsWith('https://')) {
codeRepoUrl = codeRepoUrl.replace("http://", "http://${stageAttrs.userName}@").replace("https://", "https://${stageAttrs.userName}@")
}
checkout([$class: 'GitSCM', branches: [[name: "${stageAttrs.branch}"]],
extensions: [[$class: 'CheckoutOption', timeout: 60],[$class: 'CloneOption', timeout: 60]],
userRemoteConfigs: [[url: "${codeRepoUrl}", credentialsId: getCredentialsId("${stageAttrs.userName}", "${stageAttrs.password}")]]])
}
stageResult.commitId = executeScript("git rev-parse HEAD", true)
stageResult.shortCommitId = executeScript("git rev-parse --short=7 HEAD", true)
stageResult.commitMessage = executeScript("git show -s --format=%B", true)
stageResult.committer = executeScript("git show -s --format=%cn", true)
stageResult.commitTime = executeScript("git show -s --format=%cI", true)
stageResult.trueBranchName = stageAttrs.branch
pipelineContext.global.nextData.codeCommitId = stageResult.commitId
try {
def trueBranchs = executeScript("git branch --contains ${stageAttrs.branch}", true).split('\n')
echo toJsonString(trueBranchs, true)
if (trueBranchs.length > 0) {
def trueBranchName = trueBranchs[trueBranchs.length - 1].trim()
if (trueBranchName.startsWith('*')) {
trueBranchName = trueBranchName.substring(1).trim()
}
if (trueBranchName.startsWith('/')) {
trueBranchName = trueBranchName.substring(1).trim()
}
def index = trueBranchName.indexOf('origin/')
if (index != -1) {
trueBranchName = trueBranchName.substring(index + 'origin/'.length()).trim()
}
if (!trueBranchName.contains('(')) {
stageResult.trueBranchName = trueBranchName
}
}
} catch (e) {
echo "$e"
}
}
} else {
def codeRepoUrl = stageAttrs.url
def checkoutDir = stageAttrs.checkoutDir
def userDir = stageAttrs.userDir
if (isNullOrBlank(checkoutDir)) {
checkoutDir = userDir
}
if (codeRepoUrl.startsWith('http://') || codeRepoUrl.startsWith('https://')) {
def userName = java.net.URLEncoder.encode(stageAttrs.userName)
def password = java.net.URLEncoder.encode(stageAttrs.password)
codeRepoUrl = codeRepoUrl.replace("http://", "http://${userName}:${password}@").replace("https://", "https://${userName}:${password}@")
}
executeAnsibleScript pipelineContext: pipelineContext, stageAttrs: stageAttrs, playbook: 'git/git-clone-exec.yml',
extras: """\
--extra-vars \"install_host=$targetIps repo_url=$codeRepoUrl checkout_dir=$checkoutDir refs=${stageAttrs.branch} \"\
"""
}
if (stageAttrs.repoId) {
stageResult.repoId = "${stageAttrs.repoId}"
}
stageResult.url = "${stageAttrs.url}"
stageResult.branch = "${stageAttrs.branch}"
stageResult.userName = "${stageAttrs.userName}"
stageResult.local = "${stageAttrs.checkoutDir}"
}