diff --git a/.drone.sec b/.drone.sec index 975083657..bdc95749e 100644 --- a/.drone.sec +++ b/.drone.sec @@ -1 +1 @@ -eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.Y_yR-WIrz_Shk94TlGh1mhLM5NocQeh1nRTz65Hfn8jo78WtVF9ZDUQsf6z2bpipTp4y0TmSjWHLvQHQf1O_LZ-AxLHkG3_XGqeWl7Jd3lE_xsBeWeOIrC3QKx8dNiyU0FKVCoPwfaMpjpXAFpG4ZesNfpTrxaaoT-0PBYYpbVvAi-lIhh5lqKSDlWRkrzR3pvzLPeY-Pq4yb-DE0wLzAOh5nasde3qIKo9VT2fnRQIPIIC6V4vNyi7EdYDhNcdgx9LCFKIYVkCPRRI1R9mG8tyjpLy47t0gs3y-Gcr8MyNE1lPlToX4JaQ4EKcZZAAf2CVCJ92s1Bp6CZWLiFt9aQ.RoWDoMbR2ICXQPlo.7d_JtqJvLPPAdmIMOoD961rO7KZXwY-Fz8GHx98hdHqr27igkoKm1BXqYqlh043wKkHwVaAy5jNAOUdweJKrb5YJGPdk3Lyh4ZpEDvcAiwPgwsf4_Q1Xhw9A1jSOyapFlaSAR2aKj_370yx0y8htgxTeXj5Nv5n7ZZ4ezYstU8xCF9JxzsBxOfFACqiyZr9ZJcm-47mlftcU1nZy1x9005Vq6oZv9FAk4zwKLpjMC3KC3H252PqbL_U-e9j1i5tlrcDiEkW8gLoMq_RbMj5J6w3j0u4eoUV5e-WfjentG5ZIf_e8c8-oKxXF2A1vdVdswzWhQWoIEs2KDqBWmSPsCNHKHE2t4grnDPxipMJByAUSsEE9_NVWRS2ofjw01YpQd8cRV6v-eKBlF-c1QD4ABqBu0iMYTEuScg.AAAOUFIZbiVDhUdpARCZ9g \ No newline at end of file +eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.RPXRWzmKbZ6KShRzmd1adFofHAofk6weJrzfjEd0-gRoscpoHrbvGy-DOUVOjwfI41H52EBFV8L_-8uXdRKkCsLnzDpWYmbSKAkKLGDMAILlQcGr4-C_6_duP8nK6Lw2MuAeP8KACBnHVXaosks-ARr7bC5hzQBgJLMuxw3O40jkMS7J62T1Oo7CRkSlLpAIu0axmWQ6ZlfRhijyZUMAJTfegykgrMbpL-FlyDsRpHYkCC0Ny5tN2Q4Gocmxwgitv8_uNZqXYuOAxQXEwXTSyLkXhBdIDtNLDXkAmglTdCB7mis-SmOw4lxSEMAU20bFqOrtbOP_5jwIPoLO8FR4Fw.TU_hlTlqCRZdI-Jw.NBZUu2Wv6ZRzoRpcrEyg2V-me_XEAI22HT_BOJ1NS1bhPLWx1zCbu8hfWNI9RBUMvppSzFw6leeUXgFGqOjGquVvAOCBi0pKuPVW5jGkmv3kM43ciUzzR5MErAg_VPqQjKkV5RvsSu7gKHr6PTSOmc8hPU9JyfNNMUh4MGOHJnvv3I-oKJva3oOt-y9KPsuGLo-6hM1WbhYyPvcm3PSiBrKkOZM5f0_2nqcZZzHQ8gvo5BmzxcSAYVmKRo8_rLROMqT1fycnWg_4qsJuD6molP9b-88Vb0vrZ2jpvm-f_Cq2psSPPMQIcxSvlweO-dP7u0WvdLnOsIb-cq6HxqorTxtKSKcsGbqv66gLsThsa8KDivreyRFyHhTiKwTugv8Kw4Fxsfhj6hzbuI6Vy5Utyr6OJ30MRpg8kg.qKp-C7QNFaeU0HZJ6Qwp8g \ No newline at end of file diff --git a/.drone.yml b/.drone.yml index 52b56f9bf..e78534349 100644 --- a/.drone.yml +++ b/.drone.yml @@ -35,7 +35,7 @@ publish: password: $$DOCKER_PASS email: $$DOCKER_EMAIL repo: drone/drone - tag: [ "latest", "0.4.2" ] + tag: [ "0.5.0" ] when: repo: drone/drone branch: master diff --git a/.gitignore b/.gitignore index 01abb6461..3252b2c16 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ drone/drone .env temp/ -api/swagger/files/* +server/swagger/files/*.json # vendored repositories that we don't actually need # to vendor. so exclude them diff --git a/Dockerfile b/Dockerfile index 9a634564d..363afd3b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ ADD drone/drone /drone #RUN echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf ENTRYPOINT ["/drone"] -CMD ["serve"] +CMD ["daemon"] diff --git a/api/node.go b/api/node.go deleted file mode 100644 index 592ec0c1b..000000000 --- a/api/node.go +++ /dev/null @@ -1,80 +0,0 @@ -package api - -import ( - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - - "github.com/drone/drone/engine" - "github.com/drone/drone/model" - "github.com/drone/drone/store" -) - -func GetNodes(c *gin.Context) { - nodes, err := store.GetNodeList(c) - if err != nil { - c.String(400, err.Error()) - } else { - c.JSON(200, nodes) - } -} - -func GetNode(c *gin.Context) { - -} - -func PostNode(c *gin.Context) { - engine := engine.FromContext(c) - - in := struct { - Addr string `json:"address"` - Arch string `json:"architecture"` - Cert string `json:"cert"` - Key string `json:"key"` - CA string `json:"ca"` - }{} - err := c.Bind(&in) - if err != nil { - c.AbortWithStatus(http.StatusBadRequest) - return - } - - node := &model.Node{} - node.Addr = in.Addr - node.Cert = in.Cert - node.Key = in.Key - node.CA = in.CA - node.Arch = "linux_amd64" - - err = engine.Allocate(node) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - - err = store.CreateNode(c, node) - if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) - return - } - - c.IndentedJSON(http.StatusOK, node) -} - -func DeleteNode(c *gin.Context) { - engine := engine.FromContext(c) - - id, _ := strconv.Atoi(c.Param("node")) - node, err := store.GetNode(c, int64(id)) - if err != nil { - c.AbortWithStatus(http.StatusNotFound) - return - } - err = store.DeleteNode(c, node) - if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) - return - } - engine.Deallocate(node) -} diff --git a/api/swagger/swagger.go b/api/swagger/swagger.go deleted file mode 100644 index 7921031e5..000000000 --- a/api/swagger/swagger.go +++ /dev/null @@ -1,3 +0,0 @@ -package swagger - -//go:generate go-bindata -pkg swagger -o swagger_gen.go files/ diff --git a/drone/agent/agent.go b/drone/agent/agent.go index cae875f11..5f499e845 100644 --- a/drone/agent/agent.go +++ b/drone/agent/agent.go @@ -141,9 +141,9 @@ func start(c *cli.Context) { c.String("drone-token"), ) - tls, _ := dockerclient.TLSConfigFromCertPath(c.String("docker-cert-path")) - if c.Bool("docker-host") { - tls.InsecureSkipVerify = true + tls, err := dockerclient.TLSConfigFromCertPath(c.String("docker-cert-path")) + if err == nil { + tls.InsecureSkipVerify = c.Bool("docker-tls-verify") } docker, err := dockerclient.NewDockerClient(c.String("docker-host"), tls) if err != nil { diff --git a/drone/daemon.go b/drone/daemon.go new file mode 100644 index 000000000..91b271ccb --- /dev/null +++ b/drone/daemon.go @@ -0,0 +1,481 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "time" + + "github.com/drone/drone/router" + "github.com/drone/drone/router/middleware" + "github.com/gin-gonic/contrib/ginrus" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" +) + +// DaemonCmd is the exported command for starting the drone server daemon. +var DaemonCmd = cli.Command{ + Name: "daemon", + Usage: "starts the drone server daemon", + Action: func(c *cli.Context) { + if err := start(c); err != nil { + logrus.Fatal(err) + } + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + EnvVar: "DRONE_DEBUG", + Name: "debug", + Usage: "start the server in debug mode", + }, + cli.StringFlag{ + EnvVar: "DRONE_SERVER_ADDR", + Name: "server-addr", + Usage: "server address", + Value: ":8000", + }, + cli.StringFlag{ + EnvVar: "DRONE_SERVER_CERT", + Name: "server-cert", + Usage: "server ssl cert", + }, + cli.StringFlag{ + EnvVar: "DRONE_SERVER_KEY", + Name: "server-key", + Usage: "server ssl key", + }, + cli.StringSliceFlag{ + EnvVar: "DRONE_ADMIN", + Name: "admin", + Usage: "list of admin users", + }, + cli.StringSliceFlag{ + EnvVar: "DRONE_ORGS", + Name: "orgs", + Usage: "list of approved organizations", + }, + cli.BoolFlag{ + EnvVar: "DRONE_OPEN", + Name: "open", + Usage: "open user registration", + }, + cli.StringFlag{ + EnvVar: "DRONE_YAML", + Name: "yaml", + Usage: "build configuraton file name", + Value: ".drone.yml", + }, + cli.DurationFlag{ + EnvVar: "DRONE_CACHE_TTY", + Name: "cache-tty", + Usage: "cache duration", + Value: time.Minute * 15, + }, + cli.StringFlag{ + EnvVar: "DRONE_AGENT_SECRET", + Name: "agent-secret", + Usage: "agent secret passcode", + }, + cli.StringFlag{ + EnvVar: "DRONE_DATABASE_DRIVER,DATABASE_DRIVER", + Name: "driver", + Usage: "database driver", + Value: "sqite3", + }, + cli.StringFlag{ + EnvVar: "DRONE_DATABASE_DATASOURCE,DATABASE_CONFIG", + Name: "datasource", + Usage: "database driver configuration string", + Value: "drone.sqlite", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GITHUB", + Name: "github", + Usage: "github driver is enabled", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITHUB_URL", + Name: "github-server", + Usage: "github server address", + Value: "https://github.com", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITHUB_CLIENT", + Name: "github-client", + Usage: "github oauth2 client id", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITHUB_SECRET", + Name: "github-sercret", + Usage: "github oauth2 client secret", + }, + cli.StringSliceFlag{ + EnvVar: "DRONE_GITHUB_SCOPE", + Name: "github-scope", + Usage: "github oauth scope", + Value: &cli.StringSlice{ + "repo", + "repo:status", + "user:email", + "read:org", + }, + }, + cli.BoolTFlag{ + EnvVar: "DRONE_GITHUB_MERGE_REF", + Name: "github-merge-ref", + Usage: "github pull requests use merge ref", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GITHUB_PRIVATE_MODE", + Name: "github-private-mode", + Usage: "github is running in private mode", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GITHUB_SKIP_VERIFY", + Name: "github-skip-verify", + Usage: "github skip ssl verification", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GOGS", + Name: "gogs", + Usage: "gogs driver is enabled", + }, + cli.StringFlag{ + EnvVar: "DRONE_GOGS_URL", + Name: "gogs-server", + Usage: "gogs server address", + Value: "https://github.com", + }, + cli.StringFlag{ + EnvVar: "DRONE_GOGS_GIT_USERNAME", + Name: "gogs-git-username", + Usage: "gogs service account username", + }, + cli.StringFlag{ + EnvVar: "DRONE_GOGS_GIT_PASSWORD", + Name: "gogs-git-password", + Usage: "gogs service account password", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GOGS_PRIVATE_MODE", + Name: "gogs-private-mode", + Usage: "gogs private mode enabled", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GOGS_SKIP_VERIFY", + Name: "gogs-skip-verify", + Usage: "gogs skip ssl verification", + }, + cli.BoolFlag{ + EnvVar: "DRONE_BITBUCKET", + Name: "bitbucket", + Usage: "bitbucket driver is enabled", + }, + cli.StringFlag{ + EnvVar: "DRONE_BITBUCKET_CLIENT", + Name: "bitbucket-client", + Usage: "bitbucket oauth2 client id", + }, + cli.StringFlag{ + EnvVar: "DRONE_BITBUCKET_SECRET", + Name: "bitbucket-secret", + Usage: "bitbucket oauth2 client secret", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GITLAB", + Name: "gitlab", + Usage: "gitlab driver is enabled", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITLAB_URL", + Name: "gitlab-server", + Usage: "gitlab server address", + Value: "https://gitlab.com", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITLAB_CLIENT", + Name: "gitlab-client", + Usage: "gitlab oauth2 client id", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITLAB_SECRET", + Name: "gitlab-sercret", + Usage: "gitlab oauth2 client secret", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITLAB_GIT_USERNAME", + Name: "gitlab-git-username", + Usage: "gitlab service account username", + }, + cli.StringFlag{ + EnvVar: "DRONE_GITLAB_GIT_PASSWORD", + Name: "gitlab-git-password", + Usage: "gitlab service account password", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GITLAB_SKIP_VERIFY", + Name: "gitlab-skip-verify", + Usage: "gitlab skip ssl verification", + }, + cli.BoolFlag{ + EnvVar: "DRONE_GITLAB_PRIVATE_MODE", + Name: "gitlab-private-mode", + Usage: "gitlab is running in private mode", + }, + cli.BoolFlag{ + EnvVar: "DRONE_STASH", + Name: "stash", + Usage: "stash driver is enabled", + }, + cli.StringFlag{ + EnvVar: "DRONE_STASH_URL", + Name: "stash-server", + Usage: "stash server address", + }, + cli.StringFlag{ + EnvVar: "DRONE_STASH_CONSUMER_KEY", + Name: "stash-consumer-key", + Usage: "stash oauth1 consumer key", + }, + cli.StringFlag{ + EnvVar: "DRONE_STASH_CONSUMER_RSA", + Name: "stash-consumer-rsa", + Usage: "stash oauth1 private key file", + }, + cli.StringFlag{ + EnvVar: "DRONE_STASH_GIT_USERNAME", + Name: "stash-git-username", + Usage: "stash service account username", + }, + cli.StringFlag{ + EnvVar: "DRONE_STASH_GIT_PASSWORD", + Name: "stash-git-password", + Usage: "stash service account password", + }, + cli.BoolFlag{ + EnvVar: "DRONE_STASH_SKIP_VERIFY", + Name: "stash-skip-verify", + Usage: "stash skip ssl verification", + }, + // + // remove these eventually + // + + cli.BoolFlag{ + Name: "agreement.ack", + EnvVar: "I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION", + Usage: "agree to terms of use.", + }, + cli.BoolFlag{ + Name: "agreement.fix", + EnvVar: "I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS", + Usage: "agree to terms of use.", + }, + }, +} + +func start(c *cli.Context) error { + + if c.Bool("agreement.ack") == false || c.Bool("agreement.fix") == false { + fmt.Println(agreement) + os.Exit(1) + } + + // debug level if requested by user + if c.Bool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } else { + logrus.SetLevel(logrus.WarnLevel) + } + + // setup the server and start the listener + handler := router.Load( + ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), + middleware.Version, + middleware.Config(c), + middleware.Queue(c), + middleware.Stream(c), + middleware.Bus(c), + middleware.Cache(c), + middleware.Store(c), + middleware.Remote(c), + middleware.Agents(c), + ) + + // start the server with tls enabled + if c.String("server-cert") != "" { + return http.ListenAndServeTLS( + c.String("server-addr"), + c.String("server-cert"), + c.String("server-key"), + handler, + ) + } + + // start the server without tls enabled + return http.ListenAndServe( + c.String("server-addr"), + handler, + ) +} + +// +// func setupCache(c *cli.Context) cache.Cache { +// return cache.NewTTL( +// c.Duration("cache-ttl"), +// ) +// } +// +// func setupBus(c *cli.Context) bus.Bus { +// return bus.New() +// } +// +// func setupQueue(c *cli.Context) queue.Queue { +// return queue.New() +// } +// +// func setupStream(c *cli.Context) stream.Stream { +// return stream.New() +// } +// +// func setupStore(c *cli.Context) store.Store { +// return datastore.New( +// c.String("driver"), +// c.String("datasource"), +// ) +// } +// +// func setupRemote(c *cli.Context) remote.Remote { +// var remote remote.Remote +// var err error +// switch { +// case c.Bool("github"): +// remote, err = setupGithub(c) +// case c.Bool("gitlab"): +// remote, err = setupGitlab(c) +// case c.Bool("bitbucket"): +// remote, err = setupBitbucket(c) +// case c.Bool("stash"): +// remote, err = setupStash(c) +// case c.Bool("gogs"): +// remote, err = setupGogs(c) +// default: +// err = fmt.Errorf("version control system not configured") +// } +// if err != nil { +// logrus.Fatalln(err) +// } +// return remote +// } +// +// func setupBitbucket(c *cli.Context) (remote.Remote, error) { +// return bitbucket.New( +// c.String("bitbucket-client"), +// c.String("bitbucket-server"), +// ), nil +// } +// +// func setupGogs(c *cli.Context) (remote.Remote, error) { +// return gogs.New(gogs.Opts{ +// URL: c.String("gogs-server"), +// Username: c.String("gogs-git-username"), +// Password: c.String("gogs-git-password"), +// PrivateMode: c.Bool("gogs-private-mode"), +// SkipVerify: c.Bool("gogs-skip-verify"), +// }) +// } +// +// func setupStash(c *cli.Context) (remote.Remote, error) { +// return bitbucketserver.New(bitbucketserver.Opts{ +// URL: c.String("stash-server"), +// Username: c.String("stash-git-username"), +// Password: c.String("stash-git-password"), +// ConsumerKey: c.String("stash-consumer-key"), +// ConsumerRSA: c.String("stash-consumer-rsa"), +// SkipVerify: c.Bool("stash-skip-verify"), +// }) +// } +// +// func setupGitlab(c *cli.Context) (remote.Remote, error) { +// return gitlab.New(gitlab.Opts{ +// URL: c.String("gitlab-server"), +// Client: c.String("gitlab-client"), +// Secret: c.String("gitlab-sercret"), +// Username: c.String("gitlab-git-username"), +// Password: c.String("gitlab-git-password"), +// PrivateMode: c.Bool("gitlab-private-mode"), +// SkipVerify: c.Bool("gitlab-skip-verify"), +// }) +// } +// +// func setupGithub(c *cli.Context) (remote.Remote, error) { +// return github.New( +// c.String("github-server"), +// c.String("github-client"), +// c.String("github-sercret"), +// c.StringSlice("github-scope"), +// c.Bool("github-private-mode"), +// c.Bool("github-skip-verify"), +// c.BoolT("github-merge-ref"), +// ) +// } +// +// func setupConfig(c *cli.Context) *server.Config { +// return &server.Config{ +// Open: c.Bool("open"), +// Yaml: c.String("yaml"), +// Secret: c.String("agent-secret"), +// Admins: sliceToMap(c.StringSlice("admin")), +// Orgs: sliceToMap(c.StringSlice("orgs")), +// } +// } +// +// func sliceToMap(s []string) map[string]bool { +// v := map[string]bool{} +// for _, ss := range s { +// v[ss] = true +// } +// return v +// } +// +// func printSecret(c *cli.Context) error { +// secret := c.String("agent-secret") +// if secret == "" { +// return fmt.Errorf("missing DRONE_AGENT_SECRET configuration parameter") +// } +// t := token.New(secret, "") +// s, err := t.Sign(secret) +// if err != nil { +// return fmt.Errorf("invalid value for DRONE_AGENT_SECRET. %s", s) +// } +// +// logrus.Infof("using agent secret %s", secret) +// logrus.Warnf("agents can connect with token %s", s) +// return nil +// } + +var agreement = ` +--- + + +You are attempting to use the unstable channel. This build is experimental and +has known bugs and compatibility issues. It is not intended for general use. + +Please consider using the latest stable release instead: + + drone/drone:0.4.2 + +If you are attempting to build from source please use the latest stable tag: + + v0.4.2 + +If you are interested in testing this experimental build AND assisting with +development you may proceed by setting the following environment: + + I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION=true + I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS=true + + +--- +` diff --git a/drone/drone.go b/drone/drone.go index 2a92660da..310b1fc1b 100644 --- a/drone/drone.go +++ b/drone/drone.go @@ -4,7 +4,6 @@ import ( "os" "github.com/drone/drone/drone/agent" - "github.com/drone/drone/drone/server" "github.com/drone/drone/version" "github.com/codegangsta/cli" @@ -12,7 +11,7 @@ import ( _ "github.com/joho/godotenv/autoload" ) -func main2() { +func main() { envflag.Parse() app := cli.NewApp() @@ -35,7 +34,7 @@ func main2() { } app.Commands = []cli.Command{ agent.AgentCmd, - server.ServeCmd, + DaemonCmd, SignCmd, SecretCmd, } diff --git a/drone/main.go b/drone/main.go deleted file mode 100644 index e08585855..000000000 --- a/drone/main.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "net/http" - "os" - "time" - - "github.com/drone/drone/router" - "github.com/drone/drone/router/middleware" - - "github.com/Sirupsen/logrus" - "github.com/gin-gonic/contrib/ginrus" - "github.com/ianschenck/envflag" - _ "github.com/joho/godotenv/autoload" -) - -var ( - addr = envflag.String("SERVER_ADDR", ":8000", "") - cert = envflag.String("SERVER_CERT", "", "") - key = envflag.String("SERVER_KEY", "", "") - - debug = envflag.Bool("DEBUG", false, "") -) - -func main() { - if os.Getenv("CANARY") == "true" { - main2() - return - } - - envflag.Parse() - - // debug level if requested by user - if *debug { - logrus.SetLevel(logrus.DebugLevel) - } else { - logrus.SetLevel(logrus.WarnLevel) - } - - // setup the server and start the listener - handler := router.Load( - ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), - middleware.Version, - middleware.Queue(), - middleware.Stream(), - middleware.Bus(), - middleware.Cache(), - middleware.Store(), - middleware.Remote(), - middleware.Engine(), - ) - - if *cert != "" { - logrus.Fatal( - http.ListenAndServeTLS(*addr, *cert, *key, handler), - ) - } else { - logrus.Fatal( - http.ListenAndServe(*addr, handler), - ) - } -} diff --git a/drone/server/server.go b/drone/server/server.go deleted file mode 100644 index e22be9467..000000000 --- a/drone/server/server.go +++ /dev/null @@ -1,130 +0,0 @@ -package server - -import ( - "net/http" - "os" - "time" - - "github.com/drone/drone/router" - "github.com/drone/drone/router/middleware" - - "github.com/Sirupsen/logrus" - "github.com/codegangsta/cli" - "github.com/gin-gonic/contrib/ginrus" -) - -// ServeCmd is the exported command for starting the drone server. -var ServeCmd = cli.Command{ - Name: "serve", - Usage: "starts the drone server", - Action: func(c *cli.Context) { - if err := start(c); err != nil { - logrus.Fatal(err) - } - }, - Flags: []cli.Flag{ - cli.StringFlag{ - EnvVar: "SERVER_ADDR", - Name: "server-addr", - Usage: "server address", - Value: ":8000", - }, - cli.StringFlag{ - EnvVar: "SERVER_CERT", - Name: "server-cert", - Usage: "server ssl cert", - }, - cli.StringFlag{ - EnvVar: "SERVER_KEY", - Name: "server-key", - Usage: "server ssl key", - }, - cli.BoolFlag{ - EnvVar: "DEBUG", - Name: "debug", - Usage: "start the server in debug mode", - }, - cli.BoolFlag{ - EnvVar: "EXPERIMENTAL", - Name: "experimental", - Usage: "start the server with experimental features", - }, - cli.BoolFlag{ - Name: "agreement.ack", - EnvVar: "I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION", - Usage: "agree to terms of use.", - }, - cli.BoolFlag{ - Name: "agreement.fix", - EnvVar: "I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS", - Usage: "agree to terms of use.", - }, - }, -} - -func start(c *cli.Context) error { - - if c.Bool("agreement.ack") == false || c.Bool("agreement.fix") == false { - println(agreement) - os.Exit(1) - } - - // debug level if requested by user - if c.Bool("debug") { - logrus.SetLevel(logrus.DebugLevel) - } else { - logrus.SetLevel(logrus.WarnLevel) - } - - // setup the server and start the listener - handler := router.Load( - ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), - middleware.Version, - middleware.Queue(), - middleware.Stream(), - middleware.Bus(), - middleware.Cache(), - middleware.Store(), - middleware.Remote(), - middleware.Engine(), - ) - - if c.String("server-cert") != "" { - return http.ListenAndServeTLS( - c.String("server-addr"), - c.String("server-cert"), - c.String("server-key"), - handler, - ) - } - - return http.ListenAndServe( - c.String("server-addr"), - handler, - ) -} - -var agreement = ` ---- - - -You are attempting to use the unstable channel. This build is experimental and -has known bugs and compatibility issues, and is not intended for general use. - -Please consider using the latest stable release instead: - - drone/drone:0.4.2 - -If you are attempting to build from source please use the latest stable tag: - - v0.4.2 - -If you are interested in testing this experimental build and assisting with -development you will need to set the following environment variables to proceed: - - I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION=true - I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS=true - - ---- -` diff --git a/engine/bus.go b/engine/bus.go deleted file mode 100644 index 3ab1ece03..000000000 --- a/engine/bus.go +++ /dev/null @@ -1,48 +0,0 @@ -package engine - -import ( - "sync" -) - -type eventbus struct { - sync.Mutex - subs map[chan *Event]bool -} - -// New creates a new eventbus that manages a list of -// subscribers to which events are published. -func newEventbus() *eventbus { - return &eventbus{ - subs: make(map[chan *Event]bool), - } -} - -// Subscribe adds the channel to the list of -// subscribers. Each subscriber in the list will -// receive broadcast events. -func (b *eventbus) subscribe(c chan *Event) { - b.Lock() - b.subs[c] = true - b.Unlock() -} - -// Unsubscribe removes the channel from the -// list of subscribers. -func (b *eventbus) unsubscribe(c chan *Event) { - b.Lock() - delete(b.subs, c) - b.Unlock() -} - -// Send dispatches a message to all subscribers. -func (b *eventbus) send(event *Event) { - b.Lock() - defer b.Unlock() - - for s := range b.subs { - go func(c chan *Event) { - defer recover() - c <- event - }(s) - } -} diff --git a/engine/bus_test.go b/engine/bus_test.go deleted file mode 100644 index 85b2e681e..000000000 --- a/engine/bus_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package engine - -import ( - "testing" - - . "github.com/franela/goblin" -) - -func TestBus(t *testing.T) { - g := Goblin(t) - g.Describe("Event bus", func() { - - g.It("Should unsubscribe", func() { - c1 := make(chan *Event) - c2 := make(chan *Event) - b := newEventbus() - b.subscribe(c1) - b.subscribe(c2) - g.Assert(len(b.subs)).Equal(2) - }) - - g.It("Should subscribe", func() { - c1 := make(chan *Event) - c2 := make(chan *Event) - b := newEventbus() - b.subscribe(c1) - b.subscribe(c2) - g.Assert(len(b.subs)).Equal(2) - b.unsubscribe(c1) - b.unsubscribe(c2) - g.Assert(len(b.subs)).Equal(0) - }) - - g.It("Should send", func() { - em := map[string]bool{"foo": true, "bar": true} - e1 := &Event{Name: "foo"} - e2 := &Event{Name: "bar"} - c := make(chan *Event) - b := newEventbus() - b.subscribe(c) - b.send(e1) - b.send(e2) - r1 := <-c - r2 := <-c - g.Assert(em[r1.Name]).Equal(true) - g.Assert(em[r2.Name]).Equal(true) - }) - }) - -} diff --git a/engine/context.go b/engine/context.go deleted file mode 100644 index 2321fa071..000000000 --- a/engine/context.go +++ /dev/null @@ -1,23 +0,0 @@ -package engine - -import ( - "golang.org/x/net/context" -) - -const key = "engine" - -// Setter defines a context that enables setting values. -type Setter interface { - Set(string, interface{}) -} - -// FromContext returns the Engine associated with this context. -func FromContext(c context.Context) Engine { - return c.Value(key).(Engine) -} - -// ToContext adds the Engine to this context if it supports -// the Setter interface. -func ToContext(c Setter, engine Engine) { - c.Set(key, engine) -} diff --git a/engine/engine.go b/engine/engine.go deleted file mode 100644 index 32a2c6a10..000000000 --- a/engine/engine.go +++ /dev/null @@ -1,444 +0,0 @@ -package engine - -import ( - "bytes" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "runtime" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/stdcopy" - "github.com/drone/drone/model" - "github.com/drone/drone/shared/docker" - "github.com/drone/drone/store" - "github.com/samalba/dockerclient" - "golang.org/x/net/context" -) - -type Engine interface { - Schedule(context.Context, *Task) - Cancel(int64, int64, *model.Node) error - Stream(int64, int64, *model.Node) (io.ReadCloser, error) - Deallocate(*model.Node) - Allocate(*model.Node) error - Subscribe(chan *Event) - Unsubscribe(chan *Event) -} - -var ( - // options to fetch the stdout and stderr logs - logOpts = &dockerclient.LogOptions{ - Stdout: true, - Stderr: true, - } - - // options to fetch the stdout and stderr logs - // by tailing the output. - logOptsTail = &dockerclient.LogOptions{ - Follow: true, - Stdout: true, - Stderr: true, - } - - // error when the system cannot find logs - errLogging = errors.New("Logs not available") -) - -type engine struct { - bus *eventbus - updater *updater - pool *pool - envs []string -} - -// Load creates a new build engine, loaded with registered nodes from the -// database. The registered nodes are added to the pool of nodes to immediately -// start accepting workloads. -func Load(s store.Store) Engine { - engine := &engine{} - engine.bus = newEventbus() - engine.pool = newPool() - engine.updater = &updater{engine.bus} - - // quick fix to propagate HTTP_PROXY variables - // throughout the build environment. - var proxyVars = []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"} - for _, proxyVar := range proxyVars { - proxyVal := os.Getenv(proxyVar) - if len(proxyVal) != 0 { - engine.envs = append(engine.envs, proxyVar+"="+proxyVal) - } - } - - nodes, err := s.GetNodeList() - if err != nil { - log.Fatalf("failed to get nodes from database. %s", err) - } - for _, node := range nodes { - engine.pool.allocate(node) - log.Infof("registered docker daemon %s", node.Addr) - } - - return engine -} - -// Cancel cancels the job running on the specified Node. -func (e *engine) Cancel(build, job int64, node *model.Node) error { - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - return err - } - - id := fmt.Sprintf("drone_build_%d_job_%d", build, job) - return client.StopContainer(id, 30) -} - -// Stream streams the job output from the specified Node. -func (e *engine) Stream(build, job int64, node *model.Node) (io.ReadCloser, error) { - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - log.Errorf("cannot create Docker client for node %s", node.Addr) - return nil, err - } - - id := fmt.Sprintf("drone_build_%d_job_%d", build, job) - log.Debugf("streaming container logs %s", id) - return client.ContainerLogs(id, logOptsTail) -} - -// Subscribe subscribes the channel to all build events. -func (e *engine) Subscribe(c chan *Event) { - e.bus.subscribe(c) -} - -// Unsubscribe unsubscribes the channel from all build events. -func (e *engine) Unsubscribe(c chan *Event) { - e.bus.unsubscribe(c) -} - -func (e *engine) Allocate(node *model.Node) error { - - // run the full build! - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - log.Errorf("error creating docker client %s. %s.", node.Addr, err) - return err - } - version, err := client.Version() - if err != nil { - log.Errorf("error connecting to docker daemon %s. %s.", node.Addr, err) - return err - } - - log.Infof("registered docker daemon %s running version %s", node.Addr, version.Version) - e.pool.allocate(node) - return nil -} - -func (e *engine) Deallocate(n *model.Node) { - nodes := e.pool.list() - for _, node := range nodes { - if node.ID == n.ID { - log.Infof("un-registered docker daemon %s", node.Addr) - e.pool.deallocate(node) - break - } - } -} - -func (e *engine) Schedule(c context.Context, req *Task) { - node := <-e.pool.reserve() - - // since we are probably running in a go-routine - // make sure we recover from any panics so that - // a bug doesn't crash the whole system. - defer func() { - if err := recover(); err != nil { - - const size = 64 << 10 - buf := make([]byte, size) - buf = buf[:runtime.Stack(buf, false)] - log.Errorf("panic running build: %v\n%s", err, string(buf)) - } - e.pool.release(node) - }() - - // update the node that was allocated to each job - func(id int64) { - for _, job := range req.Jobs { - job.NodeID = id - store.UpdateJob(c, job) - } - }(node.ID) - - // run the full build! - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - log.Errorln("error creating docker client", err) - } - - // update the build state if any of the sub-tasks - // had a non-success status - req.Build.Started = time.Now().UTC().Unix() - req.Build.Status = model.StatusRunning - e.updater.SetBuild(c, req) - - // run all bulid jobs - for _, job := range req.Jobs { - req.Job = job - e.runJob(c, req, e.updater, client) - } - - // update overall status based on each job - req.Build.Status = model.StatusSuccess - for _, job := range req.Jobs { - if job.Status != model.StatusSuccess { - req.Build.Status = job.Status - break - } - } - req.Build.Finished = time.Now().UTC().Unix() - err = e.updater.SetBuild(c, req) - if err != nil { - log.Errorf("error updating build completion status. %s", err) - } - - // run notifications - err = e.runJobNotify(req, client) - if err != nil { - log.Errorf("error executing notification step. %s", err) - } -} - -func newDockerClient(addr, cert, key, ca string) (dockerclient.Client, error) { - var tlc *tls.Config - - // create the Docket client TLS config - if len(cert) != 0 { - pem, err := tls.X509KeyPair([]byte(cert), []byte(key)) - if err != nil { - log.Errorf("error loading X509 key pair. %s.", err) - return dockerclient.NewDockerClient(addr, nil) - } - - // create the TLS configuration for secure - // docker communications. - tlc = &tls.Config{} - tlc.Certificates = []tls.Certificate{pem} - - // use the certificate authority if provided. - // else don't use a certificate authority and set - // skip verify to true - if len(ca) != 0 { - log.Infof("creating docker client %s with CA", addr) - pool := x509.NewCertPool() - pool.AppendCertsFromPEM([]byte(ca)) - tlc.RootCAs = pool - - } else { - log.Infof("creating docker client %s WITHOUT CA", addr) - tlc.InsecureSkipVerify = true - } - } - - // create the Docker client. In this version of Drone (alpha) - // we do not spread builds across clients, but this can and - // (probably) will change in the future. - return dockerclient.NewDockerClient(addr, tlc) -} - -func (e *engine) runJob(c context.Context, r *Task, updater *updater, client dockerclient.Client) error { - - name := fmt.Sprintf("drone_build_%d_job_%d", r.Build.ID, r.Job.ID) - - defer func() { - if r.Job.Status == model.StatusRunning { - r.Job.Status = model.StatusError - r.Job.Finished = time.Now().UTC().Unix() - r.Job.ExitCode = 255 - } - if r.Job.Status == model.StatusPending { - r.Job.Status = model.StatusError - r.Job.Started = time.Now().UTC().Unix() - r.Job.Finished = time.Now().UTC().Unix() - r.Job.ExitCode = 255 - } - updater.SetJob(c, r) - - client.KillContainer(name, "9") - client.RemoveContainer(name, true, true) - }() - - // marks the task as running - r.Job.Status = model.StatusRunning - r.Job.Started = time.Now().UTC().Unix() - - // encode the build payload to write to stdin - // when launching the build container - in, err := encodeToLegacyFormat(r) - if err != nil { - log.Errorf("failure to marshal work. %s", err) - return err - } - - // CREATE AND START BUILD - args := DefaultBuildArgs - if r.Build.Event == model.EventPull { - args = DefaultPullRequestArgs - } - args = append(args, "--") - args = append(args, string(in)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - Env: e.envs, - HostConfig: dockerclient.HostConfig{ - Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, - MemorySwappiness: -1, - }, - Volumes: map[string]struct{}{ - "/var/run/docker.sock": {}, - }, - } - - log.Infof("preparing container %s", name) - client.PullImage(conf.Image, nil) - - _, err = docker.RunDaemon(client, conf, name) - if err != nil { - log.Errorf("error starting build container. %s", err) - return err - } - - // UPDATE STATUS - - err = updater.SetJob(c, r) - if err != nil { - log.Errorf("error updating job status as running. %s", err) - return err - } - - // WAIT FOR OUTPUT - info, builderr := docker.Wait(client, name) - - switch { - case info.State.Running: - // A build unblocked before actually being completed. - log.Errorf("incomplete build: %s", name) - r.Job.ExitCode = 1 - r.Job.Status = model.StatusError - case info.State.ExitCode == 128: - r.Job.ExitCode = info.State.ExitCode - r.Job.Status = model.StatusKilled - case info.State.ExitCode == 130: - r.Job.ExitCode = info.State.ExitCode - r.Job.Status = model.StatusKilled - case builderr != nil: - r.Job.Status = model.StatusError - case info.State.ExitCode != 0: - r.Job.ExitCode = info.State.ExitCode - r.Job.Status = model.StatusFailure - default: - r.Job.Status = model.StatusSuccess - } - - // send the logs to the datastore - var buf bytes.Buffer - rc, err := client.ContainerLogs(name, docker.LogOpts) - if err != nil && builderr != nil { - buf.WriteString("Error launching build") - buf.WriteString(builderr.Error()) - } else if err != nil { - buf.WriteString("Error launching build") - buf.WriteString(err.Error()) - log.Errorf("error opening connection to logs. %s", err) - return err - } else { - defer rc.Close() - stdcopy.StdCopy(&buf, &buf, io.LimitReader(rc, 5000000)) - } - - // update the task in the datastore - r.Job.Finished = time.Now().UTC().Unix() - err = updater.SetJob(c, r) - if err != nil { - log.Errorf("error updating job after completion. %s", err) - return err - } - - err = updater.SetLogs(c, r, ioutil.NopCloser(&buf)) - if err != nil { - log.Errorf("error updating logs. %s", err) - return err - } - - log.Debugf("completed job %d with status %s.", r.Job.ID, r.Job.Status) - return nil -} - -func (e *engine) runJobNotify(r *Task, client dockerclient.Client) error { - - name := fmt.Sprintf("drone_build_%d_notify", r.Build.ID) - - defer func() { - client.KillContainer(name, "9") - client.RemoveContainer(name, true, true) - }() - - // encode the build payload to write to stdin - // when launching the build container - in, err := encodeToLegacyFormat(r) - if err != nil { - log.Errorf("failure to marshal work. %s", err) - return err - } - - args := DefaultNotifyArgs - args = append(args, "--") - args = append(args, string(in)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - Env: e.envs, - HostConfig: dockerclient.HostConfig{ - Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, - MemorySwappiness: -1, - }, - Volumes: map[string]struct{}{ - "/var/run/docker.sock": {}, - }, - } - - log.Infof("preparing container %s", name) - info, err := docker.Run(client, conf, name) - if err != nil { - log.Errorf("Error starting notification container %s. %s", name, err) - } - - // for debugging purposes we print a failed notification executions - // output to the logs. Otherwise we have no way to troubleshoot failed - // notifications. This is temporary code until I've come up with - // a better solution. - if info != nil && info.State.ExitCode != 0 && log.GetLevel() >= log.InfoLevel { - var buf bytes.Buffer - rc, err := client.ContainerLogs(name, docker.LogOpts) - if err == nil { - defer rc.Close() - stdcopy.StdCopy(&buf, &buf, io.LimitReader(rc, 50000)) - } - log.Infof("Notification container %s exited with %d", name, info.State.ExitCode) - log.Infoln(buf.String()) - } - - return err -} diff --git a/engine/pool.go b/engine/pool.go deleted file mode 100644 index a886b47de..000000000 --- a/engine/pool.go +++ /dev/null @@ -1,86 +0,0 @@ -package engine - -import ( - "sync" - - "github.com/drone/drone/model" -) - -type pool struct { - sync.Mutex - nodes map[*model.Node]bool - nodec chan *model.Node -} - -func newPool() *pool { - return &pool{ - nodes: make(map[*model.Node]bool), - nodec: make(chan *model.Node, 999), - } -} - -// Allocate allocates a node to the pool to -// be available to accept work. -func (p *pool) allocate(n *model.Node) bool { - if p.isAllocated(n) { - return false - } - - p.Lock() - p.nodes[n] = true - p.Unlock() - - p.nodec <- n - return true -} - -// IsAllocated is a helper function that returns -// true if the node is currently allocated to -// the pool. -func (p *pool) isAllocated(n *model.Node) bool { - p.Lock() - defer p.Unlock() - _, ok := p.nodes[n] - return ok -} - -// Deallocate removes the node from the pool of -// available nodes. If the node is currently -// reserved and performing work it will finish, -// but no longer be given new work. -func (p *pool) deallocate(n *model.Node) { - p.Lock() - defer p.Unlock() - delete(p.nodes, n) -} - -// List returns a list of all model.Nodes currently -// allocated to the pool. -func (p *pool) list() []*model.Node { - p.Lock() - defer p.Unlock() - - var nodes []*model.Node - for n := range p.nodes { - nodes = append(nodes, n) - } - return nodes -} - -// Reserve reserves the next available node to -// start doing work. Once work is complete, the -// node should be released back to the pool. -func (p *pool) reserve() <-chan *model.Node { - return p.nodec -} - -// Release releases the node back to the pool -// of available nodes. -func (p *pool) release(n *model.Node) bool { - if !p.isAllocated(n) { - return false - } - - p.nodec <- n - return true -} diff --git a/engine/pool_test.go b/engine/pool_test.go deleted file mode 100644 index 852847396..000000000 --- a/engine/pool_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package engine - -import ( - "testing" - - "github.com/drone/drone/model" - "github.com/franela/goblin" -) - -func TestPool(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Pool", func() { - - g.It("Should allocate nodes", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n) - g.Assert(len(pool.nodes)).Equal(1) - g.Assert(len(pool.nodec)).Equal(1) - g.Assert(pool.nodes[n]).Equal(true) - }) - - g.It("Should not re-allocate an allocated node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - g.Assert(pool.allocate(n)).Equal(true) - g.Assert(pool.allocate(n)).Equal(false) - }) - - g.It("Should reserve a node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n) - g.Assert(<-pool.reserve()).Equal(n) - }) - - g.It("Should release a node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n) - g.Assert(len(pool.nodec)).Equal(1) - g.Assert(<-pool.reserve()).Equal(n) - g.Assert(len(pool.nodec)).Equal(0) - pool.release(n) - g.Assert(len(pool.nodec)).Equal(1) - g.Assert(<-pool.reserve()).Equal(n) - g.Assert(len(pool.nodec)).Equal(0) - }) - - g.It("Should not release an unallocated node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.nodec)).Equal(0) - pool.release(n) - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.nodec)).Equal(0) - pool.release(nil) - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.nodec)).Equal(0) - }) - - g.It("Should list all allocated nodes", func() { - n1 := &model.Node{Addr: "unix:///var/run/docker.sock"} - n2 := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n1) - pool.allocate(n2) - g.Assert(len(pool.nodes)).Equal(2) - g.Assert(len(pool.nodec)).Equal(2) - g.Assert(len(pool.list())).Equal(2) - }) - - g.It("Should remove a node", func() { - n1 := &model.Node{Addr: "unix:///var/run/docker.sock"} - n2 := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n1) - pool.allocate(n2) - g.Assert(len(pool.nodes)).Equal(2) - pool.deallocate(n1) - pool.deallocate(n2) - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.list())).Equal(0) - }) - - }) -} diff --git a/engine/types.go b/engine/types.go deleted file mode 100644 index 8ba2dd3c3..000000000 --- a/engine/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package engine - -import ( - "github.com/drone/drone/model" -) - -type Event struct { - Name string - Msg []byte -} - -type Task struct { - User *model.User `json:"-"` - Repo *model.Repo `json:"repo"` - Build *model.Build `json:"build"` - BuildPrev *model.Build `json:"build_last"` - Jobs []*model.Job `json:"-"` - Job *model.Job `json:"job"` - Keys *model.Key `json:"keys"` - Netrc *model.Netrc `json:"netrc"` - Config string `json:"config"` - Secret string `json:"secret"` - System *model.System `json:"system"` -} diff --git a/engine/updater.go b/engine/updater.go deleted file mode 100644 index 79c8454d3..000000000 --- a/engine/updater.go +++ /dev/null @@ -1,66 +0,0 @@ -package engine - -import ( - "encoding/json" - "fmt" - "io" - - "github.com/drone/drone/model" - "github.com/drone/drone/remote" - "github.com/drone/drone/store" - "golang.org/x/net/context" -) - -type updater struct { - bus *eventbus -} - -func (u *updater) SetBuild(c context.Context, r *Task) error { - err := store.UpdateBuild(c, r.Build) - if err != nil { - return err - } - - err = remote.FromContext(c).Status(r.User, r.Repo, r.Build, fmt.Sprintf("%s/%s/%d", r.System.Link, r.Repo.FullName, r.Build.Number)) - if err != nil { - // log err - } - - msg, err := json.Marshal(&payload{r.Build, r.Jobs}) - if err != nil { - return err - } - - u.bus.send(&Event{ - Name: r.Repo.FullName, - Msg: msg, - }) - return nil -} - -func (u *updater) SetJob(c context.Context, r *Task) error { - err := store.UpdateJob(c, r.Job) - if err != nil { - return err - } - - msg, err := json.Marshal(&payload{r.Build, r.Jobs}) - if err != nil { - return err - } - - u.bus.send(&Event{ - Name: r.Repo.FullName, - Msg: msg, - }) - return nil -} - -func (u *updater) SetLogs(c context.Context, r *Task, rc io.ReadCloser) error { - return store.WriteLog(c, r.Job, rc) -} - -type payload struct { - *model.Build - Jobs []*model.Job `json:"jobs"` -} diff --git a/engine/util.go b/engine/util.go deleted file mode 100644 index b8f9068bd..000000000 --- a/engine/util.go +++ /dev/null @@ -1,35 +0,0 @@ -package engine - -import ( - "encoding/json" -) - -func encodeToLegacyFormat(t *Task) ([]byte, error) { - // t.System.Plugins = append(t.System.Plugins, "plugins/*") - - // s := map[string]interface{}{} - // s["repo"] = t.Repo - // s["config"] = t.Config - // s["secret"] = t.Secret - // s["job"] = t.Job - // s["system"] = t.System - // s["workspace"] = map[string]interface{}{ - // "netrc": t.Netrc, - // "keys": t.Keys, - // } - // s["build"] = map[string]interface{}{ - // "number": t.Build.Number, - // "status": t.Build.Status, - // "head_commit": map[string]interface{}{ - // "sha": t.Build.Commit, - // "ref": t.Build.Ref, - // "branch": t.Build.Branch, - // "message": t.Build.Message, - // "author": map[string]interface{}{ - // "login": t.Build.Author, - // "email": t.Build.Email, - // }, - // }, - // } - return json.Marshal(t) -} diff --git a/engine/worker.go b/engine/worker.go deleted file mode 100644 index 9b3c8d8e9..000000000 --- a/engine/worker.go +++ /dev/null @@ -1,115 +0,0 @@ -package engine - -import ( - "fmt" - "io" - - "github.com/drone/drone/shared/docker" - "github.com/samalba/dockerclient" -) - -var ( - // name of the build agent container. - DefaultAgent = "drone/drone-exec:latest" - - // default name of the build agent executable - DefaultEntrypoint = []string{"/bin/drone-exec"} - - // default argument to invoke build steps - DefaultBuildArgs = []string{"--pull", "--cache", "--clone", "--build", "--deploy"} - - // default argument to invoke build steps - DefaultPullRequestArgs = []string{"--pull", "--cache", "--clone", "--build"} - - // default arguments to invoke notify steps - DefaultNotifyArgs = []string{"--pull", "--notify"} -) - -type worker struct { - client dockerclient.Client - build *dockerclient.ContainerInfo - notify *dockerclient.ContainerInfo -} - -func newWorker(client dockerclient.Client) *worker { - return &worker{client: client} -} - -// Build executes the clone, build and deploy steps. -func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) { - // the command line arguments passed into the - // build agent container. - args := DefaultBuildArgs - if pr { - args = DefaultPullRequestArgs - } - args = append(args, "--") - args = append(args, string(stdin)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - HostConfig: dockerclient.HostConfig{ - Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, - }, - Volumes: map[string]struct{}{ - "/var/run/docker.sock": {}, - }, - } - - // TEMPORARY: always try to pull the new image for now - // since we'll be frequently updating the build image - // for the next few weeks - w.client.PullImage(conf.Image, nil) - - w.build, err = docker.Run(w.client, conf, name) - if err != nil { - return 1, err - } - if w.build.State.OOMKilled { - return 1, fmt.Errorf("OOMKill received") - } - return w.build.State.ExitCode, err -} - -// Notify executes the notification steps. -func (w *worker) Notify(stdin []byte) error { - - args := DefaultNotifyArgs - args = append(args, "--") - args = append(args, string(stdin)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - HostConfig: dockerclient.HostConfig{}, - } - - var err error - w.notify, err = docker.Run(w.client, conf, "") - return err -} - -// Logs returns a multi-reader that fetches the logs -// from the build and deploy agents. -func (w *worker) Logs() (io.ReadCloser, error) { - if w.build == nil { - return nil, errLogging - } - return w.client.ContainerLogs(w.build.Id, logOpts) -} - -// Remove stops and removes the build, deploy and -// notification agents created for the build task. -func (w *worker) Remove() { - if w.notify != nil { - w.client.KillContainer(w.notify.Id, "9") - w.client.RemoveContainer(w.notify.Id, true, true) - } - if w.build != nil { - w.client.KillContainer(w.build.Id, "9") - w.client.RemoveContainer(w.build.Id, true, true) - } -} diff --git a/model/config.go b/model/config.go new file mode 100644 index 000000000..393d5e3ba --- /dev/null +++ b/model/config.go @@ -0,0 +1,26 @@ +package model + +// Config defines system configuration parameters. +type Config struct { + Open bool // Enables open registration + Yaml string // Customize the Yaml configuration file name + Shasum string // Customize the Yaml checksum file name + Secret string // Secret token used to authenticate agents + Admins map[string]bool // Administrative users + Orgs map[string]bool // Organization whitelist +} + +// IsAdmin returns true if the user is a member of the administrator list. +func (c *Config) IsAdmin(user *User) bool { + return c.Admins[user.Login] +} + +// IsMember returns true if the user is a member of the whitelisted teams. +func (c *Config) IsMember(teams []*Team) bool { + for _, team := range teams { + if c.Orgs[team.Login] { + return true + } + } + return false +} diff --git a/model/node.go b/model/node.go deleted file mode 100644 index 03e6f8dfc..000000000 --- a/model/node.go +++ /dev/null @@ -1,36 +0,0 @@ -package model - -const ( - Freebsd_386 uint = iota - Freebsd_amd64 - Freebsd_arm - Linux_386 - Linux_amd64 - Linux_arm - Linux_arm64 - Solaris_amd64 - Windows_386 - Windows_amd64 -) - -var Archs = map[string]uint{ - "freebsd_386": Freebsd_386, - "freebsd_amd64": Freebsd_amd64, - "freebsd_arm": Freebsd_arm, - "linux_386": Linux_386, - "linux_amd64": Linux_amd64, - "linux_arm": Linux_arm, - "linux_arm64": Linux_arm64, - "solaris_amd64": Solaris_amd64, - "windows_386": Windows_386, - "windows_amd64": Windows_amd64, -} - -type Node struct { - ID int64 `meddler:"node_id,pk" json:"id"` - Addr string `meddler:"node_addr" json:"address"` - Arch string `meddler:"node_arch" json:"architecture"` - Cert string `meddler:"node_cert" json:"-"` - Key string `meddler:"node_key" json:"-"` - CA string `meddler:"node_ca" json:"-"` -} diff --git a/model/team.go b/model/team.go new file mode 100644 index 000000000..c91d58666 --- /dev/null +++ b/model/team.go @@ -0,0 +1,12 @@ +package model + +// Team represents a team or organization in the remote version control system. +// +// swagger:model user +type Team struct { + // Login is the username for this team. + Login string `json:"login"` + + // the avatar url for this team. + Avatar string `json:"avatar_url"` +} diff --git a/model/user.go b/model/user.go index e33d1fc3e..fdd46168a 100644 --- a/model/user.go +++ b/model/user.go @@ -32,11 +32,17 @@ type User struct { Avatar string `json:"avatar_url" meddler:"user_avatar"` // Activate indicates the user is active in the system. - Active bool `json:"active," meddler:"user_active"` + Active bool `json:"active" meddler:"user_active"` // Admin indicates the user is a system administrator. - Admin bool `json:"admin," meddler:"user_admin"` + // + // NOTE: This is sourced from the DRONE_ADMINS environment variable and is no + // longer persisted in the database. + Admin bool `json:"admin,omitempty" meddler:"-"` // Hash is a unique token used to sign tokens. Hash string `json:"-" meddler:"user_hash"` + + // DEPRECATED Admin indicates the user is a system administrator. + XAdmin bool `json:"-" meddler:"user_admin"` } diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index c37319a2c..b3e51a97f 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -1,133 +1,71 @@ package bitbucket import ( - "encoding/json" "fmt" - "io/ioutil" "net/http" "net/url" - "strconv" "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/drone/drone/remote/bitbucket/internal" "github.com/drone/drone/shared/httputil" - log "github.com/Sirupsen/logrus" "golang.org/x/oauth2" - "golang.org/x/oauth2/bitbucket" ) -type Bitbucket struct { +// Bitbucket cloud endpoints. +const ( + DefaultAPI = "https://api.bitbucket.org" + DefaultURL = "https://bitbucket.org" +) + +type config struct { + API string + URL string Client string Secret string - Orgs []string - Open bool } -func Load(config string) *Bitbucket { - - // parse the remote DSN configuration string - url_, err := url.Parse(config) - if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) +// New returns a new remote Configuration for integrating with the Bitbucket +// repository hosting service at https://bitbucket.org +func New(client, secret string) remote.Remote { + return &config{ + API: DefaultAPI, + URL: DefaultURL, + Client: client, + Secret: secret, } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - - // create the Githbub remote using parameters from - // the parsed DSN configuration string. - bitbucket := Bitbucket{} - bitbucket.Client = params.Get("client_id") - bitbucket.Secret = params.Get("client_secret") - bitbucket.Orgs = params["orgs"] - bitbucket.Open, _ = strconv.ParseBool(params.Get("open")) - - return &bitbucket } -// Login authenticates the session and returns the -// remote user details. -func (bb *Bitbucket) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// Login authenticates an account with Bitbucket using the oauth2 protocol. The +// Bitbucket account details are returned when the user is successfully authenticated. +func (c *config) Login(w http.ResponseWriter, r *http.Request) (*model.User, error) { + redirect := httputil.GetURL(r) + config := c.newConfig(redirect) - config := &oauth2.Config{ - ClientID: bb.Client, - ClientSecret: bb.Secret, - Endpoint: bitbucket.Endpoint, - RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)), - } - - // get the OAuth code - var code = req.FormValue("code") + code := r.FormValue("code") if len(code) == 0 { - http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) - return nil, false, nil + http.Redirect(w, r, config.AuthCodeURL("drone"), http.StatusSeeOther) + return nil, nil } - var token, err = config.Exchange(oauth2.NoContext, code) + token, err := config.Exchange(oauth2.NoContext, code) if err != nil { - return nil, false, fmt.Errorf("Error exchanging token. %s", err) + return nil, err } - client := NewClient(config.Client(oauth2.NoContext, token)) + client := internal.NewClient(c.API, config.Client(oauth2.NoContext, token)) curr, err := client.FindCurrent() if err != nil { - return nil, false, err + return nil, err } - - // convers the current bitbucket user to the - // common drone user structure. - user := model.User{} - user.Login = curr.Login - user.Token = token.AccessToken - user.Secret = token.RefreshToken - user.Expiry = token.Expiry.UTC().Unix() - user.Avatar = curr.Links.Avatar.Href - - // gets the primary, confirmed email from bitbucket - emails, err := client.ListEmail() - if err != nil { - return nil, false, err - } - for _, email := range emails.Values { - if email.IsPrimary && email.IsConfirmed { - user.Email = email.Email - break - } - } - - // if the installation is restricted to a subset - // of organizations, get the orgs and verify the - // user is a member. - if len(bb.Orgs) != 0 { - resp, err := client.ListTeams(&ListTeamOpts{Page: 1, PageLen: 100, Role: "member"}) - if err != nil { - return nil, false, err - } - - var member bool - for _, team := range resp.Values { - for _, team_ := range bb.Orgs { - if team.Login == team_ { - member = true - break - } - } - } - - if !member { - return nil, false, fmt.Errorf("User does not belong to correct org. Must belong to %v", bb.Orgs) - } - } - - return &user, bb.Open, nil + return convertUser(curr, token), nil } -// Auth authenticates the session and returns the remote user -// login for the given token and secret -func (bb *Bitbucket) Auth(token, secret string) (string, error) { - token_ := oauth2.Token{AccessToken: token, RefreshToken: secret} - client := NewClientToken(bb.Client, bb.Secret, &token_) - +// Auth uses the Bitbucket oauth2 access token and refresh token to authenticate +// a session and return the Bitbucket account login. +func (c *config) Auth(token, secret string) (string, error) { + client := c.newClientToken(token, secret) user, err := client.FindCurrent() if err != nil { return "", err @@ -135,84 +73,83 @@ func (bb *Bitbucket) Auth(token, secret string) (string, error) { return user.Login, nil } -// Refresh refreshes an oauth token and expiration for the given -// user. It returns true if the token was refreshed, false if the -// token was not refreshed, and error if it failed to refersh. -func (bb *Bitbucket) Refresh(user *model.User) (bool, error) { - config := &oauth2.Config{ - ClientID: bb.Client, - ClientSecret: bb.Secret, - Endpoint: bitbucket.Endpoint, - } - - // creates a token source with just the refresh token. - // this will ensure an access token is automatically - // requested. +// Refresh refreshes the Bitbucket oauth2 access token. If the token is +// refreshed the user is updated and a true value is returned. +func (c *config) Refresh(user *model.User) (bool, error) { + config := c.newConfig("") source := config.TokenSource( oauth2.NoContext, &oauth2.Token{RefreshToken: user.Secret}) - // requesting the token automatically refreshes and - // returns a new access token. token, err := source.Token() if err != nil || len(token.AccessToken) == 0 { return false, err } - // update the user to include tne new access token user.Token = token.AccessToken user.Secret = token.RefreshToken user.Expiry = token.Expiry.UTC().Unix() return true, nil } -// Repo fetches the named repository from the remote system. -func (bb *Bitbucket) Repo(u *model.User, owner, name string) (*model.Repo, error) { - token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} - client := NewClientToken(bb.Client, bb.Secret, &token) +// Teams returns a list of all team membership for the Bitbucket account. +func (c *config) Teams(u *model.User) ([]*model.Team, error) { + opts := &internal.ListTeamOpts{ + PageLen: 100, + Role: "member", + } + resp, err := c.newClient(u).ListTeams(opts) + if err != nil { + return nil, err + } + return convertTeamList(resp.Values), nil +} - repo, err := client.FindRepo(owner, name) +// Repo returns the named Bitbucket repository. +func (c *config) Repo(u *model.User, owner, name string) (*model.Repo, error) { + repo, err := c.newClient(u).FindRepo(owner, name) if err != nil { return nil, err } return convertRepo(repo), nil } -// Repos fetches a list of repos from the remote system. -func (bb *Bitbucket) Repos(u *model.User) ([]*model.RepoLite, error) { - token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} - client := NewClientToken(bb.Client, bb.Secret, &token) - var repos []*model.RepoLite +// Repos returns a list of all repositories for Bitbucket account, including +// organization repositories. +func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) { + client := c.newClient(u) - // gets a list of all accounts to query, including the - // user's account and all team accounts. - logins := []string{u.Login} - resp, err := client.ListTeams(&ListTeamOpts{PageLen: 100, Role: "member"}) + var all []*model.RepoLite + + accounts := []string{u.Login} + resp, err := client.ListTeams(&internal.ListTeamOpts{ + PageLen: 100, + Role: "member", + }) if err != nil { - return repos, err + return all, err } for _, team := range resp.Values { - logins = append(logins, team.Login) + accounts = append(accounts, team.Login) } - // for each account, get the list of repos - for _, login := range logins { - repos_, err := client.ListReposAll(login) + for _, account := range accounts { + repos, err := client.ListReposAll(account) if err != nil { - return repos, err + return all, err } - for _, repo := range repos_ { - repos = append(repos, convertRepoLite(repo)) + for _, repo := range repos { + all = append(all, convertRepoLite(repo)) } } - - return repos, nil + return all, nil } -// Perm fetches the named repository permissions from -// the remote system for the specified user. -func (bb *Bitbucket) Perm(u *model.User, owner, name string) (*model.Perm, error) { - token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} - client := NewClientToken(bb.Client, bb.Secret, &token) +// Perm returns the user permissions for the named repository. Because Bitbucket +// does not have an endpoint to access user permissions, we attempt to fetch +// the repository hook list, which is restricted to administrators to calculate +// administrative access to a repository. +func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) { + client := c.newClient(u) perms := new(model.Perm) _, err := client.FindRepo(owner, name) @@ -220,69 +157,72 @@ func (bb *Bitbucket) Perm(u *model.User, owner, name string) (*model.Perm, error return perms, err } - // if we've gotten this far we know that the user at - // least has read access to the repository. - perms.Pull = true - - // if the user has access to the repository hooks we - // can deduce that the user has push and admin access. - _, err = client.ListHooks(owner, name, &ListOpts{}) + _, err = client.ListHooks(owner, name, &internal.ListOpts{}) if err == nil { perms.Push = true perms.Admin = true } - + perms.Pull = true return perms, nil } -// File fetches a file from the remote repository and returns in string format. -func (bb *Bitbucket) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - config, err := client.FindSource(r.Owner, r.Name, b.Commit, f) +// File fetches the file from the Bitbucket repository and returns its contents. +func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { + config, err := c.newClient(u).FindSource(r.Owner, r.Name, b.Commit, f) if err != nil { return nil, err } - return []byte(config.Data), err } -// Status sends the commit status to the remote system. -// An example would be the GitHub pull request status. -func (bb *Bitbucket) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - status := getStatus(b.Status) - desc := getDesc(b.Status) - - data := BuildStatus{ - State: status, +// Status creates a build status for the Bitbucket commit. +func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + status := internal.BuildStatus{ + State: convertStatus(b.Status), + Desc: convertDesc(b.Status), Key: "Drone", Url: link, - Desc: desc, } - - err := client.CreateStatus(r.Owner, r.Name, b.Commit, &data) - return err + return c.newClient(u).CreateStatus(r.Owner, r.Name, b.Commit, &status) } -// Netrc returns a .netrc file that can be used to clone -// private repositories from a remote system. -func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { +// Activate activates the repository by registering repository push hooks with +// the Bitbucket repository. Prior to registering hook, previously created hooks +// are deleted. +func (c *config) Activate(u *model.User, r *model.Repo, link string) error { + rawurl, err := url.Parse(link) + if err != nil { + return err + } + c.Deactivate(u, r, link) + + return c.newClient(u).CreateHook(r.Owner, r.Name, &internal.Hook{ + Active: true, + Desc: rawurl.Host, + Events: []string{"repo:push"}, + Url: link, + }) +} + +// Deactivate deactives the repository be removing repository push hooks from +// the Bitbucket repository. +func (c *config) Deactivate(u *model.User, r *model.Repo, link string) error { + client := c.newClient(u) + + hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{}) + if err != nil { + return err + } + hook := matchingHooks(hooks.Values, link) + if hook != nil { + return client.DeleteHook(r.Owner, r.Name, hook.Uuid) + } + return nil +} + +// Netrc returns a netrc file capable of authenticating Bitbucket requests and +// cloning Bitbucket repositories. +func (c *config) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { return &model.Netrc{ Machine: "bitbucket.org", Login: "x-token-auth", @@ -290,226 +230,54 @@ func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { }, nil } -// Activate activates a repository by creating the post-commit hook and -// adding the SSH deploy key, if applicable. -func (bb *Bitbucket) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - linkurl, err := url.Parse(link) - if err != nil { - log.Errorf("malformed hook url %s. %s", link, err) - return err - } - - // see if the hook already exists. If yes be sure to - // delete so that multiple messages aren't sent. - hooks, _ := client.ListHooks(r.Owner, r.Name, &ListOpts{}) - for _, hook := range hooks.Values { - hookurl, err := url.Parse(hook.Url) - if err != nil { - continue - } - if hookurl.Host == linkurl.Host { - err = client.DeleteHook(r.Owner, r.Name, hook.Uuid) - if err != nil { - log.Errorf("unable to delete hook %s. %s", hookurl.Host, err) - } - break - } - } - - err = client.CreateHook(r.Owner, r.Name, &Hook{ - Active: true, - Desc: linkurl.Host, - Events: []string{"repo:push"}, - Url: link, - }) - if err != nil { - log.Errorf("unable to create hook %s. %s", link, err) - } - return err +// Hook parses the incoming Bitbucket hook and returns the Repository and +// Build details. If the hook is unsupported nil values are returned. +func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) { + return parseHook(r) } -// Deactivate removes a repository by removing all the post-commit hooks -// which are equal to link and removing the SSH deploy key. -func (bb *Bitbucket) Deactivate(u *model.User, r *model.Repo, link string) error { - client := NewClientToken( - bb.Client, - bb.Secret, +// helper function to return the bitbucket oauth2 client +func (c *config) newClient(u *model.User) *internal.Client { + return c.newClientToken(u.Token, u.Secret) +} + +// helper function to return the bitbucket oauth2 client +func (c *config) newClientToken(token, secret string) *internal.Client { + return internal.NewClientToken( + c.API, + c.Client, + c.Secret, &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, + AccessToken: token, + RefreshToken: secret, }, ) +} - linkurl, err := url.Parse(link) +// helper function to return the bitbucket oauth2 config +func (c *config) newConfig(redirect string) *oauth2.Config { + return &oauth2.Config{ + ClientID: c.Client, + ClientSecret: c.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", c.URL), + TokenURL: fmt.Sprintf("%s/site/oauth2/access_token", c.URL), + }, + RedirectURL: fmt.Sprintf("%s/authorize", redirect), + } +} + +// helper function to return matching hooks. +func matchingHooks(hooks []*internal.Hook, rawurl string) *internal.Hook { + link, err := url.Parse(rawurl) if err != nil { - return err + return nil } - - // see if the hook already exists. If yes be sure to - // delete so that multiple messages aren't sent. - hooks, _ := client.ListHooks(r.Owner, r.Name, &ListOpts{}) - for _, hook := range hooks.Values { + for _, hook := range hooks { hookurl, err := url.Parse(hook.Url) - if err != nil { - return err - } - if hookurl.Host == linkurl.Host { - client.DeleteHook(r.Owner, r.Name, hook.Uuid) - break + if err == nil && hookurl.Host == link.Host { + return hook } } - return nil } - -// Hook parses the post-commit hook from the Request body -// and returns the required data in a standard format. -func (bb *Bitbucket) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - - switch r.Header.Get("X-Event-Key") { - case "repo:push": - return bb.pushHook(r) - case "pullrequest:created", "pullrequest:updated": - return bb.pullHook(r) - } - - return nil, nil, nil -} - -func (bb *Bitbucket) String() string { - return "bitbucket" -} - -func (bb *Bitbucket) pushHook(r *http.Request) (*model.Repo, *model.Build, error) { - payload := []byte(r.FormValue("payload")) - if len(payload) == 0 { - defer r.Body.Close() - payload, _ = ioutil.ReadAll(r.Body) - } - - hook := PushHook{} - err := json.Unmarshal(payload, &hook) - if err != nil { - return nil, nil, err - } - - // the hook can container one or many changes. Since I don't - // fully understand this yet, we will just pick the first - // change that has branch information. - for _, change := range hook.Push.Changes { - - // must have sha information - if change.New.Target.Hash == "" { - continue - } - // we only support tag and branch pushes for now - buildEventType := model.EventPush - buildRef := fmt.Sprintf("refs/heads/%s", change.New.Name) - if change.New.Type == "tag" || change.New.Type == "annotated_tag" || change.New.Type == "bookmark" { - buildEventType = model.EventTag - buildRef = fmt.Sprintf("refs/tags/%s", change.New.Name) - } else if change.New.Type != "branch" && change.New.Type != "named_branch" { - continue - } - - // return the updated repository information and the - // build information. - return convertRepo(&hook.Repo), &model.Build{ - Event: buildEventType, - Commit: change.New.Target.Hash, - Ref: buildRef, - Link: change.New.Target.Links.Html.Href, - Branch: change.New.Name, - Message: change.New.Target.Message, - Avatar: hook.Actor.Links.Avatar.Href, - Author: hook.Actor.Login, - Timestamp: change.New.Target.Date.UTC().Unix(), - }, nil - } - - return nil, nil, nil -} - -func (bb *Bitbucket) pullHook(r *http.Request) (*model.Repo, *model.Build, error) { - payload := []byte(r.FormValue("payload")) - if len(payload) == 0 { - defer r.Body.Close() - payload, _ = ioutil.ReadAll(r.Body) - } - - hook := PullRequestHook{} - err := json.Unmarshal(payload, &hook) - if err != nil { - return nil, nil, err - } - if hook.PullRequest.State != "OPEN" { - return nil, nil, nil - } - - return convertRepo(&hook.Repo), &model.Build{ - Event: model.EventPull, - Commit: hook.PullRequest.Dest.Commit.Hash, - Ref: fmt.Sprintf("refs/heads/%s", hook.PullRequest.Dest.Branch.Name), - Refspec: fmt.Sprintf("https://bitbucket.org/%s.git", hook.PullRequest.Source.Repo.FullName), - Remote: cloneLink(hook.PullRequest.Dest.Repo), - Link: hook.PullRequest.Links.Html.Href, - Branch: hook.PullRequest.Dest.Branch.Name, - Message: hook.PullRequest.Desc, - Avatar: hook.Actor.Links.Avatar.Href, - Author: hook.Actor.Login, - Timestamp: hook.PullRequest.Updated.UTC().Unix(), - }, nil -} - -const ( - StatusPending = "INPROGRESS" - StatusSuccess = "SUCCESSFUL" - StatusFailure = "FAILED" -) - -const ( - DescPending = "this build is pending" - DescSuccess = "the build was successful" - DescFailure = "the build failed" - DescError = "oops, something went wrong" -) - -// converts a Drone status to a BitBucket status. -func getStatus(status string) string { - switch status { - case model.StatusPending, model.StatusRunning: - return StatusPending - case model.StatusSuccess: - return StatusSuccess - case model.StatusFailure, model.StatusError, model.StatusKilled: - return StatusFailure - default: - return StatusFailure - } -} - -// generates a description for the build based on -// the Drone status -func getDesc(status string) string { - switch status { - case model.StatusPending, model.StatusRunning: - return DescPending - case model.StatusSuccess: - return DescSuccess - case model.StatusFailure: - return DescFailure - case model.StatusError, model.StatusKilled: - return DescError - default: - return DescError - } -} diff --git a/remote/bitbucket/bitbucket_test.go b/remote/bitbucket/bitbucket_test.go new file mode 100644 index 000000000..ea1a8281a --- /dev/null +++ b/remote/bitbucket/bitbucket_test.go @@ -0,0 +1,337 @@ +package bitbucket + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/fixtures" + "github.com/drone/drone/remote/bitbucket/internal" + + "github.com/franela/goblin" + "github.com/gin-gonic/gin" +) + +func Test_bitbucket(t *testing.T) { + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(fixtures.Handler()) + c := &config{URL: s.URL, API: s.URL} + + g := goblin.Goblin(t) + g.Describe("Bitbucket client", func() { + + g.After(func() { + s.Close() + }) + + g.It("Should return client with default endpoint", func() { + remote := New("4vyW6b49Z", "a5012f6c6") + g.Assert(remote.(*config).URL).Equal(DefaultURL) + g.Assert(remote.(*config).API).Equal(DefaultAPI) + g.Assert(remote.(*config).Client).Equal("4vyW6b49Z") + g.Assert(remote.(*config).Secret).Equal("a5012f6c6") + }) + + g.It("Should return the netrc file", func() { + remote := New("", "") + netrc, _ := remote.Netrc(fakeUser, nil) + g.Assert(netrc.Machine).Equal("bitbucket.org") + g.Assert(netrc.Login).Equal("x-token-auth") + g.Assert(netrc.Password).Equal(fakeUser.Token) + }) + + g.Describe("Given an authorization request", func() { + g.It("Should redirect to authorize", func() { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "", nil) + _, err := c.Login(w, r) + g.Assert(err == nil).IsTrue() + g.Assert(w.Code).Equal(http.StatusSeeOther) + }) + g.It("Should return authenticated user", func() { + r, _ := http.NewRequest("GET", "?code=code", nil) + u, err := c.Login(nil, r) + g.Assert(err == nil).IsTrue() + g.Assert(u.Login).Equal(fakeUser.Login) + g.Assert(u.Token).Equal("2YotnFZFEjr1zCsicMWpAA") + g.Assert(u.Secret).Equal("tGzv3JOkF0XG5Qx2TlKWIA") + }) + g.It("Should handle failure to exchange code", func() { + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "?code=code_bad_request", nil) + _, err := c.Login(w, r) + g.Assert(err != nil).IsTrue() + }) + g.It("Should handle failure to resolve user", func() { + r, _ := http.NewRequest("GET", "?code=code_user_not_found", nil) + _, err := c.Login(nil, r) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Given an access token", func() { + g.It("Should return the authenticated user", func() { + login, err := c.Auth( + fakeUser.Token, + fakeUser.Secret, + ) + g.Assert(err == nil).IsTrue() + g.Assert(login).Equal(fakeUser.Login) + }) + g.It("Should handle a failure to resolve user", func() { + _, err := c.Auth( + fakeUserNotFound.Token, + fakeUserNotFound.Secret, + ) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Given a refresh token", func() { + g.It("Should return a refresh access token", func() { + ok, err := c.Refresh(fakeUserRefresh) + g.Assert(err == nil).IsTrue() + g.Assert(ok).IsTrue() + g.Assert(fakeUserRefresh.Token).Equal("2YotnFZFEjr1zCsicMWpAA") + g.Assert(fakeUserRefresh.Secret).Equal("tGzv3JOkF0XG5Qx2TlKWIA") + }) + g.It("Should handle an empty access token", func() { + ok, err := c.Refresh(fakeUserRefreshEmpty) + g.Assert(err == nil).IsTrue() + g.Assert(ok).IsFalse() + }) + g.It("Should handle a failure to refresh", func() { + ok, err := c.Refresh(fakeUserRefreshFail) + g.Assert(err != nil).IsTrue() + g.Assert(ok).IsFalse() + }) + }) + + g.Describe("When requesting a repository", func() { + g.It("Should return the details", func() { + repo, err := c.Repo( + fakeUser, + fakeRepo.Owner, + fakeRepo.Name, + ) + g.Assert(err == nil).IsTrue() + g.Assert(repo.FullName).Equal(fakeRepo.FullName) + }) + g.It("Should handle not found errors", func() { + _, err := c.Repo( + fakeUser, + fakeRepoNotFound.Owner, + fakeRepoNotFound.Name, + ) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("When requesting repository permissions", func() { + g.It("Should handle not found errors", func() { + _, err := c.Perm( + fakeUser, + fakeRepoNotFound.Owner, + fakeRepoNotFound.Name, + ) + g.Assert(err != nil).IsTrue() + }) + g.It("Should authorize read access", func() { + perm, err := c.Perm( + fakeUser, + fakeRepoNoHooks.Owner, + fakeRepoNoHooks.Name, + ) + g.Assert(err == nil).IsTrue() + g.Assert(perm.Pull).IsTrue() + g.Assert(perm.Push).IsFalse() + g.Assert(perm.Admin).IsFalse() + }) + g.It("Should authorize admin access", func() { + perm, err := c.Perm( + fakeUser, + fakeRepo.Owner, + fakeRepo.Name, + ) + g.Assert(err == nil).IsTrue() + g.Assert(perm.Pull).IsTrue() + g.Assert(perm.Push).IsTrue() + g.Assert(perm.Admin).IsTrue() + }) + }) + + g.Describe("When requesting user repositories", func() { + g.It("Should return the details", func() { + repos, err := c.Repos(fakeUser) + g.Assert(err == nil).IsTrue() + g.Assert(repos[0].FullName).Equal(fakeRepo.FullName) + }) + g.It("Should handle organization not found errors", func() { + _, err := c.Repos(fakeUserNoTeams) + g.Assert(err != nil).IsTrue() + }) + g.It("Should handle not found errors", func() { + _, err := c.Repos(fakeUserNoRepos) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("When requesting user teams", func() { + g.It("Should return the details", func() { + teams, err := c.Teams(fakeUser) + g.Assert(err == nil).IsTrue() + g.Assert(teams[0].Login).Equal("superfriends") + g.Assert(teams[0].Avatar).Equal("http://i.imgur.com/ZygP55A.jpg") + }) + g.It("Should handle not found error", func() { + _, err := c.Teams(fakeUserNoTeams) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("When downloading a file", func() { + g.It("Should return the bytes", func() { + raw, err := c.File(fakeUser, fakeRepo, fakeBuild, "file") + g.Assert(err == nil).IsTrue() + g.Assert(len(raw) != 0).IsTrue() + }) + g.It("Should handle not found error", func() { + _, err := c.File(fakeUser, fakeRepo, fakeBuild, "file_not_found") + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("When activating a repository", func() { + g.It("Should error when malformed hook", func() { + err := c.Activate(fakeUser, fakeRepo, "%gh&%ij") + g.Assert(err != nil).IsTrue() + }) + g.It("Should create the hook", func() { + err := c.Activate(fakeUser, fakeRepo, "http://127.0.0.1") + g.Assert(err == nil).IsTrue() + }) + }) + + g.Describe("When deactivating a repository", func() { + g.It("Should error when listing hooks fails", func() { + err := c.Deactivate(fakeUser, fakeRepoNoHooks, "http://127.0.0.1") + g.Assert(err != nil).IsTrue() + }) + g.It("Should successfully remove hooks", func() { + err := c.Deactivate(fakeUser, fakeRepo, "http://127.0.0.1") + g.Assert(err == nil).IsTrue() + }) + g.It("Should successfully deactivate when hook already removed", func() { + err := c.Deactivate(fakeUser, fakeRepoEmptyHook, "http://127.0.0.1") + g.Assert(err == nil).IsTrue() + }) + }) + + g.Describe("Given a list of hooks", func() { + g.It("Should return the matching hook", func() { + hooks := []*internal.Hook{ + {Url: "http://127.0.0.1/hook"}, + } + hook := matchingHooks(hooks, "http://127.0.0.1/") + g.Assert(hook).Equal(hooks[0]) + }) + g.It("Should handle no matches", func() { + hooks := []*internal.Hook{ + {Url: "http://localhost/hook"}, + } + hook := matchingHooks(hooks, "http://127.0.0.1/") + g.Assert(hook == nil).IsTrue() + }) + g.It("Should handle malformed hook urls", func() { + var hooks []*internal.Hook + hook := matchingHooks(hooks, "%gh&%ij") + g.Assert(hook == nil).IsTrue() + }) + }) + + g.It("Should update the status", func() { + err := c.Status(fakeUser, fakeRepo, fakeBuild, "http://127.0.0.1") + g.Assert(err == nil).IsTrue() + }) + + g.It("Should parse the hook", func() { + buf := bytes.NewBufferString(fixtures.HookPush) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPush) + + r, _, err := c.Hook(req) + g.Assert(err == nil).IsTrue() + g.Assert(r.FullName).Equal("user_name/repo_name") + }) + + }) +} + +var ( + fakeUser = &model.User{ + Login: "superman", + Token: "cfcd2084", + } + + fakeUserRefresh = &model.User{ + Login: "superman", + Secret: "cfcd2084", + } + + fakeUserRefreshFail = &model.User{ + Login: "superman", + Secret: "refresh_token_not_found", + } + + fakeUserRefreshEmpty = &model.User{ + Login: "superman", + Secret: "refresh_token_is_empty", + } + + fakeUserNotFound = &model.User{ + Login: "superman", + Token: "user_not_found", + } + + fakeUserNoTeams = &model.User{ + Login: "superman", + Token: "teams_not_found", + } + + fakeUserNoRepos = &model.User{ + Login: "superman", + Token: "repos_not_found", + } + + fakeRepo = &model.Repo{ + Owner: "test_name", + Name: "repo_name", + FullName: "test_name/repo_name", + } + + fakeRepoNotFound = &model.Repo{ + Owner: "test_name", + Name: "repo_not_found", + FullName: "test_name/repo_not_found", + } + + fakeRepoNoHooks = &model.Repo{ + Owner: "test_name", + Name: "hooks_not_found", + FullName: "test_name/hooks_not_found", + } + + fakeRepoEmptyHook = &model.Repo{ + Owner: "test_name", + Name: "hook_empty", + FullName: "test_name/hook_empty", + } + + fakeBuild = &model.Build{ + Commit: "9ecad50", + } +) diff --git a/remote/bitbucket/convert.go b/remote/bitbucket/convert.go new file mode 100644 index 000000000..b64863cd1 --- /dev/null +++ b/remote/bitbucket/convert.go @@ -0,0 +1,186 @@ +package bitbucket + +import ( + "fmt" + "net/url" + "strings" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/internal" + + "golang.org/x/oauth2" +) + +const ( + statusPending = "INPROGRESS" + statusSuccess = "SUCCESSFUL" + statusFailure = "FAILED" +) + +const ( + descPending = "this build is pending" + descSuccess = "the build was successful" + descFailure = "the build failed" + descError = "oops, something went wrong" +) + +// convertStatus is a helper function used to convert a Drone status to a +// Bitbucket commit status. +func convertStatus(status string) string { + switch status { + case model.StatusPending, model.StatusRunning: + return statusPending + case model.StatusSuccess: + return statusSuccess + default: + return statusFailure + } +} + +// convertDesc is a helper function used to convert a Drone status to a +// Bitbucket status description. +func convertDesc(status string) string { + switch status { + case model.StatusPending, model.StatusRunning: + return descPending + case model.StatusSuccess: + return descSuccess + case model.StatusFailure: + return descFailure + default: + return descError + } +} + +// convertRepo is a helper function used to convert a Bitbucket repository +// structure to the common Drone repository structure. +func convertRepo(from *internal.Repo) *model.Repo { + repo := model.Repo{ + Clone: cloneLink(from), + Owner: strings.Split(from.FullName, "/")[0], + Name: strings.Split(from.FullName, "/")[1], + FullName: from.FullName, + Link: from.Links.Html.Href, + IsPrivate: from.IsPrivate, + Avatar: from.Owner.Links.Avatar.Href, + Kind: from.Scm, + Branch: "master", + } + if repo.Kind == model.RepoHg { + repo.Branch = "default" + } + return &repo +} + +// cloneLink is a helper function that tries to extract the clone url from the +// repository object. +func cloneLink(repo *internal.Repo) string { + var clone string + + // above we manually constructed the repository clone url. below we will + // iterate through the list of clone links and attempt to instead use the + // clone url provided by bitbucket. + for _, link := range repo.Links.Clone { + if link.Name == "https" { + clone = link.Href + } + } + + // if no repository name is provided, we use the Html link. this excludes the + // .git suffix, but will still clone the repo. + if len(clone) == 0 { + clone = repo.Links.Html.Href + } + + // if bitbucket tries to automatically populate the user in the url we must + // strip it out. + cloneurl, err := url.Parse(clone) + if err == nil { + cloneurl.User = nil + clone = cloneurl.String() + } + + return clone +} + +// convertRepoLite is a helper function used to convert a Bitbucket repository +// structure to the simplified Drone repository structure. +func convertRepoLite(from *internal.Repo) *model.RepoLite { + return &model.RepoLite{ + Owner: strings.Split(from.FullName, "/")[0], + Name: strings.Split(from.FullName, "/")[1], + FullName: from.FullName, + Avatar: from.Owner.Links.Avatar.Href, + } +} + +// convertUser is a helper function used to convert a Bitbucket user account +// structure to the Drone User structure. +func convertUser(from *internal.Account, token *oauth2.Token) *model.User { + return &model.User{ + Login: from.Login, + Token: token.AccessToken, + Secret: token.RefreshToken, + Expiry: token.Expiry.UTC().Unix(), + Avatar: from.Links.Avatar.Href, + } +} + +// convertTeamList is a helper function used to convert a Bitbucket team list +// structure to the Drone Team structure. +func convertTeamList(from []*internal.Account) []*model.Team { + var teams []*model.Team + for _, team := range from { + teams = append(teams, convertTeam(team)) + } + return teams +} + +// convertTeam is a helper function used to convert a Bitbucket team account +// structure to the Drone Team structure. +func convertTeam(from *internal.Account) *model.Team { + return &model.Team{ + Login: from.Login, + Avatar: from.Links.Avatar.Href, + } +} + +// convertPullHook is a helper function used to convert a Bitbucket pull request +// hook to the Drone build struct holding commit information. +func convertPullHook(from *internal.PullRequestHook) *model.Build { + return &model.Build{ + Event: model.EventPull, + Commit: from.PullRequest.Dest.Commit.Hash, + Ref: fmt.Sprintf("refs/heads/%s", from.PullRequest.Dest.Branch.Name), + Remote: cloneLink(&from.PullRequest.Dest.Repo), + Link: from.PullRequest.Links.Html.Href, + Branch: from.PullRequest.Dest.Branch.Name, + Message: from.PullRequest.Desc, + Avatar: from.Actor.Links.Avatar.Href, + Author: from.Actor.Login, + Timestamp: from.PullRequest.Updated.UTC().Unix(), + } +} + +// convertPushHook is a helper function used to convert a Bitbucket push +// hook to the Drone build struct holding commit information. +func convertPushHook(hook *internal.PushHook, change *internal.Change) *model.Build { + build := &model.Build{ + Commit: change.New.Target.Hash, + Link: change.New.Target.Links.Html.Href, + Branch: change.New.Name, + Message: change.New.Target.Message, + Avatar: hook.Actor.Links.Avatar.Href, + Author: hook.Actor.Login, + Timestamp: change.New.Target.Date.UTC().Unix(), + } + switch change.New.Type { + case "tag", "annotated_tag", "bookmark": + build.Event = model.EventTag + build.Ref = fmt.Sprintf("refs/tags/%s", change.New.Name) + default: + build.Event = model.EventPush + build.Ref = fmt.Sprintf("refs/heads/%s", change.New.Name) + } + return build +} diff --git a/remote/bitbucket/convert_test.go b/remote/bitbucket/convert_test.go new file mode 100644 index 000000000..9f10e2499 --- /dev/null +++ b/remote/bitbucket/convert_test.go @@ -0,0 +1,195 @@ +package bitbucket + +import ( + "testing" + "time" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/internal" + + "github.com/franela/goblin" + "golang.org/x/oauth2" +) + +func Test_helper(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Bitbucket converter", func() { + + g.It("should convert passing status", func() { + g.Assert(convertStatus(model.StatusSuccess)).Equal(statusSuccess) + }) + + g.It("should convert pending status", func() { + g.Assert(convertStatus(model.StatusPending)).Equal(statusPending) + g.Assert(convertStatus(model.StatusRunning)).Equal(statusPending) + }) + + g.It("should convert failing status", func() { + g.Assert(convertStatus(model.StatusFailure)).Equal(statusFailure) + g.Assert(convertStatus(model.StatusKilled)).Equal(statusFailure) + g.Assert(convertStatus(model.StatusError)).Equal(statusFailure) + }) + + g.It("should convert passing desc", func() { + g.Assert(convertDesc(model.StatusSuccess)).Equal(descSuccess) + }) + + g.It("should convert pending desc", func() { + g.Assert(convertDesc(model.StatusPending)).Equal(descPending) + g.Assert(convertDesc(model.StatusRunning)).Equal(descPending) + }) + + g.It("should convert failing desc", func() { + g.Assert(convertDesc(model.StatusFailure)).Equal(descFailure) + }) + + g.It("should convert error desc", func() { + g.Assert(convertDesc(model.StatusKilled)).Equal(descError) + g.Assert(convertDesc(model.StatusError)).Equal(descError) + }) + + g.It("should convert repository lite", func() { + from := &internal.Repo{} + from.FullName = "octocat/hello-world" + from.Owner.Links.Avatar.Href = "http://..." + + to := convertRepoLite(from) + g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href) + g.Assert(to.FullName).Equal(from.FullName) + g.Assert(to.Owner).Equal("octocat") + g.Assert(to.Name).Equal("hello-world") + }) + + g.It("should convert repository", func() { + from := &internal.Repo{ + FullName: "octocat/hello-world", + IsPrivate: true, + Scm: "hg", + } + from.Owner.Links.Avatar.Href = "http://..." + from.Links.Html.Href = "https://bitbucket.org/foo/bar" + + to := convertRepo(from) + g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href) + g.Assert(to.FullName).Equal(from.FullName) + g.Assert(to.Owner).Equal("octocat") + g.Assert(to.Name).Equal("hello-world") + g.Assert(to.Branch).Equal("default") + g.Assert(to.Kind).Equal(from.Scm) + g.Assert(to.IsPrivate).Equal(from.IsPrivate) + g.Assert(to.Clone).Equal(from.Links.Html.Href) + g.Assert(to.Link).Equal(from.Links.Html.Href) + }) + + g.It("should convert team", func() { + from := &internal.Account{Login: "octocat"} + from.Links.Avatar.Href = "http://..." + to := convertTeam(from) + g.Assert(to.Avatar).Equal(from.Links.Avatar.Href) + g.Assert(to.Login).Equal(from.Login) + }) + + g.It("should convert team list", func() { + from := &internal.Account{Login: "octocat"} + from.Links.Avatar.Href = "http://..." + to := convertTeamList([]*internal.Account{from}) + g.Assert(to[0].Avatar).Equal(from.Links.Avatar.Href) + g.Assert(to[0].Login).Equal(from.Login) + }) + + g.It("should convert user", func() { + token := &oauth2.Token{ + AccessToken: "foo", + RefreshToken: "bar", + Expiry: time.Now(), + } + user := &internal.Account{Login: "octocat"} + user.Links.Avatar.Href = "http://..." + + result := convertUser(user, token) + g.Assert(result.Avatar).Equal(user.Links.Avatar.Href) + g.Assert(result.Login).Equal(user.Login) + g.Assert(result.Token).Equal(token.AccessToken) + g.Assert(result.Token).Equal(token.AccessToken) + g.Assert(result.Secret).Equal(token.RefreshToken) + g.Assert(result.Expiry).Equal(token.Expiry.UTC().Unix()) + }) + + g.It("should use clone url", func() { + repo := &internal.Repo{} + repo.Links.Clone = append(repo.Links.Clone, internal.Link{ + Name: "https", + Href: "https://bitbucket.org/foo/bar.git", + }) + link := cloneLink(repo) + g.Assert(link).Equal(repo.Links.Clone[0].Href) + }) + + g.It("should build clone url", func() { + repo := &internal.Repo{} + repo.Links.Html.Href = "https://foo:bar@bitbucket.org/foo/bar.git" + link := cloneLink(repo) + g.Assert(link).Equal("https://bitbucket.org/foo/bar.git") + }) + + g.It("should convert pull hook to build", func() { + hook := &internal.PullRequestHook{} + hook.Actor.Login = "octocat" + hook.Actor.Links.Avatar.Href = "https://..." + hook.PullRequest.Dest.Commit.Hash = "73f9c44d" + hook.PullRequest.Dest.Branch.Name = "master" + hook.PullRequest.Dest.Repo.Links.Html.Href = "https://bitbucket.org/foo/bar" + hook.PullRequest.Links.Html.Href = "https://bitbucket.org/foo/bar/pulls/5" + hook.PullRequest.Desc = "updated README" + hook.PullRequest.Updated = time.Now() + + build := convertPullHook(hook) + g.Assert(build.Event).Equal(model.EventPull) + g.Assert(build.Author).Equal(hook.Actor.Login) + g.Assert(build.Avatar).Equal(hook.Actor.Links.Avatar.Href) + g.Assert(build.Commit).Equal(hook.PullRequest.Dest.Commit.Hash) + g.Assert(build.Branch).Equal(hook.PullRequest.Dest.Branch.Name) + g.Assert(build.Link).Equal(hook.PullRequest.Links.Html.Href) + g.Assert(build.Ref).Equal("refs/heads/master") + g.Assert(build.Message).Equal(hook.PullRequest.Desc) + g.Assert(build.Timestamp).Equal(hook.PullRequest.Updated.Unix()) + }) + + g.It("should convert push hook to build", func() { + change := internal.Change{} + change.New.Target.Hash = "73f9c44d" + change.New.Name = "master" + change.New.Target.Links.Html.Href = "https://bitbucket.org/foo/bar/commits/73f9c44d" + change.New.Target.Message = "updated README" + change.New.Target.Date = time.Now() + + hook := internal.PushHook{} + hook.Actor.Login = "octocat" + hook.Actor.Links.Avatar.Href = "https://..." + + build := convertPushHook(&hook, &change) + g.Assert(build.Event).Equal(model.EventPush) + g.Assert(build.Author).Equal(hook.Actor.Login) + g.Assert(build.Avatar).Equal(hook.Actor.Links.Avatar.Href) + g.Assert(build.Commit).Equal(change.New.Target.Hash) + g.Assert(build.Branch).Equal(change.New.Name) + g.Assert(build.Link).Equal(change.New.Target.Links.Html.Href) + g.Assert(build.Ref).Equal("refs/heads/master") + g.Assert(build.Message).Equal(change.New.Target.Message) + g.Assert(build.Timestamp).Equal(change.New.Target.Date.Unix()) + }) + + g.It("should convert tag hook to build", func() { + change := internal.Change{} + change.New.Name = "v1.0.0" + change.New.Type = "tag" + + hook := internal.PushHook{} + + build := convertPushHook(&hook, &change) + g.Assert(build.Event).Equal(model.EventTag) + g.Assert(build.Ref).Equal("refs/tags/v1.0.0") + }) + }) +} diff --git a/remote/bitbucket/fixtures/handler.go b/remote/bitbucket/fixtures/handler.go new file mode 100644 index 000000000..f5cbbb40e --- /dev/null +++ b/remote/bitbucket/fixtures/handler.go @@ -0,0 +1,221 @@ +package fixtures + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Handler returns an http.Handler that is capable of handling a variety of mock +// Bitbucket requests and returning mock responses. +func Handler() http.Handler { + gin.SetMode(gin.TestMode) + + e := gin.New() + e.POST("/site/oauth2/access_token", getOauth) + e.GET("/2.0/repositories/:owner/:name", getRepo) + e.GET("/2.0/repositories/:owner/:name/hooks", getRepoHooks) + e.GET("/1.0/repositories/:owner/:name/src/:commit/:file", getRepoFile) + e.DELETE("/2.0/repositories/:owner/:name/hooks/:hook", deleteRepoHook) + e.POST("/2.0/repositories/:owner/:name/hooks", createRepoHook) + e.POST("/2.0/repositories/:owner/:name/commit/:commit/statuses/build", createRepoStatus) + e.GET("/2.0/repositories/:owner", getUserRepos) + e.GET("/2.0/teams/", getUserTeams) + e.GET("/2.0/user/", getUser) + + return e +} + +func getOauth(c *gin.Context) { + switch c.PostForm("code") { + case "code_bad_request": + c.String(500, "") + return + case "code_user_not_found": + c.String(200, tokenNotFoundPayload) + return + } + switch c.PostForm("refresh_token") { + case "refresh_token_not_found": + c.String(404, "") + case "refresh_token_is_empty": + c.Header("Content-Type", "application/json") + c.String(200, "{}") + default: + c.Header("Content-Type", "application/json") + c.String(200, tokenPayload) + } +} + +func getRepo(c *gin.Context) { + switch c.Param("name") { + case "not_found", "repo_unknown", "repo_not_found": + c.String(404, "") + default: + c.String(200, repoPayload) + } +} + +func getRepoHooks(c *gin.Context) { + switch c.Param("name") { + case "hooks_not_found", "repo_no_hooks": + c.String(404, "") + case "hook_empty": + c.String(200, "{}") + default: + c.String(200, repoHookPayload) + } +} + +func getRepoFile(c *gin.Context) { + switch c.Param("file") { + case "file_not_found": + c.String(404, "") + default: + c.String(200, repoFilePayload) + } +} + +func createRepoStatus(c *gin.Context) { + switch c.Param("name") { + case "repo_not_found": + c.String(404, "") + default: + c.String(200, "") + } +} + +func createRepoHook(c *gin.Context) { + c.String(200, "") +} + +func deleteRepoHook(c *gin.Context) { + switch c.Param("name") { + case "hook_not_found": + c.String(404, "") + default: + c.String(200, "") + } +} + +func getUser(c *gin.Context) { + switch c.Request.Header.Get("Authorization") { + case "Bearer user_not_found", "Bearer a87ff679": + c.String(404, "") + default: + c.String(200, userPayload) + } +} + +func getUserTeams(c *gin.Context) { + switch c.Request.Header.Get("Authorization") { + case "Bearer teams_not_found", "Bearer c81e728d": + c.String(404, "") + default: + c.String(200, userTeamPayload) + } +} + +func getUserRepos(c *gin.Context) { + switch c.Request.Header.Get("Authorization") { + case "Bearer repos_not_found", "Bearer 70efdf2e": + c.String(404, "") + default: + c.String(200, userRepoPayload) + } +} + +const tokenPayload = ` +{ + "access_token":"2YotnFZFEjr1zCsicMWpAA", + "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", + "token_type":"Bearer", + "expires_in":3600 +} +` + +const tokenNotFoundPayload = ` +{ + "access_token":"user_not_found", + "refresh_token":"user_not_found", + "token_type":"Bearer", + "expires_in":3600 +} +` + +const repoPayload = ` +{ + "full_name": "test_name/repo_name", + "scm": "git", + "is_private": true +} +` + +const repoHookPayload = ` +{ + "pagelen": 10, + "values": [ + { + "uuid": "{afe61e14-2c5f-49e8-8b68-ad1fb55fc052}", + "url": "http://127.0.0.1" + } + ], + "page": 1, + "size": 1 +} +` + +const repoFilePayload = ` +{ + "data": "{ platform: linux/amd64 }" +} +` + +const userPayload = ` +{ + "username": "superman", + "links": { + "avatar": { + "href": "http:\/\/i.imgur.com\/ZygP55A.jpg" + } + }, + "type": "user" +} +` + +const userRepoPayload = ` +{ + "page": 1, + "pagelen": 10, + "size": 1, + "values": [ + { + "links": { + "avatar": { + "href": "http:\/\/i.imgur.com\/ZygP55A.jpg" + } + }, + "full_name": "test_name/repo_name", + "scm": "git", + "is_private": true + } + ] +} +` + +const userTeamPayload = ` +{ + "pagelen": 100, + "values": [ + { + "username": "superfriends", + "links": { + "avatar": { + "href": "http:\/\/i.imgur.com\/ZygP55A.jpg" + } + }, + "type": "team" + } + ] +} +` diff --git a/remote/bitbucket/fixtures/hooks.go b/remote/bitbucket/fixtures/hooks.go new file mode 100644 index 000000000..2b68e24e9 --- /dev/null +++ b/remote/bitbucket/fixtures/hooks.go @@ -0,0 +1,164 @@ +package fixtures + +const HookPush = ` +{ + "actor": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + }, + "push": { + "changes": [ + { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": { + "type": "commit", + "hash": "709d658dc5b6d6afcd46049c2f332ee3f515a67d", + "author": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "message": "new commit message\n", + "date": "2015-06-09T03:34:49+00:00" + } + } + } + ] + } +} +` + +const HookPushEmptyHash = ` +{ + "push": { + "changes": [ + { + "new": { + "type": "branch", + "target": { "hash": "" } + } + } + ] + } +} +` + +const HookPull = ` +{ + "actor": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "pullrequest": { + "id": 1, + "title": "Title of pull request", + "description": "Description of pull request", + "state": "OPEN", + "author": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "source": { + "branch": { + "name": "branch2" + }, + "commit": { + "hash": "d3022fc0ca3d" + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + } + }, + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "hash": "ce5965ddd289" + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + } + }, + "links": { + "self": { + "href": "https:\/\/api.bitbucket.org\/api\/2.0\/pullrequests\/pullrequest_id" + }, + "html": { + "href": "https:\/\/api.bitbucket.org\/pullrequest_id" + } + } + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + } +} +` + +const HookMerged = ` +{ + "pullrequest": { + "state": "MERGED" + } +} +` diff --git a/remote/bitbucket/helper.go b/remote/bitbucket/helper.go deleted file mode 100644 index b6072bd6a..000000000 --- a/remote/bitbucket/helper.go +++ /dev/null @@ -1,101 +0,0 @@ -package bitbucket - -import ( - "net/url" - "strings" - - "github.com/drone/drone/model" -) - -// convertRepo is a helper function used to convert a Bitbucket -// repository structure to the common Drone repository structure. -func convertRepo(from *Repo) *model.Repo { - repo := model.Repo{ - Owner: strings.Split(from.FullName, "/")[0], - Name: strings.Split(from.FullName, "/")[1], - FullName: from.FullName, - Link: from.Links.Html.Href, - IsPrivate: from.IsPrivate, - Avatar: from.Owner.Links.Avatar.Href, - Kind: from.Scm, - Branch: "master", - } - - if repo.Kind == model.RepoHg { - repo.Branch = "default" - } - - // in some cases, the owner of the repository is not - // provided, however, we do have the full name. - if len(repo.Owner) == 0 { - repo.Owner = strings.Split(repo.FullName, "/")[0] - } - - // above we manually constructed the repository clone url. - // below we will iterate through the list of clone links and - // attempt to instead use the clone url provided by bitbucket. - for _, link := range from.Links.Clone { - if link.Name == "https" { - repo.Clone = link.Href - break - } - } - - // if no repository name is provided, we use the Html link. - // this excludes the .git suffix, but will still clone the repo. - if len(repo.Clone) == 0 { - repo.Clone = repo.Link - } - - // if bitbucket tries to automatically populate the user - // in the url we must strip it out. - clone, err := url.Parse(repo.Clone) - if err == nil { - clone.User = nil - repo.Clone = clone.String() - } - - return &repo -} - -// cloneLink is a helper function that tries to extract the -// clone url from the repository object. -func cloneLink(repo Repo) string { - var clone string - - // above we manually constructed the repository clone url. - // below we will iterate through the list of clone links and - // attempt to instead use the clone url provided by bitbucket. - for _, link := range repo.Links.Clone { - if link.Name == "https" { - clone = link.Href - } - } - - // if no repository name is provided, we use the Html link. - // this excludes the .git suffix, but will still clone the repo. - if len(clone) == 0 { - clone = repo.Links.Html.Href - } - - // if bitbucket tries to automatically populate the user - // in the url we must strip it out. - cloneurl, err := url.Parse(clone) - if err == nil { - cloneurl.User = nil - clone = cloneurl.String() - } - - return clone -} - -// convertRepoLite is a helper function used to convert a Bitbucket -// repository structure to the simplified Drone repository structure. -func convertRepoLite(from *Repo) *model.RepoLite { - return &model.RepoLite{ - Owner: strings.Split(from.FullName, "/")[0], - Name: strings.Split(from.FullName, "/")[1], - FullName: from.FullName, - Avatar: from.Owner.Links.Avatar.Href, - } -} diff --git a/remote/bitbucket/client.go b/remote/bitbucket/internal/client.go similarity index 80% rename from remote/bitbucket/client.go rename to remote/bitbucket/internal/client.go index e0caed656..781b3d07b 100644 --- a/remote/bitbucket/client.go +++ b/remote/bitbucket/internal/client.go @@ -1,4 +1,4 @@ -package bitbucket +package internal import ( "bytes" @@ -20,8 +20,6 @@ const ( ) const ( - base = "https://api.bitbucket.org" - pathUser = "%s/2.0/user/" pathEmails = "%s/2.0/user/emails" pathTeams = "%s/2.0/teams/?%s" @@ -35,52 +33,53 @@ const ( type Client struct { *http.Client + base string } -func NewClient(client *http.Client) *Client { - return &Client{client} +func NewClient(url string, client *http.Client) *Client { + return &Client{client, url} } -func NewClientToken(client, secret string, token *oauth2.Token) *Client { +func NewClientToken(url, client, secret string, token *oauth2.Token) *Client { config := &oauth2.Config{ ClientID: client, ClientSecret: secret, Endpoint: bitbucket.Endpoint, } - return NewClient(config.Client(oauth2.NoContext, token)) + return NewClient(url, config.Client(oauth2.NoContext, token)) } func (c *Client) FindCurrent() (*Account, error) { out := new(Account) - uri := fmt.Sprintf(pathUser, base) + uri := fmt.Sprintf(pathUser, c.base) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListEmail() (*EmailResp, error) { out := new(EmailResp) - uri := fmt.Sprintf(pathEmails, base) + uri := fmt.Sprintf(pathEmails, c.base) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListTeams(opts *ListTeamOpts) (*AccountResp, error) { out := new(AccountResp) - uri := fmt.Sprintf(pathTeams, base, opts.Encode()) + uri := fmt.Sprintf(pathTeams, c.base, opts.Encode()) err := c.do(uri, get, nil, out) return out, err } func (c *Client) FindRepo(owner, name string) (*Repo, error) { out := new(Repo) - uri := fmt.Sprintf(pathRepo, base, owner, name) + uri := fmt.Sprintf(pathRepo, c.base, owner, name) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListRepos(account string, opts *ListOpts) (*RepoResp, error) { out := new(RepoResp) - uri := fmt.Sprintf(pathRepos, base, account, opts.Encode()) + uri := fmt.Sprintf(pathRepos, c.base, account, opts.Encode()) err := c.do(uri, get, nil, out) return out, err } @@ -105,37 +104,37 @@ func (c *Client) ListReposAll(account string) ([]*Repo, error) { func (c *Client) FindHook(owner, name, id string) (*Hook, error) { out := new(Hook) - uri := fmt.Sprintf(pathHook, base, owner, name, id) + uri := fmt.Sprintf(pathHook, c.base, owner, name, id) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListHooks(owner, name string, opts *ListOpts) (*HookResp, error) { out := new(HookResp) - uri := fmt.Sprintf(pathHooks, base, owner, name, opts.Encode()) + uri := fmt.Sprintf(pathHooks, c.base, owner, name, opts.Encode()) err := c.do(uri, get, nil, out) return out, err } func (c *Client) CreateHook(owner, name string, hook *Hook) error { - uri := fmt.Sprintf(pathHooks, base, owner, name, "") + uri := fmt.Sprintf(pathHooks, c.base, owner, name, "") return c.do(uri, post, hook, nil) } func (c *Client) DeleteHook(owner, name, id string) error { - uri := fmt.Sprintf(pathHook, base, owner, name, id) + uri := fmt.Sprintf(pathHook, c.base, owner, name, id) return c.do(uri, del, nil, nil) } func (c *Client) FindSource(owner, name, revision, path string) (*Source, error) { out := new(Source) - uri := fmt.Sprintf(pathSource, base, owner, name, revision, path) + uri := fmt.Sprintf(pathSource, c.base, owner, name, revision, path) err := c.do(uri, get, nil, out) return out, err } func (c *Client) CreateStatus(owner, name, revision string, status *BuildStatus) error { - uri := fmt.Sprintf(pathStatus, base, owner, name, revision) + uri := fmt.Sprintf(pathStatus, c.base, owner, name, revision) return c.do(uri, post, status, nil) } diff --git a/remote/bitbucket/types.go b/remote/bitbucket/internal/types.go similarity index 88% rename from remote/bitbucket/types.go rename to remote/bitbucket/internal/types.go index 1ad189720..b92d22e08 100644 --- a/remote/bitbucket/types.go +++ b/remote/bitbucket/internal/types.go @@ -1,4 +1,4 @@ -package bitbucket +package internal import ( "net/url" @@ -100,27 +100,29 @@ type Source struct { Size int64 `json:"size"` } +type Change struct { + New struct { + Type string `json:"type"` + Name string `json:"name"` + Target struct { + Type string `json:"type"` + Hash string `json:"hash"` + Message string `json:"message"` + Date time.Time `json:"date"` + Links Links `json:"links"` + Author struct { + Raw string `json:"raw"` + User Account `json:"user"` + } `json:"author"` + } `json:"target"` + } `json:"new"` +} + type PushHook struct { Actor Account `json:"actor"` Repo Repo `json:"repository"` Push struct { - Changes []struct { - New struct { - Type string `json:"type"` - Name string `json:"name"` - Target struct { - Type string `json:"type"` - Hash string `json:"hash"` - Message string `json:"message"` - Date time.Time `json:"date"` - Links Links `json:"links"` - Author struct { - Raw string `json:"raw"` - User Account `json:"user"` - } `json:"author"` - } `json:"target"` - } `json:"new"` - } `json:"changes"` + Changes []Change `json:"changes"` } `json:"push"` } diff --git a/remote/bitbucket/parse.go b/remote/bitbucket/parse.go new file mode 100644 index 000000000..7ab385d38 --- /dev/null +++ b/remote/bitbucket/parse.go @@ -0,0 +1,71 @@ +package bitbucket + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/internal" +) + +const ( + hookEvent = "X-Event-Key" + hookPush = "repo:push" + hookPullCreated = "pullrequest:created" + hookPullUpdated = "pullrequest:updated" + + changeBranch = "branch" + changeNamedBranch = "named_branch" + + stateMerged = "MERGED" + stateDeclined = "DECLINED" + stateOpen = "OPEN" +) + +// parseHook parses a Bitbucket hook from an http.Request request and returns +// Repo and Build detail. If a hook type is unsupported nil values are returned. +func parseHook(r *http.Request) (*model.Repo, *model.Build, error) { + payload, _ := ioutil.ReadAll(r.Body) + + switch r.Header.Get(hookEvent) { + case hookPush: + return parsePushHook(payload) + case hookPullCreated, hookPullUpdated: + return parsePullHook(payload) + } + return nil, nil, nil +} + +// parsePushHook parses a push hook and returns the Repo and Build details. +// If the commit type is unsupported nil values are returned. +func parsePushHook(payload []byte) (*model.Repo, *model.Build, error) { + hook := internal.PushHook{} + + err := json.Unmarshal(payload, &hook) + if err != nil { + return nil, nil, err + } + + for _, change := range hook.Push.Changes { + if change.New.Target.Hash == "" { + continue + } + return convertRepo(&hook.Repo), convertPushHook(&hook, &change), nil + } + return nil, nil, nil +} + +// parsePullHook parses a pull request hook and returns the Repo and Build +// details. If the pull request is closed nil values are returned. +func parsePullHook(payload []byte) (*model.Repo, *model.Build, error) { + hook := internal.PullRequestHook{} + + if err := json.Unmarshal(payload, &hook); err != nil { + return nil, nil, err + } + if hook.PullRequest.State != stateOpen { + return nil, nil, nil + } + return convertRepo(&hook.Repo), convertPullHook(&hook), nil +} diff --git a/remote/bitbucket/parse_test.go b/remote/bitbucket/parse_test.go new file mode 100644 index 000000000..eee2bda36 --- /dev/null +++ b/remote/bitbucket/parse_test.go @@ -0,0 +1,104 @@ +package bitbucket + +import ( + "bytes" + "net/http" + "testing" + + "github.com/drone/drone/remote/bitbucket/fixtures" + + "github.com/franela/goblin" +) + +func Test_parser(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Bitbucket parser", func() { + + g.It("Should ignore unsupported hook", func() { + buf := bytes.NewBufferString(fixtures.HookPush) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, "issue:created") + + r, b, err := parseHook(req) + g.Assert(r == nil).IsTrue() + g.Assert(b == nil).IsTrue() + g.Assert(err == nil).IsTrue() + }) + + g.Describe("Given a pull request hook payload", func() { + + g.It("Should return err when malformed", func() { + buf := bytes.NewBufferString("[]") + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPullCreated) + + _, _, err := parseHook(req) + g.Assert(err != nil).IsTrue() + }) + + g.It("Should return nil if not open", func() { + buf := bytes.NewBufferString(fixtures.HookMerged) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPullCreated) + + r, b, err := parseHook(req) + g.Assert(r == nil).IsTrue() + g.Assert(b == nil).IsTrue() + g.Assert(err == nil).IsTrue() + }) + + g.It("Should return pull request details", func() { + buf := bytes.NewBufferString(fixtures.HookPull) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPullCreated) + + r, b, err := parseHook(req) + g.Assert(err == nil).IsTrue() + g.Assert(r.FullName).Equal("user_name/repo_name") + g.Assert(b.Commit).Equal("ce5965ddd289") + }) + }) + + g.Describe("Given a push hook payload", func() { + + g.It("Should return err when malformed", func() { + buf := bytes.NewBufferString("[]") + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPush) + + _, _, err := parseHook(req) + g.Assert(err != nil).IsTrue() + }) + + g.It("Should return nil if missing commit sha", func() { + buf := bytes.NewBufferString(fixtures.HookPushEmptyHash) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPush) + + r, b, err := parseHook(req) + g.Assert(r == nil).IsTrue() + g.Assert(b == nil).IsTrue() + g.Assert(err == nil).IsTrue() + }) + + g.It("Should return push details", func() { + buf := bytes.NewBufferString(fixtures.HookPush) + req, _ := http.NewRequest("POST", "/hook", buf) + req.Header = http.Header{} + req.Header.Set(hookEvent, hookPush) + + r, b, err := parseHook(req) + g.Assert(err == nil).IsTrue() + g.Assert(r.FullName).Equal("user_name/repo_name") + g.Assert(b.Commit).Equal("709d658dc5b6d6afcd46049c2f332ee3f515a67d") + }) + }) + }) +} diff --git a/remote/bitbucketserver/bitbucketserver.go b/remote/bitbucketserver/bitbucketserver.go index d78de27f2..6b73ad136 100644 --- a/remote/bitbucketserver/bitbucketserver.go +++ b/remote/bitbucketserver/bitbucketserver.go @@ -1,141 +1,150 @@ package bitbucketserver -// Requires the following to be set -// REMOTE_DRIVER=bitbucketserver -// REMOTE_CONFIG=https://{servername}?consumer_key={key added on the stash server for oath1}&git_username={username for clone}&git_password={password for clone}&consumer_rsa=/path/to/pem.file&open={not used yet} -// Configure application links in the bitbucket server -- -// application url needs to be the base url to drone -// incoming auth needs to have the consumer key (same as the key in REMOTE_CONFIG) -// set the public key (public key from the private key added to /var/lib/bitbucketserver/private_key.pem name matters) -// consumer call back is the base url to drone plus /authorize/ -// Needs a pem private key added to /var/lib/bitbucketserver/private_key.pem -// After that you should be good to go +// WARNING! This is an work-in-progress patch and does not yet conform to the coding, +// quality or security standards expected of this project. Please use with caution. import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" - log "github.com/Sirupsen/logrus" - "github.com/drone/drone/model" - "github.com/mrjones/oauth" "io/ioutil" "net/http" "net/url" - "strconv" + + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/mrjones/oauth" ) -type BitbucketServer struct { +// Opts defines configuration options. +type Opts struct { + URL string // Stash server url. + Username string // Git machine account username. + Password string // Git machine account password. + ConsumerKey string // Oauth1 consumer key. + ConsumerRSA string // Oauth1 consumer key file. + + SkipVerify bool // Skip ssl verification. +} + +// New returns a Remote implementation that integrates with Bitbucket Server, +// the on-premise edition of Bitbucket Cloud, formerly known as Stash. +func New(opts Opts) (remote.Remote, error) { + bb := &client{ + URL: opts.URL, + ConsumerKey: opts.ConsumerKey, + ConsumerRSA: opts.ConsumerRSA, + GitUserName: opts.Username, + GitPassword: opts.Password, + } + + switch { + case bb.GitUserName == "": + return nil, fmt.Errorf("Must have a git machine account username") + case bb.GitPassword == "": + return nil, fmt.Errorf("Must have a git machine account password") + case bb.ConsumerKey == "": + return nil, fmt.Errorf("Must have a oauth1 consumer key") + case bb.ConsumerRSA == "": + return nil, fmt.Errorf("Must have a oauth1 consumer key file") + } + + keyfile, err := ioutil.ReadFile(bb.ConsumerRSA) + if err != nil { + return nil, err + } + block, _ := pem.Decode(keyfile) + bb.PrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + // TODO de-referencing is a bit weird and may not behave as expected, and could + // have race conditions. Instead store the parsed key (I already did this above) + // and then pass the parsed private key when creating the Bitbucket client. + bb.Consumer = *NewClient(bb.ConsumerRSA, bb.ConsumerKey, bb.URL) + return bb, nil +} + +type client struct { URL string ConsumerKey string GitUserName string GitPassword string ConsumerRSA string - Open bool + PrivateKey *rsa.PrivateKey Consumer oauth.Consumer } -func Load(config string) *BitbucketServer { - - url_, err := url.Parse(config) +func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { + requestToken, url, err := c.Consumer.GetRequestTokenAndUrl("oob") if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) - } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - - bitbucketserver := BitbucketServer{} - bitbucketserver.URL = url_.String() - bitbucketserver.GitUserName = params.Get("git_username") - if bitbucketserver.GitUserName == "" { - log.Fatalln("Must have a git_username") - } - bitbucketserver.GitPassword = params.Get("git_password") - if bitbucketserver.GitPassword == "" { - log.Fatalln("Must have a git_password") - } - bitbucketserver.ConsumerKey = params.Get("consumer_key") - if bitbucketserver.ConsumerKey == "" { - log.Fatalln("Must have a consumer_key") - } - bitbucketserver.ConsumerRSA = params.Get("consumer_rsa") - if bitbucketserver.ConsumerRSA == "" { - log.Fatalln("Must have a consumer_rsa") - } - - bitbucketserver.Open, _ = strconv.ParseBool(params.Get("open")) - - bitbucketserver.Consumer = *NewClient(bitbucketserver.ConsumerRSA, bitbucketserver.ConsumerKey, bitbucketserver.URL) - - return &bitbucketserver -} - -func (bs *BitbucketServer) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { - log.Info("Starting to login for bitbucketServer") - - log.Info("getting the requestToken") - requestToken, url, err := bs.Consumer.GetRequestTokenAndUrl("oob") - if err != nil { - log.Error(err) + return nil, err } var code = req.FormValue("oauth_verifier") if len(code) == 0 { - log.Info("redirecting to %s", url) http.Redirect(res, req, url, http.StatusSeeOther) - return nil, false, nil + return nil, nil } - var request_oauth_token = req.FormValue("oauth_token") - requestToken.Token = request_oauth_token - accessToken, err := bs.Consumer.AuthorizeToken(requestToken, code) + requestToken.Token = req.FormValue("oauth_token") + accessToken, err := c.Consumer.AuthorizeToken(requestToken, code) if err != nil { - log.Error(err) + return nil, err } - client, err := bs.Consumer.MakeHttpClient(accessToken) + client, err := c.Consumer.MakeHttpClient(accessToken) if err != nil { - log.Error(err) + return nil, err } - response, err := client.Get(fmt.Sprintf("%s/plugins/servlet/applinks/whoami", bs.URL)) + response, err := client.Get(fmt.Sprintf("%s/plugins/servlet/applinks/whoami", c.URL)) if err != nil { - log.Error(err) + return nil, err } defer response.Body.Close() bits, err := ioutil.ReadAll(response.Body) - userName := string(bits) + if err != nil { + return nil, err + } + login := string(bits) - response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s",bs.URL, userName)) + // TODO errors should never be ignored like this + response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s", c.URL, login)) contents, err := ioutil.ReadAll(response1.Body) defer response1.Body.Close() - var mUser User - json.Unmarshal(contents, &mUser) + var mUser User // TODO prefixing with m* is not a common convention in Go + json.Unmarshal(contents, &mUser) // TODO should not ignore error - user := model.User{} - user.Login = userName - user.Email = mUser.EmailAddress - user.Token = accessToken.Token - - user.Avatar = avatarLink(mUser.EmailAddress) - - return &user, bs.Open, nil + return &model.User{ + Login: login, + Email: mUser.EmailAddress, + Token: accessToken.Token, + Avatar: avatarLink(mUser.EmailAddress), + }, nil } -func (bs *BitbucketServer) Auth(token, secret string) (string, error) { - log.Info("Staring to auth for bitbucketServer. %s", token) - if len(token) == 0 { - return "", fmt.Errorf("Hasn't logged in yet") - } - return token, nil +// Auth is not supported by the Stash driver. +func (*client) Auth(token, secret string) (string, error) { + return "", fmt.Errorf("Not Implemented") } -func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, error) { - log.Info("Staring repo for bitbucketServer with user " + u.Login + " " + owner + " " + name) +// Teams is not supported by the Stash driver. +func (*client) Teams(u *model.User) ([]*model.Team, error) { + var teams []*model.Team + return teams, nil +} - client := NewClientWithToken(&bs.Consumer, u.Token) +func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) { + + client := NewClientWithToken(&c.Consumer, u.Token) + + url := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s", c.URL, owner, name) - url := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s",bs.URL,owner,name) - log.Info("Trying to get " + url) response, err := client.Get(url) if err != nil { log.Error(err) @@ -145,40 +154,36 @@ func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, bsRepo := BSRepo{} json.Unmarshal(contents, &bsRepo) - cloneLink := "" - repoLink := "" + repo := &model.Repo{ + Name: bsRepo.Slug, + Owner: bsRepo.Project.Key, + Branch: "master", + Kind: model.RepoGit, + IsPrivate: !bsRepo.Project.Public, // TODO(josmo) verify this is corrrect + FullName: fmt.Sprintf("%s/%s", bsRepo.Project.Key, bsRepo.Slug), + } for _, item := range bsRepo.Links.Clone { if item.Name == "http" { - cloneLink = item.Href + repo.Clone = item.Href } } for _, item := range bsRepo.Links.Self { if item.Href != "" { - repoLink = item.Href + repo.Link = item.Href } } - //TODO: get the real allow tag+ infomration - repo := &model.Repo{} - repo.Clone = cloneLink - repo.Link = repoLink - repo.Name = bsRepo.Slug - repo.Owner = bsRepo.Project.Key - repo.AllowPush = true - repo.FullName = fmt.Sprintf("%s/%s",bsRepo.Project.Key,bsRepo.Slug) - repo.Branch = "master" - repo.Kind = model.RepoGit return repo, nil } -func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) { - log.Info("Staring repos for bitbucketServer " + u.Login) +func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) { + var repos = []*model.RepoLite{} - client := NewClientWithToken(&bs.Consumer, u.Token) + client := NewClientWithToken(&c.Consumer, u.Token) - response, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/repos?limit=10000",bs.URL)) + response, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/repos?limit=10000", c.URL)) if err != nil { log.Error(err) } @@ -198,10 +203,8 @@ func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) { return repos, nil } -func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm, error) { - - //TODO: find the real permissions - log.Info("Staring perm for bitbucketServer") +func (c *client) Perm(u *model.User, owner, repo string) (*model.Perm, error) { + // TODO need to fetch real permissions here perms := new(model.Perm) perms.Pull = true perms.Admin = true @@ -209,11 +212,11 @@ func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm, return perms, nil } -func (bs *BitbucketServer) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { +func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { log.Info(fmt.Sprintf("Staring file for bitbucketServer login: %s repo: %s buildevent: %s string: %s", u.Login, r.Name, b.Event, f)) - client := NewClientWithToken(&bs.Consumer, u.Token) - fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", bs.URL, r.Owner, r.Name, f) + client := NewClientWithToken(&c.Consumer, u.Token) + fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", c.URL, r.Owner, r.Name, f) log.Info(fileURL) response, err := client.Get(fileURL) if err != nil { @@ -231,28 +234,26 @@ func (bs *BitbucketServer) File(u *model.User, r *model.Repo, b *model.Build, f return responseBytes, nil } -func (bs *BitbucketServer) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - log.Info("Staring status for bitbucketServer") +// Status is not supported by the Gogs driver. +func (*client) Status(*model.User, *model.Repo, *model.Build, string) error { return nil } -func (bs *BitbucketServer) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { - log.Info("Starting the Netrc lookup") - u, err := url.Parse(bs.URL) +func (c *client) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { + u, err := url.Parse(c.URL) // TODO strip port from url if err != nil { return nil, err } return &model.Netrc{ Machine: u.Host, - Login: bs.GitUserName, - Password: bs.GitPassword, + Login: c.GitUserName, + Password: c.GitPassword, }, nil } -func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - log.Info(fmt.Sprintf("Staring activate for bitbucketServer user: %s repo: %s key: %s link: %s", u.Login, r.Name, k, link)) - client := NewClientWithToken(&bs.Consumer, u.Token) - hook, err := bs.CreateHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) +func (c *client) Activate(u *model.User, r *model.Repo, link string) error { + client := NewClientWithToken(&c.Consumer, u.Token) + hook, err := c.CreateHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) if err != nil { return err } @@ -260,68 +261,56 @@ func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key, return nil } -func (bs *BitbucketServer) Deactivate(u *model.User, r *model.Repo, link string) error { - log.Info(fmt.Sprintf("Staring deactivating for bitbucketServer user: %s repo: %s link: %s", u.Login, r.Name, link)) - client := NewClientWithToken(&bs.Consumer, u.Token) - err := bs.DeleteHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) +func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error { + client := NewClientWithToken(&c.Consumer, u.Token) + err := c.DeleteHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) if err != nil { return err } return nil } -func (bs *BitbucketServer) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - log.Info("Staring hook for bitbucketServer") - defer r.Body.Close() - contents, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Info(err) +func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { + hook := new(postHook) + if err := json.NewDecoder(r.Body).Decode(hook); err != nil { + return nil, nil, err } - var hookPost postHook - json.Unmarshal(contents, &hookPost) + build := &model.Build{ + Event: model.EventPush, + Ref: hook.RefChanges[0].RefID, // TODO check for index Values + Author: hook.Changesets.Values[0].ToCommit.Author.EmailAddress, // TODO check for index Values + Commit: hook.RefChanges[0].ToHash, // TODO check for index value + Avatar: avatarLink(hook.Changesets.Values[0].ToCommit.Author.EmailAddress), + } - buildModel := &model.Build{} - buildModel.Event = model.EventPush - buildModel.Ref = hookPost.RefChanges[0].RefID - buildModel.Author = hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress - buildModel.Commit = hookPost.RefChanges[0].ToHash - buildModel.Avatar = avatarLink(hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress) + repo := &model.Repo{ + Name: hook.Repository.Slug, + Owner: hook.Repository.Project.Key, + FullName: fmt.Sprintf("%s/%s", hook.Repository.Project.Key, hook.Repository.Slug), + Branch: "master", + Kind: model.RepoGit, + } - //All you really need is the name and owner. That's what creates the lookup key, so it needs to match the repo info. Just an FYI - repo := &model.Repo{} - repo.Name = hookPost.Repository.Slug - repo.Owner = hookPost.Repository.Project.Key - repo.AllowTag = false - repo.AllowDeploy = false - repo.AllowPull = false - repo.AllowPush = true - repo.FullName = fmt.Sprintf("%s/%s",hookPost.Repository.Project.Key,hookPost.Repository.Slug) - repo.Branch = "master" - repo.Kind = model.RepoGit - - return repo, buildModel, nil -} -func (bs *BitbucketServer) String() string { - return "bitbucketserver" + return repo, build, nil } type HookDetail struct { - Key string `"json:key"` - Name string `"json:name"` - Type string `"json:type"` - Description string `"json:description"` - Version string `"json:version"` - ConfigFormKey string `"json:configFormKey"` + Key string `json:"key"` + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Version string `json:"version"` + ConfigFormKey string `json:"configFormKey"` } type Hook struct { - Enabled bool `"json:enabled"` - Details *HookDetail `"json:details"` + Enabled bool `json:"enabled"` + Details *HookDetail `json:"details"` } // Enable hook for named repository -func (bs *BitbucketServer) CreateHook(client *http.Client, project, slug, hook_key, link string) (*Hook, error) { +func (bs *client) CreateHook(client *http.Client, project, slug, hook_key, link string) (*Hook, error) { // Set hook hookBytes := []byte(fmt.Sprintf(`{"hook-url-0":"%s"}`, link)) @@ -336,7 +325,7 @@ func (bs *BitbucketServer) CreateHook(client *http.Client, project, slug, hook_k } // Disable hook for named repository -func (bs *BitbucketServer) DeleteHook(client *http.Client, project, slug, hook_key, link string) error { +func (bs *client) DeleteHook(client *http.Client, project, slug, hook_key, link string) error { enablePath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled", project, slug, hook_key) doDelete(client, bs.URL+enablePath) diff --git a/remote/github/github.go b/remote/github/github.go index e935f20f7..ac9b1a6aa 100644 --- a/remote/github/github.go +++ b/remote/github/github.go @@ -11,22 +11,18 @@ import ( "strings" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/oauth2" - log "github.com/Sirupsen/logrus" "github.com/google/go-github/github" ) const ( - DefaultURL = "https://github.com" - DefaultAPI = "https://api.github.com" - DefaultScope = "repo,repo:status,user:email" - DefaultMergeRef = "merge" + DefaultURL = "https://github.com" // Default GitHub URL + DefaultAPI = "https://api.github.com" // Default GitHub API URL ) -var githubDeployRegex = regexp.MustCompile(".+/deployments/(\\d+)") - type Github struct { URL string API string @@ -34,58 +30,35 @@ type Github struct { Secret string Scope string MergeRef string - Orgs []string - Open bool PrivateMode bool SkipVerify bool - GitSSH bool } -func Load(config string) *Github { - - // parse the remote DSN configuration string - url_, err := url.Parse(config) - if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) +func New(url, client, secret string, scope []string, private, skipverify, mergeref bool) (remote.Remote, error) { + remote := &Github{ + URL: strings.TrimSuffix(url, "/"), + Client: client, + Secret: secret, + Scope: strings.Join(scope, ","), + PrivateMode: private, + SkipVerify: skipverify, + MergeRef: "head", } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - // create the Githbub remote using parameters from - // the parsed DSN configuration string. - github := Github{} - github.URL = url_.String() - github.Client = params.Get("client_id") - github.Secret = params.Get("client_secret") - github.Scope = params.Get("scope") - github.Orgs = params["orgs"] - github.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) - github.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) - github.Open, _ = strconv.ParseBool(params.Get("open")) - github.GitSSH, _ = strconv.ParseBool(params.Get("ssh")) - github.MergeRef = params.Get("merge_ref") - - if github.URL == DefaultURL { - github.API = DefaultAPI + if remote.URL == DefaultURL { + remote.API = DefaultAPI } else { - github.API = github.URL + "/api/v3/" + remote.API = remote.URL + "/api/v3/" + } + if mergeref { + remote.MergeRef = "merge" } - if github.Scope == "" { - github.Scope = DefaultScope - } - - if github.MergeRef == "" { - github.MergeRef = DefaultMergeRef - } - - return &github + return remote, nil } -// Login authenticates the session and returns the -// remote user details. -func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// Login authenticates the session and returns the remote user details. +func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { var config = &oauth2.Config{ ClientId: g.Client, @@ -101,7 +74,7 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, if len(code) == 0 { var random = GetRandom() http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther) - return nil, false, nil + return nil, nil } var trans = &oauth2.Transport{ @@ -117,23 +90,13 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, } var token, err = trans.Exchange(code) if err != nil { - return nil, false, fmt.Errorf("Error exchanging token. %s", err) + return nil, fmt.Errorf("Error exchanging token. %s", err) } var client = NewClient(g.API, token.AccessToken, g.SkipVerify) var useremail, errr = GetUserEmail(client) if errr != nil { - return nil, false, fmt.Errorf("Error retrieving user or verified email. %s", errr) - } - - if len(g.Orgs) > 0 { - allowedOrg, err := UserBelongsToOrg(client, g.Orgs) - if err != nil { - return nil, false, fmt.Errorf("Could not check org membership. %s", err) - } - if !allowedOrg { - return nil, false, fmt.Errorf("User does not belong to correct org. Must belong to %v", g.Orgs) - } + return nil, fmt.Errorf("Error retrieving user or verified email. %s", errr) } user := model.User{} @@ -141,7 +104,7 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, user.Email = *useremail.Email user.Token = token.AccessToken user.Avatar = *useremail.AvatarURL - return &user, g.Open, nil + return &user, nil } // Auth authenticates the session and returns the remote user @@ -155,37 +118,52 @@ func (g *Github) Auth(token, secret string) (string, error) { return *user.Login, nil } -// Repo fetches the named repository from the remote system. -func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) { +func (g *Github) Teams(u *model.User) ([]*model.Team, error) { client := NewClient(g.API, u.Token, g.SkipVerify) - repo_, err := GetRepo(client, owner, name) + orgs, err := GetOrgs(client) if err != nil { return nil, err } - repo := &model.Repo{} - repo.Owner = owner - repo.Name = name - repo.FullName = *repo_.FullName - repo.Link = *repo_.HTMLURL - repo.IsPrivate = *repo_.Private - repo.Clone = *repo_.CloneURL - repo.Branch = "master" - repo.Avatar = *repo_.Owner.AvatarURL - repo.Kind = model.RepoGit + var teams []*model.Team + for _, org := range orgs { + teams = append(teams, &model.Team{ + Login: *org.Login, + Avatar: *org.AvatarURL, + }) + } + return teams, nil +} - if repo_.DefaultBranch != nil { - repo.Branch = *repo_.DefaultBranch +// Repo fetches the named repository from the remote system. +func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) { + client := NewClient(g.API, u.Token, g.SkipVerify) + r, err := GetRepo(client, owner, name) + if err != nil { + return nil, err + } + + repo := &model.Repo{ + Owner: owner, + Name: name, + FullName: *r.FullName, + Link: *r.HTMLURL, + IsPrivate: *r.Private, + Clone: *r.CloneURL, + Avatar: *r.Owner.AvatarURL, + Kind: model.RepoGit, + } + + if r.DefaultBranch != nil { + repo.Branch = *r.DefaultBranch + } else { + repo.Branch = "master" } if g.PrivateMode { repo.IsPrivate = true } - if g.GitSSH && repo.IsPrivate { - repo.Clone = *repo_.SSHURL - } - return repo, err } @@ -257,8 +235,10 @@ func repoStatus(client *github.Client, r *model.Repo, b *model.Build, link strin return err } +var reDeploy = regexp.MustCompile(".+/deployments/(\\d+)") + func deploymentStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error { - matches := githubDeployRegex.FindStringSubmatch(b.Link) + matches := reDeploy.FindStringSubmatch(b.Link) // if the deployment was not triggered from github, don't send a deployment status if len(matches) != 2 { return nil @@ -292,23 +272,9 @@ func (g *Github) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. -func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { +func (g *Github) Activate(u *model.User, r *model.Repo, link string) error { client := NewClient(g.API, u.Token, g.SkipVerify) - title, err := GetKeyTitle(link) - if err != nil { - return err - } - - // if the CloneURL is using the SSHURL then we know that - // we need to add an SSH key to GitHub. - if r.IsPrivate || g.PrivateMode { - _, err = CreateUpdateKey(client, r.Owner, r.Name, title, k.Public) - if err != nil { - return err - } - } - - _, err = CreateUpdateHook(client, r.Owner, r.Name, link) + _, err := CreateUpdateHook(client, r.Owner, r.Name, link) return err } @@ -316,18 +282,6 @@ func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link strin // which are equal to link and removing the SSH deploy key. func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error { client := NewClient(g.API, u.Token, g.SkipVerify) - title, err := GetKeyTitle(link) - if err != nil { - return err - } - - // remove the deploy-key if it is installed remote. - if r.IsPrivate || g.PrivateMode { - if err := DeleteKey(client, r.Owner, r.Name, title); err != nil { - return err - } - } - return DeleteHook(client, r.Owner, r.Name, link) } diff --git a/remote/github/github_test.go b/remote/github/github_test.go index 85ad35724..dcf673370 100644 --- a/remote/github/github_test.go +++ b/remote/github/github_test.go @@ -46,31 +46,32 @@ func TestHook(t *testing.T) { }) } -func TestLoad(t *testing.T) { - conf := "https://github.com?client_id=client&client_secret=secret&scope=scope1,scope2" - - g := Load(conf) - if g.URL != "https://github.com" { - t.Errorf("g.URL = %q; want https://github.com", g.URL) - } - if g.Client != "client" { - t.Errorf("g.Client = %q; want client", g.Client) - } - if g.Secret != "secret" { - t.Errorf("g.Secret = %q; want secret", g.Secret) - } - if g.Scope != "scope1,scope2" { - t.Errorf("g.Scope = %q; want scope1,scope2", g.Scope) - } - if g.API != DefaultAPI { - t.Errorf("g.API = %q; want %q", g.API, DefaultAPI) - } - if g.MergeRef != DefaultMergeRef { - t.Errorf("g.MergeRef = %q; want %q", g.MergeRef, DefaultMergeRef) - } - - g = Load("") - if g.Scope != DefaultScope { - t.Errorf("g.Scope = %q; want %q", g.Scope, DefaultScope) - } -} +// +// func TestLoad(t *testing.T) { +// conf := "https://github.com?client_id=client&client_secret=secret&scope=scope1,scope2" +// +// g := Load(conf) +// if g.URL != "https://github.com" { +// t.Errorf("g.URL = %q; want https://github.com", g.URL) +// } +// if g.Client != "client" { +// t.Errorf("g.Client = %q; want client", g.Client) +// } +// if g.Secret != "secret" { +// t.Errorf("g.Secret = %q; want secret", g.Secret) +// } +// if g.Scope != "scope1,scope2" { +// t.Errorf("g.Scope = %q; want scope1,scope2", g.Scope) +// } +// if g.API != DefaultAPI { +// t.Errorf("g.API = %q; want %q", g.API, DefaultAPI) +// } +// if g.MergeRef != DefaultMergeRef { +// t.Errorf("g.MergeRef = %q; want %q", g.MergeRef, DefaultMergeRef) +// } +// +// g = Load("") +// if g.Scope != DefaultScope { +// t.Errorf("g.Scope = %q; want %q", g.Scope, DefaultScope) +// } +// } diff --git a/remote/github/helper.go b/remote/github/helper.go index f86aadc6d..362df938c 100644 --- a/remote/github/helper.go +++ b/remote/github/helper.go @@ -72,53 +72,6 @@ func GetRepo(client *github.Client, owner, repo string) (*github.Repository, err return r, err } -// GetAllRepos is a helper function that returns an aggregated list -// of all user and organization repositories. -func GetAllRepos(client *github.Client) ([]github.Repository, error) { - orgs, err := GetOrgs(client) - if err != nil { - return nil, err - } - - repos, err := GetUserRepos(client) - if err != nil { - return nil, err - } - - for _, org := range orgs { - list, err := GetOrgRepos(client, *org.Login) - if err != nil { - return nil, err - } - repos = append(repos, list...) - } - - return repos, nil -} - -// GetSubscriptions is a helper function that returns an aggregated list -// of all user and organization repositories. -// func GetSubscriptions(client *github.Client) ([]github.Repository, error) { -// var repos []github.Repository -// var opts = github.ListOptions{} -// opts.PerPage = 100 -// opts.Page = 1 - -// // loop through user repository list -// for opts.Page > 0 { -// list, resp, err := client.Activity.ListWatched(""), &opts) -// if err != nil { -// return nil, err -// } -// repos = append(repos, list...) - -// // increment the next page to retrieve -// opts.Page = resp.NextPage -// } - -// return repos, nil -// } - // GetUserRepos is a helper function that returns a list of // all user repositories. Paginated results are aggregated into // a single list. @@ -143,30 +96,6 @@ func GetUserRepos(client *github.Client) ([]github.Repository, error) { return repos, nil } -// GetOrgRepos is a helper function that returns a list of -// all org repositories. Paginated results are aggregated into -// a single list. -func GetOrgRepos(client *github.Client, org string) ([]github.Repository, error) { - var repos []github.Repository - var opts = github.RepositoryListByOrgOptions{} - opts.PerPage = 100 - opts.Page = 1 - - // loop through user repository list - for opts.Page > 0 { - list, resp, err := client.Repositories.ListByOrg(org, &opts) - if err != nil { - return nil, err - } - repos = append(repos, list...) - - // increment the next page to retrieve - opts.Page = resp.NextPage - } - - return repos, nil -} - // GetOrgs is a helper function that returns a list of // all orgs that a user belongs to. func GetOrgs(client *github.Client) ([]github.Organization, error) { @@ -250,70 +179,6 @@ func CreateUpdateHook(client *github.Client, owner, name, url string) (*github.H return CreateHook(client, owner, name, url) } -// GetKey is a heper function that retrieves a public Key by -// title. To do this, it will retrieve a list of all keys -// and iterate through the list. -func GetKey(client *github.Client, owner, name, title string) (*github.Key, error) { - keys, _, err := client.Repositories.ListKeys(owner, name, nil) - if err != nil { - return nil, err - } - for _, key := range keys { - if *key.Title == title { - return &key, nil - } - } - return nil, nil -} - -// GetKeyTitle is a helper function that generates a title for the -// RSA public key based on the username and domain name. -func GetKeyTitle(rawurl string) (string, error) { - var uri, err = url.Parse(rawurl) - if err != nil { - return "", err - } - return fmt.Sprintf("drone@%s", uri.Host), nil -} - -// DeleteKey is a helper function that deletes a deploy key -// for the specified repository. -func DeleteKey(client *github.Client, owner, name, title string) error { - var k, err = GetKey(client, owner, name, title) - if err != nil { - return err - } - if k == nil { - return nil - } - _, err = client.Repositories.DeleteKey(owner, name, *k.ID) - return err -} - -// CreateKey is a helper function that creates a deploy key -// for the specified repository. -func CreateKey(client *github.Client, owner, name, title, key string) (*github.Key, error) { - var k = new(github.Key) - k.Title = github.String(title) - k.Key = github.String(key) - created, _, err := client.Repositories.CreateKey(owner, name, k) - return created, err -} - -// CreateUpdateKey is a helper function that creates a deployment key -// for the specified repository if it does not already exist, otherwise -// it updates the existing key -func CreateUpdateKey(client *github.Client, owner, name, title, key string) (*github.Key, error) { - var k, _ = GetKey(client, owner, name, title) - if k != nil { - k.Title = github.String(title) - k.Key = github.String(key) - client.Repositories.DeleteKey(owner, name, *k.ID) - } - - return CreateKey(client, owner, name, title, key) -} - // GetFile is a heper function that retrieves a file from // GitHub and returns its contents in byte array format. func GetFile(client *github.Client, owner, name, path, ref string) ([]byte, error) { @@ -344,25 +209,3 @@ func GetPayload(req *http.Request) []byte { } return []byte(payload) } - -// UserBelongsToOrg returns true if the currently authenticated user is a -// member of any of the organizations provided. -func UserBelongsToOrg(client *github.Client, permittedOrgs []string) (bool, error) { - userOrgs, err := GetOrgs(client) - if err != nil { - return false, err - } - - userOrgSet := make(map[string]struct{}, len(userOrgs)) - for _, org := range userOrgs { - userOrgSet[*org.Login] = struct{}{} - } - - for _, org := range permittedOrgs { - if _, ok := userOrgSet[org]; ok { - return true, nil - } - } - - return false, nil -} diff --git a/remote/gitlab/gitlab.go b/remote/gitlab/gitlab.go index 60ffc5ed2..488dbd9d7 100644 --- a/remote/gitlab/gitlab.go +++ b/remote/gitlab/gitlab.go @@ -4,30 +4,63 @@ import ( "crypto/tls" "fmt" "io/ioutil" + "net" "net/http" "net/url" "strconv" "strings" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/oauth2" - "github.com/drone/drone/shared/token" "github.com/drone/drone/remote/gitlab/client" ) -const ( - DefaultScope = "api" -) +const DefaultScope = "api" + +// Opts defines configuration options. +type Opts struct { + URL string // Gogs server url. + Client string // Oauth2 client id. + Secret string // Oauth2 client secret. + Username string // Optional machine account username. + Password string // Optional machine account password. + PrivateMode bool // Gogs is running in private mode. + SkipVerify bool // Skip ssl verification. +} + +// New returns a Remote implementation that integrates with Gitlab, an open +// source Git service. See https://gitlab.com +func New(opts Opts) (remote.Remote, error) { + url, err := url.Parse(opts.URL) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(url.Host) + if err == nil { + url.Host = host + } + return &Gitlab{ + URL: opts.URL, + Client: opts.Client, + Secret: opts.Secret, + Machine: url.Host, + Username: opts.Username, + Password: opts.Password, + PrivateMode: opts.PrivateMode, + SkipVerify: opts.SkipVerify, + }, nil +} type Gitlab struct { URL string Client string Secret string - AllowedOrgs []string - CloneMode string - Open bool + Machine string + Username string + Password string PrivateMode bool SkipVerify bool HideArchives bool @@ -46,17 +79,17 @@ func Load(config string) *Gitlab { gitlab.URL = url_.String() gitlab.Client = params.Get("client_id") gitlab.Secret = params.Get("client_secret") - gitlab.AllowedOrgs = params["orgs"] + // gitlab.AllowedOrgs = params["orgs"] gitlab.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) gitlab.HideArchives, _ = strconv.ParseBool(params.Get("hide_archives")) - gitlab.Open, _ = strconv.ParseBool(params.Get("open")) + // gitlab.Open, _ = strconv.ParseBool(params.Get("open")) - switch params.Get("clone_mode") { - case "oauth": - gitlab.CloneMode = "oauth" - default: - gitlab.CloneMode = "token" - } + // switch params.Get("clone_mode") { + // case "oauth": + // gitlab.CloneMode = "oauth" + // default: + // gitlab.CloneMode = "token" + // } // this is a temp workaround gitlab.Search, _ = strconv.ParseBool(params.Get("search")) @@ -66,7 +99,7 @@ func Load(config string) *Gitlab { // Login authenticates the session and returns the // remote user details. -func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { var config = &oauth2.Config{ ClientId: g.Client, @@ -86,41 +119,41 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, var code = req.FormValue("code") if len(code) == 0 { http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) - return nil, false, nil + return nil, nil } var trans = &oauth2.Transport{Config: config, Transport: trans_} var token_, err = trans.Exchange(code) if err != nil { - return nil, false, fmt.Errorf("Error exchanging token. %s", err) + return nil, fmt.Errorf("Error exchanging token. %s", err) } client := NewClient(g.URL, token_.AccessToken, g.SkipVerify) login, err := client.CurrentUser() if err != nil { - return nil, false, err + return nil, err } - if len(g.AllowedOrgs) != 0 { - groups, err := client.AllGroups() - if err != nil { - return nil, false, fmt.Errorf("Could not check org membership. %s", err) - } - - var member bool - for _, group := range groups { - for _, allowedOrg := range g.AllowedOrgs { - if group.Path == allowedOrg { - member = true - break - } - } - } - - if !member { - return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs) - } - } + // if len(g.AllowedOrgs) != 0 { + // groups, err := client.AllGroups() + // if err != nil { + // return nil, fmt.Errorf("Could not check org membership. %s", err) + // } + // + // var member bool + // for _, group := range groups { + // for _, allowedOrg := range g.AllowedOrgs { + // if group.Path == allowedOrg { + // member = true + // break + // } + // } + // } + // + // if !member { + // return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs) + // } + // } user := &model.User{} user.Login = login.Username @@ -134,7 +167,7 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, user.Avatar = g.URL + "/" + login.AvatarUrl } - return user, g.Open, nil + return user, nil } func (g *Gitlab) Auth(token, secret string) (string, error) { @@ -146,6 +179,21 @@ func (g *Gitlab) Auth(token, secret string) (string, error) { return login.Username, nil } +func (g *Gitlab) Teams(u *model.User) ([]*model.Team, error) { + client := NewClient(g.URL, u.Token, g.SkipVerify) + groups, err := client.AllGroups() + if err != nil { + return nil, err + } + var teams []*model.Team + for _, group := range groups { + teams = append(teams, &model.Team{ + Login: group.Name, + }) + } + return teams, nil +} + // Repo fetches the named repository from the remote system. func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) { client := NewClient(g.URL, u.Token, g.SkipVerify) @@ -284,29 +332,47 @@ func (g *Gitlab) Status(u *model.User, repo *model.Repo, b *model.Build, link st // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. -func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - url_, err := url.Parse(g.URL) - if err != nil { - return nil, err - } - netrc := &model.Netrc{} - netrc.Machine = url_.Host +// func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { +// url_, err := url.Parse(g.URL) +// if err != nil { +// return nil, err +// } +// netrc := &model.Netrc{} +// netrc.Machine = url_.Host +// +// switch g.CloneMode { +// case "oauth": +// netrc.Login = "oauth2" +// netrc.Password = u.Token +// case "token": +// t := token.New(token.HookToken, r.FullName) +// netrc.Login = "drone-ci-token" +// netrc.Password, err = t.Sign(r.Hash) +// } +// return netrc, err +// } - switch g.CloneMode { - case "oauth": - netrc.Login = "oauth2" - netrc.Password = u.Token - case "token": - t := token.New(token.HookToken, r.FullName) - netrc.Login = "drone-ci-token" - netrc.Password, err = t.Sign(r.Hash) +// Netrc returns a netrc file capable of authenticating Gitlab requests and +// cloning Gitlab repositories. The netrc will use the global machine account +// when configured. +func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + if g.Password != "" { + return &model.Netrc{ + Login: g.Username, + Password: g.Password, + Machine: g.Machine, + }, nil } - return netrc, err + return &model.Netrc{ + Login: "oauth2", + Password: u.Token, + Machine: g.Machine, + }, nil } // Activate activates a repository by adding a Post-commit hook and // a Public Deploy key, if applicable. -func (g *Gitlab) Activate(user *model.User, repo *model.Repo, k *model.Key, link string) error { +func (g *Gitlab) Activate(user *model.User, repo *model.Repo, link string) error { var client = NewClient(g.URL, user.Token, g.SkipVerify) id, err := GetProjectId(g, client, repo.Owner, repo.Name) if err != nil { diff --git a/remote/gitlab/gitlab_test.go b/remote/gitlab/gitlab_test.go index 944afbb70..6e4f7003f 100644 --- a/remote/gitlab/gitlab_test.go +++ b/remote/gitlab/gitlab_test.go @@ -94,13 +94,13 @@ func Test_Gitlab(t *testing.T) { // Test activate method g.Describe("Activate", func() { g.It("Should be success", func() { - err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test?access_token=token") + err := gitlab.Activate(&user, &repo, "http://example.com/api/hook/test/test?access_token=token") g.Assert(err == nil).IsTrue() }) g.It("Should be failed, when token not given", func() { - err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test") + err := gitlab.Activate(&user, &repo, "http://example.com/api/hook/test/test") g.Assert(err != nil).IsTrue() }) diff --git a/remote/gogs/fixtures/handler.go b/remote/gogs/fixtures/handler.go new file mode 100644 index 000000000..26300644c --- /dev/null +++ b/remote/gogs/fixtures/handler.go @@ -0,0 +1,109 @@ +package fixtures + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Handler returns an http.Handler that is capable of handling a variety of mock +// Bitbucket requests and returning mock responses. +func Handler() http.Handler { + gin.SetMode(gin.TestMode) + + e := gin.New() + e.GET("/api/v1/repos/:owner/:name", getRepo) + e.GET("/api/v1/repos/:owner/:name/raw/:commit/:file", getRepoFile) + e.POST("/api/v1/repos/:owner/:name/hooks", createRepoHook) + e.GET("/api/v1/user/repos", getUserRepos) + + return e +} + +func getRepo(c *gin.Context) { + switch c.Param("name") { + case "repo_not_found": + c.String(404, "") + default: + c.String(200, repoPayload) + } +} + +func getRepoFile(c *gin.Context) { + switch c.Param("file") { + case "file_not_found": + c.String(404, "") + default: + c.String(200, repoFilePayload) + } +} + +func createRepoHook(c *gin.Context) { + in := struct { + Type string `json:"type"` + Conf struct { + Type string `json:"content_type"` + URL string `json:"url"` + } `json:"config"` + }{} + c.BindJSON(&in) + if in.Type != "gogs" || + in.Conf.Type != "json" || + in.Conf.URL != "http://localhost" { + c.String(500, "") + return + } + + c.String(200, "{}") +} + +func getUserRepos(c *gin.Context) { + switch c.Request.Header.Get("Authorization") { + case "token repos_not_found": + c.String(404, "") + default: + c.String(200, userRepoPayload) + } +} + +const repoPayload = ` +{ + "owner": { + "username": "test_name", + "email": "octocat@github.com", + "avatar_url": "https:\/\/secure.gravatar.com\/avatar\/8c58a0be77ee441bb8f8595b7f1b4e87" + }, + "full_name": "test_name\/repo_name", + "private": true, + "html_url": "http:\/\/localhost\/test_name\/repo_name", + "clone_url": "http:\/\/localhost\/test_name\/repo_name.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } +} +` + +const repoFilePayload = `{ platform: linux/amd64 }` + +const userRepoPayload = ` +[ + { + "owner": { + "username": "test_name", + "email": "octocat@github.com", + "avatar_url": "https:\/\/secure.gravatar.com\/avatar\/8c58a0be77ee441bb8f8595b7f1b4e87" + }, + "full_name": "test_name\/repo_name", + "private": true, + "html_url": "http:\/\/localhost\/test_name\/repo_name", + "clone_url": "http:\/\/localhost\/test_name\/repo_name.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } + } +] +` diff --git a/remote/gogs/testdata/hook_push.go b/remote/gogs/fixtures/hooks.go similarity index 97% rename from remote/gogs/testdata/hook_push.go rename to remote/gogs/fixtures/hooks.go index 868f1db6f..f73ced4be 100644 --- a/remote/gogs/testdata/hook_push.go +++ b/remote/gogs/fixtures/hooks.go @@ -1,6 +1,6 @@ -package testdata +package fixtures -var PushHook = ` +var HookPush = ` { "ref": "refs/heads/master", "before": "4b2626259b5a97b6b4eab5e6cca66adb986b672b", diff --git a/remote/gogs/gogs.go b/remote/gogs/gogs.go index d30a63236..b4306a827 100644 --- a/remote/gogs/gogs.go +++ b/remote/gogs/gogs.go @@ -6,189 +6,187 @@ import ( "net" "net/http" "net/url" - "strconv" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/gogits/go-gogs-client" - - log "github.com/Sirupsen/logrus" ) -type Gogs struct { +// Opts defines configuration options. +type Opts struct { + URL string // Gogs server url. + Username string // Optional machine account username. + Password string // Optional machine account password. + PrivateMode bool // Gogs is running in private mode. + SkipVerify bool // Skip ssl verification. +} + +type client struct { URL string - Open bool + Machine string + Username string + Password string PrivateMode bool SkipVerify bool } -func Load(config string) *Gogs { - // parse the remote DSN configuration string - url_, err := url.Parse(config) +// New returns a Remote implementation that integrates with Gogs, an open +// source Git service written in Go. See https://gogs.io/ +func New(opts Opts) (remote.Remote, error) { + url, err := url.Parse(opts.URL) if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) + return nil, err } - params := url_.Query() - url_.RawQuery = "" - - // create the Githbub remote using parameters from - // the parsed DSN configuration string. - gogs := Gogs{} - gogs.URL = url_.String() - gogs.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) - gogs.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) - gogs.Open, _ = strconv.ParseBool(params.Get("open")) - - return &gogs + host, _, err := net.SplitHostPort(url.Host) + if err == nil { + url.Host = host + } + return &client{ + URL: opts.URL, + Machine: url.Host, + Username: opts.Username, + Password: opts.Password, + PrivateMode: opts.PrivateMode, + SkipVerify: opts.SkipVerify, + }, nil } -// Login authenticates the session and returns the -// remote user details. -func (g *Gogs) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// Login authenticates an account with Gogs using basic authenticaiton. The +// Gogs account details are returned when the user is successfully authenticated. +func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { var ( username = req.FormValue("username") password = req.FormValue("password") ) - // if the username or password doesn't exist we re-direct - // the user to the login screen. + // if the username or password is empty we re-direct to the login screen. if len(username) == 0 || len(password) == 0 { http.Redirect(res, req, "/login/form", http.StatusSeeOther) - return nil, false, nil + return nil, nil } - client := NewGogsClient(g.URL, "", g.SkipVerify) + client := c.newClient() // try to fetch drone token if it exists var accessToken string tokens, err := client.ListAccessTokens(username, password) - if err != nil { - return nil, false, err - } - for _, token := range tokens { - if token.Name == "drone" { - accessToken = token.Sha1 - break + if err == nil { + for _, token := range tokens { + if token.Name == "drone" { + accessToken = token.Sha1 + break + } } } // if drone token not found, create it if accessToken == "" { - token, err := client.CreateAccessToken(username, password, gogs.CreateAccessTokenOption{Name: "drone"}) - if err != nil { - return nil, false, err + token, terr := client.CreateAccessToken( + username, + password, + gogs.CreateAccessTokenOption{Name: "drone"}, + ) + if terr != nil { + return nil, terr } accessToken = token.Sha1 } - client = NewGogsClient(g.URL, accessToken, g.SkipVerify) - userInfo, err := client.GetUserInfo(username) - if err != nil { - return nil, false, err - } - - user := model.User{} - user.Token = accessToken - user.Login = userInfo.UserName - user.Email = userInfo.Email - user.Avatar = expandAvatar(g.URL, userInfo.AvatarUrl) - return &user, g.Open, nil -} - -// Auth authenticates the session and returns the remote user -// login for the given token and secret -func (g *Gogs) Auth(token, secret string) (string, error) { - return "", fmt.Errorf("Method not supported") -} - -// Repo fetches the named repository from the remote system. -func (g *Gogs) Repo(u *model.User, owner, name string) (*model.Repo, error) { - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) - repos_, err := client.ListMyRepos() + client = c.newClientToken(accessToken) + account, err := client.GetUserInfo(username) if err != nil { return nil, err } - fullName := owner + "/" + name - for _, repo := range repos_ { - if repo.FullName == fullName { - return toRepo(repo), nil - } - } - - return nil, fmt.Errorf("Not Found") + return &model.User{ + Token: accessToken, + Login: account.UserName, + Email: account.Email, + Avatar: expandAvatar(c.URL, account.AvatarUrl), + }, nil } -// Repos fetches a list of repos from the remote system. -func (g *Gogs) Repos(u *model.User) ([]*model.RepoLite, error) { +// Auth is not supported by the Gogs driver. +func (c *client) Auth(token, secret string) (string, error) { + return "", fmt.Errorf("Not Implemented") +} + +// Teams is not supported by the Gogs driver. +func (c *client) Teams(u *model.User) ([]*model.Team, error) { + var empty []*model.Team + return empty, nil +} + +// Repo returns the named Gogs repository. +func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) { + client := c.newClientToken(u.Token) + repo, err := client.GetRepo(owner, name) + if err != nil { + return nil, err + } + return toRepo(repo), nil +} + +// Repos returns a list of all repositories for the Gogs account, including +// organization repositories. +func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) { repos := []*model.RepoLite{} - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) - repos_, err := client.ListMyRepos() + client := c.newClientToken(u.Token) + all, err := client.ListMyRepos() if err != nil { return repos, err } - for _, repo := range repos_ { + for _, repo := range all { repos = append(repos, toRepoLite(repo)) } - return repos, err } -// Perm fetches the named repository permissions from -// the remote system for the specified user. -func (g *Gogs) Perm(u *model.User, owner, name string) (*model.Perm, error) { - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) - repos_, err := client.ListMyRepos() +// Perm returns the user permissions for the named Gogs repository. +func (c *client) Perm(u *model.User, owner, name string) (*model.Perm, error) { + client := c.newClientToken(u.Token) + repo, err := client.GetRepo(owner, name) if err != nil { return nil, err } - - fullName := owner + "/" + name - for _, repo := range repos_ { - if repo.FullName == fullName { - return toPerm(repo.Permissions), nil - } - } - - return nil, fmt.Errorf("Not Found") - + return toPerm(repo.Permissions), nil } -// File fetches a file from the remote repository and returns in string format. -func (g *Gogs) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) +// File fetches the file from the Gogs repository and returns its contents. +func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { + client := c.newClientToken(u.Token) cfg, err := client.GetFile(r.Owner, r.Name, b.Commit, f) return cfg, err } -// Status sends the commit status to the remote system. -// An example would be the GitHub pull request status. -func (g *Gogs) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - return fmt.Errorf("Not Implemented") +// Status is not supported by the Gogs driver. +func (c *client) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + return nil } -// Netrc returns a .netrc file that can be used to clone -// private repositories from a remote system. -func (g *Gogs) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - url_, err := url.Parse(g.URL) - if err != nil { - return nil, err - } - host, _, err := net.SplitHostPort(url_.Host) - if err == nil { - url_.Host = host +// Netrc returns a netrc file capable of authenticating Gogs requests and +// cloning Gogs repositories. The netrc will use the global machine account +// when configured. +func (c *client) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + if c.Password != "" { + return &model.Netrc{ + Login: c.Username, + Password: c.Password, + Machine: c.Machine, + }, nil } return &model.Netrc{ Login: u.Token, Password: "x-oauth-basic", - Machine: url_.Host, + Machine: c.Machine, }, nil } -// Activate activates a repository by creating the post-commit hook and -// adding the SSH deploy key, if applicable. -func (g *Gogs) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { +// Activate activates the repository by registering post-commit hooks with +// the Gogs repository. +func (c *client) Activate(u *model.User, r *model.Repo, link string) error { config := map[string]string{ "url": link, "secret": r.Hash, @@ -200,20 +198,19 @@ func (g *Gogs) Activate(u *model.User, r *model.Repo, k *model.Key, link string) Active: true, } - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) + client := c.newClientToken(u.Token) _, err := client.CreateRepoHook(r.Owner, r.Name, hook) return err } -// Deactivate removes a repository by removing all the post-commit hooks -// which are equal to link and removing the SSH deploy key. -func (g *Gogs) Deactivate(u *model.User, r *model.Repo, link string) error { - return fmt.Errorf("Not Implemented") +// Deactivate is not supported by the Gogs driver. +func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error { + return nil } -// Hook parses the post-commit hook from the Request body -// and returns the required data in a standard format. -func (g *Gogs) Hook(r *http.Request) (*model.Repo, *model.Build, error) { +// Hook parses the incoming Gogs hook and returns the Repository and Build +// details. If the hook is unsupported nil values are returned. +func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { var ( err error repo *model.Repo @@ -222,7 +219,7 @@ func (g *Gogs) Hook(r *http.Request) (*model.Repo, *model.Build, error) { switch r.Header.Get("X-Gogs-Event") { case "push": - var push *PushHook + var push *pushHook push, err = parsePush(r.Body) if err == nil { repo = repoFromPush(push) @@ -232,20 +229,20 @@ func (g *Gogs) Hook(r *http.Request) (*model.Repo, *model.Build, error) { return repo, build, err } -// NewClient initializes and returns a API client. -func NewGogsClient(url, token string, skipVerify bool) *gogs.Client { - sslClient := &http.Client{} - c := gogs.NewClient(url, token) +// helper function to return the Gogs client +func (c *client) newClient() *gogs.Client { + return c.newClientToken("") +} - if skipVerify { - sslClient.Transport = &http.Transport{ +// helper function to return the Gogs client +func (c *client) newClientToken(token string) *gogs.Client { + client := gogs.NewClient(c.URL, token) + if c.SkipVerify { + httpClient := &http.Client{} + httpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - c.SetHTTPClient(sslClient) + client.SetHTTPClient(httpClient) } - return c -} - -func (g *Gogs) String() string { - return "gogs" + return client } diff --git a/remote/gogs/gogs_test.go b/remote/gogs/gogs_test.go new file mode 100644 index 000000000..792935956 --- /dev/null +++ b/remote/gogs/gogs_test.go @@ -0,0 +1,183 @@ +package gogs + +import ( + "net/http/httptest" + "testing" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/gogs/fixtures" + + "github.com/franela/goblin" + "github.com/gin-gonic/gin" +) + +func Test_gogs(t *testing.T) { + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(fixtures.Handler()) + c, _ := New(Opts{ + URL: s.URL, + SkipVerify: true, + }) + + g := goblin.Goblin(t) + g.Describe("Gogs", func() { + + g.After(func() { + s.Close() + }) + + g.Describe("Creating a remote", func() { + g.It("Should return client with specified options", func() { + remote, _ := New(Opts{ + URL: "http://localhost:8080", + Username: "someuser", + Password: "password", + SkipVerify: true, + PrivateMode: true, + }) + g.Assert(remote.(*client).URL).Equal("http://localhost:8080") + g.Assert(remote.(*client).Machine).Equal("localhost") + g.Assert(remote.(*client).Username).Equal("someuser") + g.Assert(remote.(*client).Password).Equal("password") + g.Assert(remote.(*client).SkipVerify).Equal(true) + g.Assert(remote.(*client).PrivateMode).Equal(true) + }) + g.It("Should handle malformed url", func() { + _, err := New(Opts{URL: "%gh&%ij"}) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Generating a netrc file", func() { + g.It("Should return a netrc with the user token", func() { + remote, _ := New(Opts{ + URL: "http://gogs.com", + }) + netrc, _ := remote.Netrc(fakeUser, nil) + g.Assert(netrc.Machine).Equal("gogs.com") + g.Assert(netrc.Login).Equal(fakeUser.Token) + g.Assert(netrc.Password).Equal("x-oauth-basic") + }) + g.It("Should return a netrc with the machine account", func() { + remote, _ := New(Opts{ + URL: "http://gogs.com", + Username: "someuser", + Password: "password", + }) + netrc, _ := remote.Netrc(nil, nil) + g.Assert(netrc.Machine).Equal("gogs.com") + g.Assert(netrc.Login).Equal("someuser") + g.Assert(netrc.Password).Equal("password") + }) + }) + + g.Describe("Requesting a repository", func() { + g.It("Should return the repository details", func() { + repo, err := c.Repo(fakeUser, fakeRepo.Owner, fakeRepo.Name) + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal(fakeRepo.Owner) + g.Assert(repo.Name).Equal(fakeRepo.Name) + g.Assert(repo.FullName).Equal(fakeRepo.Owner + "/" + fakeRepo.Name) + g.Assert(repo.IsPrivate).IsTrue() + g.Assert(repo.Clone).Equal("http://localhost/test_name/repo_name.git") + g.Assert(repo.Link).Equal("http://localhost/test_name/repo_name") + }) + g.It("Should handle a not found error", func() { + _, err := c.Repo(fakeUser, fakeRepoNotFound.Owner, fakeRepoNotFound.Name) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Requesting repository permissions", func() { + g.It("Should return the permission details", func() { + perm, err := c.Perm(fakeUser, fakeRepo.Owner, fakeRepo.Name) + g.Assert(err == nil).IsTrue() + g.Assert(perm.Admin).IsTrue() + g.Assert(perm.Push).IsTrue() + g.Assert(perm.Pull).IsTrue() + }) + g.It("Should handle a not found error", func() { + _, err := c.Perm(fakeUser, fakeRepoNotFound.Owner, fakeRepoNotFound.Name) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Requesting a repository list", func() { + g.It("Should return the repository list", func() { + repos, err := c.Repos(fakeUser) + g.Assert(err == nil).IsTrue() + g.Assert(repos[0].Owner).Equal(fakeRepo.Owner) + g.Assert(repos[0].Name).Equal(fakeRepo.Name) + g.Assert(repos[0].FullName).Equal(fakeRepo.Owner + "/" + fakeRepo.Name) + }) + g.It("Should handle a not found error", func() { + _, err := c.Repos(fakeUserNoRepos) + g.Assert(err != nil).IsTrue() + }) + }) + + g.It("Should register repositroy hooks", func() { + err := c.Activate(fakeUser, fakeRepo, "http://localhost") + g.Assert(err == nil).IsTrue() + }) + + g.It("Should return a repository file", func() { + raw, err := c.File(fakeUser, fakeRepo, fakeBuild, ".drone.yml") + g.Assert(err == nil).IsTrue() + g.Assert(string(raw)).Equal("{ platform: linux/amd64 }") + }) + + g.Describe("Given an authentication request", func() { + g.It("Should redirect to login form") + g.It("Should create an access token") + g.It("Should handle an access token error") + g.It("Should return the authenticated user") + }) + + g.Describe("Given a repository hook", func() { + g.It("Should skip non-push events") + g.It("Should return push details") + g.It("Should handle a parsing error") + }) + + g.It("Should return no-op for usupporeted features", func() { + _, err1 := c.Auth("octocat", "4vyW6b49Z") + _, err2 := c.Teams(nil) + err3 := c.Status(nil, nil, nil, "") + err4 := c.Deactivate(nil, nil, "") + g.Assert(err1 != nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(err4 == nil).IsTrue() + }) + }) +} + +var ( + fakeUser = &model.User{ + Login: "someuser", + Token: "cfcd2084", + } + + fakeUserNoRepos = &model.User{ + Login: "someuser", + Token: "repos_not_found", + } + + fakeRepo = &model.Repo{ + Owner: "test_name", + Name: "repo_name", + FullName: "test_name/repo_name", + } + + fakeRepoNotFound = &model.Repo{ + Owner: "test_name", + Name: "repo_not_found", + FullName: "test_name/repo_not_found", + } + + fakeBuild = &model.Build{ + Commit: "9ecad50", + } +) diff --git a/remote/gogs/helper.go b/remote/gogs/helper.go index bc698c04d..c6941faa3 100644 --- a/remote/gogs/helper.go +++ b/remote/gogs/helper.go @@ -12,8 +12,7 @@ import ( "github.com/gogits/go-gogs-client" ) -// helper function that converts a Gogs repository -// to a Drone repository. +// helper function that converts a Gogs repository to a Drone repository. func toRepoLite(from *gogs.Repository) *model.RepoLite { name := strings.Split(from.FullName, "/")[1] avatar := expandAvatar( @@ -28,8 +27,7 @@ func toRepoLite(from *gogs.Repository) *model.RepoLite { } } -// helper function that converts a Gogs repository -// to a Drone repository. +// helper function that converts a Gogs repository to a Drone repository. func toRepo(from *gogs.Repository) *model.Repo { name := strings.Split(from.FullName, "/")[1] avatar := expandAvatar( @@ -49,8 +47,7 @@ func toRepo(from *gogs.Repository) *model.Repo { } } -// helper function that converts a Gogs permission -// to a Drone permission. +// helper function that converts a Gogs permission to a Drone permission. func toPerm(from gogs.Permission) *model.Perm { return &model.Perm{ Pull: from.Pull, @@ -59,11 +56,10 @@ func toPerm(from gogs.Permission) *model.Perm { } } -// helper function that extracts the Build data -// from a Gogs push hook -func buildFromPush(hook *PushHook) *model.Build { +// helper function that extracts the Build data from a Gogs push hook +func buildFromPush(hook *pushHook) *model.Build { avatar := expandAvatar( - hook.Repo.Url, + hook.Repo.URL, fixMalformedAvatar(hook.Sender.Avatar), ) return &model.Build{ @@ -79,9 +75,8 @@ func buildFromPush(hook *PushHook) *model.Build { } } -// helper function that extracts the Repository data -// from a Gogs push hook -func repoFromPush(hook *PushHook) *model.Repo { +// helper function that extracts the Repository data from a Gogs push hook +func repoFromPush(hook *pushHook) *model.Repo { fullName := fmt.Sprintf( "%s/%s", hook.Repo.Owner.Username, @@ -91,20 +86,19 @@ func repoFromPush(hook *PushHook) *model.Repo { Name: hook.Repo.Name, Owner: hook.Repo.Owner.Username, FullName: fullName, - Link: hook.Repo.Url, + Link: hook.Repo.URL, } } -// helper function that parses a push hook from -// a read closer. -func parsePush(r io.Reader) (*PushHook, error) { - push := new(PushHook) +// helper function that parses a push hook from a read closer. +func parsePush(r io.Reader) (*pushHook, error) { + push := new(pushHook) err := json.NewDecoder(r).Decode(push) return push, err } -// fixMalformedAvatar is a helper function that fixes -// an avatar url if malformed (known bug with gogs) +// fixMalformedAvatar is a helper function that fixes an avatar url if malformed +// (currently a known bug with gogs) func fixMalformedAvatar(url string) string { index := strings.Index(url, "///") if index != -1 { @@ -117,16 +111,16 @@ func fixMalformedAvatar(url string) string { return url } -// expandAvatar is a helper function that converts -// a relative avatar URL to the abosolute url. +// expandAvatar is a helper function that converts a relative avatar URL to the +// abosolute url. func expandAvatar(repo, rawurl string) string { if !strings.HasPrefix(rawurl, "/avatars/") { return rawurl } - url_, err := url.Parse(repo) + url, err := url.Parse(repo) if err != nil { return rawurl } - url_.Path = rawurl - return url_.String() + url.Path = rawurl + return url.String() } diff --git a/remote/gogs/helper_test.go b/remote/gogs/helper_test.go index 444596e54..9ec3d635b 100644 --- a/remote/gogs/helper_test.go +++ b/remote/gogs/helper_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/drone/drone/model" - "github.com/drone/drone/remote/gogs/testdata" + "github.com/drone/drone/remote/gogs/fixtures" "github.com/franela/goblin" "github.com/gogits/go-gogs-client" @@ -17,7 +17,7 @@ func Test_parse(t *testing.T) { g.Describe("Gogs", func() { g.It("Should parse push hook payload", func() { - buf := bytes.NewBufferString(testdata.PushHook) + buf := bytes.NewBufferString(fixtures.HookPush) hook, err := parsePush(buf) g.Assert(err == nil).IsTrue() g.Assert(hook.Ref).Equal("refs/heads/master") @@ -25,7 +25,7 @@ func Test_parse(t *testing.T) { g.Assert(hook.Before).Equal("4b2626259b5a97b6b4eab5e6cca66adb986b672b") g.Assert(hook.Compare).Equal("http://gogs.golang.org/gordon/hello-world/compare/4b2626259b5a97b6b4eab5e6cca66adb986b672b...ef98532add3b2feb7a137426bba1248724367df5") g.Assert(hook.Repo.Name).Equal("hello-world") - g.Assert(hook.Repo.Url).Equal("http://gogs.golang.org/gordon/hello-world") + g.Assert(hook.Repo.URL).Equal("http://gogs.golang.org/gordon/hello-world") g.Assert(hook.Repo.Owner.Name).Equal("gordon") g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org") g.Assert(hook.Repo.Owner.Username).Equal("gordon") @@ -38,7 +38,7 @@ func Test_parse(t *testing.T) { }) g.It("Should return a Build struct from a push hook", func() { - buf := bytes.NewBufferString(testdata.PushHook) + buf := bytes.NewBufferString(fixtures.HookPush) hook, _ := parsePush(buf) build := buildFromPush(hook) g.Assert(build.Event).Equal(model.EventPush) @@ -53,13 +53,13 @@ func Test_parse(t *testing.T) { }) g.It("Should return a Repo struct from a push hook", func() { - buf := bytes.NewBufferString(testdata.PushHook) + buf := bytes.NewBufferString(fixtures.HookPush) hook, _ := parsePush(buf) repo := repoFromPush(hook) g.Assert(repo.Name).Equal(hook.Repo.Name) g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username) g.Assert(repo.FullName).Equal("gordon/hello-world") - g.Assert(repo.Link).Equal(hook.Repo.Url) + g.Assert(repo.Link).Equal(hook.Repo.URL) }) g.It("Should return a Perm struct from a Gogs Perm", func() { diff --git a/remote/gogs/types.go b/remote/gogs/types.go index ac1947357..2b9f38ed1 100644 --- a/remote/gogs/types.go +++ b/remote/gogs/types.go @@ -1,6 +1,6 @@ package gogs -type PushHook struct { +type pushHook struct { Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` @@ -15,7 +15,7 @@ type PushHook struct { Repo struct { ID int64 `json:"id"` Name string `json:"name"` - Url string `json:"url"` + URL string `json:"url"` Private bool `json:"private"` Owner struct { Name string `json:"name"` @@ -27,7 +27,7 @@ type PushHook struct { Commits []struct { ID string `json:"id"` Message string `json:"message"` - Url string `json:"url"` + URL string `json:"url"` } `json:"commits"` Sender struct { diff --git a/remote/mock/remote.go b/remote/mock/remote.go index 28928b9fa..917a85c50 100644 --- a/remote/mock/remote.go +++ b/remote/mock/remote.go @@ -1,42 +1,32 @@ package mock -import "github.com/stretchr/testify/mock" +import ( + "net/http" -import "net/http" -import "github.com/drone/drone/model" + "github.com/drone/drone/model" + "github.com/stretchr/testify/mock" +) +// This is an autogenerated mock type for the Remote type type Remote struct { mock.Mock } -func (_m *Remote) Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error) { - ret := _m.Called(w, r) +// Activate provides a mock function with given fields: u, r, link +func (_m *Remote) Activate(u *model.User, r *model.Repo, link string) error { + ret := _m.Called(u, r, link) - var r0 *model.User - if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { - r0 = rf(w, r) + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { + r0 = rf(u, r, link) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) - } + r0 = ret.Error(0) } - var r1 bool - if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) bool); ok { - r1 = rf(w, r) - } else { - r1 = ret.Get(1).(bool) - } - - var r2 error - if rf, ok := ret.Get(2).(func(http.ResponseWriter, *http.Request) error); ok { - r2 = rf(w, r) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 + return r0 } + +// Auth provides a mock function with given fields: token, secret func (_m *Remote) Auth(token string, secret string) (string, error) { ret := _m.Called(token, secret) @@ -56,69 +46,22 @@ func (_m *Remote) Auth(token string, secret string) (string, error) { return r0, r1 } -func (_m *Remote) Repo(u *model.User, owner string, repo string) (*model.Repo, error) { - ret := _m.Called(u, owner, repo) - var r0 *model.Repo - if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { - r0 = rf(u, owner, repo) +// Deactivate provides a mock function with given fields: u, r, link +func (_m *Remote) Deactivate(u *model.User, r *model.Repo, link string) error { + ret := _m.Called(u, r, link) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { + r0 = rf(u, r, link) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Repo) - } + r0 = ret.Error(0) } - var r1 error - if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { - r1 = rf(u, owner, repo) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } -func (_m *Remote) Repos(u *model.User) ([]*model.RepoLite, error) { - ret := _m.Called(u) - var r0 []*model.RepoLite - if rf, ok := ret.Get(0).(func(*model.User) []*model.RepoLite); ok { - r0 = rf(u) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.RepoLite) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.User) error); ok { - r1 = rf(u) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} -func (_m *Remote) Perm(u *model.User, owner string, repo string) (*model.Perm, error) { - ret := _m.Called(u, owner, repo) - - var r0 *model.Perm - if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Perm); ok { - r0 = rf(u, owner, repo) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Perm) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { - r1 = rf(u, owner, repo) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} +// File provides a mock function with given fields: u, r, b, f func (_m *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { ret := _m.Called(u, r, b, f) @@ -140,63 +83,8 @@ func (_m *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ( return r0, r1 } -func (_m *Remote) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - ret := _m.Called(u, r, b, link) - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Build, string) error); ok { - r0 = rf(u, r, b, link) - } else { - r0 = ret.Error(0) - } - - return r0 -} -func (_m *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - ret := _m.Called(u, r) - - var r0 *model.Netrc - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo) *model.Netrc); ok { - r0 = rf(u, r) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Netrc) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.User, *model.Repo) error); ok { - r1 = rf(u, r) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} -func (_m *Remote) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - ret := _m.Called(u, r, k, link) - - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Key, string) error); ok { - r0 = rf(u, r, k, link) - } else { - r0 = ret.Error(0) - } - - return r0 -} -func (_m *Remote) Deactivate(u *model.User, r *model.Repo, link string) error { - ret := _m.Called(u, r, link) - - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { - r0 = rf(u, r, link) - } else { - r0 = ret.Error(0) - } - - return r0 -} +// Hook provides a mock function with given fields: r func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { ret := _m.Called(r) @@ -227,3 +115,155 @@ func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { return r0, r1, r2 } + +// Login provides a mock function with given fields: w, r +func (_m *Remote) Login(w http.ResponseWriter, r *http.Request) (*model.User, error) { + ret := _m.Called(w, r) + + var r0 *model.User + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { + r0 = rf(w, r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) error); ok { + r1 = rf(w, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Netrc provides a mock function with given fields: u, r +func (_m *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + ret := _m.Called(u, r) + + var r0 *model.Netrc + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo) *model.Netrc); ok { + r0 = rf(u, r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Netrc) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo) error); ok { + r1 = rf(u, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Perm provides a mock function with given fields: u, owner, repo +func (_m *Remote) Perm(u *model.User, owner string, repo string) (*model.Perm, error) { + ret := _m.Called(u, owner, repo) + + var r0 *model.Perm + if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Perm); ok { + r0 = rf(u, owner, repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Perm) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { + r1 = rf(u, owner, repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repo provides a mock function with given fields: u, owner, repo +func (_m *Remote) Repo(u *model.User, owner string, repo string) (*model.Repo, error) { + ret := _m.Called(u, owner, repo) + + var r0 *model.Repo + if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { + r0 = rf(u, owner, repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Repo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { + r1 = rf(u, owner, repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repos provides a mock function with given fields: u +func (_m *Remote) Repos(u *model.User) ([]*model.RepoLite, error) { + ret := _m.Called(u) + + var r0 []*model.RepoLite + if rf, ok := ret.Get(0).(func(*model.User) []*model.RepoLite); ok { + r0 = rf(u) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.RepoLite) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(u) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Status provides a mock function with given fields: u, r, b, link +func (_m *Remote) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + ret := _m.Called(u, r, b, link) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Build, string) error); ok { + r0 = rf(u, r, b, link) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Teams provides a mock function with given fields: u +func (_m *Remote) Teams(u *model.User) ([]*model.Team, error) { + ret := _m.Called(u) + + var r0 []*model.Team + if rf, ok := ret.Get(0).(func(*model.User) []*model.Team); ok { + r0 = rf(u) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Team) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(u) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/remote/remote.go b/remote/remote.go index 875988d20..cfcdafdf0 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -13,12 +13,15 @@ import ( type Remote interface { // Login authenticates the session and returns the // remote user details. - Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error) + 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) + // Teams fetches a list of team memberships from the remote system. + Teams(u *model.User) ([]*model.Team, error) + // Repo fetches the named repository from the remote system. Repo(u *model.User, owner, repo string) (*model.Repo, error) @@ -41,29 +44,28 @@ type Remote interface { // private repositories from a remote system. Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) - // Activate activates a repository by creating the post-commit hook and - // adding the SSH deploy key, if applicable. - Activate(u *model.User, r *model.Repo, k *model.Key, link string) error + // Activate activates a repository by creating the post-commit hook. + Activate(u *model.User, r *model.Repo, link string) error - // Deactivate removes a repository by removing all the post-commit hooks - // which are equal to link and removing the SSH deploy key. + // Deactivate deactivates a repository by removing all previously created + // post-commit hooks matching the given link. Deactivate(u *model.User, r *model.Repo, link string) error - // Hook parses the post-commit hook from the Request body - // and returns the required data in a standard format. + // Hook parses the post-commit hook from the Request body and returns the + // required data in a standard format. Hook(r *http.Request) (*model.Repo, *model.Build, error) } +// Refresher refreshes an oauth token and expiration for the given user. It +// returns true if the token was refreshed, false if the token was not refreshed, +// and error if it failed to refersh. type Refresher interface { - // Refresh refreshes an oauth token and expiration for the given - // user. It returns true if the token was refreshed, false if the - // token was not refreshed, and error if it failed to refersh. Refresh(*model.User) (bool, error) } // Login authenticates the session and returns the // remote user details. -func Login(c context.Context, w http.ResponseWriter, r *http.Request) (*model.User, bool, error) { +func Login(c context.Context, w http.ResponseWriter, r *http.Request) (*model.User, error) { return FromContext(c).Login(w, r) } @@ -73,6 +75,11 @@ func Auth(c context.Context, token, secret string) (string, error) { return FromContext(c).Auth(token, secret) } +// Teams fetches a list of team memberships from the remote system. +func Teams(c context.Context, u *model.User) ([]*model.Team, error) { + return FromContext(c).Teams(u) +} + // Repo fetches the named repository from the remote system. func Repo(c context.Context, u *model.User, owner, repo string) (*model.Repo, error) { return FromContext(c).Repo(u, owner, repo) @@ -108,8 +115,8 @@ func Netrc(c context.Context, u *model.User, r *model.Repo) (*model.Netrc, error // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. -func Activate(c context.Context, u *model.User, r *model.Repo, k *model.Key, link string) error { - return FromContext(c).Activate(u, r, k, link) +func Activate(c context.Context, u *model.User, r *model.Repo, link string) error { + return FromContext(c).Activate(u, r, link) } // Deactivate removes a repository by removing all the post-commit hooks diff --git a/router/middleware/agent.go b/router/middleware/agent.go index e227089a9..f3d884a72 100644 --- a/router/middleware/agent.go +++ b/router/middleware/agent.go @@ -1,45 +1,33 @@ package middleware import ( + "github.com/codegangsta/cli" "github.com/drone/drone/shared/token" "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" ) -var ( - secret = envflag.String("AGENT_SECRET", "", "") - noauth = envflag.Bool("AGENT_NO_AUTH", false, "") -) +const agentKey = "agent" -// Agent is a middleware function that initializes the authorization middleware +// Agents is a middleware function that initializes the authorization middleware // for agents to connect to the queue. -func AgentMust() gin.HandlerFunc { - - if *secret == "" { - logrus.Fatalf("please provide the agent secret to authenticate agent requests") +func Agents(cli *cli.Context) gin.HandlerFunc { + secret := cli.String("agent-secret") + if secret == "" { + logrus.Fatalf("failed to generate token from DRONE_AGENT_SECRET") } - t := token.New(token.AgentToken, "") - s, err := t.Sign(*secret) + t := token.New(secret, "") + s, err := t.Sign(secret) if err != nil { - logrus.Fatalf("invalid agent secret. %s", err) + logrus.Fatalf("failed to generate token from DRONE_AGENT_SECRET. %s", err) } - logrus.Infof("using agent secret %s", *secret) + logrus.Infof("using agent secret %s", secret) logrus.Warnf("agents can connect with token %s", s) return func(c *gin.Context) { - parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { - return *secret, nil - }) - if err != nil { - c.AbortWithError(403, err) - } else if parsed.Kind != token.AgentToken { - c.AbortWithStatus(403) - } else { - c.Next() - } + c.Set(agentKey, secret) } } diff --git a/router/middleware/bus.go b/router/middleware/bus.go index b5f5c57d5..25665da1d 100644 --- a/router/middleware/bus.go +++ b/router/middleware/bus.go @@ -2,13 +2,16 @@ package middleware import ( "github.com/drone/drone/bus" + + "github.com/codegangsta/cli" "github.com/gin-gonic/gin" ) -func Bus() gin.HandlerFunc { - bus_ := bus.New() +// Bus is a middleware function that initializes the Event Bus and attaches to +// the context of every http.Request. +func Bus(cli *cli.Context) gin.HandlerFunc { + v := bus.New() return func(c *gin.Context) { - bus.ToContext(c, bus_) - c.Next() + bus.ToContext(c, v) } } diff --git a/router/middleware/cache.go b/router/middleware/cache.go index aa8d46b4d..6f6dec465 100644 --- a/router/middleware/cache.go +++ b/router/middleware/cache.go @@ -1,22 +1,24 @@ package middleware import ( - "time" - "github.com/drone/drone/cache" + "github.com/codegangsta/cli" "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" ) -var ttl = envflag.Duration("CACHE_TTL", time.Minute*15, "") - // Cache is a middleware function that initializes the Cache and attaches to // the context of every http.Request. -func Cache() gin.HandlerFunc { - cc := cache.NewTTL(*ttl) +func Cache(cli *cli.Context) gin.HandlerFunc { + v := setupCache(cli) return func(c *gin.Context) { - cache.ToContext(c, cc) - c.Next() + cache.ToContext(c, v) } } + +// helper function to create the cache from the CLI context. +func setupCache(c *cli.Context) cache.Cache { + return cache.NewTTL( + c.Duration("cache-ttl"), + ) +} diff --git a/router/middleware/config.go b/router/middleware/config.go new file mode 100644 index 000000000..e2f65dd10 --- /dev/null +++ b/router/middleware/config.go @@ -0,0 +1,40 @@ +package middleware + +import ( + "github.com/drone/drone/model" + + "github.com/codegangsta/cli" + "github.com/gin-gonic/gin" +) + +const configKey = "config" + +// Config is a middleware function that initializes the Configuration and +// attaches to the context of every http.Request. +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")), + } +} + +// helper function to convert a string slice to a map. +func sliceToMap(s []string) map[string]bool { + v := map[string]bool{} + for _, ss := range s { + v[ss] = true + } + return v +} diff --git a/router/middleware/engine.go b/router/middleware/engine.go deleted file mode 100644 index 01da07067..000000000 --- a/router/middleware/engine.go +++ /dev/null @@ -1,28 +0,0 @@ -package middleware - -import ( - "sync" - - "github.com/drone/drone/engine" - "github.com/drone/drone/store" - - "github.com/gin-gonic/gin" -) - -// Engine is a middleware function that initializes the Engine and attaches to -// the context of every http.Request. -func Engine() gin.HandlerFunc { - var once sync.Once - var engine_ engine.Engine - - return func(c *gin.Context) { - - once.Do(func() { - store_ := store.FromContext(c) - engine_ = engine.Load(store_) - }) - - engine.ToContext(c, engine_) - c.Next() - } -} diff --git a/router/middleware/queue.go b/router/middleware/queue.go index 692a5432e..d2791033e 100644 --- a/router/middleware/queue.go +++ b/router/middleware/queue.go @@ -2,13 +2,16 @@ package middleware import ( "github.com/drone/drone/queue" + + "github.com/codegangsta/cli" "github.com/gin-gonic/gin" ) -func Queue() gin.HandlerFunc { - queue_ := queue.New() +// Queue is a middleware function that initializes the Queue and attaches to +// the context of every http.Request. +func Queue(cli *cli.Context) gin.HandlerFunc { + v := queue.New() return func(c *gin.Context) { - queue.ToContext(c, queue_) - c.Next() + queue.ToContext(c, v) } } diff --git a/router/middleware/remote.go b/router/middleware/remote.go index 3ad33e94d..58881078f 100644 --- a/router/middleware/remote.go +++ b/router/middleware/remote.go @@ -1,48 +1,102 @@ package middleware import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" "github.com/drone/drone/remote" "github.com/drone/drone/remote/bitbucket" + "github.com/drone/drone/remote/bitbucketserver" "github.com/drone/drone/remote/github" "github.com/drone/drone/remote/gitlab" "github.com/drone/drone/remote/gogs" - - "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" - "github.com/drone/drone/remote/bitbucketserver" -) - -var ( - driver = envflag.String("REMOTE_DRIVER", "", "") - config = envflag.String("REMOTE_CONFIG", "", "") ) // Remote is a middleware function that initializes the Remote and attaches to // the context of every http.Request. -func Remote() gin.HandlerFunc { - - logrus.Infof("using remote driver %s", *driver) - logrus.Infof("using remote config %s", *config) - - var remote_ remote.Remote - switch *driver { - case "github": - remote_ = github.Load(*config) - case "bitbucket": - remote_ = bitbucket.Load(*config) - case "gogs": - remote_ = gogs.Load(*config) - case "gitlab": - remote_ = gitlab.Load(*config) - case "bitbucketserver": - remote_ = bitbucketserver.Load(*config) - default: - logrus.Fatalln("remote configuration not found") +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, remote_) - c.Next() + 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") + } +} + +// helper function to setup the Bitbucket remote from the CLI arguments. +func setupBitbucket(c *cli.Context) (remote.Remote, error) { + return bitbucket.New( + c.String("bitbucket-client"), + c.String("bitbucket-server"), + ), nil +} + +// helper function to setup the Gogs remote from the CLI arguments. +func setupGogs(c *cli.Context) (remote.Remote, error) { + return gogs.New(gogs.Opts{ + URL: c.String("gogs-server"), + Username: c.String("gogs-git-username"), + Password: c.String("gogs-git-password"), + PrivateMode: c.Bool("gogs-private-mode"), + SkipVerify: c.Bool("gogs-skip-verify"), + }) +} + +// helper function to setup the Stash remote from the CLI arguments. +func setupStash(c *cli.Context) (remote.Remote, error) { + return bitbucketserver.New(bitbucketserver.Opts{ + URL: c.String("stash-server"), + Username: c.String("stash-git-username"), + Password: c.String("stash-git-password"), + ConsumerKey: c.String("stash-consumer-key"), + ConsumerRSA: c.String("stash-consumer-rsa"), + SkipVerify: c.Bool("stash-skip-verify"), + }) +} + +// helper function to setup the Gitlab remote from the CLI arguments. +func setupGitlab(c *cli.Context) (remote.Remote, error) { + return gitlab.New(gitlab.Opts{ + URL: c.String("gitlab-server"), + Client: c.String("gitlab-client"), + Secret: c.String("gitlab-sercret"), + Username: c.String("gitlab-git-username"), + Password: c.String("gitlab-git-password"), + PrivateMode: c.Bool("gitlab-private-mode"), + SkipVerify: c.Bool("gitlab-skip-verify"), + }) +} + +// helper function to setup the GitHub remote from the CLI arguments. +func setupGithub(c *cli.Context) (remote.Remote, error) { + return github.New( + c.String("github-server"), + c.String("github-client"), + c.String("github-sercret"), + c.StringSlice("github-scope"), + c.Bool("github-private-mode"), + c.Bool("github-skip-verify"), + c.BoolT("github-merge-ref"), + ) +} diff --git a/router/middleware/session/agent.go b/router/middleware/session/agent.go new file mode 100644 index 000000000..8f8c722b8 --- /dev/null +++ b/router/middleware/session/agent.go @@ -0,0 +1,22 @@ +package session + +import ( + "github.com/drone/drone/shared/token" + "github.com/gin-gonic/gin" +) + +// AuthorizeAgent authorizes requsts from build agents to access the queue. +func AuthorizeAgent(c *gin.Context) { + secret := c.MustGet("agent").(string) + + parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + return secret, nil + }) + if err != nil { + c.AbortWithError(403, err) + } else if parsed.Kind != token.AgentToken { + c.AbortWithStatus(403) + } else { + c.Next() + } +} diff --git a/router/middleware/session/user.go b/router/middleware/session/user.go index 8453d208b..78f0a16bf 100644 --- a/router/middleware/session/user.go +++ b/router/middleware/session/user.go @@ -44,6 +44,10 @@ func SetUser() gin.HandlerFunc { return user.Hash, err }) if err == nil { + confv := c.MustGet("config") + if conf, ok := confv.(*model.Config); ok { + user.Admin = conf.IsAdmin(user) + } c.Set("user", user) // if this is a session token (ie not the API token) diff --git a/router/middleware/store.go b/router/middleware/store.go index 91439d433..33b9731bf 100644 --- a/router/middleware/store.go +++ b/router/middleware/store.go @@ -1,29 +1,27 @@ package middleware import ( + "github.com/codegangsta/cli" "github.com/drone/drone/store" "github.com/drone/drone/store/datastore" - "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" -) - -var ( - database = envflag.String("DATABASE_DRIVER", "sqlite3", "") - datasource = envflag.String("DATABASE_CONFIG", "drone.sqlite", "") ) // Store is a middleware function that initializes the Datastore and attaches to // the context of every http.Request. -func Store() gin.HandlerFunc { - db := datastore.New(*database, *datasource) - - logrus.Infof("using database driver %s", *database) - logrus.Infof("using database config %s", *datasource) - +func Store(cli *cli.Context) gin.HandlerFunc { + v := setupStore(cli) return func(c *gin.Context) { - store.ToContext(c, db) + 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"), + ) +} diff --git a/router/middleware/stream.go b/router/middleware/stream.go index 43bdabe05..d78a119c2 100644 --- a/router/middleware/stream.go +++ b/router/middleware/stream.go @@ -2,13 +2,16 @@ package middleware import ( "github.com/drone/drone/stream" + + "github.com/codegangsta/cli" "github.com/gin-gonic/gin" ) -func Stream() gin.HandlerFunc { - stream_ := stream.New() +// Stream is a middleware function that initializes the Stream and attaches to +// the context of every http.Request. +func Stream(cli *cli.Context) gin.HandlerFunc { + v := stream.New() return func(c *gin.Context) { - stream.ToContext(c, stream_) - c.Next() + stream.ToContext(c, v) } } diff --git a/router/middleware/version.go b/router/middleware/version.go index b4de05f96..20466d8ec 100644 --- a/router/middleware/version.go +++ b/router/middleware/version.go @@ -5,10 +5,8 @@ import ( "github.com/gin-gonic/gin" ) -// Version is a middleware function that appends the Drone -// version information to the HTTP response. This is intended -// for debugging and troubleshooting. +// Version is a middleware function that appends the Drone version information +// to the HTTP response. This is intended for debugging and troubleshooting. func Version(c *gin.Context) { c.Header("X-DRONE-VERSION", version.Version) - c.Next() } diff --git a/router/router.go b/router/router.go index 157e6f18e..3c4a7a44d 100644 --- a/router/router.go +++ b/router/router.go @@ -2,22 +2,20 @@ package router import ( "net/http" - "os" "strings" "github.com/gin-gonic/gin" - "github.com/drone/drone/api" - "github.com/drone/drone/router/middleware" "github.com/drone/drone/router/middleware/header" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/router/middleware/token" + "github.com/drone/drone/server" "github.com/drone/drone/static" "github.com/drone/drone/template" - "github.com/drone/drone/web" ) -func Load(middlewares ...gin.HandlerFunc) http.Handler { +func Load(middleware ...gin.HandlerFunc) http.Handler { + e := gin.New() e.Use(gin.Recovery()) @@ -27,22 +25,21 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { e.Use(header.NoCache) e.Use(header.Options) e.Use(header.Secure) - e.Use(middlewares...) + e.Use(middleware...) e.Use(session.SetUser()) e.Use(token.Refresh) - e.GET("/", web.ShowIndex) - e.GET("/repos", web.ShowAllRepos) - e.GET("/login", web.ShowLogin) - e.GET("/login/form", web.ShowLoginForm) - e.GET("/logout", web.GetLogout) + e.GET("/", server.ShowIndex) + e.GET("/repos", server.ShowAllRepos) + e.GET("/login", server.ShowLogin) + e.GET("/login/form", server.ShowLoginForm) + e.GET("/logout", server.GetLogout) + // TODO below will Go away with React UI settings := e.Group("/settings") { settings.Use(session.MustUser()) - settings.GET("/profile", web.ShowUser) - settings.GET("/people", session.MustAdmin(), web.ShowUsers) - settings.GET("/nodes", session.MustAdmin(), web.ShowNodes) + settings.GET("/profile", server.ShowUser) } repo := e.Group("/repos/:owner/:name") { @@ -50,50 +47,43 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { repo.Use(session.SetPerm()) repo.Use(session.MustPull) - repo.GET("", web.ShowRepo) - repo.GET("/builds/:number", web.ShowBuild) - repo.GET("/builds/:number/:job", web.ShowBuild) + repo.GET("", server.ShowRepo) + repo.GET("/builds/:number", server.ShowBuild) + repo.GET("/builds/:number/:job", server.ShowBuild) repo_settings := repo.Group("/settings") { - repo_settings.GET("", session.MustPush, web.ShowRepoConf) - repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) - repo_settings.GET("/badges", web.ShowRepoBadges) + repo_settings.GET("", session.MustPush, server.ShowRepoConf) + repo_settings.GET("/encrypt", session.MustPush, server.ShowRepoEncrypt) + repo_settings.GET("/badges", server.ShowRepoBadges) } } + // TODO above will Go away with React UI user := e.Group("/api/user") { user.Use(session.MustUser()) - user.GET("", api.GetSelf) - user.GET("/feed", api.GetFeed) - user.GET("/repos", api.GetRepos) - user.GET("/repos/remote", api.GetRemoteRepos) - user.POST("/token", api.PostToken) - user.DELETE("/token", api.DeleteToken) + 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) } users := e.Group("/api/users") { users.Use(session.MustAdmin()) - users.GET("", api.GetUsers) - users.POST("", api.PostUser) - users.GET("/:login", api.GetUser) - users.PATCH("/:login", api.PatchUser) - users.DELETE("/:login", api.DeleteUser) - } - - nodes := e.Group("/api/nodes") - { - nodes.Use(session.MustAdmin()) - nodes.GET("", api.GetNodes) - nodes.POST("", api.PostNode) - nodes.DELETE("/:node", api.DeleteNode) + users.GET("", server.GetUsers) + users.POST("", server.PostUser) + users.GET("/:login", server.GetUser) + users.PATCH("/:login", server.PatchUser) + users.DELETE("/:login", server.DeleteUser) } repos := e.Group("/api/repos/:owner/:name") { - repos.POST("", api.PostRepo) + repos.POST("", server.PostRepo) repo := repos.Group("") { @@ -101,37 +91,32 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { repo.Use(session.SetPerm()) repo.Use(session.MustPull) - repo.GET("", api.GetRepo) - repo.GET("/key", api.GetRepoKey) - repo.POST("/key", api.PostRepoKey) - repo.GET("/builds", api.GetBuilds) - repo.GET("/builds/:number", api.GetBuild) - repo.GET("/logs/:number/:job", api.GetBuildLogs) - repo.POST("/sign", session.MustPush, api.Sign) + repo.GET("", server.GetRepo) + repo.GET("/builds", server.GetBuilds) + repo.GET("/builds/:number", server.GetBuild) + repo.GET("/logs/:number/:job", server.GetBuildLogs) + repo.POST("/sign", session.MustPush, server.Sign) - repo.POST("/secrets", session.MustPush, api.PostSecret) - repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) - - // requires authenticated user - repo.POST("/encrypt", session.MustUser(), api.PostSecure) + repo.POST("/secrets", session.MustPush, server.PostSecret) + repo.DELETE("/secrets/:secret", session.MustPush, server.DeleteSecret) // requires push permissions - repo.PATCH("", session.MustPush, api.PatchRepo) - repo.DELETE("", session.MustPush, api.DeleteRepo) + repo.PATCH("", session.MustPush, server.PatchRepo) + repo.DELETE("", session.MustPush, server.DeleteRepo) - repo.POST("/builds/:number", session.MustPush, api.PostBuild) - repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) + repo.POST("/builds/:number", session.MustPush, server.PostBuild) + repo.DELETE("/builds/:number/:job", session.MustPush, server.DeleteBuild) } } badges := e.Group("/api/badges/:owner/:name") { - badges.GET("/status.svg", web.GetBadge) - badges.GET("/cc.xml", web.GetCC) + badges.GET("/status.svg", server.GetBadge) + badges.GET("/cc.xml", server.GetCC) } - e.POST("/hook", web.PostHook) - e.POST("/api/hook", web.PostHook) + e.POST("/hook", server.PostHook) + e.POST("/api/hook", server.PostHook) stream := e.Group("/api/stream") { @@ -139,57 +124,53 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { stream.Use(session.SetPerm()) stream.Use(session.MustPull) - if os.Getenv("CANARY") == "true" { - stream.GET("/:owner/:name", web.GetRepoEvents2) - stream.GET("/:owner/:name/:build/:number", web.GetStream2) - } else { - stream.GET("/:owner/:name", web.GetRepoEvents) - stream.GET("/:owner/:name/:build/:number", web.GetStream) - } - } - - bots := e.Group("/bots") - { - bots.Use(session.MustUser()) - bots.POST("/slack", web.Slack) - bots.POST("/slack/:command", web.Slack) + stream.GET("/:owner/:name", server.GetRepoEvents) + stream.GET("/:owner/:name/:build/:number", server.GetStream) } auth := e.Group("/authorize") { - auth.GET("", web.GetLogin) - auth.POST("", web.GetLogin) - auth.POST("/token", web.GetLoginToken) + auth.GET("", server.GetLogin) + auth.POST("", server.GetLogin) + auth.POST("/token", server.GetLoginToken) } queue := e.Group("/api/queue") { - if os.Getenv("CANARY") == "true" { - queue.Use(middleware.AgentMust()) - queue.POST("/pull", api.Pull) - queue.POST("/pull/:os/:arch", api.Pull) - queue.POST("/wait/:id", api.Wait) - queue.POST("/stream/:id", api.Stream) - queue.POST("/status/:id", api.Update) - } + queue.Use(session.AuthorizeAgent) + queue.POST("/pull", server.Pull) + queue.POST("/pull/:os/:arch", server.Pull) + queue.POST("/wait/:id", server.Wait) + queue.POST("/stream/:id", server.Stream) + queue.POST("/status/:id", server.Update) } - gitlab := e.Group("/gitlab/:owner/:name") - { - gitlab.Use(session.SetRepo()) - gitlab.GET("/commits/:sha", web.GetCommit) - gitlab.GET("/pulls/:number", web.GetPullRequest) + // DELETE THESE + // gitlab := e.Group("/gitlab/:owner/:name") + // { + // gitlab.Use(session.SetRepo()) + // gitlab.GET("/commits/:sha", GetCommit) + // gitlab.GET("/pulls/:number", GetPullRequest) + // + // redirects := gitlab.Group("/redirect") + // { + // redirects.GET("/commits/:sha", RedirectSha) + // redirects.GET("/pulls/:number", RedirectPullRequest) + // } + // } - redirects := gitlab.Group("/redirect") - { - redirects.GET("/commits/:sha", web.RedirectSha) - redirects.GET("/pulls/:number", web.RedirectPullRequest) - } - } + // bots := e.Group("/bots") + // { + // bots.Use(session.MustUser()) + // bots.POST("/slack", Slack) + // bots.POST("/slack/:command", Slack) + // } return normalize(e) } +// THIS HACK JOB IS GOING AWAY SOON. +// // normalize is a helper function to work around the following // issue with gin. https://github.com/gin-gonic/gin/issues/388 func normalize(h http.Handler) http.Handler { diff --git a/web/badge.go b/server/badge.go similarity index 99% rename from web/badge.go rename to server/badge.go index e987079c5..acece2356 100644 --- a/web/badge.go +++ b/server/badge.go @@ -1,4 +1,4 @@ -package web +package server import ( "fmt" diff --git a/api/build.go b/server/build.go similarity index 69% rename from api/build.go rename to server/build.go index d0bd37388..1d75fd4df 100644 --- a/api/build.go +++ b/server/build.go @@ -1,18 +1,13 @@ -package api +package server import ( - "fmt" "io" "net/http" - "os" - "path/filepath" "strconv" - "strings" "time" log "github.com/Sirupsen/logrus" "github.com/drone/drone/bus" - "github.com/drone/drone/engine" "github.com/drone/drone/queue" "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" @@ -24,21 +19,6 @@ import ( "github.com/drone/drone/router/middleware/session" ) -var ( - droneYml = os.Getenv("BUILD_CONFIG_FILE") - droneSec string -) - -func init() { - if droneYml == "" { - droneYml = ".drone.yml" - } - droneSec = fmt.Sprintf("%s.sec", strings.TrimSuffix(droneYml, filepath.Ext(droneYml))) - if os.Getenv("CANARY") == "true" { - droneSec = fmt.Sprintf("%s.sig", droneYml) - } -} - func GetBuilds(c *gin.Context) { repo := session.Repo(c) builds, err := store.GetBuildList(c, repo) @@ -135,7 +115,6 @@ func GetBuildLogs(c *gin.Context) { } func DeleteBuild(c *gin.Context) { - engine_ := engine.FromContext(c) repo := session.Repo(c) // parse the build number and job sequence number from @@ -155,17 +134,8 @@ func DeleteBuild(c *gin.Context) { return } - if os.Getenv("CANARY") == "true" { - bus.Publish(c, bus.NewEvent(bus.Cancelled, repo, build, job)) - return - } - - node, err := store.GetNode(c, job.NodeID) - if err != nil { - c.AbortWithError(404, err) - return - } - engine_.Cancel(build.ID, job.ID, node) + bus.Publish(c, bus.NewEvent(bus.Cancelled, repo, build, job)) + c.String(204, "") } func PostBuild(c *gin.Context) { @@ -205,7 +175,8 @@ func PostBuild(c *gin.Context) { } // fetch the .drone.yml file from the database - raw, err := remote_.File(user, repo, build, droneYml) + 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) @@ -213,12 +184,11 @@ func PostBuild(c *gin.Context) { } // Fetch secrets file but don't exit on error as it's optional - sec, err := remote_.File(user, repo, build, droneSec) + sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } - key, _ := store.GetKey(c, repo) netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) @@ -296,72 +266,42 @@ func PostBuild(c *gin.Context) { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } - // IMPORTANT. PLEASE READ - // - // The below code uses a feature flag to switch between the current - // build engine and the exerimental 0.5 build engine. This can be - // enabled using with the environment variable CANARY=true + var signed bool + var verified bool - if os.Getenv("CANARY") == "true" { - - var signed bool - var verified bool - - signature, err := jose.ParseSigned(string(sec)) + 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 parse .drone.yml.sig file. %s", err) - } else if len(sec) == 0 { - log.Debugf("cannot parse .drone.yml.sig file. empty file") + 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 { - 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 - } + verified = true } - - log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) - - bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) - for _, job := range jobs { - queue.Publish(c, &queue.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)}, - }) - } - return // EXIT NOT TO AVOID THE 0.4 ENGINE CODE BELOW } - engine_ := engine.FromContext(c) - go engine_.Schedule(c.Copy(), &engine.Task{ - User: user, - Repo: repo, - Build: build, - BuildPrev: last, - Jobs: jobs, - Keys: key, - Netrc: netrc, - Config: string(raw), - Secret: string(sec), - System: &model.System{ - Link: httputil.GetURL(c.Request), - Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), - Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), - Escalates: strings.Split(os.Getenv("ESCALATE_FILTER"), " "), - }, - }) + log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) + bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) + for _, job := range jobs { + queue.Publish(c, &queue.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)}, + }) + } } diff --git a/web/gitlab.go b/server/gitlab.go similarity index 99% rename from web/gitlab.go rename to server/gitlab.go index 36364e550..77c504c39 100644 --- a/web/gitlab.go +++ b/server/gitlab.go @@ -1,4 +1,4 @@ -package web +package server import ( "fmt" diff --git a/web/hook.go b/server/hook.go similarity index 69% rename from web/hook.go rename to server/hook.go index 6544446b3..a2f293f58 100644 --- a/web/hook.go +++ b/server/hook.go @@ -1,18 +1,14 @@ -package web +package server import ( "fmt" - "os" - "path/filepath" "regexp" - "strings" "github.com/gin-gonic/gin" "github.com/square/go-jose" log "github.com/Sirupsen/logrus" "github.com/drone/drone/bus" - "github.com/drone/drone/engine" "github.com/drone/drone/model" "github.com/drone/drone/queue" "github.com/drone/drone/remote" @@ -22,21 +18,6 @@ import ( "github.com/drone/drone/yaml" ) -var ( - droneYml = os.Getenv("BUILD_CONFIG_FILE") - droneSec string -) - -func init() { - if droneYml == "" { - droneYml = ".drone.yml" - } - droneSec = fmt.Sprintf("%s.sec", strings.TrimSuffix(droneYml, filepath.Ext(droneYml))) - if os.Getenv("CANARY") == "true" { - droneSec = fmt.Sprintf("%s.sig", droneYml) - } -} - var skipRe = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`) func PostHook(c *gin.Context) { @@ -141,13 +122,14 @@ func PostHook(c *gin.Context) { } // fetch the build file from the database - raw, err := remote_.File(user, repo, build, droneYml) + 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 } - sec, err := remote_.File(user, repo, build, droneSec) + sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) // NOTE we don't exit on failure. The sec file is optional @@ -168,8 +150,6 @@ func PostHook(c *gin.Context) { return } - key, _ := store.GetKey(c, repo) - // verify the branches can be built vs skipped branches := yaml.ParseBranch(raw) if !branches.Matches(build.Branch) && build.Event != model.EventTag && build.Event != model.EventDeploy { @@ -214,71 +194,43 @@ func PostHook(c *gin.Context) { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } - // IMPORTANT. PLEASE READ - // - // The below code uses a feature flag to switch between the current - // build engine and the exerimental 0.5 build engine. This can be - // enabled using with the environment variable CANARY=true + var signed bool + var verified bool - if os.Getenv("CANARY") == "true" { - - var signed bool - var verified bool - - signature, err := jose.ParseSigned(string(sec)) + 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 parse .drone.yml.sig file. %s", err) - } else if len(sec) == 0 { - log.Debugf("cannot parse .drone.yml.sig file. empty file") + 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") } 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") - } else { - verified = true - } + verified = true } - - log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) - - bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) - for _, job := range jobs { - queue.Publish(c, &queue.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)}, - }) - } - return // EXIT NOT TO AVOID THE 0.4 ENGINE CODE BELOW } - engine_ := engine.FromContext(c) - go engine_.Schedule(c.Copy(), &engine.Task{ - User: user, - Repo: repo, - Build: build, - BuildPrev: last, - Jobs: jobs, - Keys: key, - Netrc: netrc, - Config: string(raw), - Secret: string(sec), - System: &model.System{ - Link: httputil.GetURL(c.Request), - Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), - Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), - Escalates: strings.Split(os.Getenv("ESCALATE_FILTER"), " "), - }, - }) + log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) + + bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) + for _, job := range jobs { + queue.Publish(c, &queue.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)}, + }) + } + } diff --git a/web/login.go b/server/login.go similarity index 59% rename from web/login.go rename to server/login.go index dbeb54b5c..0934155d1 100644 --- a/web/login.go +++ b/server/login.go @@ -1,100 +1,106 @@ -package web +package server import ( "net/http" "time" - "github.com/gin-gonic/gin" - - log "github.com/Sirupsen/logrus" "github.com/drone/drone/model" "github.com/drone/drone/remote" "github.com/drone/drone/shared/crypto" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/token" "github.com/drone/drone/store" + + "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" ) func GetLogin(c *gin.Context) { - remote := remote.FromContext(c) - // when dealing with redirects we may need - // to adjust the content type. I cannot, however, - // remember why, so need to revisit this line. + // when dealing with redirects we may need to adjust the content type. I + // cannot, however, remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") - tmpuser, open, err := remote.Login(c.Writer, c.Request) + tmpuser, err := remote.Login(c, c.Writer, c.Request) if err != nil { - log.Errorf("cannot authenticate user. %s", err) + logrus.Errorf("cannot authenticate user. %s", err) c.Redirect(303, "/login?error=oauth_error") return } - // this will happen when the user is redirected by - // the remote provide as part of the oauth dance. + // this will happen when the user is redirected by the remote provider as + // part of the authorization workflow. if tmpuser == nil { return } + config := ToConfig(c) // get the user from the database u, err := store.GetUserLogin(c, tmpuser.Login) if err != nil { - count, err := store.GetUserCount(c) - if err != nil { - log.Errorf("cannot register %s. %s", tmpuser.Login, err) - c.Redirect(303, "/login?error=internal_error") - return - } - // if self-registration is disabled we should - // return a notAuthorized error. the only exception - // is if no users exist yet in the system we'll proceed. - if !open && count != 0 { - log.Errorf("cannot register %s. registration closed", tmpuser.Login) + // if self-registration is disabled we should return a not authorized error + if !config.Open { + logrus.Errorf("cannot register %s. registration closed", tmpuser.Login) c.Redirect(303, "/login?error=access_denied") return } // create the user account - u = &model.User{} - u.Login = tmpuser.Login - u.Token = tmpuser.Token - u.Secret = tmpuser.Secret - u.Email = tmpuser.Email - u.Avatar = tmpuser.Avatar - u.Hash = crypto.Rand() + u = &model.User{ + Login: tmpuser.Login, + Token: tmpuser.Token, + Secret: tmpuser.Secret, + Email: tmpuser.Email, + Avatar: tmpuser.Avatar, + Hash: crypto.Rand(), + } // insert the user into the database if err := store.CreateUser(c, u); err != nil { - log.Errorf("cannot insert %s. %s", u.Login, err) + logrus.Errorf("cannot insert %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } - - // if this is the first user, they - // should be an admin. - if count == 0 { - u.Admin = true - } } - // update the user meta data and authorization - // data and cache in the datastore. + // update the user meta data and authorization data. u.Token = tmpuser.Token u.Secret = tmpuser.Secret u.Email = tmpuser.Email u.Avatar = tmpuser.Avatar if err := store.UpdateUser(c, u); err != nil { - log.Errorf("cannot update %s. %s", u.Login, err) + logrus.Errorf("cannot update %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } + if len(config.Orgs) != 0 { + teams, terr := remote.Teams(c, u) + if terr != nil { + logrus.Errorf("cannot verify team membership for %s. %s.", tmpuser.Login, terr) + c.Redirect(303, "/login?error=access_denied") + return + } + var member bool + for _, team := range teams { + if config.Orgs[team.Login] { + member = true + break + } + } + if !member { + logrus.Errorf("cannot verify team membership for %s. %s.", tmpuser.Login, terr) + c.Redirect(303, "/login?error=access_denied") + return + } + } + exp := time.Now().Add(time.Hour * 72).Unix() token := token.New(token.SessToken, u.Login) tokenstr, err := token.SignExpires(u.Hash, exp) if err != nil { - log.Errorf("cannot create token for %s. %s", u.Login, err) + logrus.Errorf("cannot create token for %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } @@ -109,15 +115,12 @@ func GetLogin(c *gin.Context) { } func GetLogout(c *gin.Context) { - httputil.DelCookie(c.Writer, c.Request, "user_sess") httputil.DelCookie(c.Writer, c.Request, "user_last") c.Redirect(303, "/login") } func GetLoginToken(c *gin.Context) { - remote := remote.FromContext(c) - in := &tokenPayload{} err := c.Bind(in) if err != nil { @@ -125,7 +128,7 @@ func GetLoginToken(c *gin.Context) { return } - login, err := remote.Auth(in.Access, in.Refresh) + login, err := remote.Auth(c, in.Access, in.Refresh) if err != nil { c.AbortWithError(http.StatusUnauthorized, err) return @@ -156,3 +159,9 @@ type tokenPayload struct { Refresh string `json:"refresh_token,omitempty"` Expires int64 `json:"expires_in,omitempty"` } + +// ToConfig returns the config from the Context +func ToConfig(c *gin.Context) *model.Config { + v := c.MustGet("config") + return v.(*model.Config) +} diff --git a/web/pages.go b/server/pages.go similarity index 84% rename from web/pages.go rename to server/pages.go index 46da27304..4d765face 100644 --- a/web/pages.go +++ b/server/pages.go @@ -1,4 +1,4 @@ -package web +package server import ( "net/http" @@ -30,7 +30,7 @@ func ShowIndex(c *gin.Context) { } // filter to only show the currently active ones - activeRepos, err := store.GetRepoListOf(c,repos) + activeRepos, err := store.GetRepoListOf(c, repos) if err != nil { c.String(400, err.Error()) return @@ -83,26 +83,6 @@ func ShowUser(c *gin.Context) { }) } -func ShowUsers(c *gin.Context) { - user := session.User(c) - if !user.Admin { - c.AbortWithStatus(http.StatusForbidden) - return - } - users, _ := store.GetUserList(c) - - token, _ := token.New( - token.CsrfToken, - user.Login, - ).Sign(user.Hash) - - c.HTML(200, "users.html", gin.H{ - "User": user, - "Users": users, - "Csrf": token, - }) -} - func ShowRepo(c *gin.Context) { user := session.User(c) repo := session.Repo(c) @@ -136,7 +116,6 @@ func ShowRepoConf(c *gin.Context) { user := session.User(c) repo := session.Repo(c) - key, _ := store.GetKey(c, repo) token, _ := token.New( token.CsrfToken, @@ -146,7 +125,6 @@ func ShowRepoConf(c *gin.Context) { c.HTML(200, "repo_config.html", gin.H{ "User": user, "Repo": repo, - "Key": key, "Csrf": token, "Link": httputil.GetURL(c.Request), }) @@ -227,10 +205,3 @@ func ShowBuild(c *gin.Context) { "Csrf": csrf, }) } - -func ShowNodes(c *gin.Context) { - user := session.User(c) - nodes, _ := store.GetNodeList(c) - token, _ := token.New(token.CsrfToken, user.Login).Sign(user.Hash) - c.HTML(http.StatusOK, "nodes.html", gin.H{"User": user, "Nodes": nodes, "Csrf": token}) -} diff --git a/api/queue.go b/server/queue.go similarity index 99% rename from api/queue.go rename to server/queue.go index d18c544c6..2230125a1 100644 --- a/api/queue.go +++ b/server/queue.go @@ -1,4 +1,4 @@ -package api +package server import ( "fmt" diff --git a/api/repo.go b/server/repo.go similarity index 57% rename from api/repo.go rename to server/repo.go index 59a0fb4c7..a5768c8cf 100644 --- a/api/repo.go +++ b/server/repo.go @@ -1,16 +1,12 @@ -package api +package server import ( - "bytes" "fmt" - "io/ioutil" "net/http" "github.com/gin-gonic/gin" - "gopkg.in/yaml.v2" "github.com/drone/drone/cache" - "github.com/drone/drone/model" "github.com/drone/drone/remote" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/shared/crypto" @@ -74,19 +70,9 @@ func PostRepo(c *gin.Context) { sig, ) - // generate an RSA key and add to the repo - key, err := crypto.GeneratePrivateKey() - if err != nil { - c.String(500, err.Error()) - return - } - keys := new(model.Key) - keys.Public = string(crypto.MarshalPublicKey(&key.PublicKey)) - keys.Private = string(crypto.MarshalPrivateKey(key)) - // activate the repository before we make any // local changes to the database. - err = remote.Activate(user, r, keys, link) + err = remote.Activate(user, r, link) if err != nil { c.String(500, err.Error()) return @@ -98,12 +84,6 @@ func PostRepo(c *gin.Context) { c.String(500, err.Error()) return } - keys.RepoID = r.ID - err = store.CreateKey(c, keys) - if err != nil { - c.String(500, err.Error()) - return - } c.JSON(200, r) } @@ -157,45 +137,6 @@ func GetRepo(c *gin.Context) { c.JSON(http.StatusOK, session.Repo(c)) } -func GetRepoKey(c *gin.Context) { - repo := session.Repo(c) - keys, err := store.GetKey(c, repo) - if err != nil { - c.String(404, "Error fetching repository key") - } else { - c.String(http.StatusOK, keys.Public) - } -} - -func PostRepoKey(c *gin.Context) { - repo := session.Repo(c) - keys, err := store.GetKey(c, repo) - if err != nil { - c.String(404, "Error fetching repository key") - return - } - body, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - c.String(500, "Error reading private key from body. %s", err) - return - } - pkey := crypto.UnmarshalPrivateKey(body) - if pkey == nil { - c.String(500, "Cannot unmarshal private key. Invalid format.") - return - } - - keys.Public = string(crypto.MarshalPublicKey(&pkey.PublicKey)) - keys.Private = string(crypto.MarshalPrivateKey(pkey)) - - err = store.UpdateKey(c, keys) - if err != nil { - c.String(500, "Error updating repository key") - return - } - c.String(201, keys.Public) -} - func DeleteRepo(c *gin.Context) { remote := remote.FromContext(c) repo := session.Repo(c) @@ -210,44 +151,3 @@ func DeleteRepo(c *gin.Context) { remote.Deactivate(user, repo, httputil.GetURL(c.Request)) c.Writer.WriteHeader(http.StatusOK) } - -func PostSecure(c *gin.Context) { - repo := session.Repo(c) - - in, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - // we found some strange characters included in - // the yaml file when entered into a browser textarea. - // these need to be removed - in = bytes.Replace(in, []byte{'\xA0'}, []byte{' '}, -1) - - // make sure the Yaml is valid format to prevent - // a malformed value from being used in the build - err = yaml.Unmarshal(in, &yaml.MapSlice{}) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - - key, err := store.GetKey(c, repo) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - // encrypts using go-jose - out, err := crypto.Encrypt(string(in), key.Private) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.String(http.StatusOK, out) -} - -func PostReactivate(c *gin.Context) { - -} diff --git a/api/secret.go b/server/secret.go similarity index 98% rename from api/secret.go rename to server/secret.go index cc3384552..f39d97680 100644 --- a/api/secret.go +++ b/server/secret.go @@ -1,4 +1,4 @@ -package api +package server import ( "github.com/drone/drone/model" diff --git a/api/sign.go b/server/sign.go similarity index 98% rename from api/sign.go rename to server/sign.go index 95d13c5ad..df34996e6 100644 --- a/api/sign.go +++ b/server/sign.go @@ -1,4 +1,4 @@ -package api +package server import ( "io/ioutil" diff --git a/web/slack.go b/server/slack.go similarity index 99% rename from web/slack.go rename to server/slack.go index bb03422ce..f6b78c907 100644 --- a/web/slack.go +++ b/server/slack.go @@ -1,4 +1,4 @@ -package web +package server import ( "strings" diff --git a/web/stream2.go b/server/stream.go similarity index 90% rename from web/stream2.go rename to server/stream.go index 91067dd83..aae7bf2fd 100644 --- a/web/stream2.go +++ b/server/stream.go @@ -1,4 +1,4 @@ -package web +package server import ( "bufio" @@ -19,14 +19,9 @@ import ( "github.com/manucorporat/sse" ) -// IMPORTANT. PLEASE READ -// -// This file containers experimental streaming features for the 0.5 -// release. These can be enabled with the feature flag CANARY=true - // GetRepoEvents will upgrade the connection to a Websocket and will stream // event updates to the browser. -func GetRepoEvents2(c *gin.Context) { +func GetRepoEvents(c *gin.Context) { repo := session.Repo(c) c.Writer.Header().Set("Content-Type", "text/event-stream") @@ -70,7 +65,7 @@ func GetRepoEvents2(c *gin.Context) { }) } -func GetStream2(c *gin.Context) { +func GetStream(c *gin.Context) { repo := session.Repo(c) buildn, _ := strconv.Atoi(c.Param("build")) diff --git a/api/doc.go b/server/swagger/doc.go similarity index 62% rename from api/doc.go rename to server/swagger/doc.go index 6c00e13ef..2642c47d4 100644 --- a/api/doc.go +++ b/server/swagger/doc.go @@ -11,6 +11,7 @@ // - application/json // // swagger:meta -package api +package swagger -//go:generate swagger generate spec -o swagger/files/swagger.json +//go:generate swagger generate spec -o files/swagger.json +//go:generate go-bindata -pkg swagger -o swagger_gen.go files/ diff --git a/api/swagger/files/swagger.yml b/server/swagger/files/swagger.yml similarity index 100% rename from api/swagger/files/swagger.yml rename to server/swagger/files/swagger.yml diff --git a/server/swagger/swagger.go b/server/swagger/swagger.go new file mode 100644 index 000000000..2e7a006f1 --- /dev/null +++ b/server/swagger/swagger.go @@ -0,0 +1,85 @@ +package swagger + +import ( + "net/http" + + "github.com/drone/drone/model" +) + +// swagger:route GET /users/{login} user getUser +// +// Get the user with the matching login. +// +// Responses: +// 200: user +// +func userFind(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /user user getCurrentUser +// +// Get the currently authenticated user. +// +// Responses: +// 200: user +// +func userCurrent(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /users user getUserList +// +// Get the list of all registered users. +// +// Responses: +// 200: user +// +func userList(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /user/feed user getUserFeed +// +// Get the currently authenticated user's build feed. +// +// Responses: +// 200: feed +// +func userFeed(w http.ResponseWriter, r *http.Request) {} + +// swagger:route DELETE /users/{login} user deleteUserLogin +// +// Delete the user with the matching login. +// +// Responses: +// 200: user +// +func userDelete(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /user/repos user getUserRepos +// +// Get the currently authenticated user's active repository list. +// +// Responses: +// 200: repos +// +func repoList(w http.ResponseWriter, r *http.Request) {} + +// swagger:response user +type userResp struct { + // in: body + Body model.User +} + +// swagger:response users +type usersResp struct { + // in: body + Body []model.User +} + +// swagger:response feed +type feedResp struct { + // in: body + Body []model.Feed +} + +// swagger:response repos +type reposResp struct { + // in: body + Body []model.Repo +} diff --git a/api/user.go b/server/user.go similarity index 69% rename from api/user.go rename to server/user.go index 667bf4ed7..210aa7abf 100644 --- a/api/user.go +++ b/server/user.go @@ -1,4 +1,4 @@ -package api +package server import ( "net/http" @@ -6,31 +6,16 @@ import ( "github.com/gin-gonic/gin" "github.com/drone/drone/cache" - "github.com/drone/drone/model" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/shared/crypto" "github.com/drone/drone/shared/token" "github.com/drone/drone/store" ) -// swagger:route GET /user user getUser -// -// Get the currently authenticated user. -// -// Responses: -// 200: user -// func GetSelf(c *gin.Context) { c.JSON(200, session.User(c)) } -// swagger:route GET /user/feed user getUserFeed -// -// Get the currently authenticated user's build feed. -// -// Responses: -// 200: feed -// func GetFeed(c *gin.Context) { repos, err := cache.GetRepos(c, session.User(c)) if err != nil { @@ -46,13 +31,6 @@ func GetFeed(c *gin.Context) { c.JSON(200, feed) } -// swagger:route GET /user/repos user getUserRepos -// -// Get the currently authenticated user's active repository list. -// -// Responses: -// 200: repos -// func GetRepos(c *gin.Context) { repos, err := cache.GetRepos(c, session.User(c)) if err != nil { @@ -105,27 +83,3 @@ func DeleteToken(c *gin.Context) { } c.String(http.StatusOK, tokenstr) } - -// swagger:response user -type userResp struct { - // in: body - Body model.User -} - -// swagger:response users -type usersResp struct { - // in: body - Body []model.User -} - -// swagger:response feed -type feedResp struct { - // in: body - Body []model.Feed -} - -// swagger:response repos -type reposResp struct { - // in: body - Body []model.Repo -} diff --git a/api/users.go b/server/users.go similarity index 67% rename from api/users.go rename to server/users.go index 81756aec4..3661692f2 100644 --- a/api/users.go +++ b/server/users.go @@ -1,4 +1,4 @@ -package api +package server import ( "net/http" @@ -10,13 +10,6 @@ import ( "github.com/drone/drone/store" ) -// swagger:route GET /users user getUserList -// -// Get the list of all registered users. -// -// Responses: -// 200: user -// func GetUsers(c *gin.Context) { users, err := store.GetUserList(c) if err != nil { @@ -26,20 +19,13 @@ func GetUsers(c *gin.Context) { } } -// swagger:route GET /users/{login} user getUserLogin -// -// Get the user with the matching login. -// -// Responses: -// 200: user -// func GetUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.String(404, "Cannot find user. %s", err) - } else { - c.JSON(200, user) + return } + c.JSON(200, user) } func PatchUser(c *gin.Context) { @@ -74,31 +60,20 @@ func PostUser(c *gin.Context) { c.String(http.StatusBadRequest, err.Error()) return } - - user := &model.User{} - user.Login = in.Login - user.Email = in.Email - user.Admin = in.Admin - user.Avatar = in.Avatar - user.Active = true - user.Hash = crypto.Rand() - - err = store.CreateUser(c, user) - if err != nil { + user := &model.User{ + Active: true, + Login: in.Login, + Email: in.Email, + Avatar: in.Avatar, + Hash: crypto.Rand(), + } + if err = store.CreateUser(c, user); err != nil { c.String(http.StatusInternalServerError, err.Error()) return } - c.JSON(http.StatusOK, user) } -// swagger:route DELETE /users/{login} user deleteUserLogin -// -// Delete the user with the matching login. -// -// Responses: -// 200: user -// func DeleteUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { @@ -107,7 +82,7 @@ func DeleteUser(c *gin.Context) { } if err = store.DeleteUser(c, user); err != nil { c.String(500, "Error deleting user. %s", err) - } else { - c.String(200, "") + return } + c.String(200, "") } diff --git a/shared/docker/docker.go b/shared/docker/docker.go deleted file mode 100644 index 6615871e0..000000000 --- a/shared/docker/docker.go +++ /dev/null @@ -1,97 +0,0 @@ -package docker - -import ( - "errors" - - log "github.com/Sirupsen/logrus" - "github.com/samalba/dockerclient" -) - -var ( - LogOpts = &dockerclient.LogOptions{ - Stdout: true, - Stderr: true, - } - - LogOptsTail = &dockerclient.LogOptions{ - Follow: true, - Stdout: true, - Stderr: true, - } -) - -// Run creates the docker container, pulling images if necessary, starts -// the container and blocks until the container exits, returning the exit -// information. -func Run(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { - info, err := RunDaemon(client, conf, name) - if err != nil { - return nil, err - } - - return Wait(client, info.Id) -} - -// RunDaemon creates the docker container, pulling images if necessary, starts -// the container and returns the container information. It does not wait for -// the container to exit. -func RunDaemon(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { - - // attempts to create the container - id, err := client.CreateContainer(conf, name, nil) - if err != nil { - // and pull the image and re-create if that fails - err = client.PullImage(conf.Image, nil) - if err != nil { - return nil, err - } - id, err = client.CreateContainer(conf, name, nil) - if err != nil { - client.RemoveContainer(id, true, true) - return nil, err - } - } - - // fetches the container information - info, err := client.InspectContainer(id) - if err != nil { - client.RemoveContainer(id, true, true) - return nil, err - } - - // starts the container - err = client.StartContainer(id, &conf.HostConfig) - if err != nil { - client.RemoveContainer(id, true, true) - return nil, err - } - - return info, err -} - -// Wait blocks until the named container exits, returning the exit information. -func Wait(client dockerclient.Client, name string) (*dockerclient.ContainerInfo, error) { - - defer func() { - client.StopContainer(name, 5) - client.KillContainer(name, "9") - }() - - for attempts := 0; attempts < 5; attempts++ { - done := client.Wait(name) - <-done - - info, err := client.InspectContainer(name) - if err != nil { - return nil, err - } - - if !info.State.Running { - return info, nil - } - - log.Debugf("attempting to resume waiting after %d attempts.\n", attempts) - } - - return nil, errors.New("reached maximum wait attempts") -} diff --git a/store/datastore/keys.go b/store/datastore/keys.go deleted file mode 100644 index ca68b728a..000000000 --- a/store/datastore/keys.go +++ /dev/null @@ -1,31 +0,0 @@ -package datastore - -import ( - "github.com/drone/drone/model" - "github.com/russross/meddler" -) - -func (db *datastore) GetKey(repo *model.Repo) (*model.Key, error) { - var key = new(model.Key) - var err = meddler.QueryRow(db, key, rebind(keyQuery), repo.ID) - return key, err -} - -func (db *datastore) CreateKey(key *model.Key) error { - return meddler.Save(db, keyTable, key) -} - -func (db *datastore) UpdateKey(key *model.Key) error { - return meddler.Save(db, keyTable, key) -} - -func (db *datastore) DeleteKey(key *model.Key) error { - var _, err = db.Exec(rebind(keyDeleteStmt), key.ID) - return err -} - -const keyTable = "keys" - -const keyQuery = "SELECT * FROM `keys` WHERE key_repo_id=? LIMIT 1" - -const keyDeleteStmt = "DELETE FROM `keys` WHERE key_id=?" diff --git a/store/datastore/keys_test.go b/store/datastore/keys_test.go deleted file mode 100644 index 2883ee7f7..000000000 --- a/store/datastore/keys_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package datastore - -import ( - "testing" - - "github.com/drone/drone/model" - "github.com/franela/goblin" -) - -func TestKeys(t *testing.T) { - db := openTest() - defer db.Close() - - s := From(db) - g := goblin.Goblin(t) - g.Describe("Keys", func() { - - // before each test be sure to purge the package - // table data from the database. - g.BeforeEach(func() { - db.Exec(rebind("DELETE FROM `keys`")) - }) - - g.It("Should create a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err := s.CreateKey(&key) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID != 0).IsTrue() - }) - - g.It("Should update a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err := s.CreateKey(&key) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID != 0).IsTrue() - - key.Private = "" - key.Public = "" - - err1 := s.UpdateKey(&key) - getkey, err2 := s.GetKey(&model.Repo{ID: 1}) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(key.ID).Equal(getkey.ID) - g.Assert(key.Public).Equal(getkey.Public) - g.Assert(key.Private).Equal(getkey.Private) - }) - - g.It("Should get a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err := s.CreateKey(&key) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID != 0).IsTrue() - - getkey, err := s.GetKey(&model.Repo{ID: 1}) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID).Equal(getkey.ID) - g.Assert(key.Public).Equal(getkey.Public) - g.Assert(key.Private).Equal(getkey.Private) - }) - - g.It("Should delete a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err1 := s.CreateKey(&key) - err2 := s.DeleteKey(&key) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - - _, err := s.GetKey(&model.Repo{ID: 1}) - g.Assert(err == nil).IsFalse() - }) - }) -} - -var fakePublicKey = ` ------BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0 -FPqri0cb2JZfXJ/DgYSF6vUpwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/ -3j+skZ6UtW+5u09lHNsj6tQ51s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQAB ------END PUBLIC KEY----- -` - -var fakePrivateKey = ` - ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp -wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 -1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh -3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 -pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX -GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il -AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF -L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k -X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl -U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ -37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= ------END RSA PRIVATE KEY----- -` diff --git a/store/datastore/nodes.go b/store/datastore/nodes.go deleted file mode 100644 index 37e88652c..000000000 --- a/store/datastore/nodes.go +++ /dev/null @@ -1,48 +0,0 @@ -package datastore - -import ( - "github.com/drone/drone/model" - "github.com/russross/meddler" -) - -func (db *datastore) GetNode(id int64) (*model.Node, error) { - var node = new(model.Node) - var err = meddler.Load(db, nodeTable, node, id) - return node, err -} - -func (db *datastore) GetNodeList() ([]*model.Node, error) { - var nodes = []*model.Node{} - var err = meddler.QueryAll(db, &nodes, rebind(nodeListQuery)) - return nodes, err -} - -func (db *datastore) CreateNode(node *model.Node) error { - return meddler.Insert(db, nodeTable, node) -} - -func (db *datastore) UpdateNode(node *model.Node) error { - return meddler.Update(db, nodeTable, node) -} - -func (db *datastore) DeleteNode(node *model.Node) error { - var _, err = db.Exec(rebind(nodeDeleteStmt), node.ID) - return err -} - -const nodeTable = "nodes" - -const nodeListQuery = ` -SELECT * -FROM nodes -ORDER BY node_addr -` - -const nodeCountQuery = ` -SELECT COUNT(*) FROM nodes -` - -const nodeDeleteStmt = ` -DELETE FROM nodes -WHERE node_id=? -` diff --git a/store/datastore/nodes_test.go b/store/datastore/nodes_test.go deleted file mode 100644 index 3ae9898fd..000000000 --- a/store/datastore/nodes_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package datastore - -import ( - "testing" - - "github.com/drone/drone/model" - "github.com/franela/goblin" -) - -func TestNodes(t *testing.T) { - db := openTest() - defer db.Close() - - s := From(db) - g := goblin.Goblin(t) - g.Describe("Nodes", func() { - - // before each test be sure to purge the package - // table data from the database. - g.BeforeEach(func() { - db.Exec("DELETE FROM nodes") - }) - - g.It("Should create a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err := s.CreateNode(&node) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID != 0).IsTrue() - }) - - g.It("Should update a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err := s.CreateNode(&node) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID != 0).IsTrue() - - node.Addr = "unix:///var/run/docker.sock" - - err1 := s.UpdateNode(&node) - getnode, err2 := s.GetNode(node.ID) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(node.ID).Equal(getnode.ID) - g.Assert(node.Addr).Equal(getnode.Addr) - g.Assert(node.Arch).Equal(getnode.Arch) - }) - - g.It("Should get a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err := s.CreateNode(&node) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID != 0).IsTrue() - - getnode, err := s.GetNode(node.ID) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID).Equal(getnode.ID) - g.Assert(node.Addr).Equal(getnode.Addr) - g.Assert(node.Arch).Equal(getnode.Arch) - }) - - g.It("Should get a node list", func() { - node1 := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - node2 := model.Node{ - Addr: "unix:///var/run/docker.sock", - Arch: "linux_386", - } - s.CreateNode(&node1) - s.CreateNode(&node2) - - nodes, err := s.GetNodeList() - g.Assert(err == nil).IsTrue() - g.Assert(len(nodes)).Equal(2) - }) - - g.It("Should delete a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err1 := s.CreateNode(&node) - err2 := s.DeleteNode(&node) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - - _, err := s.GetNode(node.ID) - g.Assert(err == nil).IsFalse() - }) - }) -} diff --git a/store/datastore/users_test.go b/store/datastore/users_test.go index 54e22bb7d..fae9931c0 100644 --- a/store/datastore/users_test.go +++ b/store/datastore/users_test.go @@ -58,7 +58,6 @@ func TestUsers(t *testing.T) { Email: "foo@bar.com", Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", Active: true, - Admin: true, } s.CreateUser(&user) @@ -71,7 +70,6 @@ func TestUsers(t *testing.T) { g.Assert(user.Email).Equal(getuser.Email) g.Assert(user.Avatar).Equal(getuser.Avatar) g.Assert(user.Active).Equal(getuser.Active) - g.Assert(user.Admin).Equal(getuser.Admin) }) g.It("Should Get a User By Login", func() { diff --git a/store/store.go b/store/store.go index f0fd97968..2d20380aa 100644 --- a/store/store.go +++ b/store/store.go @@ -54,18 +54,6 @@ type Store interface { // DeleteRepo deletes a user repository. DeleteRepo(*model.Repo) error - // GetKey gets a key by unique repository ID. - GetKey(*model.Repo) (*model.Key, error) - - // CreateKey creates a new key. - CreateKey(*model.Key) error - - // UpdateKey updates a user key. - UpdateKey(*model.Key) error - - // DeleteKey deletes a user key. - DeleteKey(*model.Key) error - // GetSecretList gets a list of repository secrets GetSecretList(*model.Repo) ([]*model.Secret, error) @@ -125,21 +113,6 @@ type Store interface { // WriteLog writes the job logs to the datastore. WriteLog(*model.Job, io.Reader) error - - // GetNode gets a build node from the datastore. - GetNode(id int64) (*model.Node, error) - - // GetNodeList gets a build node list from the datastore. - GetNodeList() ([]*model.Node, error) - - // CreateNode add a new build node to the datastore. - CreateNode(*model.Node) error - - // UpdateNode updates a build node in the datastore. - UpdateNode(*model.Node) error - - // DeleteNode removes a build node from the datastore. - DeleteNode(*model.Node) error } // GetUser gets a user by unique ID. @@ -207,22 +180,6 @@ func DeleteRepo(c context.Context, repo *model.Repo) error { return FromContext(c).DeleteRepo(repo) } -func GetKey(c context.Context, repo *model.Repo) (*model.Key, error) { - return FromContext(c).GetKey(repo) -} - -func CreateKey(c context.Context, key *model.Key) error { - return FromContext(c).CreateKey(key) -} - -func UpdateKey(c context.Context, key *model.Key) error { - return FromContext(c).UpdateKey(key) -} - -func DeleteKey(c context.Context, key *model.Key) error { - return FromContext(c).DeleteKey(key) -} - func GetSecretList(c context.Context, r *model.Repo) ([]*model.Secret, error) { return FromContext(c).GetSecretList(r) } @@ -343,23 +300,3 @@ func ReadLog(c context.Context, job *model.Job) (io.ReadCloser, error) { func WriteLog(c context.Context, job *model.Job, r io.Reader) error { return FromContext(c).WriteLog(job, r) } - -func GetNode(c context.Context, id int64) (*model.Node, error) { - return FromContext(c).GetNode(id) -} - -func GetNodeList(c context.Context) ([]*model.Node, error) { - return FromContext(c).GetNodeList() -} - -func CreateNode(c context.Context, node *model.Node) error { - return FromContext(c).CreateNode(node) -} - -func UpdateNode(c context.Context, node *model.Node) error { - return FromContext(c).UpdateNode(node) -} - -func DeleteNode(c context.Context, node *model.Node) error { - return FromContext(c).DeleteNode(node) -} diff --git a/template/amber/base.amber b/template/amber/base.amber index 491bf3cfc..2e864a264 100644 --- a/template/amber/base.amber +++ b/template/amber/base.amber @@ -34,9 +34,6 @@ html i.material-icons expand_more div.dropdown-menu.dropdown-menu-right a.dropdown-item[href="/settings/profile"] Profile - if User.Admin - a.dropdown-item[href="/settings/people"] People - a.dropdown-item[href="/settings/nodes"] Nodes a.dropdown-item[href="/logout"] Logout diff --git a/template/amber/repo_config.amber b/template/amber/repo_config.amber index be0f3516e..f33c3201a 100644 --- a/template/amber/repo_config.amber +++ b/template/amber/repo_config.amber @@ -65,10 +65,6 @@ block content else input#trusted[type="checkbox"][hidden="hidden"] label.switch[for="trusted"] - div.row - div.col-md-3 Public Key - div.col-md-9 - pre #{Key.Public} #{Repo.Owner}-#{Repo.Name}@drone div.row div.col-md-12 div.alert.alert-danger @@ -78,4 +74,4 @@ block content block append scripts script - var view = new RepoConfigViewModel(#{Repo.FullName}); \ No newline at end of file + var view = new RepoConfigViewModel(#{Repo.FullName}); diff --git a/web/stream.go b/web/stream.go deleted file mode 100644 index 623beb827..000000000 --- a/web/stream.go +++ /dev/null @@ -1,120 +0,0 @@ -package web - -import ( - "io" - "strconv" - - "github.com/gin-gonic/gin" - - "github.com/docker/docker/pkg/stdcopy" - "github.com/drone/drone/engine" - "github.com/drone/drone/router/middleware/session" - "github.com/drone/drone/store" - - log "github.com/Sirupsen/logrus" - - "github.com/manucorporat/sse" -) - -// GetRepoEvents will upgrade the connection to a Websocket and will stream -// event updates to the browser. -func GetRepoEvents(c *gin.Context) { - engine_ := engine.FromContext(c) - repo := session.Repo(c) - c.Writer.Header().Set("Content-Type", "text/event-stream") - - eventc := make(chan *engine.Event, 1) - engine_.Subscribe(eventc) - defer func() { - engine_.Unsubscribe(eventc) - close(eventc) - log.Infof("closed event stream") - }() - - c.Stream(func(w io.Writer) bool { - select { - case event := <-eventc: - if event == nil { - log.Infof("nil event received") - return false - } - if event.Name == repo.FullName { - log.Debugf("received message %s", event.Name) - sse.Encode(w, sse.Event{ - Event: "message", - Data: string(event.Msg), - }) - } - case <-c.Writer.CloseNotify(): - return false - } - return true - }) -} - -func GetStream(c *gin.Context) { - - engine_ := engine.FromContext(c) - repo := session.Repo(c) - buildn, _ := strconv.Atoi(c.Param("build")) - jobn, _ := strconv.Atoi(c.Param("number")) - - c.Writer.Header().Set("Content-Type", "text/event-stream") - - build, err := store.GetBuildNumber(c, repo, buildn) - if err != nil { - log.Debugln("stream cannot get build number.", err) - c.AbortWithError(404, err) - return - } - job, err := store.GetJobNumber(c, build, jobn) - if err != nil { - log.Debugln("stream cannot get job number.", err) - c.AbortWithError(404, err) - return - } - node, err := store.GetNode(c, job.NodeID) - if err != nil { - log.Debugln("stream cannot get node.", err) - c.AbortWithError(404, err) - return - } - - rc, err := engine_.Stream(build.ID, job.ID, node) - if err != nil { - c.AbortWithError(404, err) - return - } - - defer func() { - rc.Close() - }() - - go func() { - defer func() { - recover() - }() - <-c.Writer.CloseNotify() - rc.Close() - }() - - rw := &StreamWriter{c.Writer, 0} - - stdcopy.StdCopy(rw, rw, rc) -} - -type StreamWriter struct { - writer gin.ResponseWriter - count int -} - -func (w *StreamWriter) Write(data []byte) (int, error) { - var err = sse.Encode(w.writer, sse.Event{ - Id: strconv.Itoa(w.count), - Event: "message", - Data: string(data), - }) - w.writer.Flush() - w.count += len(data) - return len(data), err -} diff --git a/yaml/checksum/checksum.go b/yaml/checksum/checksum.go deleted file mode 100644 index 4a021a692..000000000 --- a/yaml/checksum/checksum.go +++ /dev/null @@ -1,70 +0,0 @@ -package checksum - -import ( - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "fmt" - "io" - "strings" -) - -// Check is a calculates and verifies a file checksum. This supports the sha1, -// sha256 and sha512 values. -func Check(in, checksum string) bool { - hash, size, _ := split(checksum) - - // if a byte size is provided for the - // Yaml file it must match. - if size > 0 && int64(len(in)) != size { - return false - } - - switch len(hash) { - case 64: - return sha256sum(in) == hash - case 128: - return sha512sum(in) == hash - case 40: - return sha1sum(in) == hash - } - - return false -} - -func sha1sum(in string) string { - h := sha1.New() - io.WriteString(h, in) - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func sha256sum(in string) string { - h := sha256.New() - io.WriteString(h, in) - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func sha512sum(in string) string { - h := sha512.New() - io.WriteString(h, in) - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func split(in string) (string, int64, string) { - var hash string - var name string - var size int64 - - // the checksum might be split into multiple - // sections including the file size and name. - switch strings.Count(in, " ") { - case 1: - fmt.Sscanf(in, "%s %s", &hash, &name) - case 2: - fmt.Sscanf(in, "%s %d %s", &hash, &size, &name) - default: - hash = in - } - - return hash, size, name -} diff --git a/yaml/checksum/checksum_test.go b/yaml/checksum/checksum_test.go deleted file mode 100644 index 9dd6925c7..000000000 --- a/yaml/checksum/checksum_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package checksum - -import ( - "testing" - - "github.com/franela/goblin" -) - -func TestParse(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Shasum", func() { - - g.It("Should parse the shasum string", func() { - hash, _, _ := split("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - }) - - g.It("Should parse a two-part shasum string", func() { - hash, _, name := split("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 .drone.yml") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - g.Assert(name).Equal(".drone.yml") - }) - - g.It("Should parse a three-part shasum string", func() { - hash, size, name := split("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 42 .drone.yml") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - g.Assert(name).Equal(".drone.yml") - g.Assert(size).Equal(int64(42)) - }) - - g.It("Should calc a sha1 sum", func() { - hash := sha1sum("foo\n") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - }) - - g.It("Should calc a sha256 sum", func() { - hash := sha256sum("foo\n") - g.Assert(hash).Equal("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c") - }) - - g.It("Should calc a sha512 sum", func() { - hash := sha512sum("foo\n") - g.Assert(hash).Equal("0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6") - }) - - g.It("Should calc a sha1 sum", func() { - hash := sha1sum("foo\n") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - }) - - g.It("Should validate sha1 sum with file size", func() { - ok := Check("foo\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 4 -") - g.Assert(ok).IsTrue() - }) - - g.It("Should validate sha256 sum with file size", func() { - ok := Check("foo\n", "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c 4 -") - g.Assert(ok).IsTrue() - }) - - g.It("Should validate sha512 sum with file size", func() { - ok := Check("foo\n", "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6 4 -") - g.Assert(ok).IsTrue() - }) - - g.It("Should fail validation if incorrect sha1", func() { - ok := Check("bar\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 4 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should fail validation if incorrect sha256", func() { - ok := Check("bar\n", "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c 4 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should fail validation if incorrect sha512", func() { - ok := Check("bar\n", "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6 4 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should return false if file size mismatch", func() { - ok := Check("foo\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 12 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should return false if invalid checksum string", func() { - ok := Check("foo\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15234") - g.Assert(ok).IsFalse() - }) - - g.It("Should return false if empty checksum", func() { - ok := Check("foo\n", "") - g.Assert(ok).IsFalse() - }) - }) -}