drone 介绍

https://docs.drone.io/

Drone 是一种基于容器技术的持续交付系统。Drone 使用简单的 YAML 配置文件(docker-compose 的超集)来定义和执行 Docker 容器中的 Pipelines。

Drone 与流行的源代码管理系统无缝集成,包括 GitHub,GitHub Enterprise,Bitbucket 等。

Drone 是一个用 Go 语言开发的基于容器运行的持续集成软件。

Drone是一个Golang技术栈的CI解决方案,功能和Jenkins之类的CI工具类似。

优点

Golang 编写,镜像体积小,搭建容易,运行时占用资源小
支持主流代码托管平台Webhook沟通
构建运行时采用image优先,保证在不同平台的构建结果一致
支持插件化,提供强大的功能支持
现代化UI设计,操作简单明了

缺点

年轻,常改版
官方的各种文档写的太烂了
功能和完善程度不及一些老牌 CI

架构

由 1 台 Server 通过Webhook跟代码托管平台做沟通,
接收到事件后启动Runner来处理 Server 上产生的任务。
Runner可以在同一台主机上,也可以分散在多台不同的主机上。

核心概念

pipeline

pipeline可以帮助完成自动化软件交付过程中的步骤,例如启动代码构建、运行自动化测试以及部署到测试或生产环境。pipeline的执行由源代码储存仓库repository触发。代码更改会触发Webhook从而与Drone沟通,后者便开始运行相应的pipeline。

pipeline的种类不止一种,例如:

Docker:在临时的 Docker 容器中执行命令,保证在不同平台的构建结果一致

exec:直接在主机上执行 shell 命令而不隔离,对于不支持容器(如 macOS)的操作系统和体系结构,此运行程序尤其有用

ssh:使用 ssh 协议在远程服务器上执行 shell 命令

platform

使用platform配置目标操作系统和体系结构,并将pipeline路由到适当的运行器。如果未指定,则系统默认为Linux amd64。

workspace

Drone会自动创建一个临时卷,称为工作区,在其中 clone repository。工作区是管道中每个步骤的当前工作目录。

steps

steps定义为一系列的 shell 命令。这些命令在 git 仓库的根目录(工作区)中执行,工作区由管道中的所有步骤共享。

一个特定的steps由多个step组成,供pipeline执行,例如:

clone

安装依赖、单元测试、生成静态文件、拷贝静态文件

condition

condition决定了当前step的触发条件。

触发条件有多种,例如:

根据 Branch:e.g. master/beta
根据 Event:e.g. push/pull_request
根据 Reference:e.g. refs/heads/feature-*
根据 Repository:e.g. octocat/hello-world
根据 Instance:e.g. drone.instance1.com/drone.instance2.com
根据 Status:e.g. success/failure
更多请参考文档

trigger

trigger决定了当前pipeline的触发条件。

触发条件有多种,基本和condition一致。

结合 .drone.yml 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
---
kind: pipeline
type: exec
name: deploy-master

platform:
os: linux
arсh: amd64

steps:
- name: copy
commands:
- cp -rf ./ /srv/fix.moe

- name: install
commands:
- source /root/.nvm/nvm.sh
- cd /srv/fix.moe
- npm install

- name: run
commands:
- source /root/.nvm/nvm.sh
- export PM2_HOME=/root/.pm2
- cd /srv/fix.moe
- pm2 stop fix.moe
- pm2 delete fix.moe
- pm2 start npm --name fix.moe -- run start

trigger:
branch:
- master
event:
- push

drone-runner-exec

上面说到 pipeline 类型有多种,我们这里先选择 exec 这种类型来学习,了解其中的源码是怎么实现的。
学习他是怎么解析yml文件的,我们能不能抽离出来部分代码实现个简单的执行本地yml文件的工具??

源码地址:github.com/golang108/drone-runner-exec

main 入口

1
2
3
func main() {
command.Command()
}

Command

1
2
3
4
5
6
7
8
9
10
func Command() {
app := kingpin.New("drone", "drone exec runner")
registerCompile(app)
registerExec(app)
//registerDaemon(app)
//service.Register(app)

kingpin.Version(version)
kingpin.MustParse(app.Parse(os.Args[1:]))
}

这里注册的几个子命令,这里使用了 kingpin 这个包来解析命令行的。

registerCompile 这个 子命令是用来 编译yml 为 json 文件的?? 待定

registerExec 这个就是执行本地的yml文件的 ,我们主要就学习这里的源码

command/exec.go -> registerExec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

func registerExec(app *kingpin.Application) {
c := new(execCommand)

cmd := app.Command("exec", "executes a pipeline").
Action(c.run)

cmd.Arg("root", "root build directory").
Default("").
StringVar(&c.Root)

cmd.Arg("source", "source file location").
Default(".drone.yml").
FileVar(&c.Source)

cmd.Flag("pretty", "pretty print the output").
Default(
fmt.Sprint(
isatty.IsTerminal(
os.Stdout.Fd(),
),
),
).BoolVar(&c.Pretty)

// shared pipeline flags
c.Flags = internal.ParseFlags(cmd)
}

这个函数就是 初始化 命令行 子命令 exec的。 这样就可以在命令行中这么执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

$ ./drone
usage: drone [<flags>] <command> [<args> ...]

drone exec runner

Flags:
--help Show context-sensitive help (also try --help-long and --help-man).

Commands:
help [<command>...]
Show help.

compile [<flags>] [<root>] [<source>]
compile the yaml file

exec [<flags>] [<root>] [<source>]
executes a pipeline


默认什么参数都不带,就是显示帮助信息。从中可以看到3个子命令,help子命令 是 自带的现实帮助的。
compile 是 我们注册的一个 通过 registerCompile() 注册的。
exec 就是我们这里要讲的的。

kingpin 解析命令参数具体怎么用就不展开了。

我们比较关心的是 执行了 exec 这个子命令,入口 函数会 调哪个呢?

1
2
3
4
c := new(execCommand)

cmd := app.Command("exec", "executes a pipeline").
Action(c.run)

通过上面的 代码可以看到 入口 是 调用了 c.run 这个函数的。execCommand 是个struct,是我们定义的 exec 这个子命令对应的 结构体。 这个结构体实现了 run 函数。
这个run函数 是 type Action func(*ParseContext) error 这种类型的,
所以可以给到 Action(c.run)

command/exec.go -> run

具体的exec子命令的实现逻辑 就都在这个run函数里面了

1
2
具体代码 见 仓库的 command/exec.go 文件的 run 函数

主要的步骤

读取 yml文件, 解析yml文件
初始化一些变量,环境变量等
执行pipeline中的step
  1. 读取 yml文件

    1
    2
    rawsource, err := io.ReadAll(c.Source)

    c.Source 是命令行参数传入的,c 就是 execCommand 结构体对象

  2. 解析yml文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // parse and lint the configuration.这里比较关键,从yml文件解析为 manifest 对象
    manifest, err := manifest.ParseString(config)
    if err != nil {
    return err
    }

    // a configuration can contain multiple pipelines.
    // get a specific pipeline resource for execution.
    // 当前需要执行的 pipline 是从 manifest 列表中 根据name获取 到的
    // name 如果没匹配上就报错,资源没找到, 可以通过选项 --stage-name
    resource, err := resource.Lookup(c.Stage.Name, manifest)
    if err != nil {
    return err
    }

    主要的 解析 动作都在 manifest.ParseString(config) 中。

代码 在 https://github.com/golang108/drone-runner 仓库的 manifest/parse.go

1
2
3
4
5
6
7
//第一步解析 yml 文件 最外层的 几个属性, 对应到  RawResource   类型里面的值
resources, err := ParseRaw(r)

// 这一步就主要 解析 clone steps 等属性了,这个解析出来就
// 是 Pipeline,使用 engine/resource/parser.go
resource, err := parseRaw(raw)

第一个 ParseRaw(r) 主要就是解析 最外层的 几个属性, 对应到 RawResource 类型里面的值
调用 yaml.Unmarshal(resource.Data, resource) 反序列化出来实例对象, 要看能解析哪些
就要看 RawResource 的类型结构体怎么定义的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	// RawResource is a raw encoded resource with the common
// metadata extracted.
RawResource struct {
Version string
Kind string
Type string
Name string
Deps []string `yaml:"depends_on"`
Node map[string]string
Concurrency Concurrency
Platform Platform
Data []byte `yaml:"-"`
}
```

第二个 `parseRaw(raw)` 主要 解析 clone steps 等属性了,这个解析出来就 是 Pipeline 实例了,
也是通过 `yaml.Unmarshal(r.Data, out)` 反序列化出来实例对象, 要看能解析哪些
就要看 Pipeline 的类型结构体怎么定义的了。
// Pipeline is a pipeline resource that executes pipelines
// on the host machine without any virtualization.
Pipeline struct {
    Version   string              `json:"version,omitempty"`
    Kind      string              `json:"kind,omitempty"`
    Type      string              `json:"type,omitempty"`
    Name      string              `json:"name,omitempty"`
    Deps      []string            `json:"depends_on,omitempty"`
    Clone     manifest.Clone      `json:"clone,omitempty"`
    Platform  manifest.Platform   `json:"platform,omitempty"`
    Trigger   manifest.Conditions `json:"conditions,omitempty"`
    Workspace manifest.Workspace  `json:"workspace,omitempty"`

    Steps []*Step `json:"steps,omitempty"`
}
1
2
3
4
5
6
7

上面 通过 `manifest, err := manifest.ParseString(config)` 解析出来 多组 pipeline 对象,

下面 就需要去执行了,执行 哪一个呢,根据 name 决定,`resource, err := resource.Lookup(c.Stage.Name, manifest)` 找到匹配名字的流水线。


获取到 `resource` 然后在 包装 成 `Spec`
// compile the pipeline to an intermediate representation.
comp := &compiler.Compiler{
    Pipeline: resource, // 这个是通过yml中name获取的那个
    Manifest: manifest, // 这里面保存了全部的resource
    Build:    c.Build,
    Netrc:    nil, // c.Netrc,  // 新建 netrc 文件。这个莫名其妙的 这里直接干掉
    Repo:     c.Repo,
    Stage:    c.Stage,
    System:   c.System,
    Environ:  c.Environ,
    Secret:   secret.StaticVars(c.Secrets),
    Root:     c.Root, // 这个来自命令行
}
spec := comp.Compile(nocontext)
1
2

最后 调用执行
err = runtime.NewExecer(
    pipeline.NopReporter(),
    console.New(c.Pretty),
    engine.New(),
    c.Procs,
).Exec(ctx, spec, state)
if err != nil {
    return err
}
最后怎么执行的 先是 Execer中的 Exec函数 
-> exec函数 
-> e.engine.Run(ctx, spec, copy, wc) 函数 == engine/exec.go中的 Run函数