Merge pull request #1607 from bradrydzewski/master

bump 0.5 version
This commit is contained in:
Brad Rydzewski 2016-05-02 12:55:31 -07:00
commit f52feeceb1
93 changed files with 3577 additions and 4115 deletions

View file

@ -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 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

View file

@ -35,7 +35,7 @@ publish:
password: $$DOCKER_PASS password: $$DOCKER_PASS
email: $$DOCKER_EMAIL email: $$DOCKER_EMAIL
repo: drone/drone repo: drone/drone
tag: [ "latest", "0.4.2" ] tag: [ "0.5.0" ]
when: when:
repo: drone/drone repo: drone/drone
branch: master branch: master

2
.gitignore vendored
View file

@ -12,7 +12,7 @@ drone/drone
.env .env
temp/ temp/
api/swagger/files/* server/swagger/files/*.json
# vendored repositories that we don't actually need # vendored repositories that we don't actually need
# to vendor. so exclude them # to vendor. so exclude them

View file

@ -21,4 +21,4 @@ ADD drone/drone /drone
#RUN echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf #RUN echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
ENTRYPOINT ["/drone"] ENTRYPOINT ["/drone"]
CMD ["serve"] CMD ["daemon"]

View file

@ -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)
}

View file

@ -1,3 +0,0 @@
package swagger
//go:generate go-bindata -pkg swagger -o swagger_gen.go files/

View file

@ -141,9 +141,9 @@ func start(c *cli.Context) {
c.String("drone-token"), c.String("drone-token"),
) )
tls, _ := dockerclient.TLSConfigFromCertPath(c.String("docker-cert-path")) tls, err := dockerclient.TLSConfigFromCertPath(c.String("docker-cert-path"))
if c.Bool("docker-host") { if err == nil {
tls.InsecureSkipVerify = true tls.InsecureSkipVerify = c.Bool("docker-tls-verify")
} }
docker, err := dockerclient.NewDockerClient(c.String("docker-host"), tls) docker, err := dockerclient.NewDockerClient(c.String("docker-host"), tls)
if err != nil { if err != nil {

481
drone/daemon.go Normal file
View file

@ -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
---
`

View file

@ -4,7 +4,6 @@ import (
"os" "os"
"github.com/drone/drone/drone/agent" "github.com/drone/drone/drone/agent"
"github.com/drone/drone/drone/server"
"github.com/drone/drone/version" "github.com/drone/drone/version"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
@ -12,7 +11,7 @@ import (
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
) )
func main2() { func main() {
envflag.Parse() envflag.Parse()
app := cli.NewApp() app := cli.NewApp()
@ -35,7 +34,7 @@ func main2() {
} }
app.Commands = []cli.Command{ app.Commands = []cli.Command{
agent.AgentCmd, agent.AgentCmd,
server.ServeCmd, DaemonCmd,
SignCmd, SignCmd,
SecretCmd, SecretCmd,
} }

View file

@ -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),
)
}
}

View file

@ -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
---
`

View file

@ -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)
}
}

View file

@ -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)
})
})
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
})
})
}

View file

@ -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"`
}

View file

@ -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"`
}

View file

@ -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)
}

View file

@ -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)
}
}

26
model/config.go Normal file
View file

@ -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
}

View file

@ -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:"-"`
}

12
model/team.go Normal file
View file

@ -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"`
}

View file

@ -32,11 +32,17 @@ type User struct {
Avatar string `json:"avatar_url" meddler:"user_avatar"` Avatar string `json:"avatar_url" meddler:"user_avatar"`
// Activate indicates the user is active in the system. // 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 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 is a unique token used to sign tokens.
Hash string `json:"-" meddler:"user_hash"` Hash string `json:"-" meddler:"user_hash"`
// DEPRECATED Admin indicates the user is a system administrator.
XAdmin bool `json:"-" meddler:"user_admin"`
} }

View file

@ -1,133 +1,71 @@
package bitbucket package bitbucket
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote"
"github.com/drone/drone/remote/bitbucket/internal"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
log "github.com/Sirupsen/logrus"
"golang.org/x/oauth2" "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 Client string
Secret string Secret string
Orgs []string
Open bool
} }
func Load(config string) *Bitbucket { // New returns a new remote Configuration for integrating with the Bitbucket
// repository hosting service at https://bitbucket.org
// parse the remote DSN configuration string func New(client, secret string) remote.Remote {
url_, err := url.Parse(config) return &config{
if err != nil { API: DefaultAPI,
log.Fatalln("unable to parse remote dsn. %s", err) 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 // Login authenticates an account with Bitbucket using the oauth2 protocol. The
// remote user details. // Bitbucket account details are returned when the user is successfully authenticated.
func (bb *Bitbucket) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { func (c *config) Login(w http.ResponseWriter, r *http.Request) (*model.User, error) {
redirect := httputil.GetURL(r)
config := c.newConfig(redirect)
config := &oauth2.Config{ code := r.FormValue("code")
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")
if len(code) == 0 { if len(code) == 0 {
http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) http.Redirect(w, r, config.AuthCodeURL("drone"), http.StatusSeeOther)
return nil, false, nil return nil, nil
} }
var token, err = config.Exchange(oauth2.NoContext, code) token, err := config.Exchange(oauth2.NoContext, code)
if err != nil { 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() curr, err := client.FindCurrent()
if err != nil { if err != nil {
return nil, false, err return nil, err
} }
return convertUser(curr, token), nil
// 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
} }
// Auth authenticates the session and returns the remote user // Auth uses the Bitbucket oauth2 access token and refresh token to authenticate
// login for the given token and secret // a session and return the Bitbucket account login.
func (bb *Bitbucket) Auth(token, secret string) (string, error) { func (c *config) Auth(token, secret string) (string, error) {
token_ := oauth2.Token{AccessToken: token, RefreshToken: secret} client := c.newClientToken(token, secret)
client := NewClientToken(bb.Client, bb.Secret, &token_)
user, err := client.FindCurrent() user, err := client.FindCurrent()
if err != nil { if err != nil {
return "", err return "", err
@ -135,84 +73,83 @@ func (bb *Bitbucket) Auth(token, secret string) (string, error) {
return user.Login, nil return user.Login, nil
} }
// Refresh refreshes an oauth token and expiration for the given // Refresh refreshes the Bitbucket oauth2 access token. If the token is
// user. It returns true if the token was refreshed, false if the // refreshed the user is updated and a true value is returned.
// token was not refreshed, and error if it failed to refersh. func (c *config) Refresh(user *model.User) (bool, error) {
func (bb *Bitbucket) Refresh(user *model.User) (bool, error) { config := c.newConfig("")
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.
source := config.TokenSource( source := config.TokenSource(
oauth2.NoContext, &oauth2.Token{RefreshToken: user.Secret}) oauth2.NoContext, &oauth2.Token{RefreshToken: user.Secret})
// requesting the token automatically refreshes and
// returns a new access token.
token, err := source.Token() token, err := source.Token()
if err != nil || len(token.AccessToken) == 0 { if err != nil || len(token.AccessToken) == 0 {
return false, err return false, err
} }
// update the user to include tne new access token
user.Token = token.AccessToken user.Token = token.AccessToken
user.Secret = token.RefreshToken user.Secret = token.RefreshToken
user.Expiry = token.Expiry.UTC().Unix() user.Expiry = token.Expiry.UTC().Unix()
return true, nil return true, nil
} }
// Repo fetches the named repository from the remote system. // Teams returns a list of all team membership for the Bitbucket account.
func (bb *Bitbucket) Repo(u *model.User, owner, name string) (*model.Repo, error) { func (c *config) Teams(u *model.User) ([]*model.Team, error) {
token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} opts := &internal.ListTeamOpts{
client := NewClientToken(bb.Client, bb.Secret, &token) 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 { if err != nil {
return nil, err return nil, err
} }
return convertRepo(repo), nil return convertRepo(repo), nil
} }
// Repos fetches a list of repos from the remote system. // Repos returns a list of all repositories for Bitbucket account, including
func (bb *Bitbucket) Repos(u *model.User) ([]*model.RepoLite, error) { // organization repositories.
token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) {
client := NewClientToken(bb.Client, bb.Secret, &token) client := c.newClient(u)
var repos []*model.RepoLite
// gets a list of all accounts to query, including the var all []*model.RepoLite
// user's account and all team accounts.
logins := []string{u.Login} accounts := []string{u.Login}
resp, err := client.ListTeams(&ListTeamOpts{PageLen: 100, Role: "member"}) resp, err := client.ListTeams(&internal.ListTeamOpts{
PageLen: 100,
Role: "member",
})
if err != nil { if err != nil {
return repos, err return all, err
} }
for _, team := range resp.Values { for _, team := range resp.Values {
logins = append(logins, team.Login) accounts = append(accounts, team.Login)
} }
// for each account, get the list of repos for _, account := range accounts {
for _, login := range logins { repos, err := client.ListReposAll(account)
repos_, err := client.ListReposAll(login)
if err != nil { if err != nil {
return repos, err return all, err
} }
for _, repo := range repos_ { for _, repo := range repos {
repos = append(repos, convertRepoLite(repo)) all = append(all, convertRepoLite(repo))
} }
} }
return all, nil
return repos, nil
} }
// Perm fetches the named repository permissions from // Perm returns the user permissions for the named repository. Because Bitbucket
// the remote system for the specified user. // does not have an endpoint to access user permissions, we attempt to fetch
func (bb *Bitbucket) Perm(u *model.User, owner, name string) (*model.Perm, error) { // the repository hook list, which is restricted to administrators to calculate
token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} // administrative access to a repository.
client := NewClientToken(bb.Client, bb.Secret, &token) func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) {
client := c.newClient(u)
perms := new(model.Perm) perms := new(model.Perm)
_, err := client.FindRepo(owner, name) _, 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 return perms, err
} }
// if we've gotten this far we know that the user at _, err = client.ListHooks(owner, name, &internal.ListOpts{})
// 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{})
if err == nil { if err == nil {
perms.Push = true perms.Push = true
perms.Admin = true perms.Admin = true
} }
perms.Pull = true
return perms, nil return perms, nil
} }
// File fetches a file from the remote repository and returns in string format. // File fetches the file from the Bitbucket repository and returns its contents.
func (bb *Bitbucket) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
client := NewClientToken( config, err := c.newClient(u).FindSource(r.Owner, r.Name, b.Commit, f)
bb.Client,
bb.Secret,
&oauth2.Token{
AccessToken: u.Token,
RefreshToken: u.Secret,
},
)
config, err := client.FindSource(r.Owner, r.Name, b.Commit, f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return []byte(config.Data), err return []byte(config.Data), err
} }
// Status sends the commit status to the remote system. // Status creates a build status for the Bitbucket commit.
// An example would be the GitHub pull request status. func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
func (bb *Bitbucket) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { status := internal.BuildStatus{
client := NewClientToken( State: convertStatus(b.Status),
bb.Client, Desc: convertDesc(b.Status),
bb.Secret,
&oauth2.Token{
AccessToken: u.Token,
RefreshToken: u.Secret,
},
)
status := getStatus(b.Status)
desc := getDesc(b.Status)
data := BuildStatus{
State: status,
Key: "Drone", Key: "Drone",
Url: link, Url: link,
Desc: desc,
} }
return c.newClient(u).CreateStatus(r.Owner, r.Name, b.Commit, &status)
err := client.CreateStatus(r.Owner, r.Name, b.Commit, &data)
return err
} }
// Netrc returns a .netrc file that can be used to clone // Activate activates the repository by registering repository push hooks with
// private repositories from a remote system. // the Bitbucket repository. Prior to registering hook, previously created hooks
func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // 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{ return &model.Netrc{
Machine: "bitbucket.org", Machine: "bitbucket.org",
Login: "x-token-auth", Login: "x-token-auth",
@ -290,226 +230,54 @@ func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
}, nil }, nil
} }
// Activate activates a repository by creating the post-commit hook and // Hook parses the incoming Bitbucket hook and returns the Repository and
// adding the SSH deploy key, if applicable. // Build details. If the hook is unsupported nil values are returned.
func (bb *Bitbucket) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
client := NewClientToken( return parseHook(r)
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
} }
// Deactivate removes a repository by removing all the post-commit hooks // helper function to return the bitbucket oauth2 client
// which are equal to link and removing the SSH deploy key. func (c *config) newClient(u *model.User) *internal.Client {
func (bb *Bitbucket) Deactivate(u *model.User, r *model.Repo, link string) error { return c.newClientToken(u.Token, u.Secret)
client := NewClientToken( }
bb.Client,
bb.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{ &oauth2.Token{
AccessToken: u.Token, AccessToken: token,
RefreshToken: u.Secret, 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 { if err != nil {
return err return nil
} }
for _, hook := range hooks {
// 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) hookurl, err := url.Parse(hook.Url)
if err != nil { if err == nil && hookurl.Host == link.Host {
return err return hook
}
if hookurl.Host == linkurl.Host {
client.DeleteHook(r.Owner, r.Name, hook.Uuid)
break
} }
} }
return nil 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
}
}

View file

@ -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",
}
)

186
remote/bitbucket/convert.go Normal file
View file

@ -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
}

View file

@ -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")
})
})
}

View file

@ -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"
}
]
}
`

View file

@ -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"
}
}
`

View file

@ -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,
}
}

View file

@ -1,4 +1,4 @@
package bitbucket package internal
import ( import (
"bytes" "bytes"
@ -20,8 +20,6 @@ const (
) )
const ( const (
base = "https://api.bitbucket.org"
pathUser = "%s/2.0/user/" pathUser = "%s/2.0/user/"
pathEmails = "%s/2.0/user/emails" pathEmails = "%s/2.0/user/emails"
pathTeams = "%s/2.0/teams/?%s" pathTeams = "%s/2.0/teams/?%s"
@ -35,52 +33,53 @@ const (
type Client struct { type Client struct {
*http.Client *http.Client
base string
} }
func NewClient(client *http.Client) *Client { func NewClient(url string, client *http.Client) *Client {
return &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{ config := &oauth2.Config{
ClientID: client, ClientID: client,
ClientSecret: secret, ClientSecret: secret,
Endpoint: bitbucket.Endpoint, Endpoint: bitbucket.Endpoint,
} }
return NewClient(config.Client(oauth2.NoContext, token)) return NewClient(url, config.Client(oauth2.NoContext, token))
} }
func (c *Client) FindCurrent() (*Account, error) { func (c *Client) FindCurrent() (*Account, error) {
out := new(Account) out := new(Account)
uri := fmt.Sprintf(pathUser, base) uri := fmt.Sprintf(pathUser, c.base)
err := c.do(uri, get, nil, out) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) ListEmail() (*EmailResp, error) { func (c *Client) ListEmail() (*EmailResp, error) {
out := new(EmailResp) out := new(EmailResp)
uri := fmt.Sprintf(pathEmails, base) uri := fmt.Sprintf(pathEmails, c.base)
err := c.do(uri, get, nil, out) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) ListTeams(opts *ListTeamOpts) (*AccountResp, error) { func (c *Client) ListTeams(opts *ListTeamOpts) (*AccountResp, error) {
out := new(AccountResp) 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) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) FindRepo(owner, name string) (*Repo, error) { func (c *Client) FindRepo(owner, name string) (*Repo, error) {
out := new(Repo) 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) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) ListRepos(account string, opts *ListOpts) (*RepoResp, error) { func (c *Client) ListRepos(account string, opts *ListOpts) (*RepoResp, error) {
out := new(RepoResp) 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) err := c.do(uri, get, nil, out)
return out, err 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) { func (c *Client) FindHook(owner, name, id string) (*Hook, error) {
out := new(Hook) 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) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) ListHooks(owner, name string, opts *ListOpts) (*HookResp, error) { func (c *Client) ListHooks(owner, name string, opts *ListOpts) (*HookResp, error) {
out := new(HookResp) 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) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) CreateHook(owner, name string, hook *Hook) error { 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) return c.do(uri, post, hook, nil)
} }
func (c *Client) DeleteHook(owner, name, id string) error { 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) return c.do(uri, del, nil, nil)
} }
func (c *Client) FindSource(owner, name, revision, path string) (*Source, error) { func (c *Client) FindSource(owner, name, revision, path string) (*Source, error) {
out := new(Source) 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) err := c.do(uri, get, nil, out)
return out, err return out, err
} }
func (c *Client) CreateStatus(owner, name, revision string, status *BuildStatus) error { 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) return c.do(uri, post, status, nil)
} }

View file

@ -1,4 +1,4 @@
package bitbucket package internal
import ( import (
"net/url" "net/url"
@ -100,27 +100,29 @@ type Source struct {
Size int64 `json:"size"` 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 { type PushHook struct {
Actor Account `json:"actor"` Actor Account `json:"actor"`
Repo Repo `json:"repository"` Repo Repo `json:"repository"`
Push struct { Push struct {
Changes []struct { Changes []Change `json:"changes"`
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"`
} `json:"push"` } `json:"push"`
} }

71
remote/bitbucket/parse.go Normal file
View file

@ -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
}

View file

@ -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")
})
})
})
}

View file

@ -1,141 +1,150 @@
package bitbucketserver package bitbucketserver
// Requires the following to be set // WARNING! This is an work-in-progress patch and does not yet conform to the coding,
// REMOTE_DRIVER=bitbucketserver // quality or security standards expected of this project. Please use with caution.
// 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
import ( import (
"crypto/rsa"
"crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt" "fmt"
log "github.com/Sirupsen/logrus"
"github.com/drone/drone/model"
"github.com/mrjones/oauth"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "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 URL string
ConsumerKey string ConsumerKey string
GitUserName string GitUserName string
GitPassword string GitPassword string
ConsumerRSA string ConsumerRSA string
Open bool PrivateKey *rsa.PrivateKey
Consumer oauth.Consumer Consumer oauth.Consumer
} }
func Load(config string) *BitbucketServer { func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
requestToken, url, err := c.Consumer.GetRequestTokenAndUrl("oob")
url_, err := url.Parse(config)
if err != nil { if err != nil {
log.Fatalln("unable to parse remote dsn. %s", err) return nil, 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)
} }
var code = req.FormValue("oauth_verifier") var code = req.FormValue("oauth_verifier")
if len(code) == 0 { if len(code) == 0 {
log.Info("redirecting to %s", url)
http.Redirect(res, req, url, http.StatusSeeOther) http.Redirect(res, req, url, http.StatusSeeOther)
return nil, false, nil return nil, nil
} }
var request_oauth_token = req.FormValue("oauth_token") requestToken.Token = req.FormValue("oauth_token")
requestToken.Token = request_oauth_token accessToken, err := c.Consumer.AuthorizeToken(requestToken, code)
accessToken, err := bs.Consumer.AuthorizeToken(requestToken, code)
if err != nil { if err != nil {
log.Error(err) return nil, err
} }
client, err := bs.Consumer.MakeHttpClient(accessToken) client, err := c.Consumer.MakeHttpClient(accessToken)
if err != nil { 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 { if err != nil {
log.Error(err) return nil, err
} }
defer response.Body.Close() defer response.Body.Close()
bits, err := ioutil.ReadAll(response.Body) 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) contents, err := ioutil.ReadAll(response1.Body)
defer response1.Body.Close() defer response1.Body.Close()
var mUser User var mUser User // TODO prefixing with m* is not a common convention in Go
json.Unmarshal(contents, &mUser) json.Unmarshal(contents, &mUser) // TODO should not ignore error
user := model.User{} return &model.User{
user.Login = userName Login: login,
user.Email = mUser.EmailAddress Email: mUser.EmailAddress,
user.Token = accessToken.Token Token: accessToken.Token,
Avatar: avatarLink(mUser.EmailAddress),
user.Avatar = avatarLink(mUser.EmailAddress) }, nil
return &user, bs.Open, nil
} }
func (bs *BitbucketServer) Auth(token, secret string) (string, error) { // Auth is not supported by the Stash driver.
log.Info("Staring to auth for bitbucketServer. %s", token) func (*client) Auth(token, secret string) (string, error) {
if len(token) == 0 { return "", fmt.Errorf("Not Implemented")
return "", fmt.Errorf("Hasn't logged in yet")
}
return token, nil
} }
func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, error) { // Teams is not supported by the Stash driver.
log.Info("Staring repo for bitbucketServer with user " + u.Login + " " + owner + " " + name) 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) response, err := client.Get(url)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@ -145,40 +154,36 @@ func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo,
bsRepo := BSRepo{} bsRepo := BSRepo{}
json.Unmarshal(contents, &bsRepo) json.Unmarshal(contents, &bsRepo)
cloneLink := "" repo := &model.Repo{
repoLink := "" 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 { for _, item := range bsRepo.Links.Clone {
if item.Name == "http" { if item.Name == "http" {
cloneLink = item.Href repo.Clone = item.Href
} }
} }
for _, item := range bsRepo.Links.Self { for _, item := range bsRepo.Links.Self {
if item.Href != "" { 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 return repo, nil
} }
func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) { func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) {
log.Info("Staring repos for bitbucketServer " + u.Login)
var repos = []*model.RepoLite{} 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 { if err != nil {
log.Error(err) log.Error(err)
} }
@ -198,10 +203,8 @@ func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) {
return repos, nil return repos, nil
} }
func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm, error) { func (c *client) Perm(u *model.User, owner, repo string) (*model.Perm, error) {
// TODO need to fetch real permissions here
//TODO: find the real permissions
log.Info("Staring perm for bitbucketServer")
perms := new(model.Perm) perms := new(model.Perm)
perms.Pull = true perms.Pull = true
perms.Admin = true perms.Admin = true
@ -209,11 +212,11 @@ func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm,
return perms, nil 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)) 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) client := NewClientWithToken(&c.Consumer, u.Token)
fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", bs.URL, r.Owner, r.Name, f) fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", c.URL, r.Owner, r.Name, f)
log.Info(fileURL) log.Info(fileURL)
response, err := client.Get(fileURL) response, err := client.Get(fileURL)
if err != nil { if err != nil {
@ -231,28 +234,26 @@ func (bs *BitbucketServer) File(u *model.User, r *model.Repo, b *model.Build, f
return responseBytes, nil return responseBytes, nil
} }
func (bs *BitbucketServer) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { // Status is not supported by the Gogs driver.
log.Info("Staring status for bitbucketServer") func (*client) Status(*model.User, *model.Repo, *model.Build, string) error {
return nil return nil
} }
func (bs *BitbucketServer) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { func (c *client) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) {
log.Info("Starting the Netrc lookup") u, err := url.Parse(c.URL) // TODO strip port from url
u, err := url.Parse(bs.URL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &model.Netrc{ return &model.Netrc{
Machine: u.Host, Machine: u.Host,
Login: bs.GitUserName, Login: c.GitUserName,
Password: bs.GitPassword, Password: c.GitPassword,
}, nil }, nil
} }
func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { func (c *client) Activate(u *model.User, r *model.Repo, 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(&c.Consumer, u.Token)
client := NewClientWithToken(&bs.Consumer, u.Token) hook, err := c.CreateHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link)
hook, err := bs.CreateHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link)
if err != nil { if err != nil {
return err return err
} }
@ -260,68 +261,56 @@ func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key,
return nil return nil
} }
func (bs *BitbucketServer) Deactivate(u *model.User, r *model.Repo, link string) error { func (c *client) 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(&c.Consumer, u.Token)
client := NewClientWithToken(&bs.Consumer, u.Token) err := c.DeleteHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link)
err := bs.DeleteHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (bs *BitbucketServer) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
log.Info("Staring hook for bitbucketServer") hook := new(postHook)
defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(hook); err != nil {
contents, err := ioutil.ReadAll(r.Body) return nil, nil, err
if err != nil {
log.Info(err)
} }
var hookPost postHook build := &model.Build{
json.Unmarshal(contents, &hookPost) 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{} repo := &model.Repo{
buildModel.Event = model.EventPush Name: hook.Repository.Slug,
buildModel.Ref = hookPost.RefChanges[0].RefID Owner: hook.Repository.Project.Key,
buildModel.Author = hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress FullName: fmt.Sprintf("%s/%s", hook.Repository.Project.Key, hook.Repository.Slug),
buildModel.Commit = hookPost.RefChanges[0].ToHash Branch: "master",
buildModel.Avatar = avatarLink(hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress) 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 return repo, build, nil
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"
} }
type HookDetail struct { type HookDetail struct {
Key string `"json:key"` Key string `json:"key"`
Name string `"json:name"` Name string `json:"name"`
Type string `"json:type"` Type string `json:"type"`
Description string `"json:description"` Description string `json:"description"`
Version string `"json:version"` Version string `json:"version"`
ConfigFormKey string `"json:configFormKey"` ConfigFormKey string `json:"configFormKey"`
} }
type Hook struct { type Hook struct {
Enabled bool `"json:enabled"` Enabled bool `json:"enabled"`
Details *HookDetail `"json:details"` Details *HookDetail `json:"details"`
} }
// Enable hook for named repository // 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 // Set hook
hookBytes := []byte(fmt.Sprintf(`{"hook-url-0":"%s"}`, link)) 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 // 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", enablePath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled",
project, slug, hook_key) project, slug, hook_key)
doDelete(client, bs.URL+enablePath) doDelete(client, bs.URL+enablePath)

View file

@ -11,22 +11,18 @@ import (
"strings" "strings"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/oauth2" "github.com/drone/drone/shared/oauth2"
log "github.com/Sirupsen/logrus"
"github.com/google/go-github/github" "github.com/google/go-github/github"
) )
const ( const (
DefaultURL = "https://github.com" DefaultURL = "https://github.com" // Default GitHub URL
DefaultAPI = "https://api.github.com" DefaultAPI = "https://api.github.com" // Default GitHub API URL
DefaultScope = "repo,repo:status,user:email"
DefaultMergeRef = "merge"
) )
var githubDeployRegex = regexp.MustCompile(".+/deployments/(\\d+)")
type Github struct { type Github struct {
URL string URL string
API string API string
@ -34,58 +30,35 @@ type Github struct {
Secret string Secret string
Scope string Scope string
MergeRef string MergeRef string
Orgs []string
Open bool
PrivateMode bool PrivateMode bool
SkipVerify bool SkipVerify bool
GitSSH bool
} }
func Load(config string) *Github { func New(url, client, secret string, scope []string, private, skipverify, mergeref bool) (remote.Remote, error) {
remote := &Github{
// parse the remote DSN configuration string URL: strings.TrimSuffix(url, "/"),
url_, err := url.Parse(config) Client: client,
if err != nil { Secret: secret,
log.Fatalln("unable to parse remote dsn. %s", err) Scope: strings.Join(scope, ","),
PrivateMode: private,
SkipVerify: skipverify,
MergeRef: "head",
} }
params := url_.Query()
url_.Path = ""
url_.RawQuery = ""
// create the Githbub remote using parameters from if remote.URL == DefaultURL {
// the parsed DSN configuration string. remote.API = DefaultAPI
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
} else { } else {
github.API = github.URL + "/api/v3/" remote.API = remote.URL + "/api/v3/"
}
if mergeref {
remote.MergeRef = "merge"
} }
if github.Scope == "" { return remote, nil
github.Scope = DefaultScope
}
if github.MergeRef == "" {
github.MergeRef = DefaultMergeRef
}
return &github
} }
// Login authenticates the session and returns the // Login authenticates the session and returns the remote user details.
// remote user details. func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) {
var config = &oauth2.Config{ var config = &oauth2.Config{
ClientId: g.Client, ClientId: g.Client,
@ -101,7 +74,7 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User,
if len(code) == 0 { if len(code) == 0 {
var random = GetRandom() var random = GetRandom()
http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther) http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther)
return nil, false, nil return nil, nil
} }
var trans = &oauth2.Transport{ 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) var token, err = trans.Exchange(code)
if err != nil { 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 client = NewClient(g.API, token.AccessToken, g.SkipVerify)
var useremail, errr = GetUserEmail(client) var useremail, errr = GetUserEmail(client)
if errr != nil { if errr != nil {
return nil, false, fmt.Errorf("Error retrieving user or verified email. %s", errr) return nil, 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)
}
} }
user := model.User{} user := model.User{}
@ -141,7 +104,7 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User,
user.Email = *useremail.Email user.Email = *useremail.Email
user.Token = token.AccessToken user.Token = token.AccessToken
user.Avatar = *useremail.AvatarURL user.Avatar = *useremail.AvatarURL
return &user, g.Open, nil return &user, nil
} }
// Auth authenticates the session and returns the remote user // 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 return *user.Login, nil
} }
// Repo fetches the named repository from the remote system. func (g *Github) Teams(u *model.User) ([]*model.Team, error) {
func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) {
client := NewClient(g.API, u.Token, g.SkipVerify) client := NewClient(g.API, u.Token, g.SkipVerify)
repo_, err := GetRepo(client, owner, name) orgs, err := GetOrgs(client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
repo := &model.Repo{} var teams []*model.Team
repo.Owner = owner for _, org := range orgs {
repo.Name = name teams = append(teams, &model.Team{
repo.FullName = *repo_.FullName Login: *org.Login,
repo.Link = *repo_.HTMLURL Avatar: *org.AvatarURL,
repo.IsPrivate = *repo_.Private })
repo.Clone = *repo_.CloneURL }
repo.Branch = "master" return teams, nil
repo.Avatar = *repo_.Owner.AvatarURL }
repo.Kind = model.RepoGit
if repo_.DefaultBranch != nil { // Repo fetches the named repository from the remote system.
repo.Branch = *repo_.DefaultBranch 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 { if g.PrivateMode {
repo.IsPrivate = true repo.IsPrivate = true
} }
if g.GitSSH && repo.IsPrivate {
repo.Clone = *repo_.SSHURL
}
return repo, err return repo, err
} }
@ -257,8 +235,10 @@ func repoStatus(client *github.Client, r *model.Repo, b *model.Build, link strin
return err return err
} }
var reDeploy = regexp.MustCompile(".+/deployments/(\\d+)")
func deploymentStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error { 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 the deployment was not triggered from github, don't send a deployment status
if len(matches) != 2 { if len(matches) != 2 {
return nil 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 // Activate activates a repository by creating the post-commit hook and
// adding the SSH deploy key, if applicable. // 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) client := NewClient(g.API, u.Token, g.SkipVerify)
title, err := GetKeyTitle(link) _, err := CreateUpdateHook(client, r.Owner, r.Name, 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)
return err 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. // which are equal to link and removing the SSH deploy key.
func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error { func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify) 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) return DeleteHook(client, r.Owner, r.Name, link)
} }

View file

@ -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" // 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" { // g := Load(conf)
t.Errorf("g.URL = %q; want https://github.com", g.URL) // 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.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.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.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.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) // if g.MergeRef != DefaultMergeRef {
} // t.Errorf("g.MergeRef = %q; want %q", g.MergeRef, DefaultMergeRef)
// }
g = Load("") //
if g.Scope != DefaultScope { // g = Load("")
t.Errorf("g.Scope = %q; want %q", g.Scope, DefaultScope) // if g.Scope != DefaultScope {
} // t.Errorf("g.Scope = %q; want %q", g.Scope, DefaultScope)
} // }
// }

View file

@ -72,53 +72,6 @@ func GetRepo(client *github.Client, owner, repo string) (*github.Repository, err
return r, 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 // GetUserRepos is a helper function that returns a list of
// all user repositories. Paginated results are aggregated into // all user repositories. Paginated results are aggregated into
// a single list. // a single list.
@ -143,30 +96,6 @@ func GetUserRepos(client *github.Client) ([]github.Repository, error) {
return repos, nil 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 // GetOrgs is a helper function that returns a list of
// all orgs that a user belongs to. // all orgs that a user belongs to.
func GetOrgs(client *github.Client) ([]github.Organization, error) { 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) 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 // GetFile is a heper function that retrieves a file from
// GitHub and returns its contents in byte array format. // GitHub and returns its contents in byte array format.
func GetFile(client *github.Client, owner, name, path, ref string) ([]byte, error) { 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) 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
}

View file

@ -4,30 +4,63 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/oauth2" "github.com/drone/drone/shared/oauth2"
"github.com/drone/drone/shared/token"
"github.com/drone/drone/remote/gitlab/client" "github.com/drone/drone/remote/gitlab/client"
) )
const ( const DefaultScope = "api"
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 { type Gitlab struct {
URL string URL string
Client string Client string
Secret string Secret string
AllowedOrgs []string Machine string
CloneMode string Username string
Open bool Password string
PrivateMode bool PrivateMode bool
SkipVerify bool SkipVerify bool
HideArchives bool HideArchives bool
@ -46,17 +79,17 @@ func Load(config string) *Gitlab {
gitlab.URL = url_.String() gitlab.URL = url_.String()
gitlab.Client = params.Get("client_id") gitlab.Client = params.Get("client_id")
gitlab.Secret = params.Get("client_secret") gitlab.Secret = params.Get("client_secret")
gitlab.AllowedOrgs = params["orgs"] // gitlab.AllowedOrgs = params["orgs"]
gitlab.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) gitlab.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify"))
gitlab.HideArchives, _ = strconv.ParseBool(params.Get("hide_archives")) 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") { // switch params.Get("clone_mode") {
case "oauth": // case "oauth":
gitlab.CloneMode = "oauth" // gitlab.CloneMode = "oauth"
default: // default:
gitlab.CloneMode = "token" // gitlab.CloneMode = "token"
} // }
// this is a temp workaround // this is a temp workaround
gitlab.Search, _ = strconv.ParseBool(params.Get("search")) gitlab.Search, _ = strconv.ParseBool(params.Get("search"))
@ -66,7 +99,7 @@ func Load(config string) *Gitlab {
// Login authenticates the session and returns the // Login authenticates the session and returns the
// remote user details. // 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{ var config = &oauth2.Config{
ClientId: g.Client, ClientId: g.Client,
@ -86,41 +119,41 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User,
var code = req.FormValue("code") var code = req.FormValue("code")
if len(code) == 0 { if len(code) == 0 {
http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) 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 trans = &oauth2.Transport{Config: config, Transport: trans_}
var token_, err = trans.Exchange(code) var token_, err = trans.Exchange(code)
if err != nil { 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) client := NewClient(g.URL, token_.AccessToken, g.SkipVerify)
login, err := client.CurrentUser() login, err := client.CurrentUser()
if err != nil { if err != nil {
return nil, false, err return nil, err
} }
if len(g.AllowedOrgs) != 0 { // if len(g.AllowedOrgs) != 0 {
groups, err := client.AllGroups() // groups, err := client.AllGroups()
if err != nil { // if err != nil {
return nil, false, fmt.Errorf("Could not check org membership. %s", err) // return nil, fmt.Errorf("Could not check org membership. %s", err)
} // }
//
var member bool // var member bool
for _, group := range groups { // for _, group := range groups {
for _, allowedOrg := range g.AllowedOrgs { // for _, allowedOrg := range g.AllowedOrgs {
if group.Path == allowedOrg { // if group.Path == allowedOrg {
member = true // member = true
break // break
} // }
} // }
} // }
//
if !member { // if !member {
return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs) // return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs)
} // }
} // }
user := &model.User{} user := &model.User{}
user.Login = login.Username 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 user.Avatar = g.URL + "/" + login.AvatarUrl
} }
return user, g.Open, nil return user, nil
} }
func (g *Gitlab) Auth(token, secret string) (string, error) { 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 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. // Repo fetches the named repository from the remote system.
func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) { func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) {
client := NewClient(g.URL, u.Token, g.SkipVerify) 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 // Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system. // private repositories from a remote system.
func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
url_, err := url.Parse(g.URL) // url_, err := url.Parse(g.URL)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
netrc := &model.Netrc{} // netrc := &model.Netrc{}
netrc.Machine = url_.Host // 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 { // Netrc returns a netrc file capable of authenticating Gitlab requests and
case "oauth": // cloning Gitlab repositories. The netrc will use the global machine account
netrc.Login = "oauth2" // when configured.
netrc.Password = u.Token func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
case "token": if g.Password != "" {
t := token.New(token.HookToken, r.FullName) return &model.Netrc{
netrc.Login = "drone-ci-token" Login: g.Username,
netrc.Password, err = t.Sign(r.Hash) 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 // Activate activates a repository by adding a Post-commit hook and
// a Public Deploy key, if applicable. // 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) var client = NewClient(g.URL, user.Token, g.SkipVerify)
id, err := GetProjectId(g, client, repo.Owner, repo.Name) id, err := GetProjectId(g, client, repo.Owner, repo.Name)
if err != nil { if err != nil {

View file

@ -94,13 +94,13 @@ func Test_Gitlab(t *testing.T) {
// Test activate method // Test activate method
g.Describe("Activate", func() { g.Describe("Activate", func() {
g.It("Should be success", 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.Assert(err == nil).IsTrue()
}) })
g.It("Should be failed, when token not given", func() { 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() g.Assert(err != nil).IsTrue()
}) })

View file

@ -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
}
}
]
`

View file

@ -1,6 +1,6 @@
package testdata package fixtures
var PushHook = ` var HookPush = `
{ {
"ref": "refs/heads/master", "ref": "refs/heads/master",
"before": "4b2626259b5a97b6b4eab5e6cca66adb986b672b", "before": "4b2626259b5a97b6b4eab5e6cca66adb986b672b",

View file

@ -6,189 +6,187 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote"
"github.com/gogits/go-gogs-client" "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 URL string
Open bool Machine string
Username string
Password string
PrivateMode bool PrivateMode bool
SkipVerify bool SkipVerify bool
} }
func Load(config string) *Gogs { // New returns a Remote implementation that integrates with Gogs, an open
// parse the remote DSN configuration string // source Git service written in Go. See https://gogs.io/
url_, err := url.Parse(config) func New(opts Opts) (remote.Remote, error) {
url, err := url.Parse(opts.URL)
if err != nil { if err != nil {
log.Fatalln("unable to parse remote dsn. %s", err) return nil, err
} }
params := url_.Query() host, _, err := net.SplitHostPort(url.Host)
url_.RawQuery = "" if err == nil {
url.Host = host
// create the Githbub remote using parameters from }
// the parsed DSN configuration string. return &client{
gogs := Gogs{} URL: opts.URL,
gogs.URL = url_.String() Machine: url.Host,
gogs.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) Username: opts.Username,
gogs.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) Password: opts.Password,
gogs.Open, _ = strconv.ParseBool(params.Get("open")) PrivateMode: opts.PrivateMode,
SkipVerify: opts.SkipVerify,
return &gogs }, nil
} }
// Login authenticates the session and returns the // Login authenticates an account with Gogs using basic authenticaiton. The
// remote user details. // Gogs account details are returned when the user is successfully authenticated.
func (g *Gogs) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
var ( var (
username = req.FormValue("username") username = req.FormValue("username")
password = req.FormValue("password") password = req.FormValue("password")
) )
// if the username or password doesn't exist we re-direct // if the username or password is empty we re-direct to the login screen.
// the user to the login screen.
if len(username) == 0 || len(password) == 0 { if len(username) == 0 || len(password) == 0 {
http.Redirect(res, req, "/login/form", http.StatusSeeOther) 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 // try to fetch drone token if it exists
var accessToken string var accessToken string
tokens, err := client.ListAccessTokens(username, password) tokens, err := client.ListAccessTokens(username, password)
if err != nil { if err == nil {
return nil, false, err for _, token := range tokens {
} if token.Name == "drone" {
for _, token := range tokens { accessToken = token.Sha1
if token.Name == "drone" { break
accessToken = token.Sha1 }
break
} }
} }
// if drone token not found, create it // if drone token not found, create it
if accessToken == "" { if accessToken == "" {
token, err := client.CreateAccessToken(username, password, gogs.CreateAccessTokenOption{Name: "drone"}) token, terr := client.CreateAccessToken(
if err != nil { username,
return nil, false, err password,
gogs.CreateAccessTokenOption{Name: "drone"},
)
if terr != nil {
return nil, terr
} }
accessToken = token.Sha1 accessToken = token.Sha1
} }
client = NewGogsClient(g.URL, accessToken, g.SkipVerify) client = c.newClientToken(accessToken)
userInfo, err := client.GetUserInfo(username) account, 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()
if err != nil { if err != nil {
return nil, err return nil, err
} }
fullName := owner + "/" + name return &model.User{
for _, repo := range repos_ { Token: accessToken,
if repo.FullName == fullName { Login: account.UserName,
return toRepo(repo), nil Email: account.Email,
} Avatar: expandAvatar(c.URL, account.AvatarUrl),
} }, nil
return nil, fmt.Errorf("Not Found")
} }
// Repos fetches a list of repos from the remote system. // Auth is not supported by the Gogs driver.
func (g *Gogs) Repos(u *model.User) ([]*model.RepoLite, error) { 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{} repos := []*model.RepoLite{}
client := NewGogsClient(g.URL, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
repos_, err := client.ListMyRepos() all, err := client.ListMyRepos()
if err != nil { if err != nil {
return repos, err return repos, err
} }
for _, repo := range repos_ { for _, repo := range all {
repos = append(repos, toRepoLite(repo)) repos = append(repos, toRepoLite(repo))
} }
return repos, err return repos, err
} }
// Perm fetches the named repository permissions from // Perm returns the user permissions for the named Gogs repository.
// the remote system for the specified user. func (c *client) Perm(u *model.User, owner, name string) (*model.Perm, error) {
func (g *Gogs) Perm(u *model.User, owner, name string) (*model.Perm, error) { client := c.newClientToken(u.Token)
client := NewGogsClient(g.URL, u.Token, g.SkipVerify) repo, err := client.GetRepo(owner, name)
repos_, err := client.ListMyRepos()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return toPerm(repo.Permissions), nil
fullName := owner + "/" + name
for _, repo := range repos_ {
if repo.FullName == fullName {
return toPerm(repo.Permissions), nil
}
}
return nil, fmt.Errorf("Not Found")
} }
// File fetches a file from the remote repository and returns in string format. // File fetches the file from the Gogs repository and returns its contents.
func (g *Gogs) 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) {
client := NewGogsClient(g.URL, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
cfg, err := client.GetFile(r.Owner, r.Name, b.Commit, f) cfg, err := client.GetFile(r.Owner, r.Name, b.Commit, f)
return cfg, err return cfg, err
} }
// Status sends the commit status to the remote system. // Status is not supported by the Gogs driver.
// An example would be the GitHub pull request status. func (c *client) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
func (g *Gogs) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { return nil
return fmt.Errorf("Not Implemented")
} }
// Netrc returns a .netrc file that can be used to clone // Netrc returns a netrc file capable of authenticating Gogs requests and
// private repositories from a remote system. // cloning Gogs repositories. The netrc will use the global machine account
func (g *Gogs) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // when configured.
url_, err := url.Parse(g.URL) func (c *client) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
if err != nil { if c.Password != "" {
return nil, err return &model.Netrc{
} Login: c.Username,
host, _, err := net.SplitHostPort(url_.Host) Password: c.Password,
if err == nil { Machine: c.Machine,
url_.Host = host }, nil
} }
return &model.Netrc{ return &model.Netrc{
Login: u.Token, Login: u.Token,
Password: "x-oauth-basic", Password: "x-oauth-basic",
Machine: url_.Host, Machine: c.Machine,
}, nil }, nil
} }
// Activate activates a repository by creating the post-commit hook and // Activate activates the repository by registering post-commit hooks with
// adding the SSH deploy key, if applicable. // the Gogs repository.
func (g *Gogs) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { func (c *client) Activate(u *model.User, r *model.Repo, link string) error {
config := map[string]string{ config := map[string]string{
"url": link, "url": link,
"secret": r.Hash, "secret": r.Hash,
@ -200,20 +198,19 @@ func (g *Gogs) Activate(u *model.User, r *model.Repo, k *model.Key, link string)
Active: true, Active: true,
} }
client := NewGogsClient(g.URL, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
_, err := client.CreateRepoHook(r.Owner, r.Name, hook) _, err := client.CreateRepoHook(r.Owner, r.Name, hook)
return err return err
} }
// Deactivate removes a repository by removing all the post-commit hooks // Deactivate is not supported by the Gogs driver.
// which are equal to link and removing the SSH deploy key. func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error {
func (g *Gogs) Deactivate(u *model.User, r *model.Repo, link string) error { return nil
return fmt.Errorf("Not Implemented")
} }
// Hook parses the post-commit hook from the Request body // Hook parses the incoming Gogs hook and returns the Repository and Build
// and returns the required data in a standard format. // details. If the hook is unsupported nil values are returned.
func (g *Gogs) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
var ( var (
err error err error
repo *model.Repo 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") { switch r.Header.Get("X-Gogs-Event") {
case "push": case "push":
var push *PushHook var push *pushHook
push, err = parsePush(r.Body) push, err = parsePush(r.Body)
if err == nil { if err == nil {
repo = repoFromPush(push) repo = repoFromPush(push)
@ -232,20 +229,20 @@ func (g *Gogs) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
return repo, build, err return repo, build, err
} }
// NewClient initializes and returns a API client. // helper function to return the Gogs client
func NewGogsClient(url, token string, skipVerify bool) *gogs.Client { func (c *client) newClient() *gogs.Client {
sslClient := &http.Client{} return c.newClientToken("")
c := gogs.NewClient(url, token) }
if skipVerify { // helper function to return the Gogs client
sslClient.Transport = &http.Transport{ 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}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
} }
c.SetHTTPClient(sslClient) client.SetHTTPClient(httpClient)
} }
return c return client
}
func (g *Gogs) String() string {
return "gogs"
} }

183
remote/gogs/gogs_test.go Normal file
View file

@ -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",
}
)

View file

@ -12,8 +12,7 @@ import (
"github.com/gogits/go-gogs-client" "github.com/gogits/go-gogs-client"
) )
// helper function that converts a Gogs repository // helper function that converts a Gogs repository to a Drone repository.
// to a Drone repository.
func toRepoLite(from *gogs.Repository) *model.RepoLite { func toRepoLite(from *gogs.Repository) *model.RepoLite {
name := strings.Split(from.FullName, "/")[1] name := strings.Split(from.FullName, "/")[1]
avatar := expandAvatar( avatar := expandAvatar(
@ -28,8 +27,7 @@ func toRepoLite(from *gogs.Repository) *model.RepoLite {
} }
} }
// helper function that converts a Gogs repository // helper function that converts a Gogs repository to a Drone repository.
// to a Drone repository.
func toRepo(from *gogs.Repository) *model.Repo { func toRepo(from *gogs.Repository) *model.Repo {
name := strings.Split(from.FullName, "/")[1] name := strings.Split(from.FullName, "/")[1]
avatar := expandAvatar( avatar := expandAvatar(
@ -49,8 +47,7 @@ func toRepo(from *gogs.Repository) *model.Repo {
} }
} }
// helper function that converts a Gogs permission // helper function that converts a Gogs permission to a Drone permission.
// to a Drone permission.
func toPerm(from gogs.Permission) *model.Perm { func toPerm(from gogs.Permission) *model.Perm {
return &model.Perm{ return &model.Perm{
Pull: from.Pull, Pull: from.Pull,
@ -59,11 +56,10 @@ func toPerm(from gogs.Permission) *model.Perm {
} }
} }
// helper function that extracts the Build data // helper function that extracts the Build data from a Gogs push hook
// from a Gogs push hook func buildFromPush(hook *pushHook) *model.Build {
func buildFromPush(hook *PushHook) *model.Build {
avatar := expandAvatar( avatar := expandAvatar(
hook.Repo.Url, hook.Repo.URL,
fixMalformedAvatar(hook.Sender.Avatar), fixMalformedAvatar(hook.Sender.Avatar),
) )
return &model.Build{ return &model.Build{
@ -79,9 +75,8 @@ func buildFromPush(hook *PushHook) *model.Build {
} }
} }
// helper function that extracts the Repository data // helper function that extracts the Repository data from a Gogs push hook
// from a Gogs push hook func repoFromPush(hook *pushHook) *model.Repo {
func repoFromPush(hook *PushHook) *model.Repo {
fullName := fmt.Sprintf( fullName := fmt.Sprintf(
"%s/%s", "%s/%s",
hook.Repo.Owner.Username, hook.Repo.Owner.Username,
@ -91,20 +86,19 @@ func repoFromPush(hook *PushHook) *model.Repo {
Name: hook.Repo.Name, Name: hook.Repo.Name,
Owner: hook.Repo.Owner.Username, Owner: hook.Repo.Owner.Username,
FullName: fullName, FullName: fullName,
Link: hook.Repo.Url, Link: hook.Repo.URL,
} }
} }
// helper function that parses a push hook from // helper function that parses a push hook from a read closer.
// a read closer. func parsePush(r io.Reader) (*pushHook, error) {
func parsePush(r io.Reader) (*PushHook, error) { push := new(pushHook)
push := new(PushHook)
err := json.NewDecoder(r).Decode(push) err := json.NewDecoder(r).Decode(push)
return push, err return push, err
} }
// fixMalformedAvatar is a helper function that fixes // fixMalformedAvatar is a helper function that fixes an avatar url if malformed
// an avatar url if malformed (known bug with gogs) // (currently a known bug with gogs)
func fixMalformedAvatar(url string) string { func fixMalformedAvatar(url string) string {
index := strings.Index(url, "///") index := strings.Index(url, "///")
if index != -1 { if index != -1 {
@ -117,16 +111,16 @@ func fixMalformedAvatar(url string) string {
return url return url
} }
// expandAvatar is a helper function that converts // expandAvatar is a helper function that converts a relative avatar URL to the
// a relative avatar URL to the abosolute url. // abosolute url.
func expandAvatar(repo, rawurl string) string { func expandAvatar(repo, rawurl string) string {
if !strings.HasPrefix(rawurl, "/avatars/") { if !strings.HasPrefix(rawurl, "/avatars/") {
return rawurl return rawurl
} }
url_, err := url.Parse(repo) url, err := url.Parse(repo)
if err != nil { if err != nil {
return rawurl return rawurl
} }
url_.Path = rawurl url.Path = rawurl
return url_.String() return url.String()
} }

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/drone/drone/model" "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/franela/goblin"
"github.com/gogits/go-gogs-client" "github.com/gogits/go-gogs-client"
@ -17,7 +17,7 @@ func Test_parse(t *testing.T) {
g.Describe("Gogs", func() { g.Describe("Gogs", func() {
g.It("Should parse push hook payload", func() { g.It("Should parse push hook payload", func() {
buf := bytes.NewBufferString(testdata.PushHook) buf := bytes.NewBufferString(fixtures.HookPush)
hook, err := parsePush(buf) hook, err := parsePush(buf)
g.Assert(err == nil).IsTrue() g.Assert(err == nil).IsTrue()
g.Assert(hook.Ref).Equal("refs/heads/master") 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.Before).Equal("4b2626259b5a97b6b4eab5e6cca66adb986b672b")
g.Assert(hook.Compare).Equal("http://gogs.golang.org/gordon/hello-world/compare/4b2626259b5a97b6b4eab5e6cca66adb986b672b...ef98532add3b2feb7a137426bba1248724367df5") 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.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.Name).Equal("gordon")
g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org") g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org")
g.Assert(hook.Repo.Owner.Username).Equal("gordon") 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() { 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) hook, _ := parsePush(buf)
build := buildFromPush(hook) build := buildFromPush(hook)
g.Assert(build.Event).Equal(model.EventPush) 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() { 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) hook, _ := parsePush(buf)
repo := repoFromPush(hook) repo := repoFromPush(hook)
g.Assert(repo.Name).Equal(hook.Repo.Name) g.Assert(repo.Name).Equal(hook.Repo.Name)
g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username) g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username)
g.Assert(repo.FullName).Equal("gordon/hello-world") 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() { g.It("Should return a Perm struct from a Gogs Perm", func() {

View file

@ -1,6 +1,6 @@
package gogs package gogs
type PushHook struct { type pushHook struct {
Ref string `json:"ref"` Ref string `json:"ref"`
Before string `json:"before"` Before string `json:"before"`
After string `json:"after"` After string `json:"after"`
@ -15,7 +15,7 @@ type PushHook struct {
Repo struct { Repo struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Url string `json:"url"` URL string `json:"url"`
Private bool `json:"private"` Private bool `json:"private"`
Owner struct { Owner struct {
Name string `json:"name"` Name string `json:"name"`
@ -27,7 +27,7 @@ type PushHook struct {
Commits []struct { Commits []struct {
ID string `json:"id"` ID string `json:"id"`
Message string `json:"message"` Message string `json:"message"`
Url string `json:"url"` URL string `json:"url"`
} `json:"commits"` } `json:"commits"`
Sender struct { Sender struct {

View file

@ -1,42 +1,32 @@
package mock package mock
import "github.com/stretchr/testify/mock" import (
"net/http"
import "net/http" "github.com/drone/drone/model"
import "github.com/drone/drone/model" "github.com/stretchr/testify/mock"
)
// This is an autogenerated mock type for the Remote type
type Remote struct { type Remote struct {
mock.Mock mock.Mock
} }
func (_m *Remote) Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error) { // Activate provides a mock function with given fields: u, r, link
ret := _m.Called(w, r) func (_m *Remote) Activate(u *model.User, r *model.Repo, link string) error {
ret := _m.Called(u, r, link)
var r0 *model.User var r0 error
if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok {
r0 = rf(w, r) r0 = rf(u, r, link)
} else { } else {
if ret.Get(0) != nil { r0 = ret.Error(0)
r0 = ret.Get(0).(*model.User)
}
} }
var r1 bool return r0
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
} }
// Auth provides a mock function with given fields: token, secret
func (_m *Remote) Auth(token string, secret string) (string, error) { func (_m *Remote) Auth(token string, secret string) (string, error) {
ret := _m.Called(token, secret) ret := _m.Called(token, secret)
@ -56,69 +46,22 @@ func (_m *Remote) Auth(token string, secret string) (string, error) {
return r0, r1 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 // Deactivate provides a mock function with given fields: u, r, link
if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { func (_m *Remote) Deactivate(u *model.User, r *model.Repo, link string) error {
r0 = rf(u, owner, repo) 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 { } else {
if ret.Get(0) != nil { r0 = ret.Error(0)
r0 = ret.Get(0).(*model.Repo)
}
} }
var r1 error return r0
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
} }
func (_m *Remote) Repos(u *model.User) ([]*model.RepoLite, error) {
ret := _m.Called(u)
var r0 []*model.RepoLite // File provides a mock function with given fields: u, r, b, f
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
}
func (_m *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { func (_m *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
ret := _m.Called(u, r, b, f) 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 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 // Hook provides a mock function with given fields: r
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
}
func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
ret := _m.Called(r) ret := _m.Called(r)
@ -227,3 +115,155 @@ func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
return r0, r1, r2 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
}

View file

@ -13,12 +13,15 @@ import (
type Remote interface { type Remote interface {
// Login authenticates the session and returns the // Login authenticates the session and returns the
// remote user details. // 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 // Auth authenticates the session and returns the remote user
// login for the given token and secret // login for the given token and secret
Auth(token, secret string) (string, error) 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 fetches the named repository from the remote system.
Repo(u *model.User, owner, repo string) (*model.Repo, error) Repo(u *model.User, owner, repo string) (*model.Repo, error)
@ -41,29 +44,28 @@ type Remote interface {
// private repositories from a remote system. // private repositories from a remote system.
Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error)
// Activate activates a repository by creating the post-commit hook and // Activate activates a repository by creating the post-commit hook.
// adding the SSH deploy key, if applicable. Activate(u *model.User, r *model.Repo, link string) error
Activate(u *model.User, r *model.Repo, k *model.Key, link string) error
// Deactivate removes a repository by removing all the post-commit hooks // Deactivate deactivates a repository by removing all previously created
// which are equal to link and removing the SSH deploy key. // post-commit hooks matching the given link.
Deactivate(u *model.User, r *model.Repo, link string) error Deactivate(u *model.User, r *model.Repo, link string) error
// Hook parses the post-commit hook from the Request body // Hook parses the post-commit hook from the Request body and returns the
// and returns the required data in a standard format. // required data in a standard format.
Hook(r *http.Request) (*model.Repo, *model.Build, error) 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 { 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) Refresh(*model.User) (bool, error)
} }
// Login authenticates the session and returns the // Login authenticates the session and returns the
// remote user details. // 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) 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) 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. // Repo fetches the named repository from the remote system.
func Repo(c context.Context, u *model.User, owner, repo string) (*model.Repo, error) { func Repo(c context.Context, u *model.User, owner, repo string) (*model.Repo, error) {
return FromContext(c).Repo(u, owner, repo) 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 // Activate activates a repository by creating the post-commit hook and
// adding the SSH deploy key, if applicable. // adding the SSH deploy key, if applicable.
func Activate(c context.Context, u *model.User, r *model.Repo, k *model.Key, link string) error { func Activate(c context.Context, u *model.User, r *model.Repo, link string) error {
return FromContext(c).Activate(u, r, k, link) return FromContext(c).Activate(u, r, link)
} }
// Deactivate removes a repository by removing all the post-commit hooks // Deactivate removes a repository by removing all the post-commit hooks

View file

@ -1,45 +1,33 @@
package middleware package middleware
import ( import (
"github.com/codegangsta/cli"
"github.com/drone/drone/shared/token" "github.com/drone/drone/shared/token"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/ianschenck/envflag"
) )
var ( const agentKey = "agent"
secret = envflag.String("AGENT_SECRET", "", "")
noauth = envflag.Bool("AGENT_NO_AUTH", false, "")
)
// 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. // for agents to connect to the queue.
func AgentMust() gin.HandlerFunc { func Agents(cli *cli.Context) gin.HandlerFunc {
secret := cli.String("agent-secret")
if *secret == "" { if secret == "" {
logrus.Fatalf("please provide the agent secret to authenticate agent requests") logrus.Fatalf("failed to generate token from DRONE_AGENT_SECRET")
} }
t := token.New(token.AgentToken, "") t := token.New(secret, "")
s, err := t.Sign(*secret) s, err := t.Sign(secret)
if err != nil { 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) logrus.Warnf("agents can connect with token %s", s)
return func(c *gin.Context) { return func(c *gin.Context) {
parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { c.Set(agentKey, secret)
return *secret, nil
})
if err != nil {
c.AbortWithError(403, err)
} else if parsed.Kind != token.AgentToken {
c.AbortWithStatus(403)
} else {
c.Next()
}
} }
} }

View file

@ -2,13 +2,16 @@ package middleware
import ( import (
"github.com/drone/drone/bus" "github.com/drone/drone/bus"
"github.com/codegangsta/cli"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Bus() gin.HandlerFunc { // Bus is a middleware function that initializes the Event Bus and attaches to
bus_ := bus.New() // the context of every http.Request.
func Bus(cli *cli.Context) gin.HandlerFunc {
v := bus.New()
return func(c *gin.Context) { return func(c *gin.Context) {
bus.ToContext(c, bus_) bus.ToContext(c, v)
c.Next()
} }
} }

View file

@ -1,22 +1,24 @@
package middleware package middleware
import ( import (
"time"
"github.com/drone/drone/cache" "github.com/drone/drone/cache"
"github.com/codegangsta/cli"
"github.com/gin-gonic/gin" "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 // Cache is a middleware function that initializes the Cache and attaches to
// the context of every http.Request. // the context of every http.Request.
func Cache() gin.HandlerFunc { func Cache(cli *cli.Context) gin.HandlerFunc {
cc := cache.NewTTL(*ttl) v := setupCache(cli)
return func(c *gin.Context) { return func(c *gin.Context) {
cache.ToContext(c, cc) cache.ToContext(c, v)
c.Next()
} }
} }
// helper function to create the cache from the CLI context.
func setupCache(c *cli.Context) cache.Cache {
return cache.NewTTL(
c.Duration("cache-ttl"),
)
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -2,13 +2,16 @@ package middleware
import ( import (
"github.com/drone/drone/queue" "github.com/drone/drone/queue"
"github.com/codegangsta/cli"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Queue() gin.HandlerFunc { // Queue is a middleware function that initializes the Queue and attaches to
queue_ := queue.New() // the context of every http.Request.
func Queue(cli *cli.Context) gin.HandlerFunc {
v := queue.New()
return func(c *gin.Context) { return func(c *gin.Context) {
queue.ToContext(c, queue_) queue.ToContext(c, v)
c.Next()
} }
} }

View file

@ -1,48 +1,102 @@
package middleware package middleware
import ( import (
"fmt"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/remote/bitbucket" "github.com/drone/drone/remote/bitbucket"
"github.com/drone/drone/remote/bitbucketserver"
"github.com/drone/drone/remote/github" "github.com/drone/drone/remote/github"
"github.com/drone/drone/remote/gitlab" "github.com/drone/drone/remote/gitlab"
"github.com/drone/drone/remote/gogs" "github.com/drone/drone/remote/gogs"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin" "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 // Remote is a middleware function that initializes the Remote and attaches to
// the context of every http.Request. // the context of every http.Request.
func Remote() gin.HandlerFunc { func Remote(c *cli.Context) gin.HandlerFunc {
v, err := setupRemote(c)
logrus.Infof("using remote driver %s", *driver) if err != nil {
logrus.Infof("using remote config %s", *config) logrus.Fatalln(err)
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")
} }
return func(c *gin.Context) { return func(c *gin.Context) {
remote.ToContext(c, remote_) remote.ToContext(c, v)
c.Next()
} }
} }
// 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"),
)
}

View file

@ -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()
}
}

View file

@ -44,6 +44,10 @@ func SetUser() gin.HandlerFunc {
return user.Hash, err return user.Hash, err
}) })
if err == nil { if err == nil {
confv := c.MustGet("config")
if conf, ok := confv.(*model.Config); ok {
user.Admin = conf.IsAdmin(user)
}
c.Set("user", user) c.Set("user", user)
// if this is a session token (ie not the API token) // if this is a session token (ie not the API token)

View file

@ -1,29 +1,27 @@
package middleware package middleware
import ( import (
"github.com/codegangsta/cli"
"github.com/drone/drone/store" "github.com/drone/drone/store"
"github.com/drone/drone/store/datastore" "github.com/drone/drone/store/datastore"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin" "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 // Store is a middleware function that initializes the Datastore and attaches to
// the context of every http.Request. // the context of every http.Request.
func Store() gin.HandlerFunc { func Store(cli *cli.Context) gin.HandlerFunc {
db := datastore.New(*database, *datasource) v := setupStore(cli)
logrus.Infof("using database driver %s", *database)
logrus.Infof("using database config %s", *datasource)
return func(c *gin.Context) { return func(c *gin.Context) {
store.ToContext(c, db) store.ToContext(c, v)
c.Next() 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"),
)
}

View file

@ -2,13 +2,16 @@ package middleware
import ( import (
"github.com/drone/drone/stream" "github.com/drone/drone/stream"
"github.com/codegangsta/cli"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Stream() gin.HandlerFunc { // Stream is a middleware function that initializes the Stream and attaches to
stream_ := stream.New() // the context of every http.Request.
func Stream(cli *cli.Context) gin.HandlerFunc {
v := stream.New()
return func(c *gin.Context) { return func(c *gin.Context) {
stream.ToContext(c, stream_) stream.ToContext(c, v)
c.Next()
} }
} }

View file

@ -5,10 +5,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Version is a middleware function that appends the Drone // Version is a middleware function that appends the Drone version information
// version information to the HTTP response. This is intended // to the HTTP response. This is intended for debugging and troubleshooting.
// for debugging and troubleshooting.
func Version(c *gin.Context) { func Version(c *gin.Context) {
c.Header("X-DRONE-VERSION", version.Version) c.Header("X-DRONE-VERSION", version.Version)
c.Next()
} }

View file

@ -2,22 +2,20 @@ package router
import ( import (
"net/http" "net/http"
"os"
"strings" "strings"
"github.com/gin-gonic/gin" "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/header"
"github.com/drone/drone/router/middleware/session" "github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/router/middleware/token" "github.com/drone/drone/router/middleware/token"
"github.com/drone/drone/server"
"github.com/drone/drone/static" "github.com/drone/drone/static"
"github.com/drone/drone/template" "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 := gin.New()
e.Use(gin.Recovery()) e.Use(gin.Recovery())
@ -27,22 +25,21 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler {
e.Use(header.NoCache) e.Use(header.NoCache)
e.Use(header.Options) e.Use(header.Options)
e.Use(header.Secure) e.Use(header.Secure)
e.Use(middlewares...) e.Use(middleware...)
e.Use(session.SetUser()) e.Use(session.SetUser())
e.Use(token.Refresh) e.Use(token.Refresh)
e.GET("/", web.ShowIndex) e.GET("/", server.ShowIndex)
e.GET("/repos", web.ShowAllRepos) e.GET("/repos", server.ShowAllRepos)
e.GET("/login", web.ShowLogin) e.GET("/login", server.ShowLogin)
e.GET("/login/form", web.ShowLoginForm) e.GET("/login/form", server.ShowLoginForm)
e.GET("/logout", web.GetLogout) e.GET("/logout", server.GetLogout)
// TODO below will Go away with React UI
settings := e.Group("/settings") settings := e.Group("/settings")
{ {
settings.Use(session.MustUser()) settings.Use(session.MustUser())
settings.GET("/profile", web.ShowUser) settings.GET("/profile", server.ShowUser)
settings.GET("/people", session.MustAdmin(), web.ShowUsers)
settings.GET("/nodes", session.MustAdmin(), web.ShowNodes)
} }
repo := e.Group("/repos/:owner/:name") repo := e.Group("/repos/:owner/:name")
{ {
@ -50,50 +47,43 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler {
repo.Use(session.SetPerm()) repo.Use(session.SetPerm())
repo.Use(session.MustPull) repo.Use(session.MustPull)
repo.GET("", web.ShowRepo) repo.GET("", server.ShowRepo)
repo.GET("/builds/:number", web.ShowBuild) repo.GET("/builds/:number", server.ShowBuild)
repo.GET("/builds/:number/:job", web.ShowBuild) repo.GET("/builds/:number/:job", server.ShowBuild)
repo_settings := repo.Group("/settings") repo_settings := repo.Group("/settings")
{ {
repo_settings.GET("", session.MustPush, web.ShowRepoConf) repo_settings.GET("", session.MustPush, server.ShowRepoConf)
repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) repo_settings.GET("/encrypt", session.MustPush, server.ShowRepoEncrypt)
repo_settings.GET("/badges", web.ShowRepoBadges) repo_settings.GET("/badges", server.ShowRepoBadges)
} }
} }
// TODO above will Go away with React UI
user := e.Group("/api/user") user := e.Group("/api/user")
{ {
user.Use(session.MustUser()) user.Use(session.MustUser())
user.GET("", api.GetSelf) user.GET("", server.GetSelf)
user.GET("/feed", api.GetFeed) user.GET("/feed", server.GetFeed)
user.GET("/repos", api.GetRepos) user.GET("/repos", server.GetRepos)
user.GET("/repos/remote", api.GetRemoteRepos) user.GET("/repos/remote", server.GetRemoteRepos)
user.POST("/token", api.PostToken) user.POST("/token", server.PostToken)
user.DELETE("/token", api.DeleteToken) user.DELETE("/token", server.DeleteToken)
} }
users := e.Group("/api/users") users := e.Group("/api/users")
{ {
users.Use(session.MustAdmin()) users.Use(session.MustAdmin())
users.GET("", api.GetUsers) users.GET("", server.GetUsers)
users.POST("", api.PostUser) users.POST("", server.PostUser)
users.GET("/:login", api.GetUser) users.GET("/:login", server.GetUser)
users.PATCH("/:login", api.PatchUser) users.PATCH("/:login", server.PatchUser)
users.DELETE("/:login", api.DeleteUser) users.DELETE("/:login", server.DeleteUser)
}
nodes := e.Group("/api/nodes")
{
nodes.Use(session.MustAdmin())
nodes.GET("", api.GetNodes)
nodes.POST("", api.PostNode)
nodes.DELETE("/:node", api.DeleteNode)
} }
repos := e.Group("/api/repos/:owner/:name") repos := e.Group("/api/repos/:owner/:name")
{ {
repos.POST("", api.PostRepo) repos.POST("", server.PostRepo)
repo := repos.Group("") repo := repos.Group("")
{ {
@ -101,37 +91,32 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler {
repo.Use(session.SetPerm()) repo.Use(session.SetPerm())
repo.Use(session.MustPull) repo.Use(session.MustPull)
repo.GET("", api.GetRepo) repo.GET("", server.GetRepo)
repo.GET("/key", api.GetRepoKey) repo.GET("/builds", server.GetBuilds)
repo.POST("/key", api.PostRepoKey) repo.GET("/builds/:number", server.GetBuild)
repo.GET("/builds", api.GetBuilds) repo.GET("/logs/:number/:job", server.GetBuildLogs)
repo.GET("/builds/:number", api.GetBuild) repo.POST("/sign", session.MustPush, server.Sign)
repo.GET("/logs/:number/:job", api.GetBuildLogs)
repo.POST("/sign", session.MustPush, api.Sign)
repo.POST("/secrets", session.MustPush, api.PostSecret) repo.POST("/secrets", session.MustPush, server.PostSecret)
repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) repo.DELETE("/secrets/:secret", session.MustPush, server.DeleteSecret)
// requires authenticated user
repo.POST("/encrypt", session.MustUser(), api.PostSecure)
// requires push permissions // requires push permissions
repo.PATCH("", session.MustPush, api.PatchRepo) repo.PATCH("", session.MustPush, server.PatchRepo)
repo.DELETE("", session.MustPush, api.DeleteRepo) repo.DELETE("", session.MustPush, server.DeleteRepo)
repo.POST("/builds/:number", session.MustPush, api.PostBuild) repo.POST("/builds/:number", session.MustPush, server.PostBuild)
repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) repo.DELETE("/builds/:number/:job", session.MustPush, server.DeleteBuild)
} }
} }
badges := e.Group("/api/badges/:owner/:name") badges := e.Group("/api/badges/:owner/:name")
{ {
badges.GET("/status.svg", web.GetBadge) badges.GET("/status.svg", server.GetBadge)
badges.GET("/cc.xml", web.GetCC) badges.GET("/cc.xml", server.GetCC)
} }
e.POST("/hook", web.PostHook) e.POST("/hook", server.PostHook)
e.POST("/api/hook", web.PostHook) e.POST("/api/hook", server.PostHook)
stream := e.Group("/api/stream") stream := e.Group("/api/stream")
{ {
@ -139,57 +124,53 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler {
stream.Use(session.SetPerm()) stream.Use(session.SetPerm())
stream.Use(session.MustPull) stream.Use(session.MustPull)
if os.Getenv("CANARY") == "true" { stream.GET("/:owner/:name", server.GetRepoEvents)
stream.GET("/:owner/:name", web.GetRepoEvents2) stream.GET("/:owner/:name/:build/:number", server.GetStream)
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)
} }
auth := e.Group("/authorize") auth := e.Group("/authorize")
{ {
auth.GET("", web.GetLogin) auth.GET("", server.GetLogin)
auth.POST("", web.GetLogin) auth.POST("", server.GetLogin)
auth.POST("/token", web.GetLoginToken) auth.POST("/token", server.GetLoginToken)
} }
queue := e.Group("/api/queue") queue := e.Group("/api/queue")
{ {
if os.Getenv("CANARY") == "true" { queue.Use(session.AuthorizeAgent)
queue.Use(middleware.AgentMust()) queue.POST("/pull", server.Pull)
queue.POST("/pull", api.Pull) queue.POST("/pull/:os/:arch", server.Pull)
queue.POST("/pull/:os/:arch", api.Pull) queue.POST("/wait/:id", server.Wait)
queue.POST("/wait/:id", api.Wait) queue.POST("/stream/:id", server.Stream)
queue.POST("/stream/:id", api.Stream) queue.POST("/status/:id", server.Update)
queue.POST("/status/:id", api.Update)
}
} }
gitlab := e.Group("/gitlab/:owner/:name") // DELETE THESE
{ // gitlab := e.Group("/gitlab/:owner/:name")
gitlab.Use(session.SetRepo()) // {
gitlab.GET("/commits/:sha", web.GetCommit) // gitlab.Use(session.SetRepo())
gitlab.GET("/pulls/:number", web.GetPullRequest) // 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") // bots := e.Group("/bots")
{ // {
redirects.GET("/commits/:sha", web.RedirectSha) // bots.Use(session.MustUser())
redirects.GET("/pulls/:number", web.RedirectPullRequest) // bots.POST("/slack", Slack)
} // bots.POST("/slack/:command", Slack)
} // }
return normalize(e) return normalize(e)
} }
// THIS HACK JOB IS GOING AWAY SOON.
//
// normalize is a helper function to work around the following // normalize is a helper function to work around the following
// issue with gin. https://github.com/gin-gonic/gin/issues/388 // issue with gin. https://github.com/gin-gonic/gin/issues/388
func normalize(h http.Handler) http.Handler { func normalize(h http.Handler) http.Handler {

View file

@ -1,4 +1,4 @@
package web package server
import ( import (
"fmt" "fmt"

View file

@ -1,18 +1,13 @@
package api package server
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/drone/drone/bus" "github.com/drone/drone/bus"
"github.com/drone/drone/engine"
"github.com/drone/drone/queue" "github.com/drone/drone/queue"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
@ -24,21 +19,6 @@ import (
"github.com/drone/drone/router/middleware/session" "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) { func GetBuilds(c *gin.Context) {
repo := session.Repo(c) repo := session.Repo(c)
builds, err := store.GetBuildList(c, repo) builds, err := store.GetBuildList(c, repo)
@ -135,7 +115,6 @@ func GetBuildLogs(c *gin.Context) {
} }
func DeleteBuild(c *gin.Context) { func DeleteBuild(c *gin.Context) {
engine_ := engine.FromContext(c)
repo := session.Repo(c) repo := session.Repo(c)
// parse the build number and job sequence number from // parse the build number and job sequence number from
@ -155,17 +134,8 @@ func DeleteBuild(c *gin.Context) {
return return
} }
if os.Getenv("CANARY") == "true" { bus.Publish(c, bus.NewEvent(bus.Cancelled, repo, build, job))
bus.Publish(c, bus.NewEvent(bus.Cancelled, repo, build, job)) c.String(204, "")
return
}
node, err := store.GetNode(c, job.NodeID)
if err != nil {
c.AbortWithError(404, err)
return
}
engine_.Cancel(build.ID, job.ID, node)
} }
func PostBuild(c *gin.Context) { func PostBuild(c *gin.Context) {
@ -205,7 +175,8 @@ func PostBuild(c *gin.Context) {
} }
// fetch the .drone.yml file from the database // 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 { if err != nil {
log.Errorf("failure to get build config for %s. %s", repo.FullName, err) log.Errorf("failure to get build config for %s. %s", repo.FullName, err)
c.AbortWithError(404, 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 // 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 { if err != nil {
log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err)
} }
key, _ := store.GetKey(c, repo)
netrc, err := remote_.Netrc(user, repo) netrc, err := remote_.Netrc(user, repo)
if err != nil { if err != nil {
log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) 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) log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err)
} }
// IMPORTANT. PLEASE READ var signed bool
// var verified bool
// 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
if os.Getenv("CANARY") == "true" { signature, err := jose.ParseSigned(string(sec))
if err != nil {
var signed bool log.Debugf("cannot parse .drone.yml.sig file. %s", err)
var verified bool } else if len(sec) == 0 {
log.Debugf("cannot parse .drone.yml.sig file. empty file")
signature, err := jose.ParseSigned(string(sec)) } else {
signed = true
output, err := signature.Verify([]byte(repo.Hash))
if err != nil { if err != nil {
log.Debugf("cannot parse .drone.yml.sig file. %s", err) log.Debugf("cannot verify .drone.yml.sig file. %s", err)
} else if len(sec) == 0 { } else if string(output) != string(raw) {
log.Debugf("cannot parse .drone.yml.sig file. empty file") log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw))
} else { } else {
signed = true verified = 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
}
} }
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) log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified)
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"), " "),
},
})
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)},
})
}
} }

View file

@ -1,4 +1,4 @@
package web package server
import ( import (
"fmt" "fmt"

View file

@ -1,18 +1,14 @@
package web package server
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"regexp" "regexp"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/square/go-jose" "github.com/square/go-jose"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/drone/drone/bus" "github.com/drone/drone/bus"
"github.com/drone/drone/engine"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/queue" "github.com/drone/drone/queue"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
@ -22,21 +18,6 @@ import (
"github.com/drone/drone/yaml" "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)\]`) var skipRe = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`)
func PostHook(c *gin.Context) { func PostHook(c *gin.Context) {
@ -141,13 +122,14 @@ func PostHook(c *gin.Context) {
} }
// fetch the build file from the database // 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 { if err != nil {
log.Errorf("failure to get build config for %s. %s", repo.FullName, err) log.Errorf("failure to get build config for %s. %s", repo.FullName, err)
c.AbortWithError(404, err) c.AbortWithError(404, err)
return return
} }
sec, err := remote_.File(user, repo, build, droneSec) sec, err := remote_.File(user, repo, build, config.Shasum)
if err != nil { if err != nil {
log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err)
// NOTE we don't exit on failure. The sec file is optional // NOTE we don't exit on failure. The sec file is optional
@ -168,8 +150,6 @@ func PostHook(c *gin.Context) {
return return
} }
key, _ := store.GetKey(c, repo)
// verify the branches can be built vs skipped // verify the branches can be built vs skipped
branches := yaml.ParseBranch(raw) branches := yaml.ParseBranch(raw)
if !branches.Matches(build.Branch) && build.Event != model.EventTag && build.Event != model.EventDeploy { 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) log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err)
} }
// IMPORTANT. PLEASE READ var signed bool
// var verified bool
// 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
if os.Getenv("CANARY") == "true" { signature, err := jose.ParseSigned(string(sec))
if err != nil {
var signed bool log.Debugf("cannot parse .drone.yml.sig file. %s", err)
var verified bool } else if len(sec) == 0 {
log.Debugf("cannot parse .drone.yml.sig file. empty file")
signature, err := jose.ParseSigned(string(sec)) } else {
signed = true
output, err := signature.Verify([]byte(repo.Hash))
if err != nil { if err != nil {
log.Debugf("cannot parse .drone.yml.sig file. %s", err) log.Debugf("cannot verify .drone.yml.sig file. %s", err)
} else if len(sec) == 0 { } else if string(output) != string(raw) {
log.Debugf("cannot parse .drone.yml.sig file. empty file") log.Debugf("cannot verify .drone.yml.sig file. no match")
} else { } else {
signed = true verified = 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
}
} }
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) log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified)
go engine_.Schedule(c.Copy(), &engine.Task{
User: user, bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build))
Repo: repo, for _, job := range jobs {
Build: build, queue.Publish(c, &queue.Work{
BuildPrev: last, Signed: signed,
Jobs: jobs, Verified: verified,
Keys: key, User: user,
Netrc: netrc, Repo: repo,
Config: string(raw), Build: build,
Secret: string(sec), BuildLast: last,
System: &model.System{ Job: job,
Link: httputil.GetURL(c.Request), Netrc: netrc,
Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), Yaml: string(raw),
Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), Secrets: secs,
Escalates: strings.Split(os.Getenv("ESCALATE_FILTER"), " "), System: &model.System{Link: httputil.GetURL(c.Request)},
}, })
}) }
} }

View file

@ -1,100 +1,106 @@
package web package server
import ( import (
"net/http" "net/http"
"time" "time"
"github.com/gin-gonic/gin"
log "github.com/Sirupsen/logrus"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/shared/crypto" "github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token" "github.com/drone/drone/shared/token"
"github.com/drone/drone/store" "github.com/drone/drone/store"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
) )
func GetLogin(c *gin.Context) { func GetLogin(c *gin.Context) {
remote := remote.FromContext(c)
// when dealing with redirects we may need // when dealing with redirects we may need to adjust the content type. I
// to adjust the content type. I cannot, however, // cannot, however, remember why, so need to revisit this line.
// remember why, so need to revisit this line.
c.Writer.Header().Del("Content-Type") 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 { if err != nil {
log.Errorf("cannot authenticate user. %s", err) logrus.Errorf("cannot authenticate user. %s", err)
c.Redirect(303, "/login?error=oauth_error") c.Redirect(303, "/login?error=oauth_error")
return return
} }
// this will happen when the user is redirected by // this will happen when the user is redirected by the remote provider as
// the remote provide as part of the oauth dance. // part of the authorization workflow.
if tmpuser == nil { if tmpuser == nil {
return return
} }
config := ToConfig(c)
// get the user from the database // get the user from the database
u, err := store.GetUserLogin(c, tmpuser.Login) u, err := store.GetUserLogin(c, tmpuser.Login)
if err != nil { 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 // if self-registration is disabled we should return a not authorized error
// return a notAuthorized error. the only exception if !config.Open {
// is if no users exist yet in the system we'll proceed. logrus.Errorf("cannot register %s. registration closed", tmpuser.Login)
if !open && count != 0 {
log.Errorf("cannot register %s. registration closed", tmpuser.Login)
c.Redirect(303, "/login?error=access_denied") c.Redirect(303, "/login?error=access_denied")
return return
} }
// create the user account // create the user account
u = &model.User{} u = &model.User{
u.Login = tmpuser.Login Login: tmpuser.Login,
u.Token = tmpuser.Token Token: tmpuser.Token,
u.Secret = tmpuser.Secret Secret: tmpuser.Secret,
u.Email = tmpuser.Email Email: tmpuser.Email,
u.Avatar = tmpuser.Avatar Avatar: tmpuser.Avatar,
u.Hash = crypto.Rand() Hash: crypto.Rand(),
}
// insert the user into the database // insert the user into the database
if err := store.CreateUser(c, u); err != nil { 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") c.Redirect(303, "/login?error=internal_error")
return 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 // update the user meta data and authorization data.
// data and cache in the datastore.
u.Token = tmpuser.Token u.Token = tmpuser.Token
u.Secret = tmpuser.Secret u.Secret = tmpuser.Secret
u.Email = tmpuser.Email u.Email = tmpuser.Email
u.Avatar = tmpuser.Avatar u.Avatar = tmpuser.Avatar
if err := store.UpdateUser(c, u); err != nil { 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") c.Redirect(303, "/login?error=internal_error")
return 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() exp := time.Now().Add(time.Hour * 72).Unix()
token := token.New(token.SessToken, u.Login) token := token.New(token.SessToken, u.Login)
tokenstr, err := token.SignExpires(u.Hash, exp) tokenstr, err := token.SignExpires(u.Hash, exp)
if err != nil { 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") c.Redirect(303, "/login?error=internal_error")
return return
} }
@ -109,15 +115,12 @@ func GetLogin(c *gin.Context) {
} }
func GetLogout(c *gin.Context) { func GetLogout(c *gin.Context) {
httputil.DelCookie(c.Writer, c.Request, "user_sess") httputil.DelCookie(c.Writer, c.Request, "user_sess")
httputil.DelCookie(c.Writer, c.Request, "user_last") httputil.DelCookie(c.Writer, c.Request, "user_last")
c.Redirect(303, "/login") c.Redirect(303, "/login")
} }
func GetLoginToken(c *gin.Context) { func GetLoginToken(c *gin.Context) {
remote := remote.FromContext(c)
in := &tokenPayload{} in := &tokenPayload{}
err := c.Bind(in) err := c.Bind(in)
if err != nil { if err != nil {
@ -125,7 +128,7 @@ func GetLoginToken(c *gin.Context) {
return return
} }
login, err := remote.Auth(in.Access, in.Refresh) login, err := remote.Auth(c, in.Access, in.Refresh)
if err != nil { if err != nil {
c.AbortWithError(http.StatusUnauthorized, err) c.AbortWithError(http.StatusUnauthorized, err)
return return
@ -156,3 +159,9 @@ type tokenPayload struct {
Refresh string `json:"refresh_token,omitempty"` Refresh string `json:"refresh_token,omitempty"`
Expires int64 `json:"expires_in,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)
}

View file

@ -1,4 +1,4 @@
package web package server
import ( import (
"net/http" "net/http"
@ -30,7 +30,7 @@ func ShowIndex(c *gin.Context) {
} }
// filter to only show the currently active ones // filter to only show the currently active ones
activeRepos, err := store.GetRepoListOf(c,repos) activeRepos, err := store.GetRepoListOf(c, repos)
if err != nil { if err != nil {
c.String(400, err.Error()) c.String(400, err.Error())
return 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) { func ShowRepo(c *gin.Context) {
user := session.User(c) user := session.User(c)
repo := session.Repo(c) repo := session.Repo(c)
@ -136,7 +116,6 @@ func ShowRepoConf(c *gin.Context) {
user := session.User(c) user := session.User(c)
repo := session.Repo(c) repo := session.Repo(c)
key, _ := store.GetKey(c, repo)
token, _ := token.New( token, _ := token.New(
token.CsrfToken, token.CsrfToken,
@ -146,7 +125,6 @@ func ShowRepoConf(c *gin.Context) {
c.HTML(200, "repo_config.html", gin.H{ c.HTML(200, "repo_config.html", gin.H{
"User": user, "User": user,
"Repo": repo, "Repo": repo,
"Key": key,
"Csrf": token, "Csrf": token,
"Link": httputil.GetURL(c.Request), "Link": httputil.GetURL(c.Request),
}) })
@ -227,10 +205,3 @@ func ShowBuild(c *gin.Context) {
"Csrf": csrf, "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})
}

View file

@ -1,4 +1,4 @@
package api package server
import ( import (
"fmt" "fmt"

View file

@ -1,16 +1,12 @@
package api package server
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/yaml.v2"
"github.com/drone/drone/cache" "github.com/drone/drone/cache"
"github.com/drone/drone/model"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/router/middleware/session" "github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/crypto" "github.com/drone/drone/shared/crypto"
@ -74,19 +70,9 @@ func PostRepo(c *gin.Context) {
sig, 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 // activate the repository before we make any
// local changes to the database. // local changes to the database.
err = remote.Activate(user, r, keys, link) err = remote.Activate(user, r, link)
if err != nil { if err != nil {
c.String(500, err.Error()) c.String(500, err.Error())
return return
@ -98,12 +84,6 @@ func PostRepo(c *gin.Context) {
c.String(500, err.Error()) c.String(500, err.Error())
return return
} }
keys.RepoID = r.ID
err = store.CreateKey(c, keys)
if err != nil {
c.String(500, err.Error())
return
}
c.JSON(200, r) c.JSON(200, r)
} }
@ -157,45 +137,6 @@ func GetRepo(c *gin.Context) {
c.JSON(http.StatusOK, session.Repo(c)) 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) { func DeleteRepo(c *gin.Context) {
remote := remote.FromContext(c) remote := remote.FromContext(c)
repo := session.Repo(c) repo := session.Repo(c)
@ -210,44 +151,3 @@ func DeleteRepo(c *gin.Context) {
remote.Deactivate(user, repo, httputil.GetURL(c.Request)) remote.Deactivate(user, repo, httputil.GetURL(c.Request))
c.Writer.WriteHeader(http.StatusOK) 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) {
}

View file

@ -1,4 +1,4 @@
package api package server
import ( import (
"github.com/drone/drone/model" "github.com/drone/drone/model"

View file

@ -1,4 +1,4 @@
package api package server
import ( import (
"io/ioutil" "io/ioutil"

View file

@ -1,4 +1,4 @@
package web package server
import ( import (
"strings" "strings"

View file

@ -1,4 +1,4 @@
package web package server
import ( import (
"bufio" "bufio"
@ -19,14 +19,9 @@ import (
"github.com/manucorporat/sse" "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 // GetRepoEvents will upgrade the connection to a Websocket and will stream
// event updates to the browser. // event updates to the browser.
func GetRepoEvents2(c *gin.Context) { func GetRepoEvents(c *gin.Context) {
repo := session.Repo(c) repo := session.Repo(c)
c.Writer.Header().Set("Content-Type", "text/event-stream") 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) repo := session.Repo(c)
buildn, _ := strconv.Atoi(c.Param("build")) buildn, _ := strconv.Atoi(c.Param("build"))

View file

@ -11,6 +11,7 @@
// - application/json // - application/json
// //
// swagger:meta // 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/

85
server/swagger/swagger.go Normal file
View file

@ -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
}

View file

@ -1,4 +1,4 @@
package api package server
import ( import (
"net/http" "net/http"
@ -6,31 +6,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/drone/drone/cache" "github.com/drone/drone/cache"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/session" "github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/crypto" "github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/token" "github.com/drone/drone/shared/token"
"github.com/drone/drone/store" "github.com/drone/drone/store"
) )
// swagger:route GET /user user getUser
//
// Get the currently authenticated user.
//
// Responses:
// 200: user
//
func GetSelf(c *gin.Context) { func GetSelf(c *gin.Context) {
c.JSON(200, session.User(c)) 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) { func GetFeed(c *gin.Context) {
repos, err := cache.GetRepos(c, session.User(c)) repos, err := cache.GetRepos(c, session.User(c))
if err != nil { if err != nil {
@ -46,13 +31,6 @@ func GetFeed(c *gin.Context) {
c.JSON(200, feed) 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) { func GetRepos(c *gin.Context) {
repos, err := cache.GetRepos(c, session.User(c)) repos, err := cache.GetRepos(c, session.User(c))
if err != nil { if err != nil {
@ -105,27 +83,3 @@ func DeleteToken(c *gin.Context) {
} }
c.String(http.StatusOK, tokenstr) 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
}

View file

@ -1,4 +1,4 @@
package api package server
import ( import (
"net/http" "net/http"
@ -10,13 +10,6 @@ import (
"github.com/drone/drone/store" "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) { func GetUsers(c *gin.Context) {
users, err := store.GetUserList(c) users, err := store.GetUserList(c)
if err != nil { 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) { func GetUser(c *gin.Context) {
user, err := store.GetUserLogin(c, c.Param("login")) user, err := store.GetUserLogin(c, c.Param("login"))
if err != nil { if err != nil {
c.String(404, "Cannot find user. %s", err) c.String(404, "Cannot find user. %s", err)
} else { return
c.JSON(200, user)
} }
c.JSON(200, user)
} }
func PatchUser(c *gin.Context) { func PatchUser(c *gin.Context) {
@ -74,31 +60,20 @@ func PostUser(c *gin.Context) {
c.String(http.StatusBadRequest, err.Error()) c.String(http.StatusBadRequest, err.Error())
return return
} }
user := &model.User{
user := &model.User{} Active: true,
user.Login = in.Login Login: in.Login,
user.Email = in.Email Email: in.Email,
user.Admin = in.Admin Avatar: in.Avatar,
user.Avatar = in.Avatar Hash: crypto.Rand(),
user.Active = true }
user.Hash = crypto.Rand() if err = store.CreateUser(c, user); err != nil {
err = store.CreateUser(c, user)
if err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return
} }
c.JSON(http.StatusOK, user) 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) { func DeleteUser(c *gin.Context) {
user, err := store.GetUserLogin(c, c.Param("login")) user, err := store.GetUserLogin(c, c.Param("login"))
if err != nil { if err != nil {
@ -107,7 +82,7 @@ func DeleteUser(c *gin.Context) {
} }
if err = store.DeleteUser(c, user); err != nil { if err = store.DeleteUser(c, user); err != nil {
c.String(500, "Error deleting user. %s", err) c.String(500, "Error deleting user. %s", err)
} else { return
c.String(200, "")
} }
c.String(200, "")
} }

View file

@ -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")
}

View file

@ -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=?"

View file

@ -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-----
`

View file

@ -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=?
`

View file

@ -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()
})
})
}

View file

@ -58,7 +58,6 @@ func TestUsers(t *testing.T) {
Email: "foo@bar.com", Email: "foo@bar.com",
Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8",
Active: true, Active: true,
Admin: true,
} }
s.CreateUser(&user) s.CreateUser(&user)
@ -71,7 +70,6 @@ func TestUsers(t *testing.T) {
g.Assert(user.Email).Equal(getuser.Email) g.Assert(user.Email).Equal(getuser.Email)
g.Assert(user.Avatar).Equal(getuser.Avatar) g.Assert(user.Avatar).Equal(getuser.Avatar)
g.Assert(user.Active).Equal(getuser.Active) g.Assert(user.Active).Equal(getuser.Active)
g.Assert(user.Admin).Equal(getuser.Admin)
}) })
g.It("Should Get a User By Login", func() { g.It("Should Get a User By Login", func() {

View file

@ -54,18 +54,6 @@ type Store interface {
// DeleteRepo deletes a user repository. // DeleteRepo deletes a user repository.
DeleteRepo(*model.Repo) error 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 gets a list of repository secrets
GetSecretList(*model.Repo) ([]*model.Secret, error) GetSecretList(*model.Repo) ([]*model.Secret, error)
@ -125,21 +113,6 @@ type Store interface {
// WriteLog writes the job logs to the datastore. // WriteLog writes the job logs to the datastore.
WriteLog(*model.Job, io.Reader) error 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. // 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) 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) { func GetSecretList(c context.Context, r *model.Repo) ([]*model.Secret, error) {
return FromContext(c).GetSecretList(r) 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 { func WriteLog(c context.Context, job *model.Job, r io.Reader) error {
return FromContext(c).WriteLog(job, r) 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)
}

View file

@ -34,9 +34,6 @@ html
i.material-icons expand_more i.material-icons expand_more
div.dropdown-menu.dropdown-menu-right div.dropdown-menu.dropdown-menu-right
a.dropdown-item[href="/settings/profile"] Profile 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 a.dropdown-item[href="/logout"] Logout

View file

@ -65,10 +65,6 @@ block content
else else
input#trusted[type="checkbox"][hidden="hidden"] input#trusted[type="checkbox"][hidden="hidden"]
label.switch[for="trusted"] 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.row
div.col-md-12 div.col-md-12
div.alert.alert-danger div.alert.alert-danger

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
})
})
}