# 原子任务开发说明

# 数据库脚本定义实现

  1. 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 接口中有 afterScriptAttributesafterExecute 两个方法:afterScriptAttributes 主要用于在流水线任务执行前做一些数据准备;afterExecute 主要用于在流水线任务执行完后回调到 DevOps 中做一些入库操作。

  2. 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 进行变量引用拼串,现在默认的变量有 projectIdenvType当前控件名字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 执行脚本实现

  1. 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(...)
  2. 公共实现

    • 可以直接使用 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);

demo

# 拦截器实现

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}"
}
上次更新: 2023-4-10 17:58:23