Drone对私有镜像仓库的支持方式,以及源码改造优化

Drone介绍

Drone是新一代的CI/CD工具,基于pipeline+docker模式,可以非常灵活的支撑很多业务场景,目前,Done最新为0.8.6版本,在github上,已经斩获15K高星star。

Drone和gitlab结合,可以在项目中设置 .drone.yml 文件来定制你需要执行的各种各样的流程,比如,代码拉取、镜像构建推送、PHP composer 包管理、Golang构建、消息通知、自动部署、自动化测试等等。插件化的支持,以及插件的开发和使用模式,使得Drone的扩展性非常灵活。

目前来说,Drone官方插件仓库已经提供了很多插件来扩展Drone的功能,而实现一套插件也非常简单。基本上,靠着 编写pipeline配置文件(.drone.yml) 的灵活编写+插件模式,足够应付无限的场景。个人觉得,相比Jenkins(其实Jenkins也出了一个基于docker、k8s的新一代工具:Jenkins X),Drone灵活简单多了。


Drone对私有镜像仓库的支持

在我们的实际使用Drone过程中,有可能需要私有镜像仓库的支持的话,以下面的pipeline为例:

clone:
  git:
    image: xxx.com/plugins/drone-plugin-git

pipeline:
  build:
    image: xxx.com/octocat/hello-image
  push_image:
    image: xxx.com/plugins/docker
    repo: xxx.com/xxx/test

从这个例子中,可以看到,这个pipeline分为3个步骤

①:git步骤通过image对应的镜像拉取代码。

②:build步骤,通过对应的image镜像,执行代码构建操作。

③:push_image操作,通过image对应的镜像,完成镜像的构建,以及镜像推送到 repo对应的镜像仓库上。

这个过程中,假设我们的私有镜像仓库地址是 xxx.com,且这个镜像仓库有权限校验,那么这个Pipeline中,有2个地方涉及到私有镜像仓库的权限处理:

①:pipeline里,image对应的3个镜像,需要从私有镜像仓库拉取。

②:最后一个步骤是镜像构建和镜像推送操作,镜像构建其实就是基于具体项目里的Dockerfile,而这个Dockerfile的基础镜像,有可能也是一个私有镜像。另外,docker push也可能是推送到私有的镜像仓库。


官方方案

从上,要解决这2个方面对私有镜像仓库的需求。Drone本身提供了一套解决方案了。

第一,“pipeline里,image对应的镜像,需要从私有镜像仓库拉取”,这个解决方案,Drone提供了2种:

①:为每个image,通过 drone 客户端工具,设置镜像仓库token

drone secrets add \
  --image=octocat/hello-image \
  octocat/hello-world REGISTRY_USERNAME octocat

这种方案,比较麻烦,好处是,.drone.yml 不需要做任何改动

②:直接把pipeline里需要私有镜像仓库的image,在.drone.yml中,写好认证信息,比如写为这样:

clone:
  git:
    image: xxx.com/plugins/drone-plugin-git
    auth_config:
      username: octocat
      password: password
      email: [email protected]
pipeline:
  build:
    image: xxx.com/octocat/hello-image
    auth_config:
      username: octocat
      password: password
      email: [email protected]
  push_image:
    image: xxx.com/plugins/docker
    repo: xxx.com/xxx/test
    auth_config:
      username: octocat
      password: password
      email: [email protected]

这种方案,不需要操作客户端工具,但需要每个项目的 .drone.yml 的pipeline配置里,都明文写好。

第二、如何解决镜像FROM私有仓库,以及镜像推送到私有仓库的问题

Drone其实提供了 plugins/docker 插件,来做镜像的构建和推送。解决此问题的单子,其实是靠此插件来做的,此插件,解决此问题,同样需要你配置pipeline的时候,明确写明认证信息(最后三行)

clone:
  git:
    image: xxx.com/plugins/drone-plugin-git
    auth_config:
      username: octocat
      password: password
      email: [email protected]
pipeline:
  build:
    image: xxx.com/octocat/hello-image
    auth_config:
      username: octocat
      password: password
      email: [email protected]
  push_image:
    image: xxx.com/plugins/docker
    repo: xxx.com/xxx/test
    auth_config:
      username: octocat
      password: password
      email: [email protected]
    username: octocat
    password: password
    email: [email protected]


改造的期望效果

我们可以看到,Drone,灵活就灵活在 pipeline 的配置上,但也因此,很多东西,都会写在 pipeline的配置文件 .drone.yml 里,明文暴露,这样不够优雅,我们可以尝试对源码进行一定的改造,用来更好的支持对私有镜像仓库的支持,但又不暴露认证信息。

首先,我期望的pipeline配置效果是这样的:

clone:
  git:
    image: xxx.com/plugins/drone-plugin-git
    auth_config:
      innerid: xxx.com
pipeline:
  build:
    image: xxx.com/octocat/hello-image
    auth_config:
      innerid: xxx.com
  push_image:
    image: xxx.com/plugins/docker
    repo: xxx.com/xxx/test
    auth_config:
      innerid: xxx.com
    auth_config_innerid: xxx.com

我们对比之前的.drone.yml,发现,里边少了明文暴露的私有镜像仓库认证信息,多了一个 innerid,这个innerid,其实就是私有镜像仓库的标识,这个标识,我们就定为私有镜像仓库的域名。

我的目的是,让 Drone 程序,可以通过识别 innerid,来自动从其他地方(比如drone-server的环境变量)获取对应的私有镜像仓库的认证信息,这样一来,.drone.yml,就不用在写明文认证信息了。

我们要改造2个项目,第一个是 drone 项目,第二个是 plugins/docker 这个插件。


源码改造

改造 drone 项目(用来支持pipeline中image使用私有镜像仓库)

1、增加一个自定义的环境变量,让 drone server 知道,有这个环境变量且有值的话,这个值,就是我们配置的所有私有镜像仓库的认证信息集合了

①:更改 cmd/drone-server/server.go 文件,增加 配置的环境变量名:REGISTRY_AUTH_INNER_CONFIG

//pipeline的authconfig默认配置
cli.StringFlag{
    EnvVar: "REGISTRY_AUTH_INNER_CONFIG",
    Name:   "registry-auth-inner-config",
    Usage:  "private docker registry authentication username",
    Value:  "",
},

②:还是更改 cmd/drone-server/server.go 文件,更改 server 函数,增加刚刚环境变量的使用,目的是解析这个字符串,把它解析成golang的map类型

    //初始化全局registry认证数据
    registryInnerAuthConfig := c.String("registry-auth-inner-config")
    if registryInnerAuthConfig != "" {
        authinfo := droneserver.DecodeToMap(registryInnerAuthConfig)
        for k, v := range authinfo {
            registry := compiler.Registry{}
            if e := json.Unmarshal([]byte(v), ®istry); e == nil {
                droneserver.GlobalRegistryAuthConfig[k] = registry
            }
        }
    }

这里边,有一个将字符串,转换为map的过程,使用的是自定义函数:DecodeToMap,这个函数的定义是这样的

//将string解码为map
func DecodeToMap(s string) map[string]string {
    b := new(bytes.Buffer)
    var decodedMap map[string]string
    if data, err := base64.StdEncoding.DecodeString(s); err == nil {
        b.Write(data)
        d := gob.NewDecoder(b)
        // Decoding the serialized data
        err = d.Decode(&decodedMap)
    }
    return decodedMap
}

//将map编码为string
func EncodeMapToString(m map[string]string) string {
    b := new(bytes.Buffer)
    e := gob.NewEncoder(b)
    // Encoding the map
    err := e.Encode(m)
    if err == nil {
        return base64.StdEncoding.EncodeToString(b.Bytes())
    } else {
        return ""
    }
}

③:更改 server/hook.go 文件,这个文件的开始部分,需要一个包引用,以及一个变量

//import下面的包
"github.com/cncd/pipeline/pipeline/frontend/yaml/compiler"

//全局的私有仓库认证信息,这个数据,从环境变量里取(从环境变量,解析为map,key为私有镜像仓库标识,比如 hub.mfwdev.com,value为认证信息json)
var GlobalRegistryAuthConfig = make(map[string]compiler.Registry)

④:我们的核心,就是要把环境变量里配置的编码后的字符串,解析成上面的变量,也就是一个 string 转 map 的过程。还是更改 上面的文件:

        //找到下面的这行代码
        parsed, err := yaml.ParseString(y)

        //在上面行代码下面,补充下面代码
        //处理私有镜像仓库认证信息问题
        if parsed != nil && err == nil {
            if parsed.Pipeline.Containers != nil && len(parsed.Pipeline.Containers) > 0 {
                for _, c := range parsed.Pipeline.Containers {
                    if c.AuthConfig.Innerid != "" && GlobalRegistryAuthConfig != nil {
                        if registrytemp, ok := GlobalRegistryAuthConfig[c.AuthConfig.Innerid]; ok {
                            c.AuthConfig.Username = registrytemp.Username
                            c.AuthConfig.Password = registrytemp.Password
                            c.AuthConfig.Email = registrytemp.Email
                        }
                    }
                }
            }
        }

⑤:好了,改定义我们的innerid数据类型了,更改文件:

vendor/github.com/cncd/pipeline/pipeline/frontend/yaml/container.go

type (
    // AuthConfig defines registry authentication credentials.
    AuthConfig struct {
        //私有镜像仓库标识,比如:hub.xxx.com,有了这个标识,就可以结合环境变量,取出来这个标识对应的认证信息,取代下面的三个字段的内容
        Innerid  string

        Username string
        Password string
        Email    string

改造 plugins/docker 插件(用来改造 docker build、docker push使用私有镜像仓库)

我们需要新建一个项目,克隆 plugins/docker 作为我们自己的项目,来进行改造,通 drone 项目一样

我们为什么,既要改动 drone,又要改动 plugins/docker 项目呢?

这是因为,drone的改造,drone其实是server端,而 plugins/docker,仅仅是pipeline的一个步骤,我们可以让drone,把这个认证信息,传递给 plugins/docker。所以,回过头,我们还需要再次改造一下 drone 项目,将 drone的环境变量的认证信息,传递给 plugins/docker,这样一来,认证信息,传递给 plugins/docker,就不要 plugins/docker再去做什么从外部获取认证信息的事儿了

①:改一下 drone项目,文件:server/hook.go,在 hook函数中,设置环境变量的部分,增加下面一行

    //全局的registry认证信息
    envs["REGISTRY_AUTH_INNER_CONFIG"] = os.Getenv("REGISTRY_AUTH_INNER_CONFIG")

然后,回到 plugins/docker 项目来。

②:更改 cmd/drone-docker/main.go 文件,定义新类型结构体

type Registry struct {
    // mfwupdate 是否内部验证(pipeline的docker认证,其实是需要在pipeline的yml中配置的,不好的地方是,会导出暴露认证信息
    // 这里提供一个 Innerid 表示,优先走内部验证,也就是,通过drone-server的环境变量中取认证username、password、email信息)
    Innerid  string

    Hostname string
    Username string
    Password string
    Email    string
    Token    string
}

再同样的文件中,增加取环境变量的部分

        //use PLUGIN_AUTH_CONFIG_INNERID first,compared to PLUGIN_USERNAME、PLUGIN_PASSWORD、PLUGIN_EMAIL,做docker login
        cli.StringFlag{
            Name:   "auth_config_innerid",
            Usage:  "use auth_config_innerid first,compared to username、password、email for docker login",
            EnvVar: "PLUGIN_AUTH_CONFIG_INNERID",
        },
    }

还是这个文件,增加对 这个环境变量的处理

    //mfwupdate
    // login to the Docker registry
    authConfigInnerId := c.String("auth_config_innerid")
    registryAuthInnerInfo := os.Getenv("REGISTRY_AUTH_INNER_CONFIG")
    if registryAuthInnerInfo != "" && authConfigInnerId != "" {
        authinfo := DecodeStringToMap(registryAuthInnerInfo)
        for k, v := range authinfo {
            registry := Registry{}
            if e := json.Unmarshal([]byte(v), ®istry); e == nil {
                if k == authConfigInnerId {
                    plugin.Login.Email = registry.Email
                    plugin.Login.Username = registry.Username
                    plugin.Login.Password = registry.Password
                    plugin.Login.Registry = authConfigInnerId
                }
            }
        }
    }

至此,改造完毕。


参考:

http://readme.drone.io/0.5/

你可能感兴趣的:(Drone对私有镜像仓库的支持方式,以及源码改造优化)