Skip to content

chihongze/acolyte

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Acolyte

概述

Acolyte是一个软件发布、部署的串联工具,传统CI工具的工作流程多是专注于监听版本控制 => 创建环境 => 获取最新的代码副本 => 执行构建 => 运行测试用例 => 反馈结果。然而在复杂的工程实践中,这些事情只是其中的一个环节,真实的发布部署流程中往往会存在繁琐复杂的人机交互,甚至是需要跨部门的参与,比如SQL审核、代码review、QA对测试结果的反馈、灰度发布及回滚等等,我们需要有一个工具将这些流程串联起来,并对相关人员进行引导,同时能记录和展示每一次的工作进度、历史以及收集每个环节的数据。Acolyte就是一个这样的工具,它提供了一个流程框架,允许你根据自己的开发流程快速构建出一个串联系统。

基本元素

Acolyte本质上是一个交互式的工作流,用户通过编写一个FlowMeta类来定义一个工作流,工作流的每个环节通过一个Job来表示。

Job

Job描述了Flow中一个环节的处理逻辑,一个Job是由一个或多个Action组成的,比如在我们的开发流程中,项目经过TravisCI构建成功后,接下来会被让运维部署到沙箱环境当中,部署到沙箱的这个过程就可以被抽象为一个SandboxJob:

from acolyte.core.job import (
	AbstractJob,
	JobArg
)
from acolyte.util.validate import (
	StrField,
)

class SandboxJob(AbstractJob):

	def __init__(self):
		super().__init__(
			name="sandbox",
			description="部署jar包到沙箱",
			job_args={
				"trigger": [
					JobArg("version", StrField("version", required=True), mark=JobArg.MARK_AUTO, comment="Jar包版本号")
				],
				"reject": [
					JobArg("reason", StrField("reason", required=True, min_len=10), mark=JobArg.MARK_AUTO, comment="拒绝原因")
				]
			}
		)
		
	def on_trigger(self, context, version):
		ops_email = get_ops_email()
		send_mail(
			receiver=ops_email,
			subject="运维哥哥求部署到沙箱, 版本: {}".format(version),
			mail_content=render_mail(version=version)
		)
		
	def on_agree(self, context):
		deploy_to_sandbox()
		send_mail(
			receiver=get_dev_email(),
			subject="成功部署到沙箱!",
			mail_content=render_mail()
		)
		context.finish()  # 标记该Job已经被完成了
		
	def on_reject(self, context, reason):
		send_mail(
			receiver=get_dev_email(),
			subject="目前无法部署到沙箱",
			mail_content=render_mail(reason=reason)
		)
		context.save(data={"reason": reason})
		context.stop()  # 无法部署到沙箱,此次部署行动失败,flow终止

每个Job都需要去继承AbstractJob,并且包含一些on_xxx方法,每个这样的方法就是一个action,其中on_trigger方法是必须要定义的,它描述了该Job被触发时所进行的动作,在这个例子中,SandboxJob被触发时会给运维发送一封邮件,告诉运维jar包已经就绪,可以部署到沙箱了。运维收到邮件之后,可以选择两个动作,点击同意部署,就会触发on_agree方法,将jar包部署到沙箱,通过调用context.finish()来标记该Job已经成功结束;如果不同意,那么会触发on_disagree方法,并且传递一个reason参数来告诉开发当前因为某某原因无法部署到沙箱,通过调用context.stop()方法会标记整个flow终止,无法继续前进下去。

每个on_xxx方法都可以接受一个上下文对象和一些自定义的参数,上下文对象(context)可以提供数据存储以及一些操控flow运行流程的方法:

# 可以像字典一样存取数据,这样保存的数据在flow的执行期内有效,可用于Job之间的数据传递
context["a"] = "hehe"
print(context["a"])

# 将当前Action的计算数据保存起来,这些数据可以用于在web平台上渲染Job终端页
# 比如测试成败、测试覆盖率之类的数据都可以这样保存
context.save({"reason": reason})

# context.finish() # 标记当前Job可以成功结束,Flow可以安全运行下个Job
# context.stop() # 该Job失败,Flow无法继续前进,用该方法标记终止

每个action其余所需要的自定义参数会通过在构造方法中由bind_args进行声明,每个JobArg代表一个参数声明,其中包含了该参数的名称、验证规则、mark以及注释。

定义参数验证规则

为确保传递给action的参数是正确无误的,Acolyte要求您必须声明每个参数的验证规则,您只需要声明规则即可,Acolyte会根据这些规则自动为您把关每一次用户输入。

规则通过acolyte.util.validate.Field对象来进行声明:

Field(
	name: ...,
	type_: ...,
	required: ...,
	default: ...,
	value_of: ...,
	check_logic: ...
)
  • name: 参数名称
  • type_: 期望的类型,比如int或者str等等
  • required: 该字段是不是必须的
  • default: 如果不是必须的,请提供一个默认值
  • value_of: 如果该字段不能满足期望的类型,那么可以通过该函数来对值进行转换,比如通过int()将一个字符串转换从整形,如果该函数依旧转型失败,那么就会返回类型错误了。
  • check_logic: 用户自定义的校验逻辑

同时Field类派生了IntField和StrField这两个子类:

IntField(
	name: ...,
	required: ...,
	default: ...,
	value_of: ...,
	min_: ..., 
	max_: ...,
	check_logic: ...
)
  • min_: 该参数不能小于min_所指定的值。
  • max_: 该参数不能大于max_所指定的值。
StrField(
	name: ..., 
	required: ...,
	default: ..., 
	value_of: ...,
	min_len: ..., 
	max_len: ...,
	regex: ...,
	check_logic: ...
)
  • min_len: 该参数长度不能低于min_len所指定的值
  • max_len: 该参数长度不能大于max_len所指定的值
  • regex: 该参数必须满足此正则表达式

mark

mark参数描述了参数值的覆盖规则。

  • JobArg.MARK_AUTO: auto参数,用户的输入可以覆盖flow meta和flow template中的绑定默认值。
  • JobArg.MARK_STATIC: static参数,该参数的最终值只能由flow template和flow meta来定义,flow template可以覆盖flow meta中绑定的值,但是用户的输入是无法覆盖此值的。
  • JobArg.MARK_CONST: const参数,只能由flow meta来绑定,其余层级无法覆盖此值。

关于flow meta和flow template的概念会在flow的描述中说明。

comment

描述该参数的作用,会在帮助页面中展示。

所有的Job都会通过JobManager进行管理,通过name进行引用,JobManager通过acolyte.job_definition这个entry_point来在应用初始化时加载并实例化所有的Job,基于entry_point加载的优势在于您可以随意在您自己的项目中定义Job,acolyte都能找到并加载它们。

Flow

Flow是Job的组合,但是它拥有三个层次的形态:

  • FlowMeta: 最原始的Flow定义,由Job按顺序编排而成。
  • FlowTemplate: 由对FlowMeta一些参数的覆盖而得来,比如虽然公司完成了对部署流程的统一,大家都可以使用同一个FlowMeta定义的流程,但是各个部门之间有一些默认参数是存在差异的,那么每个部门就可以基于同一个FlowMeta配置各自的FlowTemplate。
  • FlowInstance: 当一个FlowTemplate运行起来的时候,描述此次运行的过程就是FlowInstance对象。

嗯,可以把FlowMeta理解成一个函数,FlowTemplate理解成一个FlowMeta被Currying之后的函数,FlowInstance是该函数的一次具体执行过程。

Flow Meta

定义一个FlowMeta:

class TestFlowMeta(FlowMeta):

    def __init__(self):
        super().__init__(
            name="test_flow",
            description="just a test flow",
            jobs=(
                JobRef(
                    step_name="pull_request",
                    job_name="pull_request",
                    trigger={
							
                    },
                ),
                JobRef(
                		step_name="code_review",
                		job_name="code_review",
                ),
                JobRef(
                		step_name="sandbox",
                		job_name="sandbox"
                ),
                ...
          ))
          
    def on_start(self, ctx, argA, argB, ...):
    	# do something init
    	...
    	
    def on_stop(self, ctx):
    	...
    	
    def on_finish(self, ctx):
    	...

定义FlowMeta必须继承一个FlowMeta基类,并在构造方法中传递一串Job定义,通过JobRef来引用一个Job,可以在一个FlowMeta中引用多次相同的Job,但必须通过step_name将它们区分开。通过JobRef还可以为每个Job的Action指定默认的参数值。

FlowMeta还提供了三个生命周期方法,即on_start,on_stop、on_finish:

  1. on_start: 在FlowInstance刚刚创建完成之后运行,此时还没有任何Job被执行。
  2. on_stop: 当有Job中的context.stop()被执行时会触发该方法,即Flow被人为终止。
  3. on_finish:当所有flow中的所有Job都被执行完毕之后会回调该方法。

FlowMeta需要硬编码在项目中,系统会通过一个FlowMetaManager来管理所有的FlowMeta对象,同Job一样,也是基于entry_point的管理方式:acolyte.flow_meta_definition。

Flow Template

Flow Template是基于FlowMeta创建,创建FlowTemplate无需写代码,直接调用接口即可,例如

[PUT] /v1/flow/template

req body:

{
	flow_meta_name: "deploy",
	template_name: "deploy_for_rest",
	bind_args: {
		pull_request: {
			trigger: {
				...
			}
		}
	},
	max_run_instance: 1,
	description: "REST项目组专用部署流程"
}
  • flow_meta_name: 基于的flow meta名称
  • template_name: 新建template的名称
  • bind_args: 绑定参数,不能绑定被mark为const的参数,层次为step_name=>action_name=>args
  • max_run_instance: 最大同时运行实例数目,比如我在同一时间只允许一个REST项目的部署流程在跑,那么就可以指定为1,Acolyte会在FlowInstance启动时进行检查,确保同一时间只有指定数目的实例运行。
  • description: 说明

Flow Instance

启动一个实例

当某一事件触发时(比如通过Github webhook检测到有人pull requests),调用该接口来创建一个Flow Instance

[POST] /v1/flow/template/{flow_template_id}/start

req_body

{
	initiator: 100010,
	description: "庆十五大更新!",
	start_flow_args: {
		...
	}
}
  • initiator: 触发用户ID
  • description: 此次任务描述
  • start_flow_args: 触发FlowMeta中on_start事件的参数

执行某个Job

[POST] /v1/flow/instance/{flow_instance_id}/{step_name}/{action}

# 如果是trigger,则{action}可省略

req_body

{
	actor: 100010,
	action_args: {
		...
	}
}
  • actor: 执行人ID
  • action_args: 执行指定动作所需要的参数,比如TravisCI构建完毕之后,您可以在after_success中将构建好的包上传到文件存储,然后调用该接口 [POST] /v1/flow/instance/{flow_instance_id}/sandbox接口,并将构建包的路径通过action_args传递给trigger,这样运维就可以在通知中通过该路径获取已经构建好的包。如果运维同意构建,则会调用[POST] /v1/flow/instance/{flow_instance_id}/sandbox/agree接口,不同意则会调用[POST] /v1/flow/instance/{flow_instance_id}/sandbox/disagree接口。

该接口是执行FlowInstane的主力接口,虽然是主动发起的,但是在执行时会执行严格的参数校验以及流程校验,只有当flow中定义的该step之前的所有job都运行无误后才可以运行。

REST API

除了上述控制流程的API,还有各种获取执行状态和数据的API,还未整理完全。

Job的实现

如果没有Job,那么Acolyte就是一个空壳,什么都干不了,如果有丰富的Job类型,那么Acolyte可以从CI、CD甚至是处理离职申请环节无所不能。会先针对CI先写一些通用的内置Job:Github Webhook处理的相关Job、TravisCI构建结果处理的Job,然后开发公司专用的CI组件包,比如Sandbox部署、灰度发布,这些在每个公司是不一样的。

自定义UI

设想每个Job可以为自己定义UI页面,也就说Acolyte会为Job产生的数据提供存取接口,具体的前端展示会由Job来自己定义,具体怎么抽象还没想好,做到这里再说吧。

用户与权限

最开始只有一个token系统,细致的权限控制流程跑通后再加。

About

CI串联工具

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages