个人笔记

专注互联网

Struct未导出变量序列化时的坑

Go默认使用大小写来区分变量/函数是否导出,所以如果一个Struct需要对外使用,但是又未导出就会出现意想不到的bug

特别是常用的编解码(序列化/反序列化),json,xml,gob等

package main
import (
"fmt"
"encoding/json"
)
type MyData struct {
One int
two string
}
func main() {
in := MyData{1,"two"}
fmt.Printf("%#v\n",in) //prints main.MyData{One:1, two:"two"}
encoded,_ := json.Marshal(in)
fmt.Println(string(encoded)) //prints {"One":1}
var out MyData
json.Unmarshal(encoded,&out)
fmt.Printf("%#v\n",out) //prints main.MyData{One:1, two:""}
}

为了避免大写在某些格式不符合规范的情况,通常这些框架都支持自定义序列化的tag,例如

type MyData struct {
One int `json:"one"`
Two string `json:"two"`
}

参考 http://colobu.com/2015/09/07/gotchas-and-common-mistakes-in-go-golang/#

Martini路由框架

go-martini/martini是一个非常简单,流行的Golang web框架,并且非常的小巧简单灵活易扩展,也有非常多的插件,见https://github.com/martini-contrib

package main

import "github.com/go-martini/martini"

func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}

Inject

go-martini/martini的灵活性多亏了codegangsta/inject,后者是一个Golang的注入小框架,代码非常简单,利用Golang自带的reflect库完成函数参数的自动适配。

package main

import (
"fmt"
"github.com/codegangsta/inject"
)

// 自定义一个空interface{}, 必须是interface{}类型MapTo才能接受
type SpecialString interface{}

// 原定义Foo(s1,s2 string), 改成
func Foo(s1 string, s2 SpecialString) {
fmt.Println(s1, s2.(string)) // type assertion
}

func main() {
ij := inject.New()
// 注入string类型的"a"
ij.Map("a")
// 注入SpecialString类型的"b"
ij.MapTo("b", (*SpecialString)(nil))
// 调用Foo函数时,会自动查找已经注入的值,并自动传递。
// inject包的设计确保不会有两个相同类型的值(限制)
// 若找不到会抛出运行时错误
ij.Invoke(Foo)
}

Map和MapTo的区别在于后者可以指定注入value的类型

除了自动注入函数的参数外,还支持自动注入Struct的成员变量,参见Apply函数(利用了Struct成员的tag特性)

(*SpecialString)(nil))

这种写法对于初学者很费解,原理得从interface{}的内部表述说起

为了支持reflect,interface{}内部存储了实际对象的Kind(类型)和Value(值),例如var v interface{} = “aaaa”,那么v的类型就是string,值就是”aaaa”,回到inject,由于Mapto第二个参数只需要一个类型,所以我们只需要初始化一个正确的Kind,Value为nil的interface{}即可。

由于Go是有区分Struct指针和对象,初始化一个对象需要分配额外的内存,而指针就很简单(学C的很好理解),所以这里起始就是传入的一个指针,并且value是nil,用最小的开销实现了一个类型的表述和传导。

type Str struct{
b int
}

func main() {
var b *Str = nil
var a = (*Str)(nil)
// 变量a,b都是一个类型为Str的空指针
fmt.Print(a)
fmt.Print(b)
}

而inject在内部会自动获取指针指向变量的类型,见

// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not an pointer to an interface.
func InterfaceOf(value interface{}) reflect.Type {
t := reflect.TypeOf(value)

// 获取实际的类型,用for循环是为了确保多级指针(指向指针的指针)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}

if t.Kind() != reflect.Interface {
panic("Called ... interface. (*MyInterface)(nil)")
}

return t
}

Martini

主要的逻辑参考【github.com/go-martini/martini/martini.go】和【github.com/go-martini/martini/router.go】

初始化

ClassicMartini用两个匿名对象实现继承

type ClassicMartini struct {
*Martini
Router
}

type Martini struct {
inject.Injector
handlers []Handler
action Handler
logger *log.Logger
}

func Classic() *ClassicMartini {
r := NewRouter()
m := New()
m.Use(Logger())
m.Use(Recovery())
m.Use(Static("public"))
// Mapto注入router处理器
m.MapTo(r, (*Routes)(nil))
m.Action(r.Handle)
return &ClassicMartini{m, r}
}

m.Use用于注入中间件,存储在Martini.handlers,m.Action存储实际的路由处理入口,存储在Martini.action,他们的调用顺序见

func (c *context) handler() Handler {
if c.index < len(c.handlers) {
return c.handlers[c.index]
}
// 最后调用
if c.index == len(c.handlers) {
return c.action
}
panic("invalid index for context handler")
}

运行入口

// Run the http server on a given host and port.
func (m *Martini) RunOnAddr(addr string) {
logger := m.Injector.Get(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
logger.Printf("listening on %s (%s)\n", addr, Env)
logger.Fatalln(http.ListenAndServe(addr, m))
}

可以看到传入的m变量,它必须实现接口http.Handler接口,回调如下

func (m *Martini) ServeHTTP(res http.ResponseWriter, req *http.Request) {
m.createContext(res, req).run()
}

type context struct {
inject.Injector
handlers []Handler
action Handler
rw ResponseWriter
index int
}

func (m *Martini) createContext(res http.ResponseWriter, req *http.Request) *context {
c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}
// 从m继承注入的变量,比如logger之类的全局变量
c.SetParent(m)
// 注入自己
c.MapTo(c, (*Context)(nil))
// 注入responce
c.MapTo(c.rw, (*http.ResponseWriter)(nil))
// 注入request
c.Map(req)
return c
}

// 最后运行Run函数,先依次运行middleware,然后运行路由总入口,参见handler()函数
func (c *context) run() {
for c.index <= len(c.handlers) {
// 最终会触发router.Handle
_, err := c.Invoke(c.handler())
if err != nil {
panic(err)
}
c.index += 1

if c.Written() {
return
}
}
}

在router.Handle找到最匹配的route,调用route.Handle函数

type router struct {
// 所有的路由对象
routes []*route
notFounds []Handler
groups []group
routesLock sync.RWMutex
}

type route struct {
method string
regex *regexp.Regexp
// 所有的handle
handlers []Handler
pattern string
name string
}

func (r *route) Handle(c Context, res http.ResponseWriter) {
// 构建新的对象,
context := &routeContext{c, 0, r.handlers}
// 注入新的变量
c.MapTo(context, (*Context)(nil))
c.MapTo(r, (*Route)(nil))
// 注意这里调用的是routeContext的run
context.run()
}

type routeContext struct {
// 继承了最初的Context,后者又继承了Martini注入的变量
Context
index int
handlers []Handler
}

// 依次触发所有的handle
func (r *routeContext) run() {
for r.index < len(r.handlers) {
handler := r.handlers[r.index]
// 依次触发回调
vals, err := r.Invoke(handler)
if err != nil {
panic(err)
}
r.index += 1

// if the handler returned something, write it to the http response
if len(vals) > 0 {
ev := r.Get(reflect.TypeOf(ReturnHandler(nil)))
handleReturn := ev.Interface().(ReturnHandler)
handleReturn(r, vals)
}

if r.Written() {
return
}
}
}

参考

  1. https://my.oschina.net/achun/blog/192912
  2. http://betazk.github.io/2014/11/%E5%AF%B9martini%E8%BF%99%E4%B8%AA%E6%A1%86%E6%9E%B6%E4%B8%ADinject%E9%83%A8%E5%88%86%E7%9A%84%E7%90%86%E8%A7%A3/
  3. http://memoryleak.io/go/martini/2015/10/24/martini-inject.html

Drone学习笔记5

插件

drone不仅仅只是自动编译,还可以利用插件做一些其他事情,比如结果通知,打包目标文件成一个docker镜像。drone的插件可浏览http://plugins.drone.io/,一些常用的比如

  1. http://plugins.drone.io/drone-plugins/drone-docker/
  2. http://plugins.drone.io/appleboy/drone-scp/
  3. http://plugins.drone.io/drone-plugins/drone-slack/
  4. http://plugins.drone.io/peloton/drone-rancher/

插件的.drone.yml有自己的语法,具体要根据插件的帮助文档分析

矩阵编译

http://readme.drone.io/usage/matrix-guide/ 意思就是根据几个变量的组合,基于所有的条件各执行一次pipeline

开发插件

官网提供了简单的插件教程,支持golang,bash,node和python

  1. http://readme.drone.io/plugins/creating-custom-plugins-bash/
  2. http://readme.drone.io/plugins/creating-custom-plugins-golang/

目前企业微信开发了API,理论上开发一个通知企业微信的插件也是可以的。

私有Docker Registry

随着docker越来越成熟,为了更好地管理这些docker,就冒出了docker compose这样的工具,以及更高层次的比如rancher

而运行公司业务的docker镜像,显然也有必要放在自己的私有Registry内,然后通过drone插件一键上传到私有的Registry,当然也可以直接发布到rancher的测试环境。

搭建私有的Registry很简单,官方的registry就包含镜像【registry:2.0】,直接运行即可。

docker run -p 5000:5000 registry:2.0

推送也很简单

docker push localhost:5000/hello:latest

参考http://dockone.io/article/324

Drone学习笔记4

找到一个编译成功过的resp【qjw/test】,点击restart

发起编译请求

前端发送POST请求【/api/repos/qjw/test/builds/2】,2是build的id。同时发起ws Get请求【ws://test.ycy.qiujinwu.com/ws/logs/qjw/test/2/1】,2是build的id,1是job的id。

ws.GET("/logs/:owner/:name/:build/:number",
session.SetRepo(),
session.SetPerm(),
session.MustPull,
server.LogStream,
)

func SetRepo() gin.HandlerFunc {
return func(c *gin.Context) {
var (
owner = c.Param("owner")
name = c.Param("name")
user = User(c)
)

// LogStream streams the build log output to the client.
func LogStream(c *gin.Context) {
repo := session.Repo(c)
buildn, _ := strconv.Atoi(c.Param("build"))
jobn, _ := strconv.Atoi(c.Param("number"))

发起编译的请求最终会跑到

src/github.com/drone/drone/server/build.go

func PostBuild(c *gin.Context) {
// 获得后端实例
remote_ := remote.FromContext(c)

// 从数据库中获取的resp对象,通过前置的middleware完成了查询
repo := session.Repo(c)
fork := c.DefaultQuery("fork", "false")

// 获得当前用户
user, err := store.GetUser(c, repo.UserID)
if err != nil {
log.Errorf("failure to find repo owner %s. %s", repo.FullName, err)
c.AbortWithError(500, err)
return
}

// 获得build number
build, err := store.GetBuildNumber(c, repo, num)
if err != nil {
log.Errorf("failure to get build %d. %s", num, err)
c.AbortWithError(404, err)
return
}

// 拉取.drone.yml文件
// fetch the .drone.yml file from the database
config := ToConfig(c)
raw, err := remote_.File(user, repo, build, config.Yaml)
if err != nil {
log.Errorf("failure to get build config for %s. %s", repo.FullName, err)
c.AbortWithError(404, err)
return
}

// 检查是否有签名文件
// Fetch secrets file but don't exit on error as it's optional
sec, err := remote_.File(user, repo, build, config.Shasum)
if err != nil {
log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err)
}

// 验证签名
signature, err := jose.ParseSigned(string(sec))
if err != nil {
log.Debugf("cannot parse .drone.yml.sig file. %s", err)
} else if len(sec) == 0 {
log.Debugf("cannot parse .drone.yml.sig file. empty file")
} else {
signed = true
output, err := signature.Verify([]byte(repo.Hash))
if err != nil {
log.Debugf("cannot verify .drone.yml.sig file. %s", err)
} else if string(output) != string(raw) {
log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw))
} else {
verified = true
}
}

// 发送请求到tast list
client := stomp.MustFromContext(c)
client.SendJSON("/topic/events", model.Event{
Type: model.Enqueued,
Repo: *repo,
Build: *build,
},
stomp.WithHeader("repo", repo.FullName),
stomp.WithHeader("private", strconv.FormatBool(repo.IsPrivate)),
)

for _, job := range jobs {
broker, _ := stomp.FromContext(c)
// 发送请求到agent
broker.SendJSON("/queue/pending", &model.Work{
Signed: signed,
Verified: verified,
User: user,
Repo: repo,
Build: build,
BuildLast: last,
Job: job,
Netrc: netrc,
Yaml: string(raw),
Secrets: secs,
System: &model.System{Link: httputil.GetURL(c.Request)},
},
stomp.WithHeader(
"platform",
yaml.ParsePlatformDefault(raw, "linux/amd64"),
),
stomp.WithHeaders(
yaml.ParseLabel(raw),
),
)
}
}

.drone.yml签名

签名通过cli完成

src/github.com/drone/drone/drone/sign.go

var signCmd = cli.Command{
Name: "sign",
Usage: "creates a secure yaml file",
Action: func(c *cli.Context) {
if err := sign(c); err != nil {
log.Fatalln(err)
}
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "in",
Usage: "input file",
Value: ".drone.yml",
},
cli.StringFlag{
Name: "out",
Usage: "output file signature",
Value: ".drone.yml.sig",
},
},
}

最终调用接口

repo.POST("/sign", session.MustPush, server.Sign)

agent接收

src/github.com/drone/drone/drone/agent/agent.go

func start(c *cli.Context) {
// 初始化全局的docker实例
docker, err := dockerclient.NewDockerClient(c.String("docker-host"), tls)
if err != nil {
logrus.Fatal(err)
}

var client *stomp.Client

// 收到请求的回调
handler := func(m *stomp.Message) {
running.Add(1)
defer func() {
running.Done()
client.Ack(m.Ack)
}()

r := pipeline{
drone: client,
docker: docker,
config: config{
platform: c.String("docker-os") + "/" + c.String("docker-arch"),
timeout: c.Duration("timeout"),
namespace: c.String("namespace"),
privileged: c.StringSlice("privileged"),
pull: c.BoolT("pull"),
logs: int64(c.Int("max-log-size")) * 1000000,
extension: c.StringSlice("extension"),
},
}

work := new(model.Work)
m.Unmarshal(work)
r.run(work)
}

for {
// dial the drone server to establish a TCP connection.
client, err = stomp.Dial(server)
if err != nil {
logger.Warningf("connection failed, retry in %v. %s", backoff, err)
<-time.After(backoff)
continue
}
opts := []stomp.MessageOption{
stomp.WithCredentials("x-token", accessToken),
}

// 连接mq
// initialize the stomp session and authenticate.
if err = client.Connect(opts...); err != nil {
logger.Warningf("session failed, retry in %v. %s", backoff, err)
<-time.After(backoff)
continue
}

// 订阅topic
// subscribe to the pending build queue.
client.Subscribe("/queue/pending", stomp.HandlerFunc(func(m *stomp.Message) {
go handler(m) // HACK until we a channel based Subscribe implementation
}), opts...)

在上面开始有了pipeline的概念,参见.drone.yml的pipeline定义。

在【m.Unmarshal(work)】会反序列化MQ消息到Work struct

// Work represents an item for work to be
// processed by a worker.
type Work struct {
Signed bool `json:"signed"`
Verified bool `json:"verified"`
Yaml string `json:"config"`
YamlEnc string `json:"secret"`
Repo *Repo `json:"repo"`
Build *Build `json:"build"`
BuildLast *Build `json:"build_last"`
Job *Job `json:"job"`
Netrc *Netrc `json:"netrc"`
Keys *Key `json:"keys"`
System *System `json:"system"`
Secrets []*Secret `json:"secrets"`
User *User `json:"user"`
}

重点关注Yaml字段

Docker client

drone 并没有直接使用的第三方开源的client,而是封装一个接口,代码实现在【src/github.com/drone/drone/build/docker】。在后端再使用第三方samalba/dockerclient

// Engine defines the container runtime engine.
type Engine interface {
ContainerStart(*yaml.Container) (string, error)
ContainerStop(string) error
ContainerRemove(string) error
ContainerWait(string) (*State, error)
ContainerLogs(string) (io.ReadCloser, error)
}

Topic

  1. /queue/updates server接收来自agent的执行结果
  2. /topic/logs.%d server接收来自agent的编译实时日志
  3. /topic/events 事件列表,用于刷新drone ui左边列表的任务,和agent无交互
  4. /topic/cancel 向agent发送任务取消指令
  5. /queue/pending 向agent发送编译指令,含1. 用户手动触发、2. git仓库回调触发比如push了新代码

/topic/logs 和 /topic/events最终会关联到两个ws接口,用于实时刷新web ui。

ws.GET("/feed", server.EventStream)
ws.GET("/logs/:owner/:name/:build/:number",
session.SetRepo(),
session.SetPerm(),
session.MustPull,
server.LogStream,
)

Pipeline

接下来会创建一个Agent对象,并Run

src/github.com/drone/drone/drone/agent/exec.go

func (r *pipeline) run(w *model.Work) {
// 创建cancel channel
cancel := make(chan bool, 1)
// 创建docker实例
engine := docker.NewClient(r.docker)
// 创建agent实例
a := agent.Agent{
// 发送build日志
Update: agent.NewClientUpdater(r.drone),
Logger: agent.NewClientLogger(r.drone, w.Job.ID, r.config.logs),
Engine: engine,
Timeout: r.config.timeout,
Platform: r.config.platform,
Namespace: r.config.namespace,
Escalate: r.config.privileged,
Extension: r.config.extension,
Pull: r.config.pull,
}


// 支持取消编译事件
// signal for canceling the build.
sub, err := r.drone.Subscribe("/topic/cancel", stomp.HandlerFunc(cancelFunc))
if err != nil {
logrus.Errorf("Error subscribing to /topic/cancel. %s", err)
}

a.Run(w, cancel)

}

接着解释.drone.yml,并执行docker

src/github.com/drone/drone/agent/agent.go

func (a *Agent) Run(payload *model.Work, cancel <-chan bool) error {

// 预处理
spec, err := a.prep(payload)

// Update是一个value为函数的回调值,用于更新任务状态,见agent.NewClientUpdater
a.Update(payload)

// 执行
err = a.exec(spec, payload, cancel)

a.Update(payload)
return err
}

预处理

预处理有两个任务很关键

  1. 使用配置的密文替换.drone.yml的变量,参见envsubst.Eval
  2. 解析.drone.xml,保存到Config结构中,参见conf, err := yaml.ParseString(w.Yaml)
// Config represents the build configuration Yaml document.
type struct {
Image string
Build *Build
Workspace *Workspace
Pipeline []*Container
Services []*Container
Volumes []*Volume
Networks []*Network
}
func (a *Agent) prep(w *model.Work) (*yaml.Config, error) {

envs := toEnv(w)
envSecrets := map[string]string{}

// list of secrets to interpolate in the yaml
for _, secret := range w.Secrets {
if (w.Verified || secret.SkipVerify) && secret.MatchEvent(w.Build.Event) {
envSecrets[secret.Name] = secret.Value
}
}

var err error
w.Yaml, err = envsubst.Eval(w.Yaml, func(s string) string {
env, ok := envSecrets[s]
if !ok {
env, _ = envs[s]
}
if strings.Contains(env, "\n") {
env = fmt.Sprintf("%q", env)
}
return env
})
if err != nil {
return nil, err
}


conf, err := yaml.ParseString(w.Yaml)
if err != nil {
return nil, err
}

return conf, nil
}

密文

drone数据库有一个专门的表sercrets用来存储这些字段,webui没有提供入口配置,需要使用cli,具体参见官方文档http://readme.drone.io/cli/drone-secret-add/。这些密文通过mq从server带过来,agent并不直接存储。

关于密文使用,参见官方http://readme.drone.io/usage/secret-guide/

具体的替换过程,使用函数envsubst.Eval,代码在https://github.com/drone/drone/tree/master/vendor/github.com/drone/envsubst,支持的模式如下:

Supported Functions:
${var^}
${var^^}
${var,}
${var,,}
${var:position}
${var:position:length}
${var#substring}
${var##substring}
${var%substring}
${var%%substring}
${var/substring/replacement}
${var//substring/replacement}
${var/#substring/replacement}
${var/%substring/replacement}
${#var}
${var=default}
${var:=default}
${var:-default}

Unsupported Functions:
${var-default}
${var+default}
${var:?default}
${var:+default}

解析.drone.yml

解析的入口在yaml.ParseString,代码实现在【src/github.com/drone/drone/yaml】,关于Config结构,我们重点关注以下两个字段

type  struct {
Pipeline []*Container
Services []*Container
}

他们最终都会存储为一个有序的链表,代码实现【src/github.com/drone/drone/yaml/container.go】,下面是最简单的.drone.yml

pipeline:
build:
image: golang
commands:
- go get
- go build
- go test

services:
postgres:
image: postgres:9.4.5
environment:
- POSTGRES_USER=myapp

所以.drone.yml支持哪些字段,看看【src/github.com/drone/drone/yaml】的对象定义即可,关注对象字段tag和UnmarshalYAML函数。

// Container defines a Docker container.
type Container struct {
ID string
Name string
Image string
Build string
Pull bool
AuthConfig Auth
Detached bool
Disabled bool
Privileged bool
WorkingDir string
Environment map[string]string
Labels map[string]string
Entrypoint []string
Command []string
Commands []string
ExtraHosts []string
Volumes []string
VolumesFrom []string
Devices []string
Network string
DNS []string
DNSSearch []string
MemSwapLimit int64
MemLimit int64
ShmSize int64
CPUQuota int64
CPUShares int64
CPUSet string
OomKillDisable bool
Constraints Constraints

Vargs map[string]interface{}
}

解析完.drone.xml之后,会自动在前面加上一个clone的容器

src/github.com/drone/drone/yaml/transform/clone.go

// Clone transforms the Yaml to include a clone step.
func Clone(c *yaml.Config, plugin string) error {
switch plugin {
case "", "git":
plugin = "plugins/git:latest"
case "hg":
plugin = "plugins/hg:latest"
}

for _, p := range c.Pipeline {
if p.Name == clone {
if p.Image == "" {
p.Image = plugin
}
return nil
}
}

s := &yaml.Container{
Image: plugin,
Name: clone,
}

c.Pipeline = append([]*yaml.Container{s}, c.Pipeline...)
return nil
}

最后还会加上一个busybox的容器,我的理解这个容器的目的仅仅是为了在最开始挂载磁盘,以便维持pipeline各个容器的共享代码。

// lookup ambassador configuration by architecture and os
var lookupAmbassador = map[string]ambassador{
"linux/amd64": {
image: "busybox:latest",
entrypoint: []string{"/bin/sleep"},
command: []string{"86400"},
},
"linux/arm": {
image: "armhf/alpine:latest",
entrypoint: []string{"/bin/sleep"},
command: []string{"86400"},
},
}

// Pod transforms the containers in the Yaml to use Pod networking, where every
// container shares the localhost connection.
func Pod(c *yaml.Config, platform string) error {

rand := base64.RawURLEncoding.EncodeToString(
securecookie.GenerateRandomKey(8),
)

// choose the ambassador configuration based on os and architecture
conf, ok := lookupAmbassador[platform]
if !ok {
conf = defaultAmbassador
}

for _, container := range containers {
container.VolumesFrom = append(container.VolumesFrom, ambassador.ID)
if container.Network == "" {
container.Network = network
}
}

留意pod函数最后的for循环,通过VolumesFrom实现了所有容器共享一个磁盘,从而共享代码等数据

执行容器

src/github.com/drone/drone/build/config.go

// Pipeline creates a build Pipeline using the specific configuration for
// the given Yaml specification.
func (c *Config) Pipeline(spec *yaml.Config) *Pipeline {

pipeline := Pipeline{
engine: c.Engine,
pipe: make(chan *Line, c.Buffer),
next: make(chan error),
done: make(chan error),
}

var containers []*yaml.Container
containers = append(containers, spec.Services...)
containers = append(containers, spec.Pipeline...)

for _, c := range containers {
if c.Disabled {
continue
}
next := &element{Container: c}
if pipeline.head == nil {
pipeline.head = next
pipeline.tail = next
} else {
pipeline.tail.next = next
pipeline.tail = next
}
}

go func() {
pipeline.next <- nil
}()

return &pipeline
}

func (a *Agent) exec(spec *yaml.Config, payload *model.Work, cancel <-chan bool) error {

conf := build.Config{
Engine: a.Engine,
Buffer: 500,
}

pipeline := conf.Pipeline(spec)
defer pipeline.Teardown()

// setup the build environment
if err := pipeline.Setup(); err != nil {
return err
}

replacer := NewSecretReplacer(payload.Secrets)
timeout := time.After(time.Duration(payload.Repo.Timeout) * time.Minute)

for {
select {
case <-pipeline.Done():
return pipeline.Err()
case <-cancel:
pipeline.Stop()
return fmt.Errorf("termination request received, build cancelled")
case <-timeout:
pipeline.Stop()
return fmt.Errorf("maximum time limit exceeded, build cancelled")
case <-time.After(a.Timeout):
pipeline.Stop()
return fmt.Errorf("terminal inactive for %v, build cancelled", a.Timeout)
case <-pipeline.Next():

// TODO(bradrydzewski) this entire block of code should probably get
// encapsulated in the pipeline.
status := model.StatusSuccess
if pipeline.Err() != nil {
status = model.StatusFailure
}
// updates the build status passed into each container. I realize this is
// a bit out of place and will work to resolve.
pipeline.Head().Environment["DRONE_BUILD_STATUS"] = status

if !pipeline.Head().Constraints.Match(
a.Platform,
payload.Build.Deploy,
payload.Build.Event,
payload.Build.Branch,
status, payload.Job.Environment) { // TODO: fix this whole section

pipeline.Skip()
} else {
pipeline.Exec()
}
case line := <-pipeline.Pipe():
line.Out = replacer.Replace(line.Out)
a.Logger(line)
}
}
}

结束之后会自动停止和删除容器,但不会删除镜像,参见[src/github.com/drone/drone/build/pipeline.go]

Drone学习笔记3

入口

drone入口在【src/github.com/drone/drone/drone/main.go】,整个目录【src/github.com/drone/drone/drone】都是各种入口,包括drone server/agent和各种cli调用,除了server/agent,cli都是对【src/github.com/drone/drone/client】的一个封装调用。

每一个命令行调用都包含一堆的参数,并且子命令又包含一堆参数,这依赖于urfave/cli。注意main函数的app变量。

func main() {
envflag.Parse()

app := cli.NewApp()
app.Name = "drone"
app.Version = version.Version
app.Usage = "command line utility"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "t, token",
Usage: "server auth token",
EnvVar: "DRONE_TOKEN",
},
cli.StringFlag{
Name: "s, server",
Usage: "server location",
EnvVar: "DRONE_SERVER",
},
}
app.Commands = []cli.Command{
agent.AgentCmd,
agentsCmd,
buildCmd,
deployCmd,
execCmd,
infoCmd, //-------------------
secretCmd,
serverCmd,
signCmd,
repoCmd,
userCmd,
orgCmd,
globalCmd,
}

app.Run(os.Args)
}

src/github.com/drone/drone/drone/info.go

var infoCmd = cli.Command{
Name: "info",
Usage: "show information about the current user",
Action: func(c *cli.Context) {
if err := info(c); err != nil {
log.Fatalln(err)
}
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "format",
Usage: "format output",
Value: tmplUserInfo,
},
},
}

Cli

drone cli仅仅是对【src/github.com/drone/drone/client】的一个封装调用,本质上就是一个http请求,只不过使用不同的jwt token。

src/github.com/drone/drone/drone/user_list.go

func userList(c *cli.Context) error {
client, err := newClient(c)
if err != nil {
return err
}

users, err := client.UserList() //---------------------
if err != nil || len(users) == 0 {
return err
}

src/github.com/drone/drone/client/client.go

// Client is used to communicate with a Drone server.
type Client interface {
// Self returns the currently authenticated user.
Self() (*model.User, error)

// User returns a user by login.
User(string) (*model.User, error)

// UserList returns a list of all registered users.
UserList() ([]*model.User, error)

src/github.com/drone/drone/client/client_impl.go


const (
pathUsers = "%s/api/users"
)

// UserList returns a list of all registered users.
func (c *client) UserList() ([]*model.User, error) {
var out []*model.User
uri := fmt.Sprintf(pathUsers, c.base)

log.Print("user list uri %s\n",uri)
err := c.get(uri, &out)
return out, err
}

路由

drone的路由实现在【src/github.com/drone/drone/router/router.go】,使用gin-gonic/gin

src/github.com/drone/drone/drone/server.go

func server(c *cli.Context) error {

// setup the server and start the listener
handler := router.Load(
ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true),
middleware.Version,
middleware.Config(c),
middleware.Cache(c),
middleware.Store(c),
middleware.Remote(c),
middleware.Agents(c),
middleware.Broker(c),
)

src/github.com/drone/drone/router/router.go


// Load loads the router
func Load(middleware ...gin.HandlerFunc) http.Handler {
e.Use(header.NoCache)
e.Use(header.Options)
e.Use(header.Secure)
e.Use(middleware...)
e.Use(session.SetUser())
e.Use(token.Refresh)

e.GET("/login", server.ShowLogin)
e.GET("/login/form", server.ShowLoginForm)
e.GET("/logout", server.GetLogout)
e.NoRoute(server.ShowIndex)

// TODO above will Go away with React UI

user := e.Group("/api/user")
{
user.Use(session.MustUser())
user.GET("", server.GetSelf)
user.GET("/feed", server.GetFeed)
user.GET("/repos", server.GetRepos)
user.GET("/repos/remote", server.GetRemoteRepos)
user.POST("/token", server.PostToken)
user.DELETE("/token", server.DeleteToken)
}

这其中有一个有意思的用法,用以支持同一个group内部分接口适用的middleware,但又不需要每一个适用的接口写一次

repos := e.Group("/api/repos/:owner/:name")
{
repos.POST("", server.PostRepo)

repo := repos.Group("")
{
repo.Use(session.SetRepo())
repo.Use(session.SetPerm())
repo.Use(session.MustPull)

repo.GET("", server.GetRepo)
}
}

这其中最关键的就是各种midlleware的使用,其中大量使用闭包用于支持全局的初始化

授权

通过下面的中间件设置当前的用户,如果没有无所谓,具体的校验在后面

func SetUser() gin.HandlerFunc {
return func(c *gin.Context) {
var user *model.User
// 获取token,并验证
t, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
var err error
// 继续查询数据库是否存在用户
user, err = store.GetUserLogin(c, t.Text)
return user.Hash, err
})
if err == nil {
confv := c.MustGet("config")
if conf, ok := confv.(*model.Config); ok {
// 是否admin
user.Admin = conf.IsAdmin(user)
}

// 最后设置当前的user到gin.Context
c.Set("user", user)

// 普通的web token,需要验证csrf token
if t.Kind == token.SessToken {
err = token.CheckCsrf(c.Request, func(t *token.Token) (string, error) {
return user.Hash, nil
})
}
}
c.Next()
}
}

获取当前user就比较简单

func User(c *gin.Context) *model.User {
// 从gin.Context获取当前的user,可能不存在
// 参考对比 SetUser c.Set("user", user)
v, ok := c.Get("user")
if !ok {
return nil
}
u, ok := v.(*model.User)
if !ok {
return nil
}
return u
}

通过下面两个中间件用于授权认证

func MustRepoAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
user := User(c)
perm := Perm(c)
switch {
case user == nil:
c.String(401, "User not authorized")
c.Abort()
case perm.Admin == false:
c.String(403, "User not authorized")
c.Abort()
default:
c.Next()
}
}
}

func MustUser() gin.HandlerFunc {
return func(c *gin.Context) {
user := User(c)
switch {
case user == nil:
c.String(401, "User not authorized")
c.Abort()
default:
c.Next()
}
}
}

全局配置

前面提到一个DRONE_OPEN的环境变量,drone使用一个全局配置对象保存这些配置,但是和普通的全局对象不同的时,它利用了gin.Context的Context。也就是上面保存当前用户的方式。

总之,可变(比如当前登录用户)和不变(全局配置)都使用同样的方式,即通过一个闭包获取/保存对象到gin.Context,然后在用的时候从gin.Context获取。

src/github.com/drone/drone/router/middleware/config.go

func Config(cli *cli.Context) gin.HandlerFunc {
v := setupConfig(cli)
return func(c *gin.Context) {
c.Set(configKey, v)
}
}

// helper function to create the configuration from the CLI context.
func setupConfig(c *cli.Context) *model.Config {
return &model.Config{
Open: c.Bool("open"),
Yaml: c.String("yaml"),
Shasum: c.String("yaml") + ".sig",
Secret: c.String("agent-secret"),
Admins: sliceToMap(c.StringSlice("admin")),
Orgs: sliceToMap(c.StringSlice("orgs")),
}
}

当使用oauth登录之后,回调如下

auth := e.Group("/authorize")
{
auth.GET("", server.GetLogin)
auth.POST("", server.GetLogin)
auth.POST("/token", server.GetLoginToken)
}

src/github.com/drone/drone/server/login.go

func GetLogin(c *gin.Context) {
// 获取全局配置对象
config := ToConfig(c)

// get the user from the database
u, err := store.GetUserLogin(c, tmpuser.Login)
if err != nil {

// if self-registration is disabled we should return a not authorized error
// 检查
if !config.Open && !config.IsAdmin(tmpuser) {
logrus.Errorf("cannot register %s. registration closed", tmpuser.Login)
c.Redirect(303, "/login?error=access_denied")
return
}
}
}

数据库

src/github.com/drone/drone/router/middleware/store.go

// Store is a middleware function that initializes the Datastore and attaches to
// the context of every http.Request.
func Store(cli *cli.Context) gin.HandlerFunc {
v := setupStore(cli)
return func(c *gin.Context) {
// 将数据库对象设置到gin.Context
store.ToContext(c, v)
c.Next()
}
}

// helper function to create the datastore from the CLI context.
func setupStore(c *cli.Context) store.Store {
return datastore.New(
c.String("driver"),
c.String("datasource"),
)
}

src/github.com/drone/drone/store/datastore/store.go

// New creates a database connection for the given driver and datasource
// and returns a new Store.
func New(driver, config string) store.Store {
return From(
open(driver, config),
)
}

// helper function to setup the meddler default driver
// based on the selected driver name.
func setupMeddler(driver string) {
// 根据配置,获取实际的数据库对象
switch driver {
case "sqlite3":
meddler.Default = meddler.SQLite
case "mysql":
meddler.Default = meddler.MySQL
case "postgres":
meddler.Default = meddler.PostgreSQL
}
}

// helper function to setup the databsae by performing
// automated database migration steps.
func setupDatabase(driver string, db *sql.DB) error {
var migrations = &migrate.AssetMigrationSource{
Asset: ddl.Asset,
AssetDir: ddl.AssetDir,
Dir: driver,
}
// UP 确保最新,如果没有就创建表什么的
_, err := migrate.Exec(db, driver, migrations, migrate.Up)
return err
}

// open opens a new database connection with the specified
// driver and connection string and returns a store.
func open(driver, config string) *sql.DB {
db, err := sql.Open(driver, config)
if err != nil {
logrus.Errorln(err)
logrus.Fatalln("database connection failed")
}
if driver == "mysql" {
// per issue https://github.com/go-sql-driver/mysql/issues/257
db.SetMaxIdleConns(0)
}

// 设置数据库
setupMeddler(driver)

// 确保数据库/表是最新的
if err := setupDatabase(driver, db); err != nil {
logrus.Errorln(err)
logrus.Fatalln("migration failed")
}
}

Migrate

使用rubenv/sql-migrate,一些类似的主流框架包括pressly/goose

很多migrate框架都可以用自己的格式写migrate脚本,flask-migrate甚至可以自己根据model来生成migrate脚本。这样的好处是不用sql那么啰嗦,但问题也不少

  1. 需要学习它的特定语法
  2. 大部分都不完善,容易被坑
  3. flask-migrate生成的脚本有时候并不完善,需要在手动改改

所以用sql写migrate挺好,

orm

drone没有使用大而全的orm框架,而是一个小巧的russross/meddler

src/github.com/drone/drone/store/datastore/users.go

func (db *datastore) GetUserLogin(login string) (*model.User, error) {
var usr = new(model.User)
var err = meddler.QueryRow(db, usr, rebind(userLoginQuery), login)
return usr, err
}

const userLoginQuery = `
SELECT *
FROM users
WHERE user_login=?
LIMIT 1
`

一些相对完善的如jinzhu/gorm,文档也不错http://jinzhu.me/gorm/,但还是要学习那一套语法,要命的是这种自定义的语法存在潜在的风险,可能生成的不对的sql或者低效的sql脚本。相对完善的orm如sqlalchemy,但要学习那一套复杂的语法也并不容易。

drone由于查询较为简单,所有的查询都封装在【src/github.com/drone/drone/store】,对外只暴露接口Store

src/github.com/drone/drone/store/store.go

type Store interface {
// GetUser gets a user by unique ID.
GetUser(int64) (*model.User, error)

// GetUserLogin gets a user by unique Login name.
GetUserLogin(string) (*model.User, error)

// GetUserList gets a list of all users in the system.
GetUserList() ([]*model.User, error)

git后端适配

由于drone支持多种git后端,例如github,gitlib等,所以对此做了一层封装,代码在【src/github.com/drone/drone/remote】,和数据库一样,只暴露了接口

src/github.com/drone/drone/remote/remote.go

type Remote interface {
// Login authenticates the session and returns the
// remote user details.
Login(w http.ResponseWriter, r *http.Request) (*model.User, error)

// Auth authenticates the session and returns the remote user
// login for the given token and secret
Auth(token, secret string) (string, error)

具体的初始化和设置在【src/github.com/drone/drone/router/middleware/remote.go】

// Remote is a middleware function that initializes the Remote and attaches to
// the context of every http.Request.
func Remote(c *cli.Context) gin.HandlerFunc {
v, err := setupRemote(c)
if err != nil {
logrus.Fatalln(err)
}
return func(c *gin.Context) {
remote.ToContext(c, v)
}
}

// helper function to setup the remote from the CLI arguments.
func setupRemote(c *cli.Context) (remote.Remote, error) {
switch {
case c.Bool("github"):
return setupGithub(c)
case c.Bool("gitlab"):
return setupGitlab(c)
case c.Bool("bitbucket"):
return setupBitbucket(c)
case c.Bool("stash"):
return setupStash(c)
case c.Bool("gogs"):
return setupGogs(c)
default:
return nil, fmt.Errorf("version control system not configured")
}
}

websocket

drone有两个地方用到了websocket

  1. 网站动态刷新内容时,例如编译打包的日志输出
  2. agent和server通信(drone/mq运行在websocket之上)

入口在【src/github.com/drone/drone/router/router.go】

ws := e.Group("/ws")
{
ws.GET("/broker", server.Broker)
ws.GET("/feed", server.EventStream)
ws.GET("/logs/:owner/:name/:build/:number",
session.SetRepo(),
session.SetPerm(),
session.MustPull,
server.LogStream,
)
}

一个简单的例子

src/github.com/drone/drone/server/stream.go

// LogStream streams the build log output to the client.
func LogStream(c *gin.Context) {
// 创建server
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
logrus.Errorf("Cannot upgrade websocket. %s", err)
}
return
}

// 创建沟通的channel
logs := make(chan []byte)
done := make(chan bool)
var eof bool
dest := fmt.Sprintf("/topic/logs.%d", job.ID)
client, _ := stomp.FromContext(c)

// 订阅特定的topic,用来接收agent通过drone/mq发送过来的日志
sub, err := client.Subscribe(dest, stomp.HandlerFunc(func(m *stomp.Message) {
if m.Header.GetBool("eof") {
eof = true
done <- true
} else if eof {
return
} else {
// 在mq的回调中 通过golang的channel传递出来
logs <- m.Body
}
m.Release()
}))

for {
select {
case buf := <-logs:
// 收到消息后,通过ws发送到浏览器客户端
ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.WriteMessage(websocket.TextMessage, buf)
case <-done:
return
case <-ticker.C:
err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait))
if err != nil {
return
}
}
}
}

不得不说,golang的goruntime和channel解决了C后台程序员写并发程序的痛点。

websocket服务器使用了gorilla/websocket,这个gorilla提供了不少非常优秀的组件,是golang写web程序的首选。

gorilla/websocket的用法可以参考https://github.com/gorilla/websocket/tree/master/examples/chat,当然它也提供了websocket的客户端。

mq

drone server和agent使用一个mq来通信,底层使用websocket而不是tcp。这个mq使用STOMP协议,是一个典型的订阅发布模型,并且提供确认机制。

rabbitmq之类的mq不同在于用使用go语言编写,所以可以集成到服务内部,而不用单独开启一个服务,这一点和zeromq有点类似。关于和其他主流的mq对比,见官网http://mq.drone.io/overview/

server mq初始化

通过使用闭包完成全局初始化,然后通过中间件设置到gin.Context

src/github.com/drone/drone/router/middleware/broker.go

// Broker is a middleware function that initializes the broker
// and adds the broker client to the request context.
func Broker(cli *cli.Context) gin.HandlerFunc {
// 参见环境变量DRONE_SECRET
secret := cli.String("agent-secret")
if secret == "" {
logrus.Fatalf("fatal error. please provide the DRONE_SECRET")
}

// 启动mq server
broker := server.NewServer(
server.WithCredentials("x-token", secret),
)
// 创建mq client
client := broker.Client()

var once sync.Once
return func(c *gin.Context) {
// 设置到gin.Context
c.Set(serverKey, broker)
c.Set(clientKey, client)
once.Do(func() {
// this is some really hacky stuff
// turns out I need to do some refactoring
// don't judge!
// will fix in 0.6 release
ctx := c.Copy()
client.Connect(
stomp.WithCredentials("x-token", secret),
)
client.Subscribe("/queue/updates", stomp.HandlerFunc(func(m *stomp.Message) {
go handlers.HandleUpdate(ctx, m.Copy())
}))
})
}
}

agent连接server

src/github.com/drone/drone/drone/agent/agent.go

func start(c *cli.Context) {
var client *stomp.Client
for {
// dial the drone server to establish a TCP connection.
client, err = stomp.Dial(server)
if err != nil {
logger.Warningf("connection failed, retry in %v. %s", backoff, err)
<-time.After(backoff)
continue
}
opts := []stomp.MessageOption{
stomp.WithCredentials("x-token", accessToken),
}

// initialize the stomp session and authenticate.
if err = client.Connect(opts...); err != nil {
logger.Warningf("session failed, retry in %v. %s", backoff, err)
<-time.After(backoff)
continue
}

服务器收到请求之后的处理逻辑很简单,直接转发到mq

src/github.com/drone/drone/server/broker.go

// Broker handles connections to the embedded message broker.
func Broker(c *gin.Context) {
broker := c.MustGet("broker").(http.Handler)
broker.ServeHTTP(c.Writer, c.Request)
}

agent发送消息在【src/github.com/drone/drone/drone/agent/exec.go】

func (r *pipeline) run(w *model.Work) {
a := agent.Agent{
Update: agent.NewClientUpdater(r.drone),
Logger: agent.NewClientLogger(r.drone, w.Job.ID, r.config.logs),
Engine: engine,
Timeout: r.config.timeout,
Platform: r.config.platform,
Namespace: r.config.namespace,
Escalate: r.config.privileged,
Extension: r.config.extension,
Pull: r.config.pull,
}

src/github.com/drone/drone/agent/updater.go

// NewClientUpdater returns an updater that sends updated build details
// to the drone server.
func NewClientUpdater(client *stomp.Client) UpdateFunc {
return func(w *model.Work) {
err := client.SendJSON("/queue/updates", w)
if err != nil {
logger.Warningf("Error updating %s/%s#%d.%d. %s",
w.Repo.Owner, w.Repo.Name, w.Build.Number, w.Job.Number, err)
}
if w.Job.Status != model.StatusRunning {
var dest = fmt.Sprintf("/topic/logs.%d", w.Job.ID)
var opts = []stomp.MessageOption{
stomp.WithHeader("eof", "true"),
stomp.WithRetain("all"),
}

if err := client.Send(dest, []byte("eof"), opts...); err != nil {
logger.Warningf("Error sending eof %s/%s#%d.%d. %s",
w.Repo.Owner, w.Repo.Name, w.Build.Number, w.Job.Number, err)
}
}
}
}

Cache

drone实现了一个类似于redis,但是集成在一起的缓存器,代码在【src/github.com/drone/drone/cache】

Drone学习笔记2

代码结构

king@king:~/code/go/src/github.com/drone$ tree . -L 2
.
├── drone # drone后台代码
│ ├── agent # drone agent主要实现,部分在drone/agent/
│ ├── build # 运行docker容器的逻辑
│ ├── cache # drone自己实现的一个类似redis的内置缓存器
│ ├── client # cli的通用封装
│ ├── drone # 入口代码,没什么核心逻辑
│ ├── model # 通用结构定义
│ ├── remote # 抽象封装github/bitbucket等接口逻辑
│ ├── router # drone server的路由和各种middleware
│ ├── server # drone server实现
│ ├── shared # 公共库
│ ├── store # 抽象封装数据接口,例如sqlite/mysql等
│ ├── version # 版本号
│ └── yaml # 解析.drone.yml文件的逻辑
├── drone-ui # drone server前端,使用React
│ ├── dist
│ ├── images
│ ├── index.html
│ ├── LICENSE
│ ├── package.json
│ ├── README.md
│ ├── src
│ ├── webpack.config.js
│ └── webpack.devserver.js
└── mq # drone实现的一个消息组件,实现了STOMP协议
├── build.sh
├── cmd
├── Dockerfile
├── LICENSE
├── logger
├── README
├── server
├── stomp

前端和资源

前端使用React构建,参考【src/github.com/drone/drone-ui】,还有一些模板资源,见【src/github.com/drone/drone/server/template/files】。

前端资源首先使用webpack将开发代码编译到dist目录【src/github.com/drone/drone-ui/dist/static】,然后使用go-bindata将dist下的文件打包成【src/github.com/drone/drone-ui/dist/dist_gen.go】

// Code generated by go-bindata.
// sources:
// dist/index.html
// dist/static/app.css
// dist/static/app.js
// dist/static/drone.svg
// dist/static/favicon.ico
// DO NOT EDIT!

然后使用go-bindata-assetfs将资源暴露出去。

src/github.com/drone/drone-ui/dist/dist.go

func AssetFS() *assetfs.AssetFS {
for k := range _bintree.Children {
return &assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
AssetInfo: AssetInfo, Prefix: k}
}
panic("unreachable")
}

src/github.com/drone/drone/router/router.go

fs := http.FileServer(dist.AssetFS())
e.GET("/static/*filepath", func(c *gin.Context) {
fs.ServeHTTP(c.Writer, c.Request)
})

模板

模板并不需要预先编译打包,

src/github.com/drone/drone/Makefile

gen: gen_template gen_migrations

gen_template:
go generate github.com/drone/drone/server/template

gen_migrations:
go generate github.com/drone/drone/store/datastore/ddl

关于go generate,参考Golang generate 草案

src/github.com/drone/drone/server/template/template.go

package template

//go:generate go-bindata -pkg template -o template_gen.go files/

src/github.com/drone/drone/server/template/template_gen.go

// Code generated by go-bindata.
// sources:
// files/index.html
// files/login.html
// files/logout.html
// DO NOT EDIT!

package template

src/github.com/drone/drone/router/router.go

e.SetHTMLTemplate(template.Load())

经过这么一折腾,所有的静态资源都直接包含在一个单独的可执行程序中了

数据库迁移脚本

在Makefile中还有这么一段

gen_migrations:
go generate github.com/drone/drone/store/datastore/ddl

这用于将支持的数据库的迁移(migrate)sql脚本打包

package ddl
//go:generate go-bindata -pkg ddl -o ddl_gen.go sqlite3/ mysql/ postgres/

Drone学习笔记

Drone是一个优秀的持续集成(CL)的系统,最大的特色就是它的执行流(pipeline)是在docker里执行,这就确保了每一次环境都保持一致,避免其他干扰。一些其他主流的包括但不限于

  1. Jenkins
  2. Travis
  3. Codeship
  4. Strider

安装运行

可以下载编译好的二进制包,也可以下载源码编译(很简单,参考https://github.com/drone/drone

git clone git://github.com/drone/drone.git $GOPATH/src/github.com/drone/drone
cd $GOPATH/src/github.com/drone/drone

make deps # Download required dependencies
make gen # Generate code
make build_static # Build the binary

运行

建议先仔细阅读官方文档http://readme.drone.io/,运行参考http://readme.drone.io/admin/installation-guide/,官方推荐的方式使用docker-compose来运行程序,包含两个后台

  1. drone server,web服务
  2. drone agent,代理服务,用于运行docker

其他一些组件

  1. drone/mq,a lightweight STOMP message broker
  2. drone cli,一堆的工具集合

完整的帮助如下

king@king:~/code/go/src/github.com/drone/drone/release$ ./drone
NAME:
drone - command line utility

USAGE:
./drone [global options] command [command options] [arguments...]

VERSION:
0.5.0+dev

COMMANDS:
agent starts the drone agent
agents manage agents
build manage builds
deploy deploy code
exec execute a local build
info show information about the current user
secret manage secrets
server starts the drone server daemon
sign creates a secure yaml file
repo manage repositories
user manage users
org manage organizations
global manage global state
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-t, --token server auth token [$DRONE_TOKEN]
-s, --server server location [$DRONE_SERVER]
--help, -h show help
--version, -v print the version

server

在运行服务之前,需要准备参数,可以通过命令行查看,代码中的定义见【src/github.com/drone/drone/drone/server.go】,下面是一些搭建drone hello world的参考

  1. http://www.wtoutiao.com/p/38b2N4A.html
  2. https://overflow.no/blog/2016/11/14/self-hosted-cdci-environment-with-docker-droneio-and-drone-wall-on-debian-8/
  3. http://tleyden.github.io/blog/2016/02/15/setting-up-a-self-hosted-drone-dot-io-ci-server/

以github为例,必需的参数如下

  1. DRONE_OPEN=true,自动创建账户
  2. DRONE_GITHUB=true,使用github,可选的见【src/github.com/drone/drone/router/middleware/remote.go
  3. DRONE_GITHUB_CLIENT=${DRONE_GITHUB_CLIENT},从github网站获取
  4. DRONE_GITHUB_SECRET=${DRONE_GITHUB_SECRET},从github网站获取
  5. DRONE_SECRET=${DRONE_SECRET},用于agent连接server的key,具体通过drone/mq验证,使用websocket通道
// helper function to setup the remote from the CLI arguments.
func setupRemote(c *cli.Context) (remote.Remote, error) {
switch {
case c.Bool("github"):
return setupGithub(c)
case c.Bool("gitlab"):
return setupGitlab(c)
case c.Bool("bitbucket"):
return setupBitbucket(c)
case c.Bool("stash"):
return setupStash(c)
case c.Bool("gogs"):
return setupGogs(c)
default:
return nil, fmt.Errorf("version control system not configured")
}
}

不使用docker,直接写个脚本即可运行,为了使用依赖管理员权限的功能,增加DRONE_ADMIN,所有的记录存储在后端数据库,默认是sqlite3,当前目录下的drone.sqlite,为了避免在不同的目录出现问题,通过DRONE_DATABASE_DATASOURCE写死路径。

支持的数据库见【src/github.com/drone/drone/store/datastore/store.go】

#!/bin/bash
export DRONE_OPEN=true
export DRONE_GITHUB=true
export DRONE_GITHUB_SECRET=22222
export DRONE_GITHUB_CLIENT=11111
export DRONE_SECRET=create_a_random_secret_here
export DRONE_DATABASE_DRIVER=sqlite3
export DRONE_DATABASE_DATASOURCE=/home/king/code/go/drone.sqlite
export DRONE_ADMIN=king1,king2
./drone "${@}"
// helper function to setup the meddler default driver
// based on the selected driver name.
func setupMeddler(driver string) {
switch driver {
case "sqlite3":
meddler.Default = meddler.SQLite
case "mysql":
meddler.Default = meddler.MySQL
case "postgres":
meddler.Default = meddler.PostgreSQL
}
}

由于需要使用github的oauth认证,以及接收推送,所以服务器必须支持公网访问,若要本地测试,可以参考ngrok反向代理局域网开发机

agent

agent用于代理执行docker容器,由于docker运行需要超级管理员权限,所以需要sudo,或者切换到root。为了方便测试,最好是将当前用户添加到docker组,参考https://my.oschina.net/zhouhui321/blog/788431

sudo gpasswd -a ${USER} docker
sudo service docker restart

agent完整参数见【src/github.com/drone/drone/drone/agent/agent.go】,必需的参数如下

  1. DRONE_SERVER=ws://${drone-server}/ws/broker,server的路径
  2. DRONE_SECRET=${DRONE_SECRET},在server中配置的token

同样可以export两个环境变量之后,直接drone agent即可

cli

运行上述两个后台之后,就可以用浏览器访问【http//${drone-server}】,若未登录点击【登录/login】会自动跳转到github进行认证,由于设置了【DRONE_OPEN=true】所以认证成功之后,会自动创建以github用户名的账户。

drone并未使用普通的session管理机制,而是使用jwt机制,并使用cookie传输,如下

user_sess=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEuNDg5MDUxNjc5ZSswOSwidGV4dCI6InFqdyIsInR5cGUiOiJzZXNzIn0.SD1uwHk_E7yT-74sVXHwrLPKmonbwRtQTzT8cNPrupg
_crsf=MTQ4NzkyMTYzOHxEdi1CQkFFQ180SUFBUkFCRUFBQU12LUNBQUVHYzNSeWFXNW5EQW9BQ0dOemNtWlRZV3gwQm5OMGNtbHVad3dTQUJCS2IzSk5lbVEwWlRaV05XdHdUalZrfHeLaVDFFSGOkLA19-4Y6G5gYkzJatkOn8oNnHj4eb6q;

cli运行依赖的参数包括

  1. DRONE_SERVER=http://${drone-server} ,server的地址,注意名称和agent的一样,但地址并不一致
  2. DRONE_TOKEN=fsajdflkas,token就是jwt的token,注意和DRONE_SECRET区别

我们可以直接将网站的jwt token直接拿来使用,但仅限于查询操作,其他操作会因为csrf限制无权限,参考【src/github.com/drone/drone/router/middleware/session/user.go】

// if this is a session token (ie not the API token)
// this means the user is accessing with a web browser,
// so we should implement CSRF protection measures.
if t.Kind == token.SessToken {
err = token.CheckCsrf(c.Request, func(t *token.Token) (string, error) {
return user.Hash, nil
})
// if csrf token validation fails, exit immediately
// with a not authorized error.
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}

这里实际上引申出一个session type的东西

const (
UserToken = "user"
SessToken = "sess"
HookToken = "hook"
CsrfToken = "csrf"
AgentToken = "agent"
)

网页授权使用的是SessionToken,而drone cli使用UserToken,具体通过drone网站右上角【ACCOUNT】,再回到左上角【SHOW TOKEN】,参考【src/github.com/drone/drone/router/router.go】

user := e.Group("/api/user")
{
user.POST("/token", server.PostToken)
user.DELETE("/token", server.DeleteToken)
}

cli是一堆的命令集合,具体参考官方文档,有些功能仅限cli使用

普通账户密码登录

drone代码中有一些相关的逻辑,但并不完整,所以不可用

// src/github.com/drone/drone/router/router.go
e.GET("/login", server.ShowLogin)
e.GET("/login/form", server.ShowLoginForm)
e.GET("/logout", server.GetLogout)

// src/github.com/drone/drone/server/pages.go

// ShowLoginForm displays a login form for systems like Gogs that do not
// yet support oauth workflows.
func ShowLoginForm(c *gin.Context) {
c.HTML(200, "login.html", gin.H{})
}

// src/github.com/drone/drone/server/template/files/login.html

一些环境的配置

大部分情况下(特别是生产环境)最好是直接下载docker运行,简单可靠。偶尔为了方便本地测试,直接装在本地也有一定的好处,至少没那么折腾。

比较稳定(更新没那么频繁)的东西,例如nginx,mysql,直接apt-get就行了

JDK

jdk默认情况下只有openjdk或者比较老的官方jdk,要安装最新的jdk 1.8,可以

添加jdk的ppa,参考http://tipsonubuntu.com/2016/07/31/install-oracle-java-8-9-ubuntu-16-04-linux-mint-18/

大部分ppa都需要翻墙,这很坑爹,也可以下载tar.gz包解压并配置环境变量,参考http://blog.csdn.net/chenruicsdn/article/details/53583950

sudo mkdir -p /usr/lib/jvm
sudo tar -zxvf Downloads/jdk-8u112-Linux-x64.tar.gz -C /usr/lib/jvm
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_112
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JAVA_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH

redis

默认的redis比较老,而有些新特性无法满足其他依赖需要,比如sentry

这里使用ppahttp://blog.csdn.net/chenlix/article/details/46696165

sudo apt-get install -y python-software-properties
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:rwky/redis
sudo apt-get update
sudo apt-get install -y redis-server

关于sentry,可以dockerhttps://hub.docker.com/_/sentry/

Golang

官网下载最新的包,例如go1.7.5.linux-amd64.tar.gz,解压

export GOROOT=/usr/lib/go/
export GOPATH=/home/abc/code/go
export PATH=$GOROOT/bin:$PATH

Nodejs

官网下载最新的包,例如node-v6.10.0-linux-x64.tar.xz,解压

export NODE_HOME=/usr/lib/node
export PATH=$PATH:$NODE_HOME/bin
export NODE_PATH=$NODE_HOME/lib/node_modules

Docker

直接安装官网的指引安装docker-ce,还是很老旧,可以在apt-get purge卸载已经安装的docker版本,然后从https://apt.dockerproject.org/repo/pool/main/d/docker-engine/下载(例如)docker-engine_1.13.1-0~ubuntu-xenial_amd64.deb,然后dpkg -i 安装。

配置阿里云加速

参考http://warjiang.github.io/devcat/2016/11/28/%E4%BD%BF%E7%94%A8%E9%98%BF%E9%87%8C%E4%BA%91Docker%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F/

//如果您的系统是 Ubuntu 15.04 16.04,Docker 1.9 以上

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/mirror.conf <<-'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/docker daemon -H fd:// --registry-mirror=https://2h3po24q.mirror.aliyuncs.com
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

注意,第一个ExecStart=不能省略,参见http://askubuntu.com/questions/19320/how-to-enable-or-disable-services

Python

一般安装其他软件就把Python连带安装进来了,2和3都有,默认是Python2

sudo update-alternatives --install /usr/bin/python python /usr/bin/python2 100
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 500

python --version
Python 3.5.2

rcconf

可以用rcconf启用/禁用某些服务的自启动

sudo apt-get install rcconf

Cookie和Storeage对比

如果只存储仅限前端使用的数据,毫无疑问,无必要使用cookie

Storage又区分LocalStorage(本地存储)和sessionStorage(会话存储),LocalStorage和sessionStorage功能上是一样的,但是存储持久时间不一样。

  1. LocalStorage:浏览器关闭了数据仍然可以保存下来,并可用于所有同源(相同的域名、协议和端口)窗口(或标签页)永久存储,永不失效,除非手动删除
  2. sessionStorage:数据存储在窗口对象中,窗口关闭后对应的窗口对象消失,存储的数据也会丢失。就是浏览器窗口关闭就失效了。

主要区别

  1. cookie可以设置过期时间,domain,path等,storage不支持
  2. cookie容量很小,storage相对较大,大概5M左右,而cookie只有几K
  3. 默认cookie和storage都可以被js读写,这容易招致XSS攻击,cookie支持HttpOnly属性,禁止js读写。(另一个有用的属性是secure
  4. cookie自动由浏览器带入服务器,或者由服务器下发,storage是纯客户端的东西,若要发送到服务器,必须用js提取并写入http query参数,form/json参数或者http头等正规渠道。
  5. cookie自动提交的特性招致了crsf攻击

XSS

XSS 全称“跨站脚本”,是注入攻击的一种。其特点是不对服务器端造成任何伤害,而是通过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到了页面上,其他用户访问这个页面的时候就会运行这些脚本。

又区分

  1. Stored XSS,持久化,代码是存储在服务器中的,所有访问的用户都受影响,波及方位广。
  2. Reflected XSS,非持久化,需要欺骗用户自己去点击链接才能触发XSS代码。
  3. DOM-based or local XSS,基于DOM或本地的XSS攻击。一般是提供一个免费的wifi,但是提供免费wifi的网关会往你访问的任何页面插入一段脚本或者是直接返回一个钓鱼页面,从而植入恶意脚本。这种直接存在于页面,无须经过服务器返回就是基于本地的XSS攻击。

防护

  1. 对用户的输入进行处理,只允许输入合法的值,其它值一概过滤掉。
  2. 对不可信的js来源进行完整性校验,现在前端经常使用cdn的js代码,若受到攻击就容易招致攻击
<script src="https://example.com/example-framework.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>

CRSF

CRSF是跨站请求伪造:Cross site request forgery,是一种对网站的恶意利用。它和XSS跨站脚本攻击的不同是,XSS利用站点内的信任用户;CRSF则是通过伪装来自信任用户的请求,来利用受信任的网站。举例就是:攻击者盗用了你的身份,以你的名义向第三方信任网站发送恶意请求,如发邮件,发短信,转账交易等。

防护

  1. 勿使用get作更新操作
  2. 服务器根据Referer字段等做一些简单的防护,参见盗链
  3. 在请求中放入攻击者所不能伪造的信息,并且该信息不存在于Cookie之中。通常在http头或者hidden表单中传递,可以从用js直接从cookie读取,或者直接从服务端返回,但这种情况下要配合referer防护使用。
  4. 在请求中加入验证码,但增加了用户的使用成本,影响体验。

Cookie跨域和单点登录

若只涉及到子域名的cookie共享问题,cookie有一个domain的字段,例如aaa.ccc.com和bbb.ccc.com若要共享,设置domain为【.ccc.com】即可(注意.号不能省)

若是不同的域名,那就必须想其他办法

种Cookie

还是aaa.com和bbb.com。

  1. aaa登录成功之后,服务器返回cookie
  2. aaa继续调用一个枚举所有需要种cookie的url列表

接下来分成两种情况

  1. 返回的地址可以直接登录,例如http://bbb.com?token=fasdfasf&timestamp=123。接下来使用jsonp的方式调用这个地址,完成bbb.com的域名种植,成功植入后token失效。为了安全起见,token需要设置有效期
  2. 不用jsonp,返回的地址列表无敏感信息,继续在aaa.com发起一个http://aaa.com/login_another?domain=bbb.com的请求,在请求中重定向到http://bbb.com?token=fasdfa完成cookie种植

偷Cookie

bbb.com登录时,若本地没有cookie,就用jsonp的方式发起http://aaa.com/stolen_cookie的请求,通过aaa的服务器偷到aaa本机的所有cookie,然后用js设置到当前域名,最后再发起登录

也可以用stolen_cookie获取一个可以直接登录的token,然后使用这个token完成身份验证。

更为复杂的可以综合两种方式,比如iframe直接请求http://aaa.com/stolen_cookie,aaa服务器发现已经登录就重定向到带token的bbb.com,最后bbb就种好了cookie,在通过一些渠道通知父页面刷新。

参考

  1. http://www.cnblogs.com/showker/archive/2010/01/21/1653332.html
  2. http://www.oschina.net/question/4873_18517