From 4d4003a9a16e69f3919cdbadbef6e95002714389 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Thu, 28 Apr 2016 14:10:32 -0700 Subject: [PATCH 01/10] moved 0.5 out of feature flag, removed deprecated 0.4 code and features --- .drone.yml | 2 +- api/build.go | 115 +++------ api/node.go | 80 ------ drone/drone.go | 2 +- drone/main.go | 62 ----- drone/server/server.go | 7 +- engine/bus.go | 48 ---- engine/bus_test.go | 50 ---- engine/context.go | 23 -- engine/engine.go | 444 ---------------------------------- engine/pool.go | 86 ------- engine/pool_test.go | 89 ------- engine/types.go | 24 -- engine/updater.go | 66 ----- engine/util.go | 35 --- engine/worker.go | 115 --------- model/node.go | 36 --- router/middleware/engine.go | 28 --- router/router.go | 34 +-- store/datastore/nodes.go | 48 ---- store/datastore/nodes_test.go | 101 -------- store/store.go | 35 --- template/amber/base.amber | 3 - web/hook.go | 104 +++----- web/pages.go | 29 +-- web/stream.go | 80 +++--- web/stream2.go | 121 --------- 27 files changed, 120 insertions(+), 1747 deletions(-) delete mode 100644 api/node.go delete mode 100644 drone/main.go delete mode 100644 engine/bus.go delete mode 100644 engine/bus_test.go delete mode 100644 engine/context.go delete mode 100644 engine/engine.go delete mode 100644 engine/pool.go delete mode 100644 engine/pool_test.go delete mode 100644 engine/types.go delete mode 100644 engine/updater.go delete mode 100644 engine/util.go delete mode 100644 engine/worker.go delete mode 100644 model/node.go delete mode 100644 router/middleware/engine.go delete mode 100644 store/datastore/nodes.go delete mode 100644 store/datastore/nodes_test.go delete mode 100644 web/stream2.go diff --git a/.drone.yml b/.drone.yml index 52b56f9bf..e78534349 100644 --- a/.drone.yml +++ b/.drone.yml @@ -35,7 +35,7 @@ publish: password: $$DOCKER_PASS email: $$DOCKER_EMAIL repo: drone/drone - tag: [ "latest", "0.4.2" ] + tag: [ "0.5.0" ] when: repo: drone/drone branch: master diff --git a/api/build.go b/api/build.go index d0bd37388..3ca688a44 100644 --- a/api/build.go +++ b/api/build.go @@ -5,14 +5,11 @@ import ( "io" "net/http" "os" - "path/filepath" "strconv" - "strings" "time" log "github.com/Sirupsen/logrus" "github.com/drone/drone/bus" - "github.com/drone/drone/engine" "github.com/drone/drone/queue" "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" @@ -33,10 +30,7 @@ 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) - } + droneSec = fmt.Sprintf("%s.sig", droneYml) } func GetBuilds(c *gin.Context) { @@ -135,7 +129,6 @@ func GetBuildLogs(c *gin.Context) { } func DeleteBuild(c *gin.Context) { - engine_ := engine.FromContext(c) repo := session.Repo(c) // parse the build number and job sequence number from @@ -155,17 +148,8 @@ func DeleteBuild(c *gin.Context) { return } - if os.Getenv("CANARY") == "true" { - bus.Publish(c, bus.NewEvent(bus.Cancelled, repo, build, job)) - return - } - - node, err := store.GetNode(c, job.NodeID) - if err != nil { - c.AbortWithError(404, err) - return - } - engine_.Cancel(build.ID, job.ID, node) + bus.Publish(c, bus.NewEvent(bus.Cancelled, repo, build, job)) + c.String(204, "") } func PostBuild(c *gin.Context) { @@ -218,7 +202,6 @@ func PostBuild(c *gin.Context) { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } - key, _ := store.GetKey(c, repo) netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) @@ -296,72 +279,42 @@ func PostBuild(c *gin.Context) { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } - // IMPORTANT. PLEASE READ - // - // The below code uses a feature flag to switch between the current - // build engine and the exerimental 0.5 build engine. This can be - // enabled using with the environment variable CANARY=true + var signed bool + var verified bool - if os.Getenv("CANARY") == "true" { - - var signed bool - var verified bool - - signature, err := jose.ParseSigned(string(sec)) + signature, err := jose.ParseSigned(string(sec)) + if err != nil { + log.Debugf("cannot parse .drone.yml.sig file. %s", err) + } else if len(sec) == 0 { + log.Debugf("cannot parse .drone.yml.sig file. empty file") + } else { + signed = true + output, err := signature.Verify([]byte(repo.Hash)) if err != nil { - log.Debugf("cannot parse .drone.yml.sig file. %s", err) - } else if len(sec) == 0 { - log.Debugf("cannot parse .drone.yml.sig file. empty file") + log.Debugf("cannot verify .drone.yml.sig file. %s", err) + } else if string(output) != string(raw) { + log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) } else { - signed = true - output, err := signature.Verify([]byte(repo.Hash)) - if err != nil { - log.Debugf("cannot verify .drone.yml.sig file. %s", err) - } else if string(output) != string(raw) { - log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) - } else { - verified = true - } + verified = true } - - log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) - - bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) - for _, job := range jobs { - queue.Publish(c, &queue.Work{ - Signed: signed, - Verified: verified, - User: user, - Repo: repo, - Build: build, - BuildLast: last, - Job: job, - Netrc: netrc, - Yaml: string(raw), - Secrets: secs, - System: &model.System{Link: httputil.GetURL(c.Request)}, - }) - } - return // EXIT NOT TO AVOID THE 0.4 ENGINE CODE BELOW } - engine_ := engine.FromContext(c) - go engine_.Schedule(c.Copy(), &engine.Task{ - User: user, - Repo: repo, - Build: build, - BuildPrev: last, - Jobs: jobs, - Keys: key, - Netrc: netrc, - Config: string(raw), - Secret: string(sec), - System: &model.System{ - Link: httputil.GetURL(c.Request), - Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), - Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), - Escalates: strings.Split(os.Getenv("ESCALATE_FILTER"), " "), - }, - }) + log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) + bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) + for _, job := range jobs { + queue.Publish(c, &queue.Work{ + Signed: signed, + Verified: verified, + User: user, + Repo: repo, + Build: build, + BuildLast: last, + Job: job, + Netrc: netrc, + Yaml: string(raw), + Secrets: secs, + System: &model.System{Link: httputil.GetURL(c.Request)}, + }) + } } diff --git a/api/node.go b/api/node.go deleted file mode 100644 index 592ec0c1b..000000000 --- a/api/node.go +++ /dev/null @@ -1,80 +0,0 @@ -package api - -import ( - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - - "github.com/drone/drone/engine" - "github.com/drone/drone/model" - "github.com/drone/drone/store" -) - -func GetNodes(c *gin.Context) { - nodes, err := store.GetNodeList(c) - if err != nil { - c.String(400, err.Error()) - } else { - c.JSON(200, nodes) - } -} - -func GetNode(c *gin.Context) { - -} - -func PostNode(c *gin.Context) { - engine := engine.FromContext(c) - - in := struct { - Addr string `json:"address"` - Arch string `json:"architecture"` - Cert string `json:"cert"` - Key string `json:"key"` - CA string `json:"ca"` - }{} - err := c.Bind(&in) - if err != nil { - c.AbortWithStatus(http.StatusBadRequest) - return - } - - node := &model.Node{} - node.Addr = in.Addr - node.Cert = in.Cert - node.Key = in.Key - node.CA = in.CA - node.Arch = "linux_amd64" - - err = engine.Allocate(node) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - - err = store.CreateNode(c, node) - if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) - return - } - - c.IndentedJSON(http.StatusOK, node) -} - -func DeleteNode(c *gin.Context) { - engine := engine.FromContext(c) - - id, _ := strconv.Atoi(c.Param("node")) - node, err := store.GetNode(c, int64(id)) - if err != nil { - c.AbortWithStatus(http.StatusNotFound) - return - } - err = store.DeleteNode(c, node) - if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) - return - } - engine.Deallocate(node) -} diff --git a/drone/drone.go b/drone/drone.go index 2a92660da..cfc034d60 100644 --- a/drone/drone.go +++ b/drone/drone.go @@ -12,7 +12,7 @@ import ( _ "github.com/joho/godotenv/autoload" ) -func main2() { +func main() { envflag.Parse() app := cli.NewApp() diff --git a/drone/main.go b/drone/main.go deleted file mode 100644 index e08585855..000000000 --- a/drone/main.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "net/http" - "os" - "time" - - "github.com/drone/drone/router" - "github.com/drone/drone/router/middleware" - - "github.com/Sirupsen/logrus" - "github.com/gin-gonic/contrib/ginrus" - "github.com/ianschenck/envflag" - _ "github.com/joho/godotenv/autoload" -) - -var ( - addr = envflag.String("SERVER_ADDR", ":8000", "") - cert = envflag.String("SERVER_CERT", "", "") - key = envflag.String("SERVER_KEY", "", "") - - debug = envflag.Bool("DEBUG", false, "") -) - -func main() { - if os.Getenv("CANARY") == "true" { - main2() - return - } - - envflag.Parse() - - // debug level if requested by user - if *debug { - logrus.SetLevel(logrus.DebugLevel) - } else { - logrus.SetLevel(logrus.WarnLevel) - } - - // setup the server and start the listener - handler := router.Load( - ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), - middleware.Version, - middleware.Queue(), - middleware.Stream(), - middleware.Bus(), - middleware.Cache(), - middleware.Store(), - middleware.Remote(), - middleware.Engine(), - ) - - if *cert != "" { - logrus.Fatal( - http.ListenAndServeTLS(*addr, *cert, *key, handler), - ) - } else { - logrus.Fatal( - http.ListenAndServe(*addr, handler), - ) - } -} diff --git a/drone/server/server.go b/drone/server/server.go index e22be9467..600a84fa9 100644 --- a/drone/server/server.go +++ b/drone/server/server.go @@ -86,7 +86,6 @@ func start(c *cli.Context) error { middleware.Cache(), middleware.Store(), middleware.Remote(), - middleware.Engine(), ) if c.String("server-cert") != "" { @@ -109,7 +108,7 @@ 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. +has known bugs and compatibility issues. It is not intended for general use. Please consider using the latest stable release instead: @@ -119,8 +118,8 @@ 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: +If you are interested in testing this experimental build AND assisting with +development you may proceed by setting the following environment: I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION=true I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS=true diff --git a/engine/bus.go b/engine/bus.go deleted file mode 100644 index 3ab1ece03..000000000 --- a/engine/bus.go +++ /dev/null @@ -1,48 +0,0 @@ -package engine - -import ( - "sync" -) - -type eventbus struct { - sync.Mutex - subs map[chan *Event]bool -} - -// New creates a new eventbus that manages a list of -// subscribers to which events are published. -func newEventbus() *eventbus { - return &eventbus{ - subs: make(map[chan *Event]bool), - } -} - -// Subscribe adds the channel to the list of -// subscribers. Each subscriber in the list will -// receive broadcast events. -func (b *eventbus) subscribe(c chan *Event) { - b.Lock() - b.subs[c] = true - b.Unlock() -} - -// Unsubscribe removes the channel from the -// list of subscribers. -func (b *eventbus) unsubscribe(c chan *Event) { - b.Lock() - delete(b.subs, c) - b.Unlock() -} - -// Send dispatches a message to all subscribers. -func (b *eventbus) send(event *Event) { - b.Lock() - defer b.Unlock() - - for s := range b.subs { - go func(c chan *Event) { - defer recover() - c <- event - }(s) - } -} diff --git a/engine/bus_test.go b/engine/bus_test.go deleted file mode 100644 index 85b2e681e..000000000 --- a/engine/bus_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package engine - -import ( - "testing" - - . "github.com/franela/goblin" -) - -func TestBus(t *testing.T) { - g := Goblin(t) - g.Describe("Event bus", func() { - - g.It("Should unsubscribe", func() { - c1 := make(chan *Event) - c2 := make(chan *Event) - b := newEventbus() - b.subscribe(c1) - b.subscribe(c2) - g.Assert(len(b.subs)).Equal(2) - }) - - g.It("Should subscribe", func() { - c1 := make(chan *Event) - c2 := make(chan *Event) - b := newEventbus() - b.subscribe(c1) - b.subscribe(c2) - g.Assert(len(b.subs)).Equal(2) - b.unsubscribe(c1) - b.unsubscribe(c2) - g.Assert(len(b.subs)).Equal(0) - }) - - g.It("Should send", func() { - em := map[string]bool{"foo": true, "bar": true} - e1 := &Event{Name: "foo"} - e2 := &Event{Name: "bar"} - c := make(chan *Event) - b := newEventbus() - b.subscribe(c) - b.send(e1) - b.send(e2) - r1 := <-c - r2 := <-c - g.Assert(em[r1.Name]).Equal(true) - g.Assert(em[r2.Name]).Equal(true) - }) - }) - -} diff --git a/engine/context.go b/engine/context.go deleted file mode 100644 index 2321fa071..000000000 --- a/engine/context.go +++ /dev/null @@ -1,23 +0,0 @@ -package engine - -import ( - "golang.org/x/net/context" -) - -const key = "engine" - -// Setter defines a context that enables setting values. -type Setter interface { - Set(string, interface{}) -} - -// FromContext returns the Engine associated with this context. -func FromContext(c context.Context) Engine { - return c.Value(key).(Engine) -} - -// ToContext adds the Engine to this context if it supports -// the Setter interface. -func ToContext(c Setter, engine Engine) { - c.Set(key, engine) -} diff --git a/engine/engine.go b/engine/engine.go deleted file mode 100644 index 32a2c6a10..000000000 --- a/engine/engine.go +++ /dev/null @@ -1,444 +0,0 @@ -package engine - -import ( - "bytes" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "runtime" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/stdcopy" - "github.com/drone/drone/model" - "github.com/drone/drone/shared/docker" - "github.com/drone/drone/store" - "github.com/samalba/dockerclient" - "golang.org/x/net/context" -) - -type Engine interface { - Schedule(context.Context, *Task) - Cancel(int64, int64, *model.Node) error - Stream(int64, int64, *model.Node) (io.ReadCloser, error) - Deallocate(*model.Node) - Allocate(*model.Node) error - Subscribe(chan *Event) - Unsubscribe(chan *Event) -} - -var ( - // options to fetch the stdout and stderr logs - logOpts = &dockerclient.LogOptions{ - Stdout: true, - Stderr: true, - } - - // options to fetch the stdout and stderr logs - // by tailing the output. - logOptsTail = &dockerclient.LogOptions{ - Follow: true, - Stdout: true, - Stderr: true, - } - - // error when the system cannot find logs - errLogging = errors.New("Logs not available") -) - -type engine struct { - bus *eventbus - updater *updater - pool *pool - envs []string -} - -// Load creates a new build engine, loaded with registered nodes from the -// database. The registered nodes are added to the pool of nodes to immediately -// start accepting workloads. -func Load(s store.Store) Engine { - engine := &engine{} - engine.bus = newEventbus() - engine.pool = newPool() - engine.updater = &updater{engine.bus} - - // quick fix to propagate HTTP_PROXY variables - // throughout the build environment. - var proxyVars = []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"} - for _, proxyVar := range proxyVars { - proxyVal := os.Getenv(proxyVar) - if len(proxyVal) != 0 { - engine.envs = append(engine.envs, proxyVar+"="+proxyVal) - } - } - - nodes, err := s.GetNodeList() - if err != nil { - log.Fatalf("failed to get nodes from database. %s", err) - } - for _, node := range nodes { - engine.pool.allocate(node) - log.Infof("registered docker daemon %s", node.Addr) - } - - return engine -} - -// Cancel cancels the job running on the specified Node. -func (e *engine) Cancel(build, job int64, node *model.Node) error { - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - return err - } - - id := fmt.Sprintf("drone_build_%d_job_%d", build, job) - return client.StopContainer(id, 30) -} - -// Stream streams the job output from the specified Node. -func (e *engine) Stream(build, job int64, node *model.Node) (io.ReadCloser, error) { - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - log.Errorf("cannot create Docker client for node %s", node.Addr) - return nil, err - } - - id := fmt.Sprintf("drone_build_%d_job_%d", build, job) - log.Debugf("streaming container logs %s", id) - return client.ContainerLogs(id, logOptsTail) -} - -// Subscribe subscribes the channel to all build events. -func (e *engine) Subscribe(c chan *Event) { - e.bus.subscribe(c) -} - -// Unsubscribe unsubscribes the channel from all build events. -func (e *engine) Unsubscribe(c chan *Event) { - e.bus.unsubscribe(c) -} - -func (e *engine) Allocate(node *model.Node) error { - - // run the full build! - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - log.Errorf("error creating docker client %s. %s.", node.Addr, err) - return err - } - version, err := client.Version() - if err != nil { - log.Errorf("error connecting to docker daemon %s. %s.", node.Addr, err) - return err - } - - log.Infof("registered docker daemon %s running version %s", node.Addr, version.Version) - e.pool.allocate(node) - return nil -} - -func (e *engine) Deallocate(n *model.Node) { - nodes := e.pool.list() - for _, node := range nodes { - if node.ID == n.ID { - log.Infof("un-registered docker daemon %s", node.Addr) - e.pool.deallocate(node) - break - } - } -} - -func (e *engine) Schedule(c context.Context, req *Task) { - node := <-e.pool.reserve() - - // since we are probably running in a go-routine - // make sure we recover from any panics so that - // a bug doesn't crash the whole system. - defer func() { - if err := recover(); err != nil { - - const size = 64 << 10 - buf := make([]byte, size) - buf = buf[:runtime.Stack(buf, false)] - log.Errorf("panic running build: %v\n%s", err, string(buf)) - } - e.pool.release(node) - }() - - // update the node that was allocated to each job - func(id int64) { - for _, job := range req.Jobs { - job.NodeID = id - store.UpdateJob(c, job) - } - }(node.ID) - - // run the full build! - client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA) - if err != nil { - log.Errorln("error creating docker client", err) - } - - // update the build state if any of the sub-tasks - // had a non-success status - req.Build.Started = time.Now().UTC().Unix() - req.Build.Status = model.StatusRunning - e.updater.SetBuild(c, req) - - // run all bulid jobs - for _, job := range req.Jobs { - req.Job = job - e.runJob(c, req, e.updater, client) - } - - // update overall status based on each job - req.Build.Status = model.StatusSuccess - for _, job := range req.Jobs { - if job.Status != model.StatusSuccess { - req.Build.Status = job.Status - break - } - } - req.Build.Finished = time.Now().UTC().Unix() - err = e.updater.SetBuild(c, req) - if err != nil { - log.Errorf("error updating build completion status. %s", err) - } - - // run notifications - err = e.runJobNotify(req, client) - if err != nil { - log.Errorf("error executing notification step. %s", err) - } -} - -func newDockerClient(addr, cert, key, ca string) (dockerclient.Client, error) { - var tlc *tls.Config - - // create the Docket client TLS config - if len(cert) != 0 { - pem, err := tls.X509KeyPair([]byte(cert), []byte(key)) - if err != nil { - log.Errorf("error loading X509 key pair. %s.", err) - return dockerclient.NewDockerClient(addr, nil) - } - - // create the TLS configuration for secure - // docker communications. - tlc = &tls.Config{} - tlc.Certificates = []tls.Certificate{pem} - - // use the certificate authority if provided. - // else don't use a certificate authority and set - // skip verify to true - if len(ca) != 0 { - log.Infof("creating docker client %s with CA", addr) - pool := x509.NewCertPool() - pool.AppendCertsFromPEM([]byte(ca)) - tlc.RootCAs = pool - - } else { - log.Infof("creating docker client %s WITHOUT CA", addr) - tlc.InsecureSkipVerify = true - } - } - - // create the Docker client. In this version of Drone (alpha) - // we do not spread builds across clients, but this can and - // (probably) will change in the future. - return dockerclient.NewDockerClient(addr, tlc) -} - -func (e *engine) runJob(c context.Context, r *Task, updater *updater, client dockerclient.Client) error { - - name := fmt.Sprintf("drone_build_%d_job_%d", r.Build.ID, r.Job.ID) - - defer func() { - if r.Job.Status == model.StatusRunning { - r.Job.Status = model.StatusError - r.Job.Finished = time.Now().UTC().Unix() - r.Job.ExitCode = 255 - } - if r.Job.Status == model.StatusPending { - r.Job.Status = model.StatusError - r.Job.Started = time.Now().UTC().Unix() - r.Job.Finished = time.Now().UTC().Unix() - r.Job.ExitCode = 255 - } - updater.SetJob(c, r) - - client.KillContainer(name, "9") - client.RemoveContainer(name, true, true) - }() - - // marks the task as running - r.Job.Status = model.StatusRunning - r.Job.Started = time.Now().UTC().Unix() - - // encode the build payload to write to stdin - // when launching the build container - in, err := encodeToLegacyFormat(r) - if err != nil { - log.Errorf("failure to marshal work. %s", err) - return err - } - - // CREATE AND START BUILD - args := DefaultBuildArgs - if r.Build.Event == model.EventPull { - args = DefaultPullRequestArgs - } - args = append(args, "--") - args = append(args, string(in)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - Env: e.envs, - HostConfig: dockerclient.HostConfig{ - Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, - MemorySwappiness: -1, - }, - Volumes: map[string]struct{}{ - "/var/run/docker.sock": {}, - }, - } - - log.Infof("preparing container %s", name) - client.PullImage(conf.Image, nil) - - _, err = docker.RunDaemon(client, conf, name) - if err != nil { - log.Errorf("error starting build container. %s", err) - return err - } - - // UPDATE STATUS - - err = updater.SetJob(c, r) - if err != nil { - log.Errorf("error updating job status as running. %s", err) - return err - } - - // WAIT FOR OUTPUT - info, builderr := docker.Wait(client, name) - - switch { - case info.State.Running: - // A build unblocked before actually being completed. - log.Errorf("incomplete build: %s", name) - r.Job.ExitCode = 1 - r.Job.Status = model.StatusError - case info.State.ExitCode == 128: - r.Job.ExitCode = info.State.ExitCode - r.Job.Status = model.StatusKilled - case info.State.ExitCode == 130: - r.Job.ExitCode = info.State.ExitCode - r.Job.Status = model.StatusKilled - case builderr != nil: - r.Job.Status = model.StatusError - case info.State.ExitCode != 0: - r.Job.ExitCode = info.State.ExitCode - r.Job.Status = model.StatusFailure - default: - r.Job.Status = model.StatusSuccess - } - - // send the logs to the datastore - var buf bytes.Buffer - rc, err := client.ContainerLogs(name, docker.LogOpts) - if err != nil && builderr != nil { - buf.WriteString("Error launching build") - buf.WriteString(builderr.Error()) - } else if err != nil { - buf.WriteString("Error launching build") - buf.WriteString(err.Error()) - log.Errorf("error opening connection to logs. %s", err) - return err - } else { - defer rc.Close() - stdcopy.StdCopy(&buf, &buf, io.LimitReader(rc, 5000000)) - } - - // update the task in the datastore - r.Job.Finished = time.Now().UTC().Unix() - err = updater.SetJob(c, r) - if err != nil { - log.Errorf("error updating job after completion. %s", err) - return err - } - - err = updater.SetLogs(c, r, ioutil.NopCloser(&buf)) - if err != nil { - log.Errorf("error updating logs. %s", err) - return err - } - - log.Debugf("completed job %d with status %s.", r.Job.ID, r.Job.Status) - return nil -} - -func (e *engine) runJobNotify(r *Task, client dockerclient.Client) error { - - name := fmt.Sprintf("drone_build_%d_notify", r.Build.ID) - - defer func() { - client.KillContainer(name, "9") - client.RemoveContainer(name, true, true) - }() - - // encode the build payload to write to stdin - // when launching the build container - in, err := encodeToLegacyFormat(r) - if err != nil { - log.Errorf("failure to marshal work. %s", err) - return err - } - - args := DefaultNotifyArgs - args = append(args, "--") - args = append(args, string(in)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - Env: e.envs, - HostConfig: dockerclient.HostConfig{ - Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, - MemorySwappiness: -1, - }, - Volumes: map[string]struct{}{ - "/var/run/docker.sock": {}, - }, - } - - log.Infof("preparing container %s", name) - info, err := docker.Run(client, conf, name) - if err != nil { - log.Errorf("Error starting notification container %s. %s", name, err) - } - - // for debugging purposes we print a failed notification executions - // output to the logs. Otherwise we have no way to troubleshoot failed - // notifications. This is temporary code until I've come up with - // a better solution. - if info != nil && info.State.ExitCode != 0 && log.GetLevel() >= log.InfoLevel { - var buf bytes.Buffer - rc, err := client.ContainerLogs(name, docker.LogOpts) - if err == nil { - defer rc.Close() - stdcopy.StdCopy(&buf, &buf, io.LimitReader(rc, 50000)) - } - log.Infof("Notification container %s exited with %d", name, info.State.ExitCode) - log.Infoln(buf.String()) - } - - return err -} diff --git a/engine/pool.go b/engine/pool.go deleted file mode 100644 index a886b47de..000000000 --- a/engine/pool.go +++ /dev/null @@ -1,86 +0,0 @@ -package engine - -import ( - "sync" - - "github.com/drone/drone/model" -) - -type pool struct { - sync.Mutex - nodes map[*model.Node]bool - nodec chan *model.Node -} - -func newPool() *pool { - return &pool{ - nodes: make(map[*model.Node]bool), - nodec: make(chan *model.Node, 999), - } -} - -// Allocate allocates a node to the pool to -// be available to accept work. -func (p *pool) allocate(n *model.Node) bool { - if p.isAllocated(n) { - return false - } - - p.Lock() - p.nodes[n] = true - p.Unlock() - - p.nodec <- n - return true -} - -// IsAllocated is a helper function that returns -// true if the node is currently allocated to -// the pool. -func (p *pool) isAllocated(n *model.Node) bool { - p.Lock() - defer p.Unlock() - _, ok := p.nodes[n] - return ok -} - -// Deallocate removes the node from the pool of -// available nodes. If the node is currently -// reserved and performing work it will finish, -// but no longer be given new work. -func (p *pool) deallocate(n *model.Node) { - p.Lock() - defer p.Unlock() - delete(p.nodes, n) -} - -// List returns a list of all model.Nodes currently -// allocated to the pool. -func (p *pool) list() []*model.Node { - p.Lock() - defer p.Unlock() - - var nodes []*model.Node - for n := range p.nodes { - nodes = append(nodes, n) - } - return nodes -} - -// Reserve reserves the next available node to -// start doing work. Once work is complete, the -// node should be released back to the pool. -func (p *pool) reserve() <-chan *model.Node { - return p.nodec -} - -// Release releases the node back to the pool -// of available nodes. -func (p *pool) release(n *model.Node) bool { - if !p.isAllocated(n) { - return false - } - - p.nodec <- n - return true -} diff --git a/engine/pool_test.go b/engine/pool_test.go deleted file mode 100644 index 852847396..000000000 --- a/engine/pool_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package engine - -import ( - "testing" - - "github.com/drone/drone/model" - "github.com/franela/goblin" -) - -func TestPool(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Pool", func() { - - g.It("Should allocate nodes", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n) - g.Assert(len(pool.nodes)).Equal(1) - g.Assert(len(pool.nodec)).Equal(1) - g.Assert(pool.nodes[n]).Equal(true) - }) - - g.It("Should not re-allocate an allocated node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - g.Assert(pool.allocate(n)).Equal(true) - g.Assert(pool.allocate(n)).Equal(false) - }) - - g.It("Should reserve a node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n) - g.Assert(<-pool.reserve()).Equal(n) - }) - - g.It("Should release a node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n) - g.Assert(len(pool.nodec)).Equal(1) - g.Assert(<-pool.reserve()).Equal(n) - g.Assert(len(pool.nodec)).Equal(0) - pool.release(n) - g.Assert(len(pool.nodec)).Equal(1) - g.Assert(<-pool.reserve()).Equal(n) - g.Assert(len(pool.nodec)).Equal(0) - }) - - g.It("Should not release an unallocated node", func() { - n := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.nodec)).Equal(0) - pool.release(n) - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.nodec)).Equal(0) - pool.release(nil) - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.nodec)).Equal(0) - }) - - g.It("Should list all allocated nodes", func() { - n1 := &model.Node{Addr: "unix:///var/run/docker.sock"} - n2 := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n1) - pool.allocate(n2) - g.Assert(len(pool.nodes)).Equal(2) - g.Assert(len(pool.nodec)).Equal(2) - g.Assert(len(pool.list())).Equal(2) - }) - - g.It("Should remove a node", func() { - n1 := &model.Node{Addr: "unix:///var/run/docker.sock"} - n2 := &model.Node{Addr: "unix:///var/run/docker.sock"} - pool := newPool() - pool.allocate(n1) - pool.allocate(n2) - g.Assert(len(pool.nodes)).Equal(2) - pool.deallocate(n1) - pool.deallocate(n2) - g.Assert(len(pool.nodes)).Equal(0) - g.Assert(len(pool.list())).Equal(0) - }) - - }) -} diff --git a/engine/types.go b/engine/types.go deleted file mode 100644 index 8ba2dd3c3..000000000 --- a/engine/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package engine - -import ( - "github.com/drone/drone/model" -) - -type Event struct { - Name string - Msg []byte -} - -type Task struct { - User *model.User `json:"-"` - Repo *model.Repo `json:"repo"` - Build *model.Build `json:"build"` - BuildPrev *model.Build `json:"build_last"` - Jobs []*model.Job `json:"-"` - Job *model.Job `json:"job"` - Keys *model.Key `json:"keys"` - Netrc *model.Netrc `json:"netrc"` - Config string `json:"config"` - Secret string `json:"secret"` - System *model.System `json:"system"` -} diff --git a/engine/updater.go b/engine/updater.go deleted file mode 100644 index 79c8454d3..000000000 --- a/engine/updater.go +++ /dev/null @@ -1,66 +0,0 @@ -package engine - -import ( - "encoding/json" - "fmt" - "io" - - "github.com/drone/drone/model" - "github.com/drone/drone/remote" - "github.com/drone/drone/store" - "golang.org/x/net/context" -) - -type updater struct { - bus *eventbus -} - -func (u *updater) SetBuild(c context.Context, r *Task) error { - err := store.UpdateBuild(c, r.Build) - if err != nil { - return err - } - - err = remote.FromContext(c).Status(r.User, r.Repo, r.Build, fmt.Sprintf("%s/%s/%d", r.System.Link, r.Repo.FullName, r.Build.Number)) - if err != nil { - // log err - } - - msg, err := json.Marshal(&payload{r.Build, r.Jobs}) - if err != nil { - return err - } - - u.bus.send(&Event{ - Name: r.Repo.FullName, - Msg: msg, - }) - return nil -} - -func (u *updater) SetJob(c context.Context, r *Task) error { - err := store.UpdateJob(c, r.Job) - if err != nil { - return err - } - - msg, err := json.Marshal(&payload{r.Build, r.Jobs}) - if err != nil { - return err - } - - u.bus.send(&Event{ - Name: r.Repo.FullName, - Msg: msg, - }) - return nil -} - -func (u *updater) SetLogs(c context.Context, r *Task, rc io.ReadCloser) error { - return store.WriteLog(c, r.Job, rc) -} - -type payload struct { - *model.Build - Jobs []*model.Job `json:"jobs"` -} diff --git a/engine/util.go b/engine/util.go deleted file mode 100644 index b8f9068bd..000000000 --- a/engine/util.go +++ /dev/null @@ -1,35 +0,0 @@ -package engine - -import ( - "encoding/json" -) - -func encodeToLegacyFormat(t *Task) ([]byte, error) { - // t.System.Plugins = append(t.System.Plugins, "plugins/*") - - // s := map[string]interface{}{} - // s["repo"] = t.Repo - // s["config"] = t.Config - // s["secret"] = t.Secret - // s["job"] = t.Job - // s["system"] = t.System - // s["workspace"] = map[string]interface{}{ - // "netrc": t.Netrc, - // "keys": t.Keys, - // } - // s["build"] = map[string]interface{}{ - // "number": t.Build.Number, - // "status": t.Build.Status, - // "head_commit": map[string]interface{}{ - // "sha": t.Build.Commit, - // "ref": t.Build.Ref, - // "branch": t.Build.Branch, - // "message": t.Build.Message, - // "author": map[string]interface{}{ - // "login": t.Build.Author, - // "email": t.Build.Email, - // }, - // }, - // } - return json.Marshal(t) -} diff --git a/engine/worker.go b/engine/worker.go deleted file mode 100644 index 9b3c8d8e9..000000000 --- a/engine/worker.go +++ /dev/null @@ -1,115 +0,0 @@ -package engine - -import ( - "fmt" - "io" - - "github.com/drone/drone/shared/docker" - "github.com/samalba/dockerclient" -) - -var ( - // name of the build agent container. - DefaultAgent = "drone/drone-exec:latest" - - // default name of the build agent executable - DefaultEntrypoint = []string{"/bin/drone-exec"} - - // default argument to invoke build steps - DefaultBuildArgs = []string{"--pull", "--cache", "--clone", "--build", "--deploy"} - - // default argument to invoke build steps - DefaultPullRequestArgs = []string{"--pull", "--cache", "--clone", "--build"} - - // default arguments to invoke notify steps - DefaultNotifyArgs = []string{"--pull", "--notify"} -) - -type worker struct { - client dockerclient.Client - build *dockerclient.ContainerInfo - notify *dockerclient.ContainerInfo -} - -func newWorker(client dockerclient.Client) *worker { - return &worker{client: client} -} - -// Build executes the clone, build and deploy steps. -func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) { - // the command line arguments passed into the - // build agent container. - args := DefaultBuildArgs - if pr { - args = DefaultPullRequestArgs - } - args = append(args, "--") - args = append(args, string(stdin)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - HostConfig: dockerclient.HostConfig{ - Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"}, - }, - Volumes: map[string]struct{}{ - "/var/run/docker.sock": {}, - }, - } - - // TEMPORARY: always try to pull the new image for now - // since we'll be frequently updating the build image - // for the next few weeks - w.client.PullImage(conf.Image, nil) - - w.build, err = docker.Run(w.client, conf, name) - if err != nil { - return 1, err - } - if w.build.State.OOMKilled { - return 1, fmt.Errorf("OOMKill received") - } - return w.build.State.ExitCode, err -} - -// Notify executes the notification steps. -func (w *worker) Notify(stdin []byte) error { - - args := DefaultNotifyArgs - args = append(args, "--") - args = append(args, string(stdin)) - - conf := &dockerclient.ContainerConfig{ - Image: DefaultAgent, - Entrypoint: DefaultEntrypoint, - Cmd: args, - HostConfig: dockerclient.HostConfig{}, - } - - var err error - w.notify, err = docker.Run(w.client, conf, "") - return err -} - -// Logs returns a multi-reader that fetches the logs -// from the build and deploy agents. -func (w *worker) Logs() (io.ReadCloser, error) { - if w.build == nil { - return nil, errLogging - } - return w.client.ContainerLogs(w.build.Id, logOpts) -} - -// Remove stops and removes the build, deploy and -// notification agents created for the build task. -func (w *worker) Remove() { - if w.notify != nil { - w.client.KillContainer(w.notify.Id, "9") - w.client.RemoveContainer(w.notify.Id, true, true) - } - if w.build != nil { - w.client.KillContainer(w.build.Id, "9") - w.client.RemoveContainer(w.build.Id, true, true) - } -} diff --git a/model/node.go b/model/node.go deleted file mode 100644 index 03e6f8dfc..000000000 --- a/model/node.go +++ /dev/null @@ -1,36 +0,0 @@ -package model - -const ( - Freebsd_386 uint = iota - Freebsd_amd64 - Freebsd_arm - Linux_386 - Linux_amd64 - Linux_arm - Linux_arm64 - Solaris_amd64 - Windows_386 - Windows_amd64 -) - -var Archs = map[string]uint{ - "freebsd_386": Freebsd_386, - "freebsd_amd64": Freebsd_amd64, - "freebsd_arm": Freebsd_arm, - "linux_386": Linux_386, - "linux_amd64": Linux_amd64, - "linux_arm": Linux_arm, - "linux_arm64": Linux_arm64, - "solaris_amd64": Solaris_amd64, - "windows_386": Windows_386, - "windows_amd64": Windows_amd64, -} - -type Node struct { - ID int64 `meddler:"node_id,pk" json:"id"` - Addr string `meddler:"node_addr" json:"address"` - Arch string `meddler:"node_arch" json:"architecture"` - Cert string `meddler:"node_cert" json:"-"` - Key string `meddler:"node_key" json:"-"` - CA string `meddler:"node_ca" json:"-"` -} diff --git a/router/middleware/engine.go b/router/middleware/engine.go deleted file mode 100644 index 01da07067..000000000 --- a/router/middleware/engine.go +++ /dev/null @@ -1,28 +0,0 @@ -package middleware - -import ( - "sync" - - "github.com/drone/drone/engine" - "github.com/drone/drone/store" - - "github.com/gin-gonic/gin" -) - -// Engine is a middleware function that initializes the Engine and attaches to -// the context of every http.Request. -func Engine() gin.HandlerFunc { - var once sync.Once - var engine_ engine.Engine - - return func(c *gin.Context) { - - once.Do(func() { - store_ := store.FromContext(c) - engine_ = engine.Load(store_) - }) - - engine.ToContext(c, engine_) - c.Next() - } -} diff --git a/router/router.go b/router/router.go index 157e6f18e..32a13998f 100644 --- a/router/router.go +++ b/router/router.go @@ -2,7 +2,6 @@ package router import ( "net/http" - "os" "strings" "github.com/gin-gonic/gin" @@ -41,8 +40,6 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { { settings.Use(session.MustUser()) settings.GET("/profile", web.ShowUser) - settings.GET("/people", session.MustAdmin(), web.ShowUsers) - settings.GET("/nodes", session.MustAdmin(), web.ShowNodes) } repo := e.Group("/repos/:owner/:name") { @@ -83,14 +80,6 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { users.DELETE("/:login", api.DeleteUser) } - nodes := e.Group("/api/nodes") - { - nodes.Use(session.MustAdmin()) - nodes.GET("", api.GetNodes) - nodes.POST("", api.PostNode) - nodes.DELETE("/:node", api.DeleteNode) - } - repos := e.Group("/api/repos/:owner/:name") { repos.POST("", api.PostRepo) @@ -139,13 +128,8 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { stream.Use(session.SetPerm()) stream.Use(session.MustPull) - if os.Getenv("CANARY") == "true" { - stream.GET("/:owner/:name", web.GetRepoEvents2) - stream.GET("/:owner/:name/:build/:number", web.GetStream2) - } else { - stream.GET("/:owner/:name", web.GetRepoEvents) - stream.GET("/:owner/:name/:build/:number", web.GetStream) - } + stream.GET("/:owner/:name", web.GetRepoEvents) + stream.GET("/:owner/:name/:build/:number", web.GetStream) } bots := e.Group("/bots") @@ -164,14 +148,12 @@ func Load(middlewares ...gin.HandlerFunc) http.Handler { queue := e.Group("/api/queue") { - if os.Getenv("CANARY") == "true" { - queue.Use(middleware.AgentMust()) - queue.POST("/pull", api.Pull) - queue.POST("/pull/:os/:arch", api.Pull) - queue.POST("/wait/:id", api.Wait) - queue.POST("/stream/:id", api.Stream) - queue.POST("/status/:id", api.Update) - } + queue.Use(middleware.AgentMust()) + queue.POST("/pull", api.Pull) + queue.POST("/pull/:os/:arch", api.Pull) + queue.POST("/wait/:id", api.Wait) + queue.POST("/stream/:id", api.Stream) + queue.POST("/status/:id", api.Update) } gitlab := e.Group("/gitlab/:owner/:name") diff --git a/store/datastore/nodes.go b/store/datastore/nodes.go deleted file mode 100644 index 37e88652c..000000000 --- a/store/datastore/nodes.go +++ /dev/null @@ -1,48 +0,0 @@ -package datastore - -import ( - "github.com/drone/drone/model" - "github.com/russross/meddler" -) - -func (db *datastore) GetNode(id int64) (*model.Node, error) { - var node = new(model.Node) - var err = meddler.Load(db, nodeTable, node, id) - return node, err -} - -func (db *datastore) GetNodeList() ([]*model.Node, error) { - var nodes = []*model.Node{} - var err = meddler.QueryAll(db, &nodes, rebind(nodeListQuery)) - return nodes, err -} - -func (db *datastore) CreateNode(node *model.Node) error { - return meddler.Insert(db, nodeTable, node) -} - -func (db *datastore) UpdateNode(node *model.Node) error { - return meddler.Update(db, nodeTable, node) -} - -func (db *datastore) DeleteNode(node *model.Node) error { - var _, err = db.Exec(rebind(nodeDeleteStmt), node.ID) - return err -} - -const nodeTable = "nodes" - -const nodeListQuery = ` -SELECT * -FROM nodes -ORDER BY node_addr -` - -const nodeCountQuery = ` -SELECT COUNT(*) FROM nodes -` - -const nodeDeleteStmt = ` -DELETE FROM nodes -WHERE node_id=? -` diff --git a/store/datastore/nodes_test.go b/store/datastore/nodes_test.go deleted file mode 100644 index 3ae9898fd..000000000 --- a/store/datastore/nodes_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package datastore - -import ( - "testing" - - "github.com/drone/drone/model" - "github.com/franela/goblin" -) - -func TestNodes(t *testing.T) { - db := openTest() - defer db.Close() - - s := From(db) - g := goblin.Goblin(t) - g.Describe("Nodes", func() { - - // before each test be sure to purge the package - // table data from the database. - g.BeforeEach(func() { - db.Exec("DELETE FROM nodes") - }) - - g.It("Should create a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err := s.CreateNode(&node) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID != 0).IsTrue() - }) - - g.It("Should update a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err := s.CreateNode(&node) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID != 0).IsTrue() - - node.Addr = "unix:///var/run/docker.sock" - - err1 := s.UpdateNode(&node) - getnode, err2 := s.GetNode(node.ID) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(node.ID).Equal(getnode.ID) - g.Assert(node.Addr).Equal(getnode.Addr) - g.Assert(node.Arch).Equal(getnode.Arch) - }) - - g.It("Should get a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err := s.CreateNode(&node) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID != 0).IsTrue() - - getnode, err := s.GetNode(node.ID) - g.Assert(err == nil).IsTrue() - g.Assert(node.ID).Equal(getnode.ID) - g.Assert(node.Addr).Equal(getnode.Addr) - g.Assert(node.Arch).Equal(getnode.Arch) - }) - - g.It("Should get a node list", func() { - node1 := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - node2 := model.Node{ - Addr: "unix:///var/run/docker.sock", - Arch: "linux_386", - } - s.CreateNode(&node1) - s.CreateNode(&node2) - - nodes, err := s.GetNodeList() - g.Assert(err == nil).IsTrue() - g.Assert(len(nodes)).Equal(2) - }) - - g.It("Should delete a node", func() { - node := model.Node{ - Addr: "unix:///var/run/docker/docker.sock", - Arch: "linux_amd64", - } - err1 := s.CreateNode(&node) - err2 := s.DeleteNode(&node) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - - _, err := s.GetNode(node.ID) - g.Assert(err == nil).IsFalse() - }) - }) -} diff --git a/store/store.go b/store/store.go index f0fd97968..a359a3edd 100644 --- a/store/store.go +++ b/store/store.go @@ -125,21 +125,6 @@ type Store interface { // WriteLog writes the job logs to the datastore. WriteLog(*model.Job, io.Reader) error - - // GetNode gets a build node from the datastore. - GetNode(id int64) (*model.Node, error) - - // GetNodeList gets a build node list from the datastore. - GetNodeList() ([]*model.Node, error) - - // CreateNode add a new build node to the datastore. - CreateNode(*model.Node) error - - // UpdateNode updates a build node in the datastore. - UpdateNode(*model.Node) error - - // DeleteNode removes a build node from the datastore. - DeleteNode(*model.Node) error } // GetUser gets a user by unique ID. @@ -343,23 +328,3 @@ func ReadLog(c context.Context, job *model.Job) (io.ReadCloser, error) { func WriteLog(c context.Context, job *model.Job, r io.Reader) error { return FromContext(c).WriteLog(job, r) } - -func GetNode(c context.Context, id int64) (*model.Node, error) { - return FromContext(c).GetNode(id) -} - -func GetNodeList(c context.Context) ([]*model.Node, error) { - return FromContext(c).GetNodeList() -} - -func CreateNode(c context.Context, node *model.Node) error { - return FromContext(c).CreateNode(node) -} - -func UpdateNode(c context.Context, node *model.Node) error { - return FromContext(c).UpdateNode(node) -} - -func DeleteNode(c context.Context, node *model.Node) error { - return FromContext(c).DeleteNode(node) -} diff --git a/template/amber/base.amber b/template/amber/base.amber index 491bf3cfc..2e864a264 100644 --- a/template/amber/base.amber +++ b/template/amber/base.amber @@ -34,9 +34,6 @@ html i.material-icons expand_more div.dropdown-menu.dropdown-menu-right a.dropdown-item[href="/settings/profile"] Profile - if User.Admin - a.dropdown-item[href="/settings/people"] People - a.dropdown-item[href="/settings/nodes"] Nodes a.dropdown-item[href="/logout"] Logout diff --git a/web/hook.go b/web/hook.go index 6544446b3..b61519949 100644 --- a/web/hook.go +++ b/web/hook.go @@ -3,16 +3,13 @@ package web import ( "fmt" "os" - "path/filepath" "regexp" - "strings" "github.com/gin-gonic/gin" "github.com/square/go-jose" log "github.com/Sirupsen/logrus" "github.com/drone/drone/bus" - "github.com/drone/drone/engine" "github.com/drone/drone/model" "github.com/drone/drone/queue" "github.com/drone/drone/remote" @@ -31,10 +28,7 @@ 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) - } + droneSec = fmt.Sprintf("%s.sig", droneYml) } var skipRe = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`) @@ -168,8 +162,6 @@ func PostHook(c *gin.Context) { return } - key, _ := store.GetKey(c, repo) - // verify the branches can be built vs skipped branches := yaml.ParseBranch(raw) if !branches.Matches(build.Branch) && build.Event != model.EventTag && build.Event != model.EventDeploy { @@ -214,71 +206,43 @@ func PostHook(c *gin.Context) { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } - // IMPORTANT. PLEASE READ - // - // The below code uses a feature flag to switch between the current - // build engine and the exerimental 0.5 build engine. This can be - // enabled using with the environment variable CANARY=true + var signed bool + var verified bool - if os.Getenv("CANARY") == "true" { - - var signed bool - var verified bool - - signature, err := jose.ParseSigned(string(sec)) + signature, err := jose.ParseSigned(string(sec)) + if err != nil { + log.Debugf("cannot parse .drone.yml.sig file. %s", err) + } else if len(sec) == 0 { + log.Debugf("cannot parse .drone.yml.sig file. empty file") + } else { + signed = true + output, err := signature.Verify([]byte(repo.Hash)) if err != nil { - log.Debugf("cannot parse .drone.yml.sig file. %s", err) - } else if len(sec) == 0 { - log.Debugf("cannot parse .drone.yml.sig file. empty file") + log.Debugf("cannot verify .drone.yml.sig file. %s", err) + } else if string(output) != string(raw) { + log.Debugf("cannot verify .drone.yml.sig file. no match") } else { - signed = true - output, err := signature.Verify([]byte(repo.Hash)) - if err != nil { - log.Debugf("cannot verify .drone.yml.sig file. %s", err) - } else if string(output) != string(raw) { - log.Debugf("cannot verify .drone.yml.sig file. no match") - } else { - verified = true - } + verified = true } - - log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) - - bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) - for _, job := range jobs { - queue.Publish(c, &queue.Work{ - Signed: signed, - Verified: verified, - User: user, - Repo: repo, - Build: build, - BuildLast: last, - Job: job, - Netrc: netrc, - Yaml: string(raw), - Secrets: secs, - System: &model.System{Link: httputil.GetURL(c.Request)}, - }) - } - return // EXIT NOT TO AVOID THE 0.4 ENGINE CODE BELOW } - engine_ := engine.FromContext(c) - go engine_.Schedule(c.Copy(), &engine.Task{ - User: user, - Repo: repo, - Build: build, - BuildPrev: last, - Jobs: jobs, - Keys: key, - Netrc: netrc, - Config: string(raw), - Secret: string(sec), - System: &model.System{ - Link: httputil.GetURL(c.Request), - Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), - Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), - Escalates: strings.Split(os.Getenv("ESCALATE_FILTER"), " "), - }, - }) + log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) + + bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) + for _, job := range jobs { + queue.Publish(c, &queue.Work{ + Signed: signed, + Verified: verified, + User: user, + Repo: repo, + Build: build, + BuildLast: last, + Job: job, + Netrc: netrc, + Yaml: string(raw), + Secrets: secs, + System: &model.System{Link: httputil.GetURL(c.Request)}, + }) + } + } diff --git a/web/pages.go b/web/pages.go index 46da27304..8480c1e85 100644 --- a/web/pages.go +++ b/web/pages.go @@ -30,7 +30,7 @@ func ShowIndex(c *gin.Context) { } // filter to only show the currently active ones - activeRepos, err := store.GetRepoListOf(c,repos) + activeRepos, err := store.GetRepoListOf(c, repos) if err != nil { c.String(400, err.Error()) return @@ -83,26 +83,6 @@ func ShowUser(c *gin.Context) { }) } -func ShowUsers(c *gin.Context) { - user := session.User(c) - if !user.Admin { - c.AbortWithStatus(http.StatusForbidden) - return - } - users, _ := store.GetUserList(c) - - token, _ := token.New( - token.CsrfToken, - user.Login, - ).Sign(user.Hash) - - c.HTML(200, "users.html", gin.H{ - "User": user, - "Users": users, - "Csrf": token, - }) -} - func ShowRepo(c *gin.Context) { user := session.User(c) repo := session.Repo(c) @@ -227,10 +207,3 @@ func ShowBuild(c *gin.Context) { "Csrf": csrf, }) } - -func ShowNodes(c *gin.Context) { - user := session.User(c) - nodes, _ := store.GetNodeList(c) - token, _ := token.New(token.CsrfToken, user.Login).Sign(user.Hash) - c.HTML(http.StatusOK, "nodes.html", gin.H{"User": user, "Nodes": nodes, "Csrf": token}) -} diff --git a/web/stream.go b/web/stream.go index 623beb827..cceff005c 100644 --- a/web/stream.go +++ b/web/stream.go @@ -1,15 +1,18 @@ package web import ( + "bufio" + "encoding/json" "io" "strconv" "github.com/gin-gonic/gin" - "github.com/docker/docker/pkg/stdcopy" - "github.com/drone/drone/engine" + "github.com/drone/drone/bus" + "github.com/drone/drone/model" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/store" + "github.com/drone/drone/stream" log "github.com/Sirupsen/logrus" @@ -19,14 +22,13 @@ import ( // 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) + eventc := make(chan *bus.Event, 1) + bus.Subscribe(c, eventc) defer func() { - engine_.Unsubscribe(eventc) + bus.Unsubscribe(c, eventc) close(eventc) log.Infof("closed event stream") }() @@ -38,11 +40,22 @@ func GetRepoEvents(c *gin.Context) { log.Infof("nil event received") return false } - if event.Name == repo.FullName { - log.Debugf("received message %s", event.Name) + + // TODO(bradrydzewski) This is a super hacky workaround until we improve + // the actual bus. Having a per-call database event is just plain stupid. + if event.Repo.FullName == repo.FullName { + + var payload = struct { + model.Build + Jobs []*model.Job `json:"jobs"` + }{} + payload.Build = event.Build + payload.Jobs, _ = store.GetJobList(c, &event.Build) + data, _ := json.Marshal(&payload) + sse.Encode(w, sse.Event{ Event: "message", - Data: string(event.Msg), + Data: string(data), }) } case <-c.Writer.CloseNotify(): @@ -54,7 +67,6 @@ func GetRepoEvents(c *gin.Context) { 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")) @@ -73,48 +85,32 @@ func GetStream(c *gin.Context) { 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) + rc, err := stream.Reader(c, stream.ToKey(job.ID)) 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} + var line int + var scanner = bufio.NewScanner(rc) + for scanner.Scan() { + line++ + var err = sse.Encode(c.Writer, sse.Event{ + Id: strconv.Itoa(line), + Event: "message", + Data: scanner.Text(), + }) + if err != nil { + break + } + c.Writer.Flush() + } - 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 + log.Debugf("Closed stream %s#%d", repo.FullName, build.Number) } diff --git a/web/stream2.go b/web/stream2.go deleted file mode 100644 index 91067dd83..000000000 --- a/web/stream2.go +++ /dev/null @@ -1,121 +0,0 @@ -package web - -import ( - "bufio" - "encoding/json" - "io" - "strconv" - - "github.com/gin-gonic/gin" - - "github.com/drone/drone/bus" - "github.com/drone/drone/model" - "github.com/drone/drone/router/middleware/session" - "github.com/drone/drone/store" - "github.com/drone/drone/stream" - - log "github.com/Sirupsen/logrus" - - "github.com/manucorporat/sse" -) - -// IMPORTANT. PLEASE READ -// -// This file containers experimental streaming features for the 0.5 -// release. These can be enabled with the feature flag CANARY=true - -// GetRepoEvents will upgrade the connection to a Websocket and will stream -// event updates to the browser. -func GetRepoEvents2(c *gin.Context) { - repo := session.Repo(c) - c.Writer.Header().Set("Content-Type", "text/event-stream") - - eventc := make(chan *bus.Event, 1) - bus.Subscribe(c, eventc) - defer func() { - bus.Unsubscribe(c, 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 - } - - // TODO(bradrydzewski) This is a super hacky workaround until we improve - // the actual bus. Having a per-call database event is just plain stupid. - if event.Repo.FullName == repo.FullName { - - var payload = struct { - model.Build - Jobs []*model.Job `json:"jobs"` - }{} - payload.Build = event.Build - payload.Jobs, _ = store.GetJobList(c, &event.Build) - data, _ := json.Marshal(&payload) - - sse.Encode(w, sse.Event{ - Event: "message", - Data: string(data), - }) - } - case <-c.Writer.CloseNotify(): - return false - } - return true - }) -} - -func GetStream2(c *gin.Context) { - - 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 - } - - rc, err := stream.Reader(c, stream.ToKey(job.ID)) - if err != nil { - c.AbortWithError(404, err) - return - } - - go func() { - <-c.Writer.CloseNotify() - rc.Close() - }() - - var line int - var scanner = bufio.NewScanner(rc) - for scanner.Scan() { - line++ - var err = sse.Encode(c.Writer, sse.Event{ - Id: strconv.Itoa(line), - Event: "message", - Data: scanner.Text(), - }) - if err != nil { - break - } - c.Writer.Flush() - } - - log.Debugf("Closed stream %s#%d", repo.FullName, build.Number) -} From 082570fb5bf17a69a022ddb4f59351e200a4e192 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Fri, 29 Apr 2016 12:39:56 -0700 Subject: [PATCH 02/10] refactoring input and configuration --- api/repo.go | 43 -- drone/agent/agent.go | 6 +- drone/daemon.go | 457 ++++++++++++++++++++++ drone/drone.go | 3 +- drone/server/server.go | 129 ------ model/team.go | 12 + remote/bitbucket/bitbucket.go | 384 ++++++------------ remote/bitbucket/const.go | 40 ++ remote/bitbucket/const_test.go | 43 ++ remote/bitbucket/helper.go | 99 ++--- remote/bitbucket/helper_test.go | 102 +++++ remote/bitbucket/{ => internal}/client.go | 2 +- remote/bitbucket/{ => internal}/types.go | 2 +- remote/bitbucketserver/bitbucketserver.go | 47 ++- remote/cache.go | 85 ++++ remote/github/github.go | 172 +++----- remote/github/github_test.go | 57 +-- remote/github/helper.go | 157 -------- remote/gitlab/gitlab.go | 11 + remote/gogs/gogs.go | 61 ++- remote/mock/remote.go | 318 ++++++++------- remote/remote.go | 22 +- router/middleware/agent.go | 2 +- router/middleware/bus.go | 14 - router/middleware/cache.go | 22 -- router/middleware/queue.go | 14 - router/middleware/remote.go | 48 --- router/middleware/store.go | 29 -- router/middleware/stream.go | 14 - router/middleware/version.go | 14 - router/router.go | 397 +++++++++---------- server/handler.go | 79 ++++ server/handler_test.go | 1 + server/server.go | 243 ++++++++++++ 34 files changed, 1784 insertions(+), 1345 deletions(-) create mode 100644 drone/daemon.go delete mode 100644 drone/server/server.go create mode 100644 model/team.go create mode 100644 remote/bitbucket/const.go create mode 100644 remote/bitbucket/const_test.go create mode 100644 remote/bitbucket/helper_test.go rename remote/bitbucket/{ => internal}/client.go (99%) rename remote/bitbucket/{ => internal}/types.go (99%) create mode 100644 remote/cache.go delete mode 100644 router/middleware/bus.go delete mode 100644 router/middleware/cache.go delete mode 100644 router/middleware/queue.go delete mode 100644 router/middleware/remote.go delete mode 100644 router/middleware/store.go delete mode 100644 router/middleware/stream.go delete mode 100644 router/middleware/version.go create mode 100644 server/handler.go create mode 100644 server/handler_test.go create mode 100644 server/server.go diff --git a/api/repo.go b/api/repo.go index 59a0fb4c7..c7ed5e2fd 100644 --- a/api/repo.go +++ b/api/repo.go @@ -1,13 +1,11 @@ package api import ( - "bytes" "fmt" "io/ioutil" "net/http" "github.com/gin-gonic/gin" - "gopkg.in/yaml.v2" "github.com/drone/drone/cache" "github.com/drone/drone/model" @@ -210,44 +208,3 @@ func DeleteRepo(c *gin.Context) { remote.Deactivate(user, repo, httputil.GetURL(c.Request)) c.Writer.WriteHeader(http.StatusOK) } - -func PostSecure(c *gin.Context) { - repo := session.Repo(c) - - in, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - // we found some strange characters included in - // the yaml file when entered into a browser textarea. - // these need to be removed - in = bytes.Replace(in, []byte{'\xA0'}, []byte{' '}, -1) - - // make sure the Yaml is valid format to prevent - // a malformed value from being used in the build - err = yaml.Unmarshal(in, &yaml.MapSlice{}) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } - - key, err := store.GetKey(c, repo) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - // encrypts using go-jose - out, err := crypto.Encrypt(string(in), key.Private) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.String(http.StatusOK, out) -} - -func PostReactivate(c *gin.Context) { - -} diff --git a/drone/agent/agent.go b/drone/agent/agent.go index cae875f11..5f499e845 100644 --- a/drone/agent/agent.go +++ b/drone/agent/agent.go @@ -141,9 +141,9 @@ func start(c *cli.Context) { c.String("drone-token"), ) - tls, _ := dockerclient.TLSConfigFromCertPath(c.String("docker-cert-path")) - if c.Bool("docker-host") { - tls.InsecureSkipVerify = true + tls, err := dockerclient.TLSConfigFromCertPath(c.String("docker-cert-path")) + if err == nil { + tls.InsecureSkipVerify = c.Bool("docker-tls-verify") } docker, err := dockerclient.NewDockerClient(c.String("docker-host"), tls) if err != nil { diff --git a/drone/daemon.go b/drone/daemon.go new file mode 100644 index 000000000..cc85e6d77 --- /dev/null +++ b/drone/daemon.go @@ -0,0 +1,457 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/drone/drone/bus" + "github.com/drone/drone/cache" + "github.com/drone/drone/queue" + "github.com/drone/drone/remote" + "github.com/drone/drone/remote/bitbucket" + "github.com/drone/drone/remote/bitbucketserver" + "github.com/drone/drone/remote/github" + "github.com/drone/drone/remote/gitlab" + "github.com/drone/drone/remote/gogs" + "github.com/drone/drone/server" + "github.com/drone/drone/shared/token" + "github.com/drone/drone/store" + "github.com/drone/drone/store/datastore" + "github.com/drone/drone/stream" + + "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.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.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", + }, + + // + // 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) + } + + // print the agent secret to the console + // TODO(bradrydzewski) this overall approach should be re-considered + if err := printSecret(c); err != nil { + return err + } + + // setup the server and start the listener + server := server.Server{ + Bus: setupBus(c), + Cache: setupCache(c), + Config: setupConfig(c), + Queue: setupQueue(c), + Remote: setupRemote(c), + Stream: setupStream(c), + Store: setupStore(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"), + server.Handler(), + ) + } + + // start the server without tls enabled + return http.ListenAndServe( + c.String("server-addr"), + server.Handler(), + ) +} + +func setupConfig(c *cli.Context) *server.Config { + return &server.Config{ + Open: c.Bool("open"), + Yaml: c.String("yaml"), + Secret: c.String("agent-secret"), + Admins: c.StringSlice("admin"), + Orgs: c.StringSlice("orgs"), + } +} + +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 { + 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: + logrus.Fatalln("version control system not configured") + return nil + } +} + +func setupBitbucket(c *cli.Context) remote.Remote { + return bitbucket.New( + c.String("bitbucket-client"), + c.String("bitbucket-server"), + ) +} + +func setupGogs(c *cli.Context) remote.Remote { + return gogs.New( + c.String("gogs-server"), + c.Bool("gogs-private-mode"), + c.Bool("gogs-skip-verify"), + ) +} + +func setupStash(c *cli.Context) remote.Remote { + return bitbucketserver.New( + c.String("stash-server"), + c.String("stash-consumer-key"), + c.String("stash-consumer-rsa"), + c.String("stash-git-username"), + c.String("stash-git-password"), + ) +} + +func setupGitlab(c *cli.Context) remote.Remote { + return gitlab.New( + c.String("gitlab-server"), + c.String("gitlab-client"), + c.String("gitlab-sercret"), + c.Bool("gitlab-private-mode"), + c.Bool("gitlab-skip-verify"), + ) +} + +func setupGithub(c *cli.Context) remote.Remote { + g, err := 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"), + ) + if err != nil { + log.Fatalln(err) + } + return g +} + +func printSecret(c *cli.Context) error { + secret := c.String("agent-secret") + if secret == "" { + return fmt.Errorf("missing DRONE_AGENT_SECRET configuration parameter") + } + t := token.New(secret, "") + s, err := t.Sign(secret) + if err != nil { + return fmt.Errorf("invalid value for DRONE_AGENT_SECRET. %s", s) + } + + logrus.Infof("using agent secret %s", secret) + logrus.Warnf("agents can connect with token %s", s) + return nil +} + +var agreement = ` +--- + + +You are attempting to use the unstable channel. This build is experimental and +has known bugs and compatibility issues. It is not intended for general use. + +Please consider using the latest stable release instead: + + drone/drone:0.4.2 + +If you are attempting to build from source please use the latest stable tag: + + v0.4.2 + +If you are interested in testing this experimental build AND assisting with +development you may proceed by setting the following environment: + + I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION=true + I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS=true + + +--- +` diff --git a/drone/drone.go b/drone/drone.go index cfc034d60..310b1fc1b 100644 --- a/drone/drone.go +++ b/drone/drone.go @@ -4,7 +4,6 @@ import ( "os" "github.com/drone/drone/drone/agent" - "github.com/drone/drone/drone/server" "github.com/drone/drone/version" "github.com/codegangsta/cli" @@ -35,7 +34,7 @@ func main() { } app.Commands = []cli.Command{ agent.AgentCmd, - server.ServeCmd, + DaemonCmd, SignCmd, SecretCmd, } diff --git a/drone/server/server.go b/drone/server/server.go deleted file mode 100644 index 600a84fa9..000000000 --- a/drone/server/server.go +++ /dev/null @@ -1,129 +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(), - ) - - 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. It is not intended for general use. - -Please consider using the latest stable release instead: - - drone/drone:0.4.2 - -If you are attempting to build from source please use the latest stable tag: - - v0.4.2 - -If you are interested in testing this experimental build AND assisting with -development you may proceed by setting the following environment: - - I_UNDERSTAND_I_AM_USING_AN_UNSTABLE_VERSION=true - I_AGREE_TO_FIX_BUGS_AND_NOT_FILE_BUGS=true - - ---- -` diff --git a/model/team.go b/model/team.go new file mode 100644 index 000000000..c91d58666 --- /dev/null +++ b/model/team.go @@ -0,0 +1,12 @@ +package model + +// Team represents a team or organization in the remote version control system. +// +// swagger:model user +type Team struct { + // Login is the username for this team. + Login string `json:"login"` + + // the avatar url for this team. + Avatar string `json:"avatar_url"` +} diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index c37319a2c..56a4f679e 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -6,128 +6,79 @@ import ( "io/ioutil" "net/http" "net/url" - "strconv" "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/drone/drone/remote/bitbucket/internal" "github.com/drone/drone/shared/httputil" - log "github.com/Sirupsen/logrus" "golang.org/x/oauth2" "golang.org/x/oauth2/bitbucket" ) -type Bitbucket struct { +type config struct { Client string Secret string - Orgs []string - Open bool } -func Load(config string) *Bitbucket { - - // parse the remote DSN configuration string - url_, err := url.Parse(config) - if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) +// New returns a new remote Configuration for integrating with the Bitbucket +// repository hosting service at https://bitbucket.org +func New(client, secret string) remote.Remote { + return &config{ + Client: client, + Secret: secret, } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - - // create the Githbub remote using parameters from - // the parsed DSN configuration string. - bitbucket := Bitbucket{} - bitbucket.Client = params.Get("client_id") - bitbucket.Secret = params.Get("client_secret") - bitbucket.Orgs = params["orgs"] - bitbucket.Open, _ = strconv.ParseBool(params.Get("open")) - - return &bitbucket } -// Login authenticates the session and returns the -// remote user details. -func (bb *Bitbucket) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// helper function to return the bitbucket oauth2 client +func (c *config) newClient(u *model.User) *internal.Client { + return internal.NewClientToken( + c.Client, + c.Secret, + &oauth2.Token{ + AccessToken: u.Token, + RefreshToken: u.Secret, + }, + ) +} +func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { config := &oauth2.Config{ - ClientID: bb.Client, - ClientSecret: bb.Secret, + ClientID: c.Client, + ClientSecret: c.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 { http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) - return nil, false, nil + return nil, nil } var token, err = config.Exchange(oauth2.NoContext, code) if err != nil { - return nil, false, fmt.Errorf("Error exchanging token. %s", err) + return nil, err } - client := NewClient(config.Client(oauth2.NoContext, token)) + client := internal.NewClient(config.Client(oauth2.NoContext, token)) curr, err := client.FindCurrent() if err != nil { - return nil, false, err + return nil, err } - // convers the current bitbucket user to the - // common drone user structure. - user := model.User{} - user.Login = curr.Login - user.Token = token.AccessToken - user.Secret = token.RefreshToken - user.Expiry = token.Expiry.UTC().Unix() - user.Avatar = curr.Links.Avatar.Href - - // gets the primary, confirmed email from bitbucket - emails, err := client.ListEmail() - if err != nil { - return nil, false, err - } - for _, email := range emails.Values { - if email.IsPrimary && email.IsConfirmed { - user.Email = email.Email - break - } - } - - // if the installation is restricted to a subset - // of organizations, get the orgs and verify the - // user is a member. - if len(bb.Orgs) != 0 { - resp, err := client.ListTeams(&ListTeamOpts{Page: 1, PageLen: 100, Role: "member"}) - if err != nil { - return nil, false, err - } - - var member bool - for _, team := range resp.Values { - for _, team_ := range bb.Orgs { - if team.Login == team_ { - member = true - break - } - } - } - - if !member { - return nil, false, fmt.Errorf("User does not belong to correct org. Must belong to %v", bb.Orgs) - } - } - - return &user, bb.Open, nil + return convertUser(curr, token), nil } -// Auth authenticates the session and returns the remote user -// login for the given token and secret -func (bb *Bitbucket) Auth(token, secret string) (string, error) { - token_ := oauth2.Token{AccessToken: token, RefreshToken: secret} - client := NewClientToken(bb.Client, bb.Secret, &token_) - +func (c *config) Auth(token, secret string) (string, error) { + client := internal.NewClientToken( + c.Client, + c.Secret, + &oauth2.Token{ + AccessToken: token, + RefreshToken: secret, + }, + ) user, err := client.FindCurrent() if err != nil { return "", err @@ -135,13 +86,10 @@ func (bb *Bitbucket) Auth(token, secret string) (string, error) { return user.Login, nil } -// Refresh refreshes an oauth token and expiration for the given -// user. It returns true if the token was refreshed, false if the -// token was not refreshed, and error if it failed to refersh. -func (bb *Bitbucket) Refresh(user *model.User) (bool, error) { +func (c *config) Refresh(user *model.User) (bool, error) { config := &oauth2.Config{ - ClientID: bb.Client, - ClientSecret: bb.Secret, + ClientID: c.Client, + ClientSecret: c.Secret, Endpoint: bitbucket.Endpoint, } @@ -165,28 +113,35 @@ func (bb *Bitbucket) Refresh(user *model.User) (bool, error) { return true, nil } -// Repo fetches the named repository from the remote system. -func (bb *Bitbucket) Repo(u *model.User, owner, name string) (*model.Repo, error) { - token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} - client := NewClientToken(bb.Client, bb.Secret, &token) +func (c *config) Teams(u *model.User) ([]*model.Team, error) { + opts := &internal.ListTeamOpts{ + PageLen: 100, + Role: "member", + } + resp, err := c.newClient(u).ListTeams(opts) + if err != nil { + return nil, err + } + return convertTeamList(resp.Values), nil +} - repo, err := client.FindRepo(owner, name) +func (c *config) Repo(u *model.User, owner, name string) (*model.Repo, error) { + repo, err := c.newClient(u).FindRepo(owner, name) if err != nil { return nil, err } return convertRepo(repo), nil } -// Repos fetches a list of repos from the remote system. -func (bb *Bitbucket) Repos(u *model.User) ([]*model.RepoLite, error) { - token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} - client := NewClientToken(bb.Client, bb.Secret, &token) +func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) { + client := c.newClient(u) + var repos []*model.RepoLite // gets a list of all accounts to query, including the // user's account and all team accounts. logins := []string{u.Login} - resp, err := client.ListTeams(&ListTeamOpts{PageLen: 100, Role: "member"}) + resp, err := client.ListTeams(&internal.ListTeamOpts{PageLen: 100, Role: "member"}) if err != nil { return repos, err } @@ -208,11 +163,8 @@ func (bb *Bitbucket) Repos(u *model.User) ([]*model.RepoLite, error) { return repos, nil } -// Perm fetches the named repository permissions from -// the remote system for the specified user. -func (bb *Bitbucket) Perm(u *model.User, owner, name string) (*model.Perm, error) { - token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} - client := NewClientToken(bb.Client, bb.Secret, &token) +func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) { + client := c.newClient(u) perms := new(model.Perm) _, err := client.FindRepo(owner, name) @@ -220,69 +172,39 @@ func (bb *Bitbucket) Perm(u *model.User, owner, name string) (*model.Perm, error return perms, err } - // if we've gotten this far we know that the user at - // least has read access to the repository. + // if we've gotten this far we know that the user at least has read access + // to the repository. perms.Pull = true - // if the user has access to the repository hooks we - // can deduce that the user has push and admin access. - _, err = client.ListHooks(owner, name, &ListOpts{}) + // 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, &internal.ListOpts{}) if err == nil { perms.Push = true perms.Admin = true } - return perms, nil } -// File fetches a file from the remote repository and returns in string format. -func (bb *Bitbucket) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - config, err := client.FindSource(r.Owner, r.Name, b.Commit, f) +func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { + config, err := c.newClient(u).FindSource(r.Owner, r.Name, b.Commit, f) if err != nil { return nil, err } - return []byte(config.Data), err } -// Status sends the commit status to the remote system. -// An example would be the GitHub pull request status. -func (bb *Bitbucket) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - status := getStatus(b.Status) - desc := getDesc(b.Status) - - data := BuildStatus{ - State: status, +func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + status := internal.BuildStatus{ + State: getStatus(b.Status), + Desc: getDesc(b.Status), Key: "Drone", Url: link, - Desc: desc, } - - err := client.CreateStatus(r.Owner, r.Name, b.Commit, &data) - return err + return c.newClient(u).CreateStatus(r.Owner, r.Name, b.Commit, &status) } -// Netrc returns a .netrc file that can be used to clone -// private repositories from a remote system. -func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { +func (c *config) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { return &model.Netrc{ Machine: "bitbucket.org", Login: "x-token-auth", @@ -290,113 +212,73 @@ func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { }, nil } -// Activate activates a repository by creating the post-commit hook and -// adding the SSH deploy key, if applicable. -func (bb *Bitbucket) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - linkurl, err := url.Parse(link) +func (c *config) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { + rawurl, 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{}) + // deletes any previously created hooks + if err := c.Deactivate(u, r, link); err != nil { + // we can live with failure here. Things happen and manually scrubbing + // hooks is certinaly not the end of the world. + } + + return c.newClient(u).CreateHook(r.Owner, r.Name, &internal.Hook{ + Active: true, + Desc: rawurl.Host, + Events: []string{"repo:push"}, + Url: link, + }) +} + +func (c *config) Deactivate(u *model.User, r *model.Repo, link string) error { + client := c.newClient(u) + + linkurl, err := url.Parse(link) + if err != nil { + return err + } + + hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{}) + if err != nil { + return nil // we can live with undeleted hooks + } + 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 -// which are equal to link and removing the SSH deploy key. -func (bb *Bitbucket) Deactivate(u *model.User, r *model.Repo, link string) error { - client := NewClientToken( - bb.Client, - bb.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) - - linkurl, err := url.Parse(link) - if err != nil { - 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 { - return err - } if hookurl.Host == linkurl.Host { client.DeleteHook(r.Owner, r.Name, hook.Uuid) - break + break // we can live with undeleted hooks } } 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) { +func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) { switch r.Header.Get("X-Event-Key") { case "repo:push": - return bb.pushHook(r) + return c.pushHook(r) case "pullrequest:created", "pullrequest:updated": - return bb.pullHook(r) + return c.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) { +func (c *config) 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{} + hook := internal.PushHook{} err := json.Unmarshal(payload, &hook) if err != nil { return nil, nil, err @@ -423,6 +305,7 @@ func (bb *Bitbucket) pushHook(r *http.Request) (*model.Repo, *model.Build, error // return the updated repository information and the // build information. + // TODO(bradrydzewski) uses unit tested conversion function return convertRepo(&hook.Repo), &model.Build{ Event: buildEventType, Commit: change.New.Target.Hash, @@ -439,14 +322,14 @@ func (bb *Bitbucket) pushHook(r *http.Request) (*model.Repo, *model.Build, error return nil, nil, nil } -func (bb *Bitbucket) pullHook(r *http.Request) (*model.Repo, *model.Build, error) { +func (c *config) 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{} + hook := internal.PullRequestHook{} err := json.Unmarshal(payload, &hook) if err != nil { return nil, nil, err @@ -455,12 +338,13 @@ func (bb *Bitbucket) pullHook(r *http.Request) (*model.Repo, *model.Build, error return nil, nil, nil } + // TODO(bradrydzewski) uses unit tested conversion function 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), + Remote: cloneLink(&hook.PullRequest.Dest.Repo), Link: hook.PullRequest.Links.Html.Href, Branch: hook.PullRequest.Dest.Branch.Name, Message: hook.PullRequest.Desc, @@ -469,47 +353,3 @@ func (bb *Bitbucket) pullHook(r *http.Request) (*model.Repo, *model.Build, error Timestamp: hook.PullRequest.Updated.UTC().Unix(), }, nil } - -const ( - StatusPending = "INPROGRESS" - StatusSuccess = "SUCCESSFUL" - StatusFailure = "FAILED" -) - -const ( - DescPending = "this build is pending" - DescSuccess = "the build was successful" - DescFailure = "the build failed" - DescError = "oops, something went wrong" -) - -// converts a Drone status to a BitBucket status. -func getStatus(status string) string { - switch status { - case model.StatusPending, model.StatusRunning: - return StatusPending - case model.StatusSuccess: - return StatusSuccess - case model.StatusFailure, model.StatusError, model.StatusKilled: - return StatusFailure - default: - return StatusFailure - } -} - -// generates a description for the build based on -// the Drone status -func getDesc(status string) string { - switch status { - case model.StatusPending, model.StatusRunning: - return DescPending - case model.StatusSuccess: - return DescSuccess - case model.StatusFailure: - return DescFailure - case model.StatusError, model.StatusKilled: - return DescError - default: - return DescError - } -} diff --git a/remote/bitbucket/const.go b/remote/bitbucket/const.go new file mode 100644 index 000000000..3dcbb2cb2 --- /dev/null +++ b/remote/bitbucket/const.go @@ -0,0 +1,40 @@ +package bitbucket + +import "github.com/drone/drone/model" + +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" +) + +func getStatus(status string) string { + switch status { + case model.StatusPending, model.StatusRunning: + return statusPending + case model.StatusSuccess: + return statusSuccess + default: + return statusFailure + } +} + +func getDesc(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 + } +} diff --git a/remote/bitbucket/const_test.go b/remote/bitbucket/const_test.go new file mode 100644 index 000000000..104947fd7 --- /dev/null +++ b/remote/bitbucket/const_test.go @@ -0,0 +1,43 @@ +package bitbucket + +import ( + "testing" + + "github.com/drone/drone/model" + + "github.com/franela/goblin" +) + +func Test_status(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Bitbucket status", func() { + g.It("should return passing", func() { + g.Assert(getStatus(model.StatusSuccess)).Equal(statusSuccess) + }) + g.It("should return pending", func() { + g.Assert(getStatus(model.StatusPending)).Equal(statusPending) + g.Assert(getStatus(model.StatusRunning)).Equal(statusPending) + }) + g.It("should return failing", func() { + g.Assert(getStatus(model.StatusFailure)).Equal(statusFailure) + g.Assert(getStatus(model.StatusKilled)).Equal(statusFailure) + g.Assert(getStatus(model.StatusError)).Equal(statusFailure) + }) + + g.It("should return passing desc", func() { + g.Assert(getDesc(model.StatusSuccess)).Equal(descSuccess) + }) + g.It("should return pending desc", func() { + g.Assert(getDesc(model.StatusPending)).Equal(descPending) + g.Assert(getDesc(model.StatusRunning)).Equal(descPending) + }) + g.It("should return failing desc", func() { + g.Assert(getDesc(model.StatusFailure)).Equal(descFailure) + }) + g.It("should return error desc", func() { + g.Assert(getDesc(model.StatusKilled)).Equal(descError) + g.Assert(getDesc(model.StatusError)).Equal(descError) + }) + }) +} diff --git a/remote/bitbucket/helper.go b/remote/bitbucket/helper.go index b6072bd6a..9a516b40f 100644 --- a/remote/bitbucket/helper.go +++ b/remote/bitbucket/helper.go @@ -5,12 +5,16 @@ import ( "strings" "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/internal" + + "golang.org/x/oauth2" ) -// convertRepo is a helper function used to convert a Bitbucket -// repository structure to the common Drone repository structure. -func convertRepo(from *Repo) *model.Repo { +// 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, @@ -20,66 +24,34 @@ func convertRepo(from *Repo) *model.Repo { 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 { +// 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. + // 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 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. + // 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 @@ -89,9 +61,9 @@ func cloneLink(repo Repo) 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 { +// 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], @@ -99,3 +71,34 @@ func convertRepoLite(from *Repo) *model.RepoLite { 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, + } +} diff --git a/remote/bitbucket/helper_test.go b/remote/bitbucket/helper_test.go new file mode 100644 index 000000000..c978c1291 --- /dev/null +++ b/remote/bitbucket/helper_test.go @@ -0,0 +1,102 @@ +package bitbucket + +import ( + "testing" + "time" + + "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", func() { + + 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") + }) + }) +} diff --git a/remote/bitbucket/client.go b/remote/bitbucket/internal/client.go similarity index 99% rename from remote/bitbucket/client.go rename to remote/bitbucket/internal/client.go index e0caed656..932613136 100644 --- a/remote/bitbucket/client.go +++ b/remote/bitbucket/internal/client.go @@ -1,4 +1,4 @@ -package bitbucket +package internal import ( "bytes" diff --git a/remote/bitbucket/types.go b/remote/bitbucket/internal/types.go similarity index 99% rename from remote/bitbucket/types.go rename to remote/bitbucket/internal/types.go index 1ad189720..82175e5b2 100644 --- a/remote/bitbucket/types.go +++ b/remote/bitbucket/internal/types.go @@ -1,4 +1,4 @@ -package bitbucket +package internal import ( "net/url" diff --git a/remote/bitbucketserver/bitbucketserver.go b/remote/bitbucketserver/bitbucketserver.go index d78de27f2..c40a4c7bf 100644 --- a/remote/bitbucketserver/bitbucketserver.go +++ b/remote/bitbucketserver/bitbucketserver.go @@ -14,13 +14,15 @@ package bitbucketserver import ( "encoding/json" "fmt" - log "github.com/Sirupsen/logrus" - "github.com/drone/drone/model" - "github.com/mrjones/oauth" "io/ioutil" "net/http" "net/url" "strconv" + + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/mrjones/oauth" ) type BitbucketServer struct { @@ -33,6 +35,19 @@ type BitbucketServer struct { Consumer oauth.Consumer } +func New(url, key, rsa, username, password string) remote.Remote { + bb := &BitbucketServer{ + URL: url, + ConsumerKey: key, + GitUserName: username, + GitPassword: password, + ConsumerRSA: rsa, + } + bb.Consumer = *NewClient(bb.ConsumerRSA, bb.ConsumerKey, bb.URL) + + return bb +} + func Load(config string) *BitbucketServer { url_, err := url.Parse(config) @@ -105,7 +120,7 @@ func (bs *BitbucketServer) Login(res http.ResponseWriter, req *http.Request) (*m bits, err := ioutil.ReadAll(response.Body) userName := string(bits) - response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s",bs.URL, userName)) + response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s", bs.URL, userName)) contents, err := ioutil.ReadAll(response1.Body) defer response1.Body.Close() var mUser User @@ -134,7 +149,7 @@ func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, client := NewClientWithToken(&bs.Consumer, u.Token) - url := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s",bs.URL,owner,name) + url := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s", bs.URL, owner, name) log.Info("Trying to get " + url) response, err := client.Get(url) if err != nil { @@ -165,7 +180,7 @@ func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, repo.Name = bsRepo.Slug repo.Owner = bsRepo.Project.Key repo.AllowPush = true - repo.FullName = fmt.Sprintf("%s/%s",bsRepo.Project.Key,bsRepo.Slug) + repo.FullName = fmt.Sprintf("%s/%s", bsRepo.Project.Key, bsRepo.Slug) repo.Branch = "master" repo.Kind = model.RepoGit @@ -178,7 +193,7 @@ func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) { client := NewClientWithToken(&bs.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", bs.URL)) if err != nil { log.Error(err) } @@ -296,7 +311,7 @@ func (bs *BitbucketServer) Hook(r *http.Request) (*model.Repo, *model.Build, err repo.AllowDeploy = false repo.AllowPull = false repo.AllowPush = true - repo.FullName = fmt.Sprintf("%s/%s",hookPost.Repository.Project.Key,hookPost.Repository.Slug) + repo.FullName = fmt.Sprintf("%s/%s", hookPost.Repository.Project.Key, hookPost.Repository.Slug) repo.Branch = "master" repo.Kind = model.RepoGit @@ -307,17 +322,17 @@ func (bs *BitbucketServer) String() string { } type HookDetail struct { - Key string `"json:key"` - Name string `"json:name"` - Type string `"json:type"` - Description string `"json:description"` - Version string `"json:version"` - ConfigFormKey string `"json:configFormKey"` + Key string `json:"key"` + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Version string `json:"version"` + ConfigFormKey string `json:"configFormKey"` } type Hook struct { - Enabled bool `"json:enabled"` - Details *HookDetail `"json:details"` + Enabled bool `json:"enabled"` + Details *HookDetail `json:"details"` } // Enable hook for named repository diff --git a/remote/cache.go b/remote/cache.go new file mode 100644 index 000000000..032edad93 --- /dev/null +++ b/remote/cache.go @@ -0,0 +1,85 @@ +package remote + +import ( + "time" + + "github.com/drone/drone/model" +) + +// WithCache returns a the parent Remote with a front-end Cache. Remote items +// are cached for duration d. +func WithCache(r Remote, d time.Duration) Remote { + return r +} + +// Cacher implements purge functionality so that we can evict stale data and +// force a refresh. The indended use case is when the repository list is out +// of date and requires manual refresh. +type Cacher interface { + Purge(*model.User) +} + +// Because the cache is so closely tied to the remote we should just include +// them in the same package together. The below code are stubs for merging +// the Cache with the Remote package. + +type cache struct { + Remote +} + +func (c *cache) Repos(u *model.User) ([]*model.RepoLite, error) { + // key := fmt.Sprintf("repos:%s", + // user.Login, + // ) + // // if we fetch from the cache we can return immediately + // val, err := Get(c, key) + // if err == nil { + // return val.([]*model.RepoLite), nil + // } + // // else we try to grab from the remote system and + // // populate our cache. + // repos, err := remote.Repos(c, user) + // if err != nil { + // return nil, err + // } + // + // Set(c, key, repos) + // return repos, nil + return nil, nil +} + +func (c *cache) Perm(u *model.User, owner, repo string) (*model.Perm, error) { + // key := fmt.Sprintf("perms:%s:%s/%s", + // user.Login, + // owner, + // name, + // ) + // // if we fetch from the cache we can return immediately + // val, err := Get(c, key) + // if err == nil { + // return val.(*model.Perm), nil + // } + // // else we try to grab from the remote system and + // // populate our cache. + // perm, err := remote.Perm(c, user, owner, name) + // if err != nil { + // return nil, err + // } + // Set(c, key, perm) + // return perm, nil + return nil, nil +} + +func (c *cache) Purge(*model.User) { + return +} + +func (c *cache) Refresh(u *model.User) (bool, error) { + if r, ok := c.Remote.(Refresher); ok { + return r.Refresh(u) + } + return false, nil +} + +var _ Remote = &cache{} +var _ Refresher = &cache{} diff --git a/remote/github/github.go b/remote/github/github.go index e935f20f7..258aa9cb6 100644 --- a/remote/github/github.go +++ b/remote/github/github.go @@ -11,22 +11,18 @@ import ( "strings" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/oauth2" - log "github.com/Sirupsen/logrus" "github.com/google/go-github/github" ) const ( - DefaultURL = "https://github.com" - DefaultAPI = "https://api.github.com" - DefaultScope = "repo,repo:status,user:email" - DefaultMergeRef = "merge" + DefaultURL = "https://github.com" // Default GitHub URL + DefaultAPI = "https://api.github.com" // Default GitHub API URL ) -var githubDeployRegex = regexp.MustCompile(".+/deployments/(\\d+)") - type Github struct { URL string API string @@ -34,58 +30,35 @@ type Github struct { Secret string Scope string MergeRef string - Orgs []string - Open bool PrivateMode bool SkipVerify bool - GitSSH bool } -func Load(config string) *Github { - - // parse the remote DSN configuration string - url_, err := url.Parse(config) - if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) +func New(url, client, secret string, scope []string, private, skipverify, mergeref bool) (remote.Remote, error) { + remote := &Github{ + URL: strings.TrimSuffix(url, "/"), + Client: client, + Secret: secret, + Scope: strings.Join(scope, ","), + PrivateMode: private, + SkipVerify: skipverify, + MergeRef: "head", } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - // create the Githbub remote using parameters from - // the parsed DSN configuration string. - github := Github{} - github.URL = url_.String() - github.Client = params.Get("client_id") - github.Secret = params.Get("client_secret") - github.Scope = params.Get("scope") - github.Orgs = params["orgs"] - github.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) - github.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) - github.Open, _ = strconv.ParseBool(params.Get("open")) - github.GitSSH, _ = strconv.ParseBool(params.Get("ssh")) - github.MergeRef = params.Get("merge_ref") - - if github.URL == DefaultURL { - github.API = DefaultAPI + if remote.URL == DefaultURL { + remote.API = DefaultAPI } else { - github.API = github.URL + "/api/v3/" + remote.API = remote.URL + "/api/v3/" + } + if mergeref { + remote.MergeRef = "merge" } - if github.Scope == "" { - github.Scope = DefaultScope - } - - if github.MergeRef == "" { - github.MergeRef = DefaultMergeRef - } - - return &github + return remote, nil } -// Login authenticates the session and returns the -// remote user details. -func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// Login authenticates the session and returns the remote user details. +func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { var config = &oauth2.Config{ ClientId: g.Client, @@ -101,7 +74,7 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, if len(code) == 0 { var random = GetRandom() http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther) - return nil, false, nil + return nil, nil } var trans = &oauth2.Transport{ @@ -117,23 +90,13 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, } var token, err = trans.Exchange(code) if err != nil { - return nil, false, fmt.Errorf("Error exchanging token. %s", err) + return nil, fmt.Errorf("Error exchanging token. %s", err) } var client = NewClient(g.API, token.AccessToken, g.SkipVerify) var useremail, errr = GetUserEmail(client) if errr != nil { - return nil, false, fmt.Errorf("Error retrieving user or verified email. %s", errr) - } - - if len(g.Orgs) > 0 { - allowedOrg, err := UserBelongsToOrg(client, g.Orgs) - if err != nil { - return nil, false, fmt.Errorf("Could not check org membership. %s", err) - } - if !allowedOrg { - return nil, false, fmt.Errorf("User does not belong to correct org. Must belong to %v", g.Orgs) - } + return nil, fmt.Errorf("Error retrieving user or verified email. %s", errr) } user := model.User{} @@ -141,7 +104,7 @@ func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, user.Email = *useremail.Email user.Token = token.AccessToken user.Avatar = *useremail.AvatarURL - return &user, g.Open, nil + return &user, nil } // Auth authenticates the session and returns the remote user @@ -155,37 +118,52 @@ func (g *Github) Auth(token, secret string) (string, error) { return *user.Login, nil } -// Repo fetches the named repository from the remote system. -func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) { +func (g *Github) Teams(u *model.User) ([]*model.Team, error) { client := NewClient(g.API, u.Token, g.SkipVerify) - repo_, err := GetRepo(client, owner, name) + orgs, err := GetOrgs(client) if err != nil { return nil, err } - repo := &model.Repo{} - repo.Owner = owner - repo.Name = name - repo.FullName = *repo_.FullName - repo.Link = *repo_.HTMLURL - repo.IsPrivate = *repo_.Private - repo.Clone = *repo_.CloneURL - repo.Branch = "master" - repo.Avatar = *repo_.Owner.AvatarURL - repo.Kind = model.RepoGit + var teams []*model.Team + for _, org := range orgs { + teams = append(teams, &model.Team{ + Login: *org.Login, + Avatar: *org.AvatarURL, + }) + } + return teams, nil +} - if repo_.DefaultBranch != nil { - repo.Branch = *repo_.DefaultBranch +// Repo fetches the named repository from the remote system. +func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) { + client := NewClient(g.API, u.Token, g.SkipVerify) + r, err := GetRepo(client, owner, name) + if err != nil { + return nil, err + } + + repo := &model.Repo{ + Owner: owner, + Name: name, + FullName: *r.FullName, + Link: *r.HTMLURL, + IsPrivate: *r.Private, + Clone: *r.CloneURL, + Avatar: *r.Owner.AvatarURL, + Kind: model.RepoGit, + } + + if r.DefaultBranch != nil { + repo.Branch = *r.DefaultBranch + } else { + repo.Branch = "master" } if g.PrivateMode { repo.IsPrivate = true } - if g.GitSSH && repo.IsPrivate { - repo.Clone = *repo_.SSHURL - } - return repo, err } @@ -257,8 +235,10 @@ func repoStatus(client *github.Client, r *model.Repo, b *model.Build, link strin return err } +var reDeploy = regexp.MustCompile(".+/deployments/(\\d+)") + func deploymentStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error { - matches := githubDeployRegex.FindStringSubmatch(b.Link) + matches := reDeploy.FindStringSubmatch(b.Link) // if the deployment was not triggered from github, don't send a deployment status if len(matches) != 2 { return nil @@ -294,21 +274,7 @@ func (g *Github) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // adding the SSH deploy key, if applicable. func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { client := NewClient(g.API, u.Token, g.SkipVerify) - title, err := GetKeyTitle(link) - if err != nil { - return err - } - - // if the CloneURL is using the SSHURL then we know that - // we need to add an SSH key to GitHub. - if r.IsPrivate || g.PrivateMode { - _, err = CreateUpdateKey(client, r.Owner, r.Name, title, k.Public) - if err != nil { - return err - } - } - - _, err = CreateUpdateHook(client, r.Owner, r.Name, link) + _, err := CreateUpdateHook(client, r.Owner, r.Name, link) return err } @@ -316,18 +282,6 @@ func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link strin // which are equal to link and removing the SSH deploy key. func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error { client := NewClient(g.API, u.Token, g.SkipVerify) - title, err := GetKeyTitle(link) - if err != nil { - return err - } - - // remove the deploy-key if it is installed remote. - if r.IsPrivate || g.PrivateMode { - if err := DeleteKey(client, r.Owner, r.Name, title); err != nil { - return err - } - } - return DeleteHook(client, r.Owner, r.Name, link) } diff --git a/remote/github/github_test.go b/remote/github/github_test.go index 85ad35724..dcf673370 100644 --- a/remote/github/github_test.go +++ b/remote/github/github_test.go @@ -46,31 +46,32 @@ func TestHook(t *testing.T) { }) } -func TestLoad(t *testing.T) { - conf := "https://github.com?client_id=client&client_secret=secret&scope=scope1,scope2" - - g := Load(conf) - if g.URL != "https://github.com" { - t.Errorf("g.URL = %q; want https://github.com", g.URL) - } - if g.Client != "client" { - t.Errorf("g.Client = %q; want client", g.Client) - } - if g.Secret != "secret" { - t.Errorf("g.Secret = %q; want secret", g.Secret) - } - if g.Scope != "scope1,scope2" { - t.Errorf("g.Scope = %q; want scope1,scope2", g.Scope) - } - if g.API != DefaultAPI { - t.Errorf("g.API = %q; want %q", g.API, DefaultAPI) - } - if g.MergeRef != DefaultMergeRef { - t.Errorf("g.MergeRef = %q; want %q", g.MergeRef, DefaultMergeRef) - } - - g = Load("") - if g.Scope != DefaultScope { - t.Errorf("g.Scope = %q; want %q", g.Scope, DefaultScope) - } -} +// +// func TestLoad(t *testing.T) { +// conf := "https://github.com?client_id=client&client_secret=secret&scope=scope1,scope2" +// +// g := Load(conf) +// if g.URL != "https://github.com" { +// t.Errorf("g.URL = %q; want https://github.com", g.URL) +// } +// if g.Client != "client" { +// t.Errorf("g.Client = %q; want client", g.Client) +// } +// if g.Secret != "secret" { +// t.Errorf("g.Secret = %q; want secret", g.Secret) +// } +// if g.Scope != "scope1,scope2" { +// t.Errorf("g.Scope = %q; want scope1,scope2", g.Scope) +// } +// if g.API != DefaultAPI { +// t.Errorf("g.API = %q; want %q", g.API, DefaultAPI) +// } +// if g.MergeRef != DefaultMergeRef { +// t.Errorf("g.MergeRef = %q; want %q", g.MergeRef, DefaultMergeRef) +// } +// +// g = Load("") +// if g.Scope != DefaultScope { +// t.Errorf("g.Scope = %q; want %q", g.Scope, DefaultScope) +// } +// } diff --git a/remote/github/helper.go b/remote/github/helper.go index f86aadc6d..362df938c 100644 --- a/remote/github/helper.go +++ b/remote/github/helper.go @@ -72,53 +72,6 @@ func GetRepo(client *github.Client, owner, repo string) (*github.Repository, err return r, err } -// GetAllRepos is a helper function that returns an aggregated list -// of all user and organization repositories. -func GetAllRepos(client *github.Client) ([]github.Repository, error) { - orgs, err := GetOrgs(client) - if err != nil { - return nil, err - } - - repos, err := GetUserRepos(client) - if err != nil { - return nil, err - } - - for _, org := range orgs { - list, err := GetOrgRepos(client, *org.Login) - if err != nil { - return nil, err - } - repos = append(repos, list...) - } - - return repos, nil -} - -// GetSubscriptions is a helper function that returns an aggregated list -// of all user and organization repositories. -// func GetSubscriptions(client *github.Client) ([]github.Repository, error) { -// var repos []github.Repository -// var opts = github.ListOptions{} -// opts.PerPage = 100 -// opts.Page = 1 - -// // loop through user repository list -// for opts.Page > 0 { -// list, resp, err := client.Activity.ListWatched(""), &opts) -// if err != nil { -// return nil, err -// } -// repos = append(repos, list...) - -// // increment the next page to retrieve -// opts.Page = resp.NextPage -// } - -// return repos, nil -// } - // GetUserRepos is a helper function that returns a list of // all user repositories. Paginated results are aggregated into // a single list. @@ -143,30 +96,6 @@ func GetUserRepos(client *github.Client) ([]github.Repository, error) { return repos, nil } -// GetOrgRepos is a helper function that returns a list of -// all org repositories. Paginated results are aggregated into -// a single list. -func GetOrgRepos(client *github.Client, org string) ([]github.Repository, error) { - var repos []github.Repository - var opts = github.RepositoryListByOrgOptions{} - opts.PerPage = 100 - opts.Page = 1 - - // loop through user repository list - for opts.Page > 0 { - list, resp, err := client.Repositories.ListByOrg(org, &opts) - if err != nil { - return nil, err - } - repos = append(repos, list...) - - // increment the next page to retrieve - opts.Page = resp.NextPage - } - - return repos, nil -} - // GetOrgs is a helper function that returns a list of // all orgs that a user belongs to. func GetOrgs(client *github.Client) ([]github.Organization, error) { @@ -250,70 +179,6 @@ func CreateUpdateHook(client *github.Client, owner, name, url string) (*github.H return CreateHook(client, owner, name, url) } -// GetKey is a heper function that retrieves a public Key by -// title. To do this, it will retrieve a list of all keys -// and iterate through the list. -func GetKey(client *github.Client, owner, name, title string) (*github.Key, error) { - keys, _, err := client.Repositories.ListKeys(owner, name, nil) - if err != nil { - return nil, err - } - for _, key := range keys { - if *key.Title == title { - return &key, nil - } - } - return nil, nil -} - -// GetKeyTitle is a helper function that generates a title for the -// RSA public key based on the username and domain name. -func GetKeyTitle(rawurl string) (string, error) { - var uri, err = url.Parse(rawurl) - if err != nil { - return "", err - } - return fmt.Sprintf("drone@%s", uri.Host), nil -} - -// DeleteKey is a helper function that deletes a deploy key -// for the specified repository. -func DeleteKey(client *github.Client, owner, name, title string) error { - var k, err = GetKey(client, owner, name, title) - if err != nil { - return err - } - if k == nil { - return nil - } - _, err = client.Repositories.DeleteKey(owner, name, *k.ID) - return err -} - -// CreateKey is a helper function that creates a deploy key -// for the specified repository. -func CreateKey(client *github.Client, owner, name, title, key string) (*github.Key, error) { - var k = new(github.Key) - k.Title = github.String(title) - k.Key = github.String(key) - created, _, err := client.Repositories.CreateKey(owner, name, k) - return created, err -} - -// CreateUpdateKey is a helper function that creates a deployment key -// for the specified repository if it does not already exist, otherwise -// it updates the existing key -func CreateUpdateKey(client *github.Client, owner, name, title, key string) (*github.Key, error) { - var k, _ = GetKey(client, owner, name, title) - if k != nil { - k.Title = github.String(title) - k.Key = github.String(key) - client.Repositories.DeleteKey(owner, name, *k.ID) - } - - return CreateKey(client, owner, name, title, key) -} - // GetFile is a heper function that retrieves a file from // GitHub and returns its contents in byte array format. func GetFile(client *github.Client, owner, name, path, ref string) ([]byte, error) { @@ -344,25 +209,3 @@ func GetPayload(req *http.Request) []byte { } return []byte(payload) } - -// UserBelongsToOrg returns true if the currently authenticated user is a -// member of any of the organizations provided. -func UserBelongsToOrg(client *github.Client, permittedOrgs []string) (bool, error) { - userOrgs, err := GetOrgs(client) - if err != nil { - return false, err - } - - userOrgSet := make(map[string]struct{}, len(userOrgs)) - for _, org := range userOrgs { - userOrgSet[*org.Login] = struct{}{} - } - - for _, org := range permittedOrgs { - if _, ok := userOrgSet[org]; ok { - return true, nil - } - } - - return false, nil -} diff --git a/remote/gitlab/gitlab.go b/remote/gitlab/gitlab.go index 60ffc5ed2..b5beca6d0 100644 --- a/remote/gitlab/gitlab.go +++ b/remote/gitlab/gitlab.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/oauth2" "github.com/drone/drone/shared/token" @@ -34,6 +35,16 @@ type Gitlab struct { Search bool } +func New(url, client, secret string, private, skipverify bool) remote.Remote { + return &Gitlab{ + URL: url, + Client: client, + Secret: secret, + PrivateMode: private, + SkipVerify: skipverify, + } +} + func Load(config string) *Gitlab { url_, err := url.Parse(config) if err != nil { diff --git a/remote/gogs/gogs.go b/remote/gogs/gogs.go index d30a63236..31b4b1604 100644 --- a/remote/gogs/gogs.go +++ b/remote/gogs/gogs.go @@ -6,44 +6,33 @@ import ( "net" "net/http" "net/url" - "strconv" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/gogits/go-gogs-client" - - log "github.com/Sirupsen/logrus" ) -type Gogs struct { +// Remote defines a remote implementation that integrates with Gogs, an open +// source Git service written in Go. See https://gogs.io/ +type Remote struct { URL string Open bool PrivateMode bool SkipVerify bool } -func Load(config string) *Gogs { - // parse the remote DSN configuration string - url_, err := url.Parse(config) - if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) +// New returns a Remote implementation that integrates with Gogs, an open +// source Git service written in Go. See https://gogs.io/ +func New(url string, private, skipverify bool) remote.Remote { + return &Remote{ + URL: url, + PrivateMode: private, + SkipVerify: skipverify, } - params := url_.Query() - url_.RawQuery = "" - - // create the Githbub remote using parameters from - // the parsed DSN configuration string. - gogs := Gogs{} - gogs.URL = url_.String() - gogs.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode")) - gogs.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) - gogs.Open, _ = strconv.ParseBool(params.Get("open")) - - return &gogs } -// Login authenticates the session and returns the -// remote user details. -func (g *Gogs) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// Login authenticates the session and returns the authenticated user. +func (g *Remote) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { var ( username = req.FormValue("username") password = req.FormValue("password") @@ -91,17 +80,17 @@ func (g *Gogs) Login(res http.ResponseWriter, req *http.Request) (*model.User, b user.Login = userInfo.UserName user.Email = userInfo.Email user.Avatar = expandAvatar(g.URL, userInfo.AvatarUrl) - return &user, g.Open, nil + return &user, false, 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) { +func (g *Remote) 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) { +func (g *Remote) 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 { @@ -119,7 +108,7 @@ func (g *Gogs) Repo(u *model.User, owner, name string) (*model.Repo, error) { } // Repos fetches a list of repos from the remote system. -func (g *Gogs) Repos(u *model.User) ([]*model.RepoLite, error) { +func (g *Remote) Repos(u *model.User) ([]*model.RepoLite, error) { repos := []*model.RepoLite{} client := NewGogsClient(g.URL, u.Token, g.SkipVerify) @@ -137,7 +126,7 @@ func (g *Gogs) Repos(u *model.User) ([]*model.RepoLite, error) { // Perm fetches the named repository permissions from // the remote system for the specified user. -func (g *Gogs) Perm(u *model.User, owner, name string) (*model.Perm, error) { +func (g *Remote) Perm(u *model.User, owner, name string) (*model.Perm, error) { client := NewGogsClient(g.URL, u.Token, g.SkipVerify) repos_, err := client.ListMyRepos() if err != nil { @@ -156,7 +145,7 @@ func (g *Gogs) Perm(u *model.User, owner, name string) (*model.Perm, error) { } // File fetches a file from the remote repository and returns in string format. -func (g *Gogs) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { +func (g *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { client := NewGogsClient(g.URL, u.Token, g.SkipVerify) cfg, err := client.GetFile(r.Owner, r.Name, b.Commit, f) return cfg, err @@ -164,13 +153,13 @@ func (g *Gogs) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]b // Status sends the commit status to the remote system. // An example would be the GitHub pull request status. -func (g *Gogs) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { +func (g *Remote) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { return fmt.Errorf("Not Implemented") } // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. -func (g *Gogs) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { +func (g *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { url_, err := url.Parse(g.URL) if err != nil { return nil, err @@ -188,7 +177,7 @@ func (g *Gogs) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. -func (g *Gogs) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { +func (g *Remote) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { config := map[string]string{ "url": link, "secret": r.Hash, @@ -207,13 +196,13 @@ func (g *Gogs) Activate(u *model.User, r *model.Repo, k *model.Key, link string) // Deactivate removes a repository by removing all the post-commit hooks // which are equal to link and removing the SSH deploy key. -func (g *Gogs) Deactivate(u *model.User, r *model.Repo, link string) error { +func (g *Remote) Deactivate(u *model.User, r *model.Repo, link string) error { return fmt.Errorf("Not Implemented") } // Hook parses the post-commit hook from the Request body // and returns the required data in a standard format. -func (g *Gogs) Hook(r *http.Request) (*model.Repo, *model.Build, error) { +func (g *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { var ( err error repo *model.Repo @@ -246,6 +235,6 @@ func NewGogsClient(url, token string, skipVerify bool) *gogs.Client { return c } -func (g *Gogs) String() string { +func (g *Remote) String() string { return "gogs" } diff --git a/remote/mock/remote.go b/remote/mock/remote.go index 28928b9fa..69132d20d 100644 --- a/remote/mock/remote.go +++ b/remote/mock/remote.go @@ -1,42 +1,32 @@ package mock -import "github.com/stretchr/testify/mock" +import ( + "net/http" -import "net/http" -import "github.com/drone/drone/model" + "github.com/drone/drone/model" + "github.com/stretchr/testify/mock" +) +// This is an autogenerated mock type for the Remote type type Remote struct { mock.Mock } -func (_m *Remote) Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error) { - ret := _m.Called(w, r) +// Activate provides a mock function with given fields: u, r, k, link +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 *model.User - if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { - r0 = rf(w, r) + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Key, string) error); ok { + r0 = rf(u, r, k, link) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) - } + r0 = ret.Error(0) } - var r1 bool - if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) bool); ok { - r1 = rf(w, r) - } else { - r1 = ret.Get(1).(bool) - } - - var r2 error - if rf, ok := ret.Get(2).(func(http.ResponseWriter, *http.Request) error); ok { - r2 = rf(w, r) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 + return r0 } + +// Auth provides a mock function with given fields: token, secret func (_m *Remote) Auth(token string, secret string) (string, error) { ret := _m.Called(token, secret) @@ -56,69 +46,22 @@ func (_m *Remote) Auth(token string, secret string) (string, error) { return r0, r1 } -func (_m *Remote) Repo(u *model.User, owner string, repo string) (*model.Repo, error) { - ret := _m.Called(u, owner, repo) - var r0 *model.Repo - if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { - r0 = rf(u, owner, repo) +// Deactivate provides a mock function with given fields: u, r, link +func (_m *Remote) Deactivate(u *model.User, r *model.Repo, link string) error { + ret := _m.Called(u, r, link) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { + r0 = rf(u, r, link) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Repo) - } + r0 = ret.Error(0) } - var r1 error - if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { - r1 = rf(u, owner, repo) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } -func (_m *Remote) Repos(u *model.User) ([]*model.RepoLite, error) { - ret := _m.Called(u) - var r0 []*model.RepoLite - if rf, ok := ret.Get(0).(func(*model.User) []*model.RepoLite); ok { - r0 = rf(u) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.RepoLite) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.User) error); ok { - r1 = rf(u) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} -func (_m *Remote) Perm(u *model.User, owner string, repo string) (*model.Perm, error) { - ret := _m.Called(u, owner, repo) - - var r0 *model.Perm - if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Perm); ok { - r0 = rf(u, owner, repo) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Perm) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { - r1 = rf(u, owner, repo) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} +// File provides a mock function with given fields: u, r, b, f func (_m *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { ret := _m.Called(u, r, b, f) @@ -140,63 +83,8 @@ func (_m *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ( return r0, r1 } -func (_m *Remote) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - ret := _m.Called(u, r, b, link) - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Build, string) error); ok { - r0 = rf(u, r, b, link) - } else { - r0 = ret.Error(0) - } - - return r0 -} -func (_m *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - ret := _m.Called(u, r) - - var r0 *model.Netrc - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo) *model.Netrc); ok { - r0 = rf(u, r) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Netrc) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(*model.User, *model.Repo) error); ok { - r1 = rf(u, r) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} -func (_m *Remote) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - ret := _m.Called(u, r, k, link) - - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Key, string) error); ok { - r0 = rf(u, r, k, link) - } else { - r0 = ret.Error(0) - } - - return r0 -} -func (_m *Remote) Deactivate(u *model.User, r *model.Repo, link string) error { - ret := _m.Called(u, r, link) - - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { - r0 = rf(u, r, link) - } else { - r0 = ret.Error(0) - } - - return r0 -} +// Hook provides a mock function with given fields: r func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { ret := _m.Called(r) @@ -227,3 +115,155 @@ func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { return r0, r1, r2 } + +// Login provides a mock function with given fields: w, r +func (_m *Remote) Login(w http.ResponseWriter, r *http.Request) (*model.User, error) { + ret := _m.Called(w, r) + + var r0 *model.User + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { + r0 = rf(w, r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) error); ok { + r1 = rf(w, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Netrc provides a mock function with given fields: u, r +func (_m *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + ret := _m.Called(u, r) + + var r0 *model.Netrc + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo) *model.Netrc); ok { + r0 = rf(u, r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Netrc) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo) error); ok { + r1 = rf(u, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Perm provides a mock function with given fields: u, owner, repo +func (_m *Remote) Perm(u *model.User, owner string, repo string) (*model.Perm, error) { + ret := _m.Called(u, owner, repo) + + var r0 *model.Perm + if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Perm); ok { + r0 = rf(u, owner, repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Perm) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { + r1 = rf(u, owner, repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repo provides a mock function with given fields: u, owner, repo +func (_m *Remote) Repo(u *model.User, owner string, repo string) (*model.Repo, error) { + ret := _m.Called(u, owner, repo) + + var r0 *model.Repo + if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { + r0 = rf(u, owner, repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Repo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, string, string) error); ok { + r1 = rf(u, owner, repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repos provides a mock function with given fields: u +func (_m *Remote) Repos(u *model.User) ([]*model.RepoLite, error) { + ret := _m.Called(u) + + var r0 []*model.RepoLite + if rf, ok := ret.Get(0).(func(*model.User) []*model.RepoLite); ok { + r0 = rf(u) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.RepoLite) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(u) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Status provides a mock function with given fields: u, r, b, link +func (_m *Remote) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + ret := _m.Called(u, r, b, link) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Build, string) error); ok { + r0 = rf(u, r, b, link) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Teams provides a mock function with given fields: u +func (_m *Remote) Teams(u *model.User) ([]*model.Team, error) { + ret := _m.Called(u) + + var r0 []*model.Team + if rf, ok := ret.Get(0).(func(*model.User) []*model.Team); ok { + r0 = rf(u) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Team) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(u) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/remote/remote.go b/remote/remote.go index 875988d20..a1255104d 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -13,12 +13,15 @@ import ( type Remote interface { // Login authenticates the session and returns the // remote user details. - Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error) + Login(w http.ResponseWriter, r *http.Request) (*model.User, error) // Auth authenticates the session and returns the remote user // login for the given token and secret Auth(token, secret string) (string, error) + // Teams fetches a list of team memberships from the remote system. + Teams(u *model.User) ([]*model.Team, error) + // Repo fetches the named repository from the remote system. Repo(u *model.User, owner, repo string) (*model.Repo, error) @@ -49,21 +52,21 @@ type Remote interface { // which are equal to link and removing the SSH deploy key. Deactivate(u *model.User, r *model.Repo, link string) error - // Hook parses the post-commit hook from the Request body - // and returns the required data in a standard format. + // Hook parses the post-commit hook from the Request body and returns the + // required data in a standard format. Hook(r *http.Request) (*model.Repo, *model.Build, error) } +// Refresher refreshes an oauth token and expiration for the given user. It +// returns true if the token was refreshed, false if the token was not refreshed, +// and error if it failed to refersh. type Refresher interface { - // Refresh refreshes an oauth token and expiration for the given - // user. It returns true if the token was refreshed, false if the - // token was not refreshed, and error if it failed to refersh. Refresh(*model.User) (bool, error) } // Login authenticates the session and returns the // remote user details. -func Login(c context.Context, w http.ResponseWriter, r *http.Request) (*model.User, bool, error) { +func Login(c context.Context, w http.ResponseWriter, r *http.Request) (*model.User, error) { return FromContext(c).Login(w, r) } @@ -73,6 +76,11 @@ func Auth(c context.Context, token, secret string) (string, error) { return FromContext(c).Auth(token, secret) } +// Teams fetches a list of team memberships from the remote system. +func Teams(c context.Context, u *model.User) ([]*model.Team, error) { + return FromContext(c).Teams(u) +} + // Repo fetches the named repository from the remote system. func Repo(c context.Context, u *model.User, owner, repo string) (*model.Repo, error) { return FromContext(c).Repo(u, owner, repo) diff --git a/router/middleware/agent.go b/router/middleware/agent.go index e227089a9..0a3265519 100644 --- a/router/middleware/agent.go +++ b/router/middleware/agent.go @@ -9,7 +9,7 @@ import ( ) var ( - secret = envflag.String("AGENT_SECRET", "", "") + secret = envflag.String("DRONE_AGENT_SECRET", "", "") noauth = envflag.Bool("AGENT_NO_AUTH", false, "") ) diff --git a/router/middleware/bus.go b/router/middleware/bus.go deleted file mode 100644 index b5f5c57d5..000000000 --- a/router/middleware/bus.go +++ /dev/null @@ -1,14 +0,0 @@ -package middleware - -import ( - "github.com/drone/drone/bus" - "github.com/gin-gonic/gin" -) - -func Bus() gin.HandlerFunc { - bus_ := bus.New() - return func(c *gin.Context) { - bus.ToContext(c, bus_) - c.Next() - } -} diff --git a/router/middleware/cache.go b/router/middleware/cache.go deleted file mode 100644 index aa8d46b4d..000000000 --- a/router/middleware/cache.go +++ /dev/null @@ -1,22 +0,0 @@ -package middleware - -import ( - "time" - - "github.com/drone/drone/cache" - - "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" -) - -var ttl = envflag.Duration("CACHE_TTL", time.Minute*15, "") - -// Cache is a middleware function that initializes the Cache and attaches to -// the context of every http.Request. -func Cache() gin.HandlerFunc { - cc := cache.NewTTL(*ttl) - return func(c *gin.Context) { - cache.ToContext(c, cc) - c.Next() - } -} diff --git a/router/middleware/queue.go b/router/middleware/queue.go deleted file mode 100644 index 692a5432e..000000000 --- a/router/middleware/queue.go +++ /dev/null @@ -1,14 +0,0 @@ -package middleware - -import ( - "github.com/drone/drone/queue" - "github.com/gin-gonic/gin" -) - -func Queue() gin.HandlerFunc { - queue_ := queue.New() - return func(c *gin.Context) { - queue.ToContext(c, queue_) - c.Next() - } -} diff --git a/router/middleware/remote.go b/router/middleware/remote.go deleted file mode 100644 index 3ad33e94d..000000000 --- a/router/middleware/remote.go +++ /dev/null @@ -1,48 +0,0 @@ -package middleware - -import ( - "github.com/drone/drone/remote" - "github.com/drone/drone/remote/bitbucket" - "github.com/drone/drone/remote/github" - "github.com/drone/drone/remote/gitlab" - "github.com/drone/drone/remote/gogs" - - "github.com/Sirupsen/logrus" - "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" - "github.com/drone/drone/remote/bitbucketserver" -) - -var ( - driver = envflag.String("REMOTE_DRIVER", "", "") - config = envflag.String("REMOTE_CONFIG", "", "") -) - -// Remote is a middleware function that initializes the Remote and attaches to -// the context of every http.Request. -func Remote() gin.HandlerFunc { - - logrus.Infof("using remote driver %s", *driver) - logrus.Infof("using remote config %s", *config) - - var remote_ remote.Remote - switch *driver { - case "github": - remote_ = github.Load(*config) - case "bitbucket": - remote_ = bitbucket.Load(*config) - case "gogs": - remote_ = gogs.Load(*config) - case "gitlab": - remote_ = gitlab.Load(*config) - case "bitbucketserver": - remote_ = bitbucketserver.Load(*config) - default: - logrus.Fatalln("remote configuration not found") - } - - return func(c *gin.Context) { - remote.ToContext(c, remote_) - c.Next() - } -} diff --git a/router/middleware/store.go b/router/middleware/store.go deleted file mode 100644 index 91439d433..000000000 --- a/router/middleware/store.go +++ /dev/null @@ -1,29 +0,0 @@ -package middleware - -import ( - "github.com/drone/drone/store" - "github.com/drone/drone/store/datastore" - - "github.com/Sirupsen/logrus" - "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" -) - -var ( - database = envflag.String("DATABASE_DRIVER", "sqlite3", "") - datasource = envflag.String("DATABASE_CONFIG", "drone.sqlite", "") -) - -// Store is a middleware function that initializes the Datastore and attaches to -// the context of every http.Request. -func Store() gin.HandlerFunc { - db := datastore.New(*database, *datasource) - - logrus.Infof("using database driver %s", *database) - logrus.Infof("using database config %s", *datasource) - - return func(c *gin.Context) { - store.ToContext(c, db) - c.Next() - } -} diff --git a/router/middleware/stream.go b/router/middleware/stream.go deleted file mode 100644 index 43bdabe05..000000000 --- a/router/middleware/stream.go +++ /dev/null @@ -1,14 +0,0 @@ -package middleware - -import ( - "github.com/drone/drone/stream" - "github.com/gin-gonic/gin" -) - -func Stream() gin.HandlerFunc { - stream_ := stream.New() - return func(c *gin.Context) { - stream.ToContext(c, stream_) - c.Next() - } -} diff --git a/router/middleware/version.go b/router/middleware/version.go deleted file mode 100644 index b4de05f96..000000000 --- a/router/middleware/version.go +++ /dev/null @@ -1,14 +0,0 @@ -package middleware - -import ( - "github.com/drone/drone/version" - "github.com/gin-gonic/gin" -) - -// Version is a middleware function that appends the Drone -// version information to the HTTP response. This is intended -// for debugging and troubleshooting. -func Version(c *gin.Context) { - c.Header("X-DRONE-VERSION", version.Version) - c.Next() -} diff --git a/router/router.go b/router/router.go index 32a13998f..2d62d42b3 100644 --- a/router/router.go +++ b/router/router.go @@ -1,200 +1,201 @@ package router -import ( - "net/http" - "strings" - - "github.com/gin-gonic/gin" - - "github.com/drone/drone/api" - "github.com/drone/drone/router/middleware" - "github.com/drone/drone/router/middleware/header" - "github.com/drone/drone/router/middleware/session" - "github.com/drone/drone/router/middleware/token" - "github.com/drone/drone/static" - "github.com/drone/drone/template" - "github.com/drone/drone/web" -) - -func Load(middlewares ...gin.HandlerFunc) http.Handler { - e := gin.New() - e.Use(gin.Recovery()) - - e.SetHTMLTemplate(template.Load()) - e.StaticFS("/static", static.FileSystem()) - - e.Use(header.NoCache) - e.Use(header.Options) - e.Use(header.Secure) - e.Use(middlewares...) - e.Use(session.SetUser()) - e.Use(token.Refresh) - - e.GET("/", web.ShowIndex) - e.GET("/repos", web.ShowAllRepos) - e.GET("/login", web.ShowLogin) - e.GET("/login/form", web.ShowLoginForm) - e.GET("/logout", web.GetLogout) - - settings := e.Group("/settings") - { - settings.Use(session.MustUser()) - settings.GET("/profile", web.ShowUser) - } - repo := e.Group("/repos/:owner/:name") - { - repo.Use(session.SetRepo()) - repo.Use(session.SetPerm()) - repo.Use(session.MustPull) - - repo.GET("", web.ShowRepo) - repo.GET("/builds/:number", web.ShowBuild) - repo.GET("/builds/:number/:job", web.ShowBuild) - - repo_settings := repo.Group("/settings") - { - repo_settings.GET("", session.MustPush, web.ShowRepoConf) - repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) - repo_settings.GET("/badges", web.ShowRepoBadges) - } - } - - user := e.Group("/api/user") - { - user.Use(session.MustUser()) - user.GET("", api.GetSelf) - user.GET("/feed", api.GetFeed) - user.GET("/repos", api.GetRepos) - user.GET("/repos/remote", api.GetRemoteRepos) - user.POST("/token", api.PostToken) - user.DELETE("/token", api.DeleteToken) - } - - users := e.Group("/api/users") - { - users.Use(session.MustAdmin()) - users.GET("", api.GetUsers) - users.POST("", api.PostUser) - users.GET("/:login", api.GetUser) - users.PATCH("/:login", api.PatchUser) - users.DELETE("/:login", api.DeleteUser) - } - - repos := e.Group("/api/repos/:owner/:name") - { - repos.POST("", api.PostRepo) - - repo := repos.Group("") - { - repo.Use(session.SetRepo()) - repo.Use(session.SetPerm()) - repo.Use(session.MustPull) - - repo.GET("", api.GetRepo) - repo.GET("/key", api.GetRepoKey) - repo.POST("/key", api.PostRepoKey) - repo.GET("/builds", api.GetBuilds) - repo.GET("/builds/:number", api.GetBuild) - repo.GET("/logs/:number/:job", api.GetBuildLogs) - repo.POST("/sign", session.MustPush, api.Sign) - - repo.POST("/secrets", session.MustPush, api.PostSecret) - repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) - - // requires authenticated user - repo.POST("/encrypt", session.MustUser(), api.PostSecure) - - // requires push permissions - repo.PATCH("", session.MustPush, api.PatchRepo) - repo.DELETE("", session.MustPush, api.DeleteRepo) - - repo.POST("/builds/:number", session.MustPush, api.PostBuild) - repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) - } - } - - badges := e.Group("/api/badges/:owner/:name") - { - badges.GET("/status.svg", web.GetBadge) - badges.GET("/cc.xml", web.GetCC) - } - - e.POST("/hook", web.PostHook) - e.POST("/api/hook", web.PostHook) - - stream := e.Group("/api/stream") - { - stream.Use(session.SetRepo()) - stream.Use(session.SetPerm()) - stream.Use(session.MustPull) - - 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.GET("", web.GetLogin) - auth.POST("", web.GetLogin) - auth.POST("/token", web.GetLoginToken) - } - - queue := e.Group("/api/queue") - { - queue.Use(middleware.AgentMust()) - queue.POST("/pull", api.Pull) - queue.POST("/pull/:os/:arch", api.Pull) - queue.POST("/wait/:id", api.Wait) - queue.POST("/stream/:id", api.Stream) - queue.POST("/status/:id", api.Update) - } - - gitlab := e.Group("/gitlab/:owner/:name") - { - gitlab.Use(session.SetRepo()) - gitlab.GET("/commits/:sha", web.GetCommit) - gitlab.GET("/pulls/:number", web.GetPullRequest) - - redirects := gitlab.Group("/redirect") - { - redirects.GET("/commits/:sha", web.RedirectSha) - redirects.GET("/pulls/:number", web.RedirectPullRequest) - } - } - - return normalize(e) -} - -// normalize is a helper function to work around the following -// issue with gin. https://github.com/gin-gonic/gin/issues/388 -func normalize(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - parts := strings.Split(r.URL.Path, "/")[1:] - switch parts[0] { - case "settings", "bots", "repos", "api", "login", "logout", "", "authorize", "hook", "static", "gitlab": - // no-op - default: - - if len(parts) > 2 && parts[2] != "settings" { - parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) - } - - // prefix the URL with /repo so that it - // can be effectively routed. - parts = append([]string{"", "repos"}, parts...) - - // reconstruct the path - r.URL.Path = strings.Join(parts, "/") - } - - h.ServeHTTP(w, r) - }) -} +// +// import ( +// "net/http" +// "strings" +// +// "github.com/gin-gonic/gin" +// +// "github.com/drone/drone/api" +// "github.com/drone/drone/router/middleware" +// "github.com/drone/drone/router/middleware/header" +// "github.com/drone/drone/router/middleware/session" +// "github.com/drone/drone/router/middleware/token" +// "github.com/drone/drone/static" +// "github.com/drone/drone/template" +// "github.com/drone/drone/web" +// ) +// +// func Load(middlewares ...gin.HandlerFunc) http.Handler { +// e := gin.New() +// e.Use(gin.Recovery()) +// +// e.SetHTMLTemplate(template.Load()) +// e.StaticFS("/static", static.FileSystem()) +// +// e.Use(header.NoCache) +// e.Use(header.Options) +// e.Use(header.Secure) +// e.Use(middlewares...) +// e.Use(session.SetUser()) +// e.Use(token.Refresh) +// +// e.GET("/", web.ShowIndex) +// e.GET("/repos", web.ShowAllRepos) +// e.GET("/login", web.ShowLogin) +// e.GET("/login/form", web.ShowLoginForm) +// e.GET("/logout", web.GetLogout) +// +// settings := e.Group("/settings") +// { +// settings.Use(session.MustUser()) +// settings.GET("/profile", web.ShowUser) +// } +// repo := e.Group("/repos/:owner/:name") +// { +// repo.Use(session.SetRepo()) +// repo.Use(session.SetPerm()) +// repo.Use(session.MustPull) +// +// repo.GET("", web.ShowRepo) +// repo.GET("/builds/:number", web.ShowBuild) +// repo.GET("/builds/:number/:job", web.ShowBuild) +// +// repo_settings := repo.Group("/settings") +// { +// repo_settings.GET("", session.MustPush, web.ShowRepoConf) +// repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) +// repo_settings.GET("/badges", web.ShowRepoBadges) +// } +// } +// +// user := e.Group("/api/user") +// { +// user.Use(session.MustUser()) +// user.GET("", api.GetSelf) +// user.GET("/feed", api.GetFeed) +// user.GET("/repos", api.GetRepos) +// user.GET("/repos/remote", api.GetRemoteRepos) +// user.POST("/token", api.PostToken) +// user.DELETE("/token", api.DeleteToken) +// } +// +// users := e.Group("/api/users") +// { +// users.Use(session.MustAdmin()) +// users.GET("", api.GetUsers) +// users.POST("", api.PostUser) +// users.GET("/:login", api.GetUser) +// users.PATCH("/:login", api.PatchUser) +// users.DELETE("/:login", api.DeleteUser) +// } +// +// repos := e.Group("/api/repos/:owner/:name") +// { +// repos.POST("", api.PostRepo) +// +// repo := repos.Group("") +// { +// repo.Use(session.SetRepo()) +// repo.Use(session.SetPerm()) +// repo.Use(session.MustPull) +// +// repo.GET("", api.GetRepo) +// repo.GET("/key", api.GetRepoKey) +// repo.POST("/key", api.PostRepoKey) +// repo.GET("/builds", api.GetBuilds) +// repo.GET("/builds/:number", api.GetBuild) +// repo.GET("/logs/:number/:job", api.GetBuildLogs) +// repo.POST("/sign", session.MustPush, api.Sign) +// +// repo.POST("/secrets", session.MustPush, api.PostSecret) +// repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) +// +// // requires authenticated user +// repo.POST("/encrypt", session.MustUser(), api.PostSecure) +// +// // requires push permissions +// repo.PATCH("", session.MustPush, api.PatchRepo) +// repo.DELETE("", session.MustPush, api.DeleteRepo) +// +// repo.POST("/builds/:number", session.MustPush, api.PostBuild) +// repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) +// } +// } +// +// badges := e.Group("/api/badges/:owner/:name") +// { +// badges.GET("/status.svg", web.GetBadge) +// badges.GET("/cc.xml", web.GetCC) +// } +// +// e.POST("/hook", web.PostHook) +// e.POST("/api/hook", web.PostHook) +// +// stream := e.Group("/api/stream") +// { +// stream.Use(session.SetRepo()) +// stream.Use(session.SetPerm()) +// stream.Use(session.MustPull) +// +// 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.GET("", web.GetLogin) +// auth.POST("", web.GetLogin) +// auth.POST("/token", web.GetLoginToken) +// } +// +// queue := e.Group("/api/queue") +// { +// queue.Use(middleware.AgentMust()) +// queue.POST("/pull", api.Pull) +// queue.POST("/pull/:os/:arch", api.Pull) +// queue.POST("/wait/:id", api.Wait) +// queue.POST("/stream/:id", api.Stream) +// queue.POST("/status/:id", api.Update) +// } +// +// gitlab := e.Group("/gitlab/:owner/:name") +// { +// gitlab.Use(session.SetRepo()) +// gitlab.GET("/commits/:sha", web.GetCommit) +// gitlab.GET("/pulls/:number", web.GetPullRequest) +// +// redirects := gitlab.Group("/redirect") +// { +// redirects.GET("/commits/:sha", web.RedirectSha) +// redirects.GET("/pulls/:number", web.RedirectPullRequest) +// } +// } +// +// return normalize(e) +// } +// +// // normalize is a helper function to work around the following +// // issue with gin. https://github.com/gin-gonic/gin/issues/388 +// func normalize(h http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// +// parts := strings.Split(r.URL.Path, "/")[1:] +// switch parts[0] { +// case "settings", "bots", "repos", "api", "login", "logout", "", "authorize", "hook", "static", "gitlab": +// // no-op +// default: +// +// if len(parts) > 2 && parts[2] != "settings" { +// parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) +// } +// +// // prefix the URL with /repo so that it +// // can be effectively routed. +// parts = append([]string{"", "repos"}, parts...) +// +// // reconstruct the path +// r.URL.Path = strings.Join(parts, "/") +// } +// +// h.ServeHTTP(w, r) +// }) +// } diff --git a/server/handler.go b/server/handler.go new file mode 100644 index 000000000..25bb1fd5d --- /dev/null +++ b/server/handler.go @@ -0,0 +1,79 @@ +package server + +import ( + "github.com/drone/drone/bus" + "github.com/drone/drone/cache" + "github.com/drone/drone/queue" + "github.com/drone/drone/remote" + "github.com/drone/drone/store" + "github.com/drone/drone/stream" + "github.com/drone/drone/version" + + "github.com/gin-gonic/gin" +) + +// HandlerCache returns a HandlerFunc that passes a Cache to the Context. +func HandlerCache(v cache.Cache) gin.HandlerFunc { + return func(c *gin.Context) { + cache.ToContext(c, v) + } +} + +// HandlerBus returns a HandlerFunc that passes a Bus to the Context. +func HandlerBus(v bus.Bus) gin.HandlerFunc { + return func(c *gin.Context) { + bus.ToContext(c, v) + } +} + +// HandlerStore returns a HandlerFunc that passes a Store to the Context. +func HandlerStore(v store.Store) gin.HandlerFunc { + return func(c *gin.Context) { + store.ToContext(c, v) + } +} + +// HandlerQueue returns a HandlerFunc that passes a Queue to the Context. +func HandlerQueue(v queue.Queue) gin.HandlerFunc { + return func(c *gin.Context) { + queue.ToContext(c, v) + } +} + +// HandlerStream returns a HandlerFunc that passes a Stream to the Context. +func HandlerStream(v stream.Stream) gin.HandlerFunc { + return func(c *gin.Context) { + stream.ToContext(c, v) + } +} + +// HandlerRemote returns a HandlerFunc that passes a Remote to the Context. +func HandlerRemote(v remote.Remote) gin.HandlerFunc { + return func(c *gin.Context) { + remote.ToContext(c, v) + } +} + +// HandlerConfig returns a HandlerFunc that passes server Config to the Context. +func HandlerConfig(v *Config) gin.HandlerFunc { + const k = "config" + return func(c *gin.Context) { + c.Set(k, v) + } +} + +// HandlerVersion returns a HandlerFunc that writes the Version information to +// the http.Response as a the X-Drone-Version header. +func HandlerVersion() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("X-Drone-Version", version.Version) + } +} + +// HandlerAgent returns a HandlerFunc that passes an Agent token to the Context. +func HandlerAgent(v string) gin.HandlerFunc { + const k = "agent" + return func(c *gin.Context) { + c.Set(k, v) + } +} diff --git a/server/handler_test.go b/server/handler_test.go new file mode 100644 index 000000000..abb4e431a --- /dev/null +++ b/server/handler_test.go @@ -0,0 +1 @@ +package server diff --git a/server/server.go b/server/server.go new file mode 100644 index 000000000..477722adb --- /dev/null +++ b/server/server.go @@ -0,0 +1,243 @@ +package server + +import ( + "net/http" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/drone/drone/api" + "github.com/drone/drone/bus" + "github.com/drone/drone/cache" + "github.com/drone/drone/queue" + "github.com/drone/drone/remote" + "github.com/drone/drone/router/middleware" + "github.com/drone/drone/router/middleware/header" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/router/middleware/token" + "github.com/drone/drone/static" + "github.com/drone/drone/store" + "github.com/drone/drone/stream" + "github.com/drone/drone/template" + "github.com/drone/drone/web" + + "github.com/gin-gonic/contrib/ginrus" + "github.com/gin-gonic/gin" +) + +// Config defines system configuration parameters. +type Config struct { + Open bool // Enables open registration + Yaml string // Customize the Yaml configuration file name + Secret string // Secret token used to authenticate agents + Admins []string // Administrative users + Orgs []string // Organization whitelist +} + +// Server defines the server configuration. +type Server struct { + Bus bus.Bus + Cache cache.Cache + Queue queue.Queue + Remote remote.Remote + Stream stream.Stream + Store store.Store + Config *Config +} + +// Handler returns an http.Handler for servering Drone requests. +func (s *Server) Handler() http.Handler { + + e := gin.New() + e.Use(gin.Recovery()) + + e.SetHTMLTemplate(template.Load()) + e.StaticFS("/static", static.FileSystem()) + + e.Use(header.NoCache) + e.Use(header.Options) + e.Use(header.Secure) + e.Use( + ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), + HandlerVersion(), + HandlerQueue(s.Queue), + HandlerStream(s.Stream), + HandlerBus(s.Bus), + HandlerCache(s.Cache), + HandlerStore(s.Store), + HandlerRemote(s.Remote), + HandlerConfig(s.Config), + ) + e.Use(session.SetUser()) + e.Use(token.Refresh) + + e.GET("/", web.ShowIndex) + e.GET("/repos", web.ShowAllRepos) + e.GET("/login", web.ShowLogin) + e.GET("/login/form", web.ShowLoginForm) + e.GET("/logout", web.GetLogout) + + // TODO below will Go away with React UI + settings := e.Group("/settings") + { + settings.Use(session.MustUser()) + settings.GET("/profile", web.ShowUser) + } + repo := e.Group("/repos/:owner/:name") + { + repo.Use(session.SetRepo()) + repo.Use(session.SetPerm()) + repo.Use(session.MustPull) + + repo.GET("", web.ShowRepo) + repo.GET("/builds/:number", web.ShowBuild) + repo.GET("/builds/:number/:job", web.ShowBuild) + + repo_settings := repo.Group("/settings") + { + repo_settings.GET("", session.MustPush, web.ShowRepoConf) + repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) + repo_settings.GET("/badges", web.ShowRepoBadges) + } + } + // TODO above will Go away with React UI + + user := e.Group("/api/user") + { + user.Use(session.MustUser()) + user.GET("", api.GetSelf) + user.GET("/feed", api.GetFeed) + user.GET("/repos", api.GetRepos) + user.GET("/repos/remote", api.GetRemoteRepos) + user.POST("/token", api.PostToken) + user.DELETE("/token", api.DeleteToken) + } + + users := e.Group("/api/users") + { + users.Use(session.MustAdmin()) + users.GET("", api.GetUsers) + users.POST("", api.PostUser) + users.GET("/:login", api.GetUser) + users.PATCH("/:login", api.PatchUser) + users.DELETE("/:login", api.DeleteUser) + } + + repos := e.Group("/api/repos/:owner/:name") + { + repos.POST("", api.PostRepo) + + repo := repos.Group("") + { + repo.Use(session.SetRepo()) + repo.Use(session.SetPerm()) + repo.Use(session.MustPull) + + repo.GET("", api.GetRepo) + repo.GET("/key", api.GetRepoKey) + repo.POST("/key", api.PostRepoKey) + repo.GET("/builds", api.GetBuilds) + repo.GET("/builds/:number", api.GetBuild) + repo.GET("/logs/:number/:job", api.GetBuildLogs) + repo.POST("/sign", session.MustPush, api.Sign) + + repo.POST("/secrets", session.MustPush, api.PostSecret) + repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) + + // requires push permissions + repo.PATCH("", session.MustPush, api.PatchRepo) + repo.DELETE("", session.MustPush, api.DeleteRepo) + + repo.POST("/builds/:number", session.MustPush, api.PostBuild) + repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) + } + } + + badges := e.Group("/api/badges/:owner/:name") + { + badges.GET("/status.svg", web.GetBadge) + badges.GET("/cc.xml", web.GetCC) + } + + e.POST("/hook", web.PostHook) + e.POST("/api/hook", web.PostHook) + + stream := e.Group("/api/stream") + { + stream.Use(session.SetRepo()) + stream.Use(session.SetPerm()) + stream.Use(session.MustPull) + + stream.GET("/:owner/:name", web.GetRepoEvents) + stream.GET("/:owner/:name/:build/:number", web.GetStream) + } + + auth := e.Group("/authorize") + { + auth.GET("", web.GetLogin) + auth.POST("", web.GetLogin) + auth.POST("/token", web.GetLoginToken) + } + + queue := e.Group("/api/queue") + { + queue.Use(middleware.AgentMust()) + queue.POST("/pull", api.Pull) + queue.POST("/pull/:os/:arch", api.Pull) + queue.POST("/wait/:id", api.Wait) + queue.POST("/stream/:id", api.Stream) + queue.POST("/status/:id", api.Update) + } + + // DELETE THESE + // gitlab := e.Group("/gitlab/:owner/:name") + // { + // gitlab.Use(session.SetRepo()) + // gitlab.GET("/commits/:sha", web.GetCommit) + // gitlab.GET("/pulls/:number", web.GetPullRequest) + // + // redirects := gitlab.Group("/redirect") + // { + // redirects.GET("/commits/:sha", web.RedirectSha) + // redirects.GET("/pulls/:number", web.RedirectPullRequest) + // } + // } + + // bots := e.Group("/bots") + // { + // bots.Use(session.MustUser()) + // bots.POST("/slack", web.Slack) + // bots.POST("/slack/:command", web.Slack) + // } + + return normalize(e) +} + +// THIS HACK JOB IS GOING AWAY SOON. +// +// normalize is a helper function to work around the following +// issue with gin. https://github.com/gin-gonic/gin/issues/388 +func normalize(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + parts := strings.Split(r.URL.Path, "/")[1:] + switch parts[0] { + case "settings", "bots", "repos", "api", "login", "logout", "", "authorize", "hook", "static", "gitlab": + // no-op + default: + + if len(parts) > 2 && parts[2] != "settings" { + parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) + } + + // prefix the URL with /repo so that it + // can be effectively routed. + parts = append([]string{"", "repos"}, parts...) + + // reconstruct the path + r.URL.Path = strings.Join(parts, "/") + } + + h.ServeHTTP(w, r) + }) +} From 7c5257b61e323b694f58446835595a44dfc400ed Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Sat, 30 Apr 2016 01:00:39 -0700 Subject: [PATCH 03/10] increased coverage for bitbucket package --- remote/bitbucket/bitbucket.go | 126 ++-------- remote/bitbucket/bitbucket_test.go | 253 +++++++++++++++++++++ remote/bitbucket/const.go | 40 ---- remote/bitbucket/const_test.go | 43 ---- remote/bitbucket/{helper.go => convert.go} | 82 +++++++ remote/bitbucket/convert_test.go | 195 ++++++++++++++++ remote/bitbucket/fixtures/handler.go | 181 +++++++++++++++ remote/bitbucket/fixtures/hooks.go | 164 +++++++++++++ remote/bitbucket/helper_test.go | 102 --------- remote/bitbucket/internal/client.go | 33 ++- remote/bitbucket/internal/types.go | 36 +-- remote/bitbucket/parse.go | 71 ++++++ remote/bitbucket/parse_test.go | 104 +++++++++ 13 files changed, 1103 insertions(+), 327 deletions(-) create mode 100644 remote/bitbucket/bitbucket_test.go delete mode 100644 remote/bitbucket/const.go delete mode 100644 remote/bitbucket/const_test.go rename remote/bitbucket/{helper.go => convert.go} (54%) create mode 100644 remote/bitbucket/convert_test.go create mode 100644 remote/bitbucket/fixtures/handler.go create mode 100644 remote/bitbucket/fixtures/hooks.go delete mode 100644 remote/bitbucket/helper_test.go create mode 100644 remote/bitbucket/parse.go create mode 100644 remote/bitbucket/parse_test.go diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index 56a4f679e..7f8470212 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -1,9 +1,7 @@ package bitbucket import ( - "encoding/json" "fmt" - "io/ioutil" "net/http" "net/url" @@ -16,7 +14,11 @@ import ( "golang.org/x/oauth2/bitbucket" ) +// Bitbucket Server endpoint. +const Endpoint = "https://api.bitbucket.org" + type config struct { + URL string Client string Secret string } @@ -25,6 +27,7 @@ type config struct { // repository hosting service at https://bitbucket.org func New(client, secret string) remote.Remote { return &config{ + URL: Endpoint, Client: client, Secret: secret, } @@ -33,6 +36,7 @@ func New(client, secret string) remote.Remote { // helper function to return the bitbucket oauth2 client func (c *config) newClient(u *model.User) *internal.Client { return internal.NewClientToken( + c.URL, c.Client, c.Secret, &oauth2.Token{ @@ -61,7 +65,7 @@ func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User, return nil, err } - client := internal.NewClient(config.Client(oauth2.NoContext, token)) + client := internal.NewClient(c.URL, config.Client(oauth2.NoContext, token)) curr, err := client.FindCurrent() if err != nil { return nil, err @@ -72,6 +76,7 @@ func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User, func (c *config) Auth(token, secret string) (string, error) { client := internal.NewClientToken( + c.URL, c.Client, c.Secret, &oauth2.Token{ @@ -196,8 +201,8 @@ func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([ func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { status := internal.BuildStatus{ - State: getStatus(b.Status), - Desc: getDesc(b.Status), + State: convertStatus(b.Status), + Desc: convertDesc(b.Status), Key: "Drone", Url: link, } @@ -219,10 +224,7 @@ func (c *config) Activate(u *model.User, r *model.Repo, k *model.Key, link strin } // deletes any previously created hooks - if err := c.Deactivate(u, r, link); err != nil { - // we can live with failure here. Things happen and manually scrubbing - // hooks is certinaly not the end of the world. - } + c.Deactivate(u, r, link) return c.newClient(u).CreateHook(r.Owner, r.Name, &internal.Hook{ Active: true, @@ -242,114 +244,22 @@ func (c *config) Deactivate(u *model.User, r *model.Repo, link string) error { hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{}) if err != nil { - return nil // we can live with undeleted hooks + return err } for _, hook := range hooks.Values { hookurl, err := url.Parse(hook.Url) - if err != nil { - continue - } - if hookurl.Host == linkurl.Host { - client.DeleteHook(r.Owner, r.Name, hook.Uuid) - break // we can live with undeleted hooks + if err == nil && hookurl.Host == linkurl.Host { + return client.DeleteHook(r.Owner, r.Name, hook.Uuid) } } return nil } +// Hook parses the incoming Bitbucket hook and returns the Repository and +// Build details. If the hook is unsupported nil values are returned and the +// hook should be skipped. func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - - switch r.Header.Get("X-Event-Key") { - case "repo:push": - return c.pushHook(r) - case "pullrequest:created", "pullrequest:updated": - return c.pullHook(r) - } - - return nil, nil, nil -} - -func (c *config) 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 := internal.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. - // TODO(bradrydzewski) uses unit tested conversion function - 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 (c *config) 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 := internal.PullRequestHook{} - err := json.Unmarshal(payload, &hook) - if err != nil { - return nil, nil, err - } - if hook.PullRequest.State != "OPEN" { - return nil, nil, nil - } - - // TODO(bradrydzewski) uses unit tested conversion function - 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 + return parseHook(r) } diff --git a/remote/bitbucket/bitbucket_test.go b/remote/bitbucket/bitbucket_test.go new file mode 100644 index 000000000..6d621788e --- /dev/null +++ b/remote/bitbucket/bitbucket_test.go @@ -0,0 +1,253 @@ +package bitbucket + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/fixtures" + + "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} + + 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("https://api.bitbucket.org") + 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 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 return error when request fails", func() { + _, err := c.Auth( + fakeUserNotFound.Token, + fakeUserNotFound.Secret, + ) + g.Assert(err != nil).IsTrue() + }) + }) + + 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, nil, "%gh&%ij") + g.Assert(err != nil).IsTrue() + }) + g.It("Should create the hook", func() { + err := c.Activate(fakeUser, fakeRepo, nil, "http://127.0.0.1") + g.Assert(err == nil).IsTrue() + }) + g.It("Should remove previous hooks") + }) + + g.Describe("When deactivating a repository", func() { + g.It("Should error when malformed hook", func() { + err := c.Deactivate(fakeUser, fakeRepo, "%gh&%ij") + g.Assert(err != nil).IsTrue() + }) + 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.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", + } + + fakeUserNotFound = &model.User{ + Login: "superman", + Token: "user_not_found", + } + + fakeUserNoTeams = &model.User{ + Login: "superman", + Token: "teams_not_found", + } + + fakeUserNoRepos = &model.User{ + Login: "superman", + Token: "repos_not_found", + } + + fakeRepo = &model.Repo{ + Owner: "test_name", + Name: "repo_name", + FullName: "test_name/repo_name", + } + + fakeRepoNotFound = &model.Repo{ + Owner: "test_name", + Name: "repo_not_found", + FullName: "test_name/repo_not_found", + } + + fakeRepoNoHooks = &model.Repo{ + Owner: "test_name", + Name: "hooks_not_found", + FullName: "test_name/hooks_not_found", + } + + fakeRepoEmptyHook = &model.Repo{ + Owner: "test_name", + Name: "hook_empty", + FullName: "test_name/hook_empty", + } + + fakeBuild = &model.Build{ + Commit: "9ecad50", + } +) diff --git a/remote/bitbucket/const.go b/remote/bitbucket/const.go deleted file mode 100644 index 3dcbb2cb2..000000000 --- a/remote/bitbucket/const.go +++ /dev/null @@ -1,40 +0,0 @@ -package bitbucket - -import "github.com/drone/drone/model" - -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" -) - -func getStatus(status string) string { - switch status { - case model.StatusPending, model.StatusRunning: - return statusPending - case model.StatusSuccess: - return statusSuccess - default: - return statusFailure - } -} - -func getDesc(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 - } -} diff --git a/remote/bitbucket/const_test.go b/remote/bitbucket/const_test.go deleted file mode 100644 index 104947fd7..000000000 --- a/remote/bitbucket/const_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package bitbucket - -import ( - "testing" - - "github.com/drone/drone/model" - - "github.com/franela/goblin" -) - -func Test_status(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Bitbucket status", func() { - g.It("should return passing", func() { - g.Assert(getStatus(model.StatusSuccess)).Equal(statusSuccess) - }) - g.It("should return pending", func() { - g.Assert(getStatus(model.StatusPending)).Equal(statusPending) - g.Assert(getStatus(model.StatusRunning)).Equal(statusPending) - }) - g.It("should return failing", func() { - g.Assert(getStatus(model.StatusFailure)).Equal(statusFailure) - g.Assert(getStatus(model.StatusKilled)).Equal(statusFailure) - g.Assert(getStatus(model.StatusError)).Equal(statusFailure) - }) - - g.It("should return passing desc", func() { - g.Assert(getDesc(model.StatusSuccess)).Equal(descSuccess) - }) - g.It("should return pending desc", func() { - g.Assert(getDesc(model.StatusPending)).Equal(descPending) - g.Assert(getDesc(model.StatusRunning)).Equal(descPending) - }) - g.It("should return failing desc", func() { - g.Assert(getDesc(model.StatusFailure)).Equal(descFailure) - }) - g.It("should return error desc", func() { - g.Assert(getDesc(model.StatusKilled)).Equal(descError) - g.Assert(getDesc(model.StatusError)).Equal(descError) - }) - }) -} diff --git a/remote/bitbucket/helper.go b/remote/bitbucket/convert.go similarity index 54% rename from remote/bitbucket/helper.go rename to remote/bitbucket/convert.go index 9a516b40f..b64863cd1 100644 --- a/remote/bitbucket/helper.go +++ b/remote/bitbucket/convert.go @@ -1,6 +1,7 @@ package bitbucket import ( + "fmt" "net/url" "strings" @@ -10,6 +11,47 @@ import ( "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 { @@ -102,3 +144,43 @@ func convertTeam(from *internal.Account) *model.Team { Avatar: from.Links.Avatar.Href, } } + +// convertPullHook is a helper function used to convert a Bitbucket pull request +// hook to the Drone build struct holding commit information. +func convertPullHook(from *internal.PullRequestHook) *model.Build { + return &model.Build{ + Event: model.EventPull, + Commit: from.PullRequest.Dest.Commit.Hash, + Ref: fmt.Sprintf("refs/heads/%s", from.PullRequest.Dest.Branch.Name), + Remote: cloneLink(&from.PullRequest.Dest.Repo), + Link: from.PullRequest.Links.Html.Href, + Branch: from.PullRequest.Dest.Branch.Name, + Message: from.PullRequest.Desc, + Avatar: from.Actor.Links.Avatar.Href, + Author: from.Actor.Login, + Timestamp: from.PullRequest.Updated.UTC().Unix(), + } +} + +// convertPushHook is a helper function used to convert a Bitbucket push +// hook to the Drone build struct holding commit information. +func convertPushHook(hook *internal.PushHook, change *internal.Change) *model.Build { + build := &model.Build{ + Commit: change.New.Target.Hash, + Link: change.New.Target.Links.Html.Href, + Branch: change.New.Name, + Message: change.New.Target.Message, + Avatar: hook.Actor.Links.Avatar.Href, + Author: hook.Actor.Login, + Timestamp: change.New.Target.Date.UTC().Unix(), + } + switch change.New.Type { + case "tag", "annotated_tag", "bookmark": + build.Event = model.EventTag + build.Ref = fmt.Sprintf("refs/tags/%s", change.New.Name) + default: + build.Event = model.EventPush + build.Ref = fmt.Sprintf("refs/heads/%s", change.New.Name) + } + return build +} diff --git a/remote/bitbucket/convert_test.go b/remote/bitbucket/convert_test.go new file mode 100644 index 000000000..9f10e2499 --- /dev/null +++ b/remote/bitbucket/convert_test.go @@ -0,0 +1,195 @@ +package bitbucket + +import ( + "testing" + "time" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/internal" + + "github.com/franela/goblin" + "golang.org/x/oauth2" +) + +func Test_helper(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Bitbucket converter", func() { + + g.It("should convert passing status", func() { + g.Assert(convertStatus(model.StatusSuccess)).Equal(statusSuccess) + }) + + g.It("should convert pending status", func() { + g.Assert(convertStatus(model.StatusPending)).Equal(statusPending) + g.Assert(convertStatus(model.StatusRunning)).Equal(statusPending) + }) + + g.It("should convert failing status", func() { + g.Assert(convertStatus(model.StatusFailure)).Equal(statusFailure) + g.Assert(convertStatus(model.StatusKilled)).Equal(statusFailure) + g.Assert(convertStatus(model.StatusError)).Equal(statusFailure) + }) + + g.It("should convert passing desc", func() { + g.Assert(convertDesc(model.StatusSuccess)).Equal(descSuccess) + }) + + g.It("should convert pending desc", func() { + g.Assert(convertDesc(model.StatusPending)).Equal(descPending) + g.Assert(convertDesc(model.StatusRunning)).Equal(descPending) + }) + + g.It("should convert failing desc", func() { + g.Assert(convertDesc(model.StatusFailure)).Equal(descFailure) + }) + + g.It("should convert error desc", func() { + g.Assert(convertDesc(model.StatusKilled)).Equal(descError) + g.Assert(convertDesc(model.StatusError)).Equal(descError) + }) + + g.It("should convert repository lite", func() { + from := &internal.Repo{} + from.FullName = "octocat/hello-world" + from.Owner.Links.Avatar.Href = "http://..." + + to := convertRepoLite(from) + g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href) + g.Assert(to.FullName).Equal(from.FullName) + g.Assert(to.Owner).Equal("octocat") + g.Assert(to.Name).Equal("hello-world") + }) + + g.It("should convert repository", func() { + from := &internal.Repo{ + FullName: "octocat/hello-world", + IsPrivate: true, + Scm: "hg", + } + from.Owner.Links.Avatar.Href = "http://..." + from.Links.Html.Href = "https://bitbucket.org/foo/bar" + + to := convertRepo(from) + g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href) + g.Assert(to.FullName).Equal(from.FullName) + g.Assert(to.Owner).Equal("octocat") + g.Assert(to.Name).Equal("hello-world") + g.Assert(to.Branch).Equal("default") + g.Assert(to.Kind).Equal(from.Scm) + g.Assert(to.IsPrivate).Equal(from.IsPrivate) + g.Assert(to.Clone).Equal(from.Links.Html.Href) + g.Assert(to.Link).Equal(from.Links.Html.Href) + }) + + g.It("should convert team", func() { + from := &internal.Account{Login: "octocat"} + from.Links.Avatar.Href = "http://..." + to := convertTeam(from) + g.Assert(to.Avatar).Equal(from.Links.Avatar.Href) + g.Assert(to.Login).Equal(from.Login) + }) + + g.It("should convert team list", func() { + from := &internal.Account{Login: "octocat"} + from.Links.Avatar.Href = "http://..." + to := convertTeamList([]*internal.Account{from}) + g.Assert(to[0].Avatar).Equal(from.Links.Avatar.Href) + g.Assert(to[0].Login).Equal(from.Login) + }) + + g.It("should convert user", func() { + token := &oauth2.Token{ + AccessToken: "foo", + RefreshToken: "bar", + Expiry: time.Now(), + } + user := &internal.Account{Login: "octocat"} + user.Links.Avatar.Href = "http://..." + + result := convertUser(user, token) + g.Assert(result.Avatar).Equal(user.Links.Avatar.Href) + g.Assert(result.Login).Equal(user.Login) + g.Assert(result.Token).Equal(token.AccessToken) + g.Assert(result.Token).Equal(token.AccessToken) + g.Assert(result.Secret).Equal(token.RefreshToken) + g.Assert(result.Expiry).Equal(token.Expiry.UTC().Unix()) + }) + + g.It("should use clone url", func() { + repo := &internal.Repo{} + repo.Links.Clone = append(repo.Links.Clone, internal.Link{ + Name: "https", + Href: "https://bitbucket.org/foo/bar.git", + }) + link := cloneLink(repo) + g.Assert(link).Equal(repo.Links.Clone[0].Href) + }) + + g.It("should build clone url", func() { + repo := &internal.Repo{} + repo.Links.Html.Href = "https://foo:bar@bitbucket.org/foo/bar.git" + link := cloneLink(repo) + g.Assert(link).Equal("https://bitbucket.org/foo/bar.git") + }) + + g.It("should convert pull hook to build", func() { + hook := &internal.PullRequestHook{} + hook.Actor.Login = "octocat" + hook.Actor.Links.Avatar.Href = "https://..." + hook.PullRequest.Dest.Commit.Hash = "73f9c44d" + hook.PullRequest.Dest.Branch.Name = "master" + hook.PullRequest.Dest.Repo.Links.Html.Href = "https://bitbucket.org/foo/bar" + hook.PullRequest.Links.Html.Href = "https://bitbucket.org/foo/bar/pulls/5" + hook.PullRequest.Desc = "updated README" + hook.PullRequest.Updated = time.Now() + + build := convertPullHook(hook) + g.Assert(build.Event).Equal(model.EventPull) + g.Assert(build.Author).Equal(hook.Actor.Login) + g.Assert(build.Avatar).Equal(hook.Actor.Links.Avatar.Href) + g.Assert(build.Commit).Equal(hook.PullRequest.Dest.Commit.Hash) + g.Assert(build.Branch).Equal(hook.PullRequest.Dest.Branch.Name) + g.Assert(build.Link).Equal(hook.PullRequest.Links.Html.Href) + g.Assert(build.Ref).Equal("refs/heads/master") + g.Assert(build.Message).Equal(hook.PullRequest.Desc) + g.Assert(build.Timestamp).Equal(hook.PullRequest.Updated.Unix()) + }) + + g.It("should convert push hook to build", func() { + change := internal.Change{} + change.New.Target.Hash = "73f9c44d" + change.New.Name = "master" + change.New.Target.Links.Html.Href = "https://bitbucket.org/foo/bar/commits/73f9c44d" + change.New.Target.Message = "updated README" + change.New.Target.Date = time.Now() + + hook := internal.PushHook{} + hook.Actor.Login = "octocat" + hook.Actor.Links.Avatar.Href = "https://..." + + build := convertPushHook(&hook, &change) + g.Assert(build.Event).Equal(model.EventPush) + g.Assert(build.Author).Equal(hook.Actor.Login) + g.Assert(build.Avatar).Equal(hook.Actor.Links.Avatar.Href) + g.Assert(build.Commit).Equal(change.New.Target.Hash) + g.Assert(build.Branch).Equal(change.New.Name) + g.Assert(build.Link).Equal(change.New.Target.Links.Html.Href) + g.Assert(build.Ref).Equal("refs/heads/master") + g.Assert(build.Message).Equal(change.New.Target.Message) + g.Assert(build.Timestamp).Equal(change.New.Target.Date.Unix()) + }) + + g.It("should convert tag hook to build", func() { + change := internal.Change{} + change.New.Name = "v1.0.0" + change.New.Type = "tag" + + hook := internal.PushHook{} + + build := convertPushHook(&hook, &change) + g.Assert(build.Event).Equal(model.EventTag) + g.Assert(build.Ref).Equal("refs/tags/v1.0.0") + }) + }) +} diff --git a/remote/bitbucket/fixtures/handler.go b/remote/bitbucket/fixtures/handler.go new file mode 100644 index 000000000..a1ce21911 --- /dev/null +++ b/remote/bitbucket/fixtures/handler.go @@ -0,0 +1,181 @@ +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("/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 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 repoPayload = ` +{ + "full_name": "test_name/repo_name", + "scm": "git", + "is_private": true +} +` + +const repoHookPayload = ` +{ + "pagelen": 10, + "values": [ + { + "uuid": "{afe61e14-2c5f-49e8-8b68-ad1fb55fc052}", + "url": "http://127.0.0.1" + } + ], + "page": 1, + "size": 1 +} +` + +const repoFilePayload = ` +{ + "data": "{ platform: linux/amd64 }" +} +` + +const userPayload = ` +{ + "username": "superman", + "links": { + "avatar": { + "href": "http:\/\/i.imgur.com\/ZygP55A.jpg" + } + }, + "type": "user" +} +` + +const userRepoPayload = ` +{ + "page": 1, + "pagelen": 10, + "size": 1, + "values": [ + { + "links": { + "avatar": { + "href": "http:\/\/i.imgur.com\/ZygP55A.jpg" + } + }, + "full_name": "test_name/repo_name", + "scm": "git", + "is_private": true + } + ] +} +` + +const userTeamPayload = ` +{ + "pagelen": 100, + "values": [ + { + "username": "superfriends", + "links": { + "avatar": { + "href": "http:\/\/i.imgur.com\/ZygP55A.jpg" + } + }, + "type": "team" + } + ] +} +` diff --git a/remote/bitbucket/fixtures/hooks.go b/remote/bitbucket/fixtures/hooks.go new file mode 100644 index 000000000..2b68e24e9 --- /dev/null +++ b/remote/bitbucket/fixtures/hooks.go @@ -0,0 +1,164 @@ +package fixtures + +const HookPush = ` +{ + "actor": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + }, + "push": { + "changes": [ + { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": { + "type": "commit", + "hash": "709d658dc5b6d6afcd46049c2f332ee3f515a67d", + "author": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "message": "new commit message\n", + "date": "2015-06-09T03:34:49+00:00" + } + } + } + ] + } +} +` + +const HookPushEmptyHash = ` +{ + "push": { + "changes": [ + { + "new": { + "type": "branch", + "target": { "hash": "" } + } + } + ] + } +} +` + +const HookPull = ` +{ + "actor": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "pullrequest": { + "id": 1, + "title": "Title of pull request", + "description": "Description of pull request", + "state": "OPEN", + "author": { + "username": "emmap1", + "links": { + "avatar": { + "href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png" + } + } + }, + "source": { + "branch": { + "name": "branch2" + }, + "commit": { + "hash": "d3022fc0ca3d" + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + } + }, + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "hash": "ce5965ddd289" + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + } + }, + "links": { + "self": { + "href": "https:\/\/api.bitbucket.org\/api\/2.0\/pullrequests\/pullrequest_id" + }, + "html": { + "href": "https:\/\/api.bitbucket.org\/pullrequest_id" + } + } + }, + "repository": { + "links": { + "html": { + "href": "https:\/\/api.bitbucket.org\/team_name\/repo_name" + }, + "avatar": { + "href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png" + } + }, + "full_name": "user_name\/repo_name", + "scm": "git", + "is_private": true + } +} +` + +const HookMerged = ` +{ + "pullrequest": { + "state": "MERGED" + } +} +` diff --git a/remote/bitbucket/helper_test.go b/remote/bitbucket/helper_test.go deleted file mode 100644 index c978c1291..000000000 --- a/remote/bitbucket/helper_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package bitbucket - -import ( - "testing" - "time" - - "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", func() { - - 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") - }) - }) -} diff --git a/remote/bitbucket/internal/client.go b/remote/bitbucket/internal/client.go index 932613136..781b3d07b 100644 --- a/remote/bitbucket/internal/client.go +++ b/remote/bitbucket/internal/client.go @@ -20,8 +20,6 @@ const ( ) const ( - base = "https://api.bitbucket.org" - pathUser = "%s/2.0/user/" pathEmails = "%s/2.0/user/emails" pathTeams = "%s/2.0/teams/?%s" @@ -35,52 +33,53 @@ const ( type Client struct { *http.Client + base string } -func NewClient(client *http.Client) *Client { - return &Client{client} +func NewClient(url string, client *http.Client) *Client { + return &Client{client, url} } -func NewClientToken(client, secret string, token *oauth2.Token) *Client { +func NewClientToken(url, client, secret string, token *oauth2.Token) *Client { config := &oauth2.Config{ ClientID: client, ClientSecret: secret, Endpoint: bitbucket.Endpoint, } - return NewClient(config.Client(oauth2.NoContext, token)) + return NewClient(url, config.Client(oauth2.NoContext, token)) } func (c *Client) FindCurrent() (*Account, error) { out := new(Account) - uri := fmt.Sprintf(pathUser, base) + uri := fmt.Sprintf(pathUser, c.base) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListEmail() (*EmailResp, error) { out := new(EmailResp) - uri := fmt.Sprintf(pathEmails, base) + uri := fmt.Sprintf(pathEmails, c.base) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListTeams(opts *ListTeamOpts) (*AccountResp, error) { out := new(AccountResp) - uri := fmt.Sprintf(pathTeams, base, opts.Encode()) + uri := fmt.Sprintf(pathTeams, c.base, opts.Encode()) err := c.do(uri, get, nil, out) return out, err } func (c *Client) FindRepo(owner, name string) (*Repo, error) { out := new(Repo) - uri := fmt.Sprintf(pathRepo, base, owner, name) + uri := fmt.Sprintf(pathRepo, c.base, owner, name) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListRepos(account string, opts *ListOpts) (*RepoResp, error) { out := new(RepoResp) - uri := fmt.Sprintf(pathRepos, base, account, opts.Encode()) + uri := fmt.Sprintf(pathRepos, c.base, account, opts.Encode()) err := c.do(uri, get, nil, out) return out, err } @@ -105,37 +104,37 @@ func (c *Client) ListReposAll(account string) ([]*Repo, error) { func (c *Client) FindHook(owner, name, id string) (*Hook, error) { out := new(Hook) - uri := fmt.Sprintf(pathHook, base, owner, name, id) + uri := fmt.Sprintf(pathHook, c.base, owner, name, id) err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListHooks(owner, name string, opts *ListOpts) (*HookResp, error) { out := new(HookResp) - uri := fmt.Sprintf(pathHooks, base, owner, name, opts.Encode()) + uri := fmt.Sprintf(pathHooks, c.base, owner, name, opts.Encode()) err := c.do(uri, get, nil, out) return out, err } func (c *Client) CreateHook(owner, name string, hook *Hook) error { - uri := fmt.Sprintf(pathHooks, base, owner, name, "") + uri := fmt.Sprintf(pathHooks, c.base, owner, name, "") return c.do(uri, post, hook, nil) } func (c *Client) DeleteHook(owner, name, id string) error { - uri := fmt.Sprintf(pathHook, base, owner, name, id) + uri := fmt.Sprintf(pathHook, c.base, owner, name, id) return c.do(uri, del, nil, nil) } func (c *Client) FindSource(owner, name, revision, path string) (*Source, error) { out := new(Source) - uri := fmt.Sprintf(pathSource, base, owner, name, revision, path) + uri := fmt.Sprintf(pathSource, c.base, owner, name, revision, path) err := c.do(uri, get, nil, out) return out, err } func (c *Client) CreateStatus(owner, name, revision string, status *BuildStatus) error { - uri := fmt.Sprintf(pathStatus, base, owner, name, revision) + uri := fmt.Sprintf(pathStatus, c.base, owner, name, revision) return c.do(uri, post, status, nil) } diff --git a/remote/bitbucket/internal/types.go b/remote/bitbucket/internal/types.go index 82175e5b2..b92d22e08 100644 --- a/remote/bitbucket/internal/types.go +++ b/remote/bitbucket/internal/types.go @@ -100,27 +100,29 @@ type Source struct { Size int64 `json:"size"` } +type Change struct { + New struct { + Type string `json:"type"` + Name string `json:"name"` + Target struct { + Type string `json:"type"` + Hash string `json:"hash"` + Message string `json:"message"` + Date time.Time `json:"date"` + Links Links `json:"links"` + Author struct { + Raw string `json:"raw"` + User Account `json:"user"` + } `json:"author"` + } `json:"target"` + } `json:"new"` +} + type PushHook struct { Actor Account `json:"actor"` Repo Repo `json:"repository"` Push struct { - Changes []struct { - New struct { - Type string `json:"type"` - Name string `json:"name"` - Target struct { - Type string `json:"type"` - Hash string `json:"hash"` - Message string `json:"message"` - Date time.Time `json:"date"` - Links Links `json:"links"` - Author struct { - Raw string `json:"raw"` - User Account `json:"user"` - } `json:"author"` - } `json:"target"` - } `json:"new"` - } `json:"changes"` + Changes []Change `json:"changes"` } `json:"push"` } diff --git a/remote/bitbucket/parse.go b/remote/bitbucket/parse.go new file mode 100644 index 000000000..7ab385d38 --- /dev/null +++ b/remote/bitbucket/parse.go @@ -0,0 +1,71 @@ +package bitbucket + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucket/internal" +) + +const ( + hookEvent = "X-Event-Key" + hookPush = "repo:push" + hookPullCreated = "pullrequest:created" + hookPullUpdated = "pullrequest:updated" + + changeBranch = "branch" + changeNamedBranch = "named_branch" + + stateMerged = "MERGED" + stateDeclined = "DECLINED" + stateOpen = "OPEN" +) + +// parseHook parses a Bitbucket hook from an http.Request request and returns +// Repo and Build detail. If a hook type is unsupported nil values are returned. +func parseHook(r *http.Request) (*model.Repo, *model.Build, error) { + payload, _ := ioutil.ReadAll(r.Body) + + switch r.Header.Get(hookEvent) { + case hookPush: + return parsePushHook(payload) + case hookPullCreated, hookPullUpdated: + return parsePullHook(payload) + } + return nil, nil, nil +} + +// parsePushHook parses a push hook and returns the Repo and Build details. +// If the commit type is unsupported nil values are returned. +func parsePushHook(payload []byte) (*model.Repo, *model.Build, error) { + hook := internal.PushHook{} + + err := json.Unmarshal(payload, &hook) + if err != nil { + return nil, nil, err + } + + for _, change := range hook.Push.Changes { + if change.New.Target.Hash == "" { + continue + } + return convertRepo(&hook.Repo), convertPushHook(&hook, &change), nil + } + return nil, nil, nil +} + +// parsePullHook parses a pull request hook and returns the Repo and Build +// details. If the pull request is closed nil values are returned. +func parsePullHook(payload []byte) (*model.Repo, *model.Build, error) { + hook := internal.PullRequestHook{} + + if err := json.Unmarshal(payload, &hook); err != nil { + return nil, nil, err + } + if hook.PullRequest.State != stateOpen { + return nil, nil, nil + } + return convertRepo(&hook.Repo), convertPullHook(&hook), nil +} diff --git a/remote/bitbucket/parse_test.go b/remote/bitbucket/parse_test.go new file mode 100644 index 000000000..7ff3657be --- /dev/null +++ b/remote/bitbucket/parse_test.go @@ -0,0 +1,104 @@ +package bitbucket + +import ( + "bytes" + "net/http" + "testing" + + "github.com/drone/drone/remote/bitbucket/fixtures" + + "github.com/franela/goblin" +) + +func Test_parser(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Bitbucket hook 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", 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", 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") + }) + }) + }) +} From b978ed12ebbfd4df5234d3c3ac399c772607a78b Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Sat, 30 Apr 2016 23:22:30 -0700 Subject: [PATCH 04/10] increased bitbucket test coverage --- remote/bitbucket/bitbucket.go | 209 +++++++++++++++------------ remote/bitbucket/bitbucket_test.go | 104 +++++++++++-- remote/bitbucket/fixtures/handler.go | 40 +++++ remote/bitbucket/parse_test.go | 6 +- remote/remote.go | 13 +- 5 files changed, 258 insertions(+), 114 deletions(-) diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index 7f8470212..c7acc38ed 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -11,13 +11,16 @@ import ( "github.com/drone/drone/shared/httputil" "golang.org/x/oauth2" - "golang.org/x/oauth2/bitbucket" ) -// Bitbucket Server endpoint. -const Endpoint = "https://api.bitbucket.org" +// Bitbucket cloud endpoints. +const ( + DefaultAPI = "https://api.bitbucket.org" + DefaultURL = "https://bitbucket.org" +) type config struct { + API string URL string Client string Secret string @@ -27,63 +30,42 @@ type config struct { // repository hosting service at https://bitbucket.org func New(client, secret string) remote.Remote { return &config{ - URL: Endpoint, + API: DefaultAPI, + URL: DefaultURL, Client: client, Secret: secret, } } -// helper function to return the bitbucket oauth2 client -func (c *config) newClient(u *model.User) *internal.Client { - return internal.NewClientToken( - c.URL, - c.Client, - c.Secret, - &oauth2.Token{ - AccessToken: u.Token, - RefreshToken: u.Secret, - }, - ) -} +// Login authenticates an account with Bitbucket using the oauth2 protocol. The +// Bitbucket account details are returned when the user is successfully authenticated. +func (c *config) Login(w http.ResponseWriter, r *http.Request) (*model.User, error) { + redirect := httputil.GetURL(r) + config := c.newConfig(redirect) -func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { - config := &oauth2.Config{ - ClientID: c.Client, - ClientSecret: c.Secret, - Endpoint: bitbucket.Endpoint, - RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)), - } - - var code = req.FormValue("code") + code := r.FormValue("code") if len(code) == 0 { - http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) + http.Redirect(w, r, config.AuthCodeURL("drone"), http.StatusSeeOther) return nil, nil } - var token, err = config.Exchange(oauth2.NoContext, code) + token, err := config.Exchange(oauth2.NoContext, code) if err != nil { return nil, err } - client := internal.NewClient(c.URL, config.Client(oauth2.NoContext, token)) + client := internal.NewClient(c.API, config.Client(oauth2.NoContext, token)) curr, err := client.FindCurrent() if err != nil { return nil, err } - return convertUser(curr, token), nil } +// Auth uses the Bitbucket oauth2 access token and refresh token to authenticate +// a session and return the Bitbucket account login. func (c *config) Auth(token, secret string) (string, error) { - client := internal.NewClientToken( - c.URL, - c.Client, - c.Secret, - &oauth2.Token{ - AccessToken: token, - RefreshToken: secret, - }, - ) + client := c.newClientToken(token, secret) user, err := client.FindCurrent() if err != nil { return "", err @@ -91,33 +73,25 @@ func (c *config) Auth(token, secret string) (string, error) { return user.Login, nil } +// Refresh refreshes the Bitbucket oauth2 access token. If the token is +// refreshed the user is updated and a true value is returned. func (c *config) Refresh(user *model.User) (bool, error) { - config := &oauth2.Config{ - ClientID: c.Client, - ClientSecret: c.Secret, - Endpoint: bitbucket.Endpoint, - } - - // creates a token source with just the refresh token. - // this will ensure an access token is automatically - // requested. + config := c.newConfig("") source := config.TokenSource( oauth2.NoContext, &oauth2.Token{RefreshToken: user.Secret}) - // requesting the token automatically refreshes and - // returns a new access token. token, err := source.Token() if err != nil || len(token.AccessToken) == 0 { return false, err } - // update the user to include tne new access token user.Token = token.AccessToken user.Secret = token.RefreshToken user.Expiry = token.Expiry.UTC().Unix() return true, nil } +// Teams returns a list of all team membership for the Bitbucket account. func (c *config) Teams(u *model.User) ([]*model.Team, error) { opts := &internal.ListTeamOpts{ PageLen: 100, @@ -130,6 +104,7 @@ func (c *config) Teams(u *model.User) ([]*model.Team, error) { return convertTeamList(resp.Values), nil } +// 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 { @@ -138,36 +113,43 @@ func (c *config) Repo(u *model.User, owner, name string) (*model.Repo, error) { return convertRepo(repo), nil } +// Repos returns a list of all repositories for Bitbucket account, including +// organization repositories. func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) { client := c.newClient(u) - var repos []*model.RepoLite + var all []*model.RepoLite - // gets a list of all accounts to query, including the - // user's account and all team accounts. - logins := []string{u.Login} - resp, err := client.ListTeams(&internal.ListTeamOpts{PageLen: 100, Role: "member"}) + accounts := []string{u.Login} + resp, err := client.ListTeams(&internal.ListTeamOpts{ + PageLen: 100, + Role: "member", + }) if err != nil { - return repos, err + return all, err } for _, team := range resp.Values { - logins = append(logins, team.Login) + accounts = append(accounts, team.Login) } // for each account, get the list of repos - for _, login := range logins { - repos_, err := client.ListReposAll(login) + for _, account := range accounts { + repos, err := client.ListReposAll(account) if err != nil { - return repos, err + return all, err } - for _, repo := range repos_ { - repos = append(repos, convertRepoLite(repo)) + for _, repo := range repos { + all = append(all, convertRepoLite(repo)) } } - return repos, nil + return all, nil } +// Perm returns the user permissions for the named repository. Because Bitbucket +// does not have an endpoint to access user permissions, we attempt to fetch +// the repository hook list, which is restricted to administrators to calculate +// administrative access to a repository. func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) { client := c.newClient(u) @@ -177,20 +159,17 @@ func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) { return perms, err } - // if we've gotten this far we know that the user at least has read access - // to the repository. - perms.Pull = true - - // if the user has access to the repository hooks we can deduce that the user - // has push and admin access. _, err = client.ListHooks(owner, name, &internal.ListOpts{}) if err == nil { perms.Push = true perms.Admin = true } + perms.Pull = true return perms, nil } +// File fetches the file from the Bitbucket repository and returns its contents +// in string format. func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { config, err := c.newClient(u).FindSource(r.Owner, r.Name, b.Commit, f) if err != nil { @@ -199,6 +178,7 @@ func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([ return []byte(config.Data), err } +// Status creates a build status for the Bitbucket commit. func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { status := internal.BuildStatus{ State: convertStatus(b.Status), @@ -209,21 +189,14 @@ func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link strin return c.newClient(u).CreateStatus(r.Owner, r.Name, b.Commit, &status) } -func (c *config) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - return &model.Netrc{ - Machine: "bitbucket.org", - Login: "x-token-auth", - Password: u.Token, - }, nil -} - -func (c *config) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { +// Activate activates the repository by registering repository push hooks with +// the Bitbucket repository. Prior to registering hook, previously created hooks +// are deleted. +func (c *config) Activate(u *model.User, r *model.Repo, link string) error { rawurl, err := url.Parse(link) if err != nil { return err } - - // deletes any previously created hooks c.Deactivate(u, r, link) return c.newClient(u).CreateHook(r.Owner, r.Name, &internal.Hook{ @@ -234,32 +207,80 @@ func (c *config) Activate(u *model.User, r *model.Repo, k *model.Key, link strin }) } +// 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) - linkurl, err := url.Parse(link) - if err != nil { - return err - } - hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{}) if err != nil { return err } - - for _, hook := range hooks.Values { - hookurl, err := url.Parse(hook.Url) - if err == nil && hookurl.Host == linkurl.Host { - return client.DeleteHook(r.Owner, r.Name, hook.Uuid) - } + hook := matchingHooks(hooks.Values, link) + if hook != nil { + return client.DeleteHook(r.Owner, r.Name, hook.Uuid) } - return nil } +// Netrc returns a netrc file capable of authenticating Bitbucket requests and +// cloning Bitbucket repositories. +func (c *config) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + return &model.Netrc{ + Machine: "bitbucket.org", + Login: "x-token-auth", + Password: u.Token, + }, nil +} + // Hook parses the incoming Bitbucket hook and returns the Repository and -// Build details. If the hook is unsupported nil values are returned and the -// hook should be skipped. +// Build details. If the hook is unsupported nil values are returned. func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) { return parseHook(r) } + +// helper function to return the bitbucket oauth2 client +func (c *config) newClient(u *model.User) *internal.Client { + return c.newClientToken(u.Token, u.Secret) +} + +// helper function to return the bitbucket oauth2 client +func (c *config) newClientToken(token, secret string) *internal.Client { + return internal.NewClientToken( + c.API, + c.Client, + c.Secret, + &oauth2.Token{ + AccessToken: token, + RefreshToken: secret, + }, + ) +} + +// helper function to return the bitbucket oauth2 config +func (c *config) newConfig(redirect string) *oauth2.Config { + return &oauth2.Config{ + ClientID: c.Client, + ClientSecret: c.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", c.URL), + TokenURL: fmt.Sprintf("%s/site/oauth2/access_token", c.URL), + }, + RedirectURL: fmt.Sprintf("%s/authorize", redirect), + } +} + +// helper function to return matching hooks. +func matchingHooks(hooks []*internal.Hook, rawurl string) *internal.Hook { + link, err := url.Parse(rawurl) + if err != nil { + return nil + } + for _, hook := range hooks { + hookurl, err := url.Parse(hook.Url) + if err == nil && hookurl.Host == link.Host { + return hook + } + } + return nil +} diff --git a/remote/bitbucket/bitbucket_test.go b/remote/bitbucket/bitbucket_test.go index 6d621788e..ea1a8281a 100644 --- a/remote/bitbucket/bitbucket_test.go +++ b/remote/bitbucket/bitbucket_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -17,7 +18,7 @@ func Test_bitbucket(t *testing.T) { gin.SetMode(gin.TestMode) s := httptest.NewServer(fixtures.Handler()) - c := &config{URL: s.URL} + c := &config{URL: s.URL, API: s.URL} g := goblin.Goblin(t) g.Describe("Bitbucket client", func() { @@ -28,10 +29,12 @@ func Test_bitbucket(t *testing.T) { g.It("Should return client with default endpoint", func() { remote := New("4vyW6b49Z", "a5012f6c6") - g.Assert(remote.(*config).URL).Equal("https://api.bitbucket.org") + 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) @@ -40,6 +43,35 @@ func Test_bitbucket(t *testing.T) { 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( @@ -49,7 +81,7 @@ func Test_bitbucket(t *testing.T) { g.Assert(err == nil).IsTrue() g.Assert(login).Equal(fakeUser.Login) }) - g.It("Should return error when request fails", func() { + g.It("Should handle a failure to resolve user", func() { _, err := c.Auth( fakeUserNotFound.Token, fakeUserNotFound.Secret, @@ -58,6 +90,26 @@ func Test_bitbucket(t *testing.T) { }) }) + 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( @@ -154,21 +206,16 @@ func Test_bitbucket(t *testing.T) { g.Describe("When activating a repository", func() { g.It("Should error when malformed hook", func() { - err := c.Activate(fakeUser, fakeRepo, nil, "%gh&%ij") + err := c.Activate(fakeUser, fakeRepo, "%gh&%ij") g.Assert(err != nil).IsTrue() }) g.It("Should create the hook", func() { - err := c.Activate(fakeUser, fakeRepo, nil, "http://127.0.0.1") + err := c.Activate(fakeUser, fakeRepo, "http://127.0.0.1") g.Assert(err == nil).IsTrue() }) - g.It("Should remove previous hooks") }) g.Describe("When deactivating a repository", func() { - g.It("Should error when malformed hook", func() { - err := c.Deactivate(fakeUser, fakeRepo, "%gh&%ij") - g.Assert(err != nil).IsTrue() - }) g.It("Should error when listing hooks fails", func() { err := c.Deactivate(fakeUser, fakeRepoNoHooks, "http://127.0.0.1") g.Assert(err != nil).IsTrue() @@ -183,6 +230,28 @@ func Test_bitbucket(t *testing.T) { }) }) + 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() @@ -208,6 +277,21 @@ var ( 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", diff --git a/remote/bitbucket/fixtures/handler.go b/remote/bitbucket/fixtures/handler.go index a1ce21911..f5cbbb40e 100644 --- a/remote/bitbucket/fixtures/handler.go +++ b/remote/bitbucket/fixtures/handler.go @@ -12,6 +12,7 @@ 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) @@ -25,6 +26,27 @@ func Handler() http.Handler { 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": @@ -103,6 +125,24 @@ func getUserRepos(c *gin.Context) { } } +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", diff --git a/remote/bitbucket/parse_test.go b/remote/bitbucket/parse_test.go index 7ff3657be..eee2bda36 100644 --- a/remote/bitbucket/parse_test.go +++ b/remote/bitbucket/parse_test.go @@ -13,7 +13,7 @@ import ( func Test_parser(t *testing.T) { g := goblin.Goblin(t) - g.Describe("Bitbucket hook parser", func() { + g.Describe("Bitbucket parser", func() { g.It("Should ignore unsupported hook", func() { buf := bytes.NewBufferString(fixtures.HookPush) @@ -27,7 +27,7 @@ func Test_parser(t *testing.T) { g.Assert(err == nil).IsTrue() }) - g.Describe("Given a pull request hook", func() { + g.Describe("Given a pull request hook payload", func() { g.It("Should return err when malformed", func() { buf := bytes.NewBufferString("[]") @@ -64,7 +64,7 @@ func Test_parser(t *testing.T) { }) }) - g.Describe("Given a push hook", func() { + g.Describe("Given a push hook payload", func() { g.It("Should return err when malformed", func() { buf := bytes.NewBufferString("[]") diff --git a/remote/remote.go b/remote/remote.go index a1255104d..cfcdafdf0 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -44,12 +44,11 @@ type Remote interface { // private repositories from a remote system. Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) - // Activate activates a repository by creating the post-commit hook and - // adding the SSH deploy key, if applicable. - Activate(u *model.User, r *model.Repo, k *model.Key, link string) error + // Activate activates a repository by creating the post-commit hook. + Activate(u *model.User, r *model.Repo, link string) error - // Deactivate removes a repository by removing all the post-commit hooks - // which are equal to link and removing the SSH deploy key. + // Deactivate deactivates a repository by removing all previously created + // post-commit hooks matching the given link. Deactivate(u *model.User, r *model.Repo, link string) error // Hook parses the post-commit hook from the Request body and returns the @@ -116,8 +115,8 @@ func Netrc(c context.Context, u *model.User, r *model.Repo) (*model.Netrc, error // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. -func Activate(c context.Context, u *model.User, r *model.Repo, k *model.Key, link string) error { - return FromContext(c).Activate(u, r, k, link) +func Activate(c context.Context, u *model.User, r *model.Repo, link string) error { + return FromContext(c).Activate(u, r, link) } // Deactivate removes a repository by removing all the post-commit hooks From ebd547deacce9de774d609261d4049c99883945c Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Sun, 1 May 2016 16:30:00 -0700 Subject: [PATCH 05/10] refactoring remotes to remove deprecated variables, adding tests --- api/repo.go | 59 +--- drone/daemon.go | 107 +++--- remote/bitbucket/bitbucket.go | 5 +- remote/bitbucketserver/bitbucketserver.go | 320 ++++++++---------- remote/github/github.go | 2 +- remote/gitlab/gitlab.go | 189 +++++++---- remote/gitlab/gitlab_test.go | 4 +- remote/gogs/fixtures/handler.go | 109 ++++++ .../hook_push.go => fixtures/hooks.go} | 4 +- remote/gogs/gogs.go | 248 +++++++------- remote/gogs/gogs_test.go | 183 ++++++++++ remote/gogs/helper.go | 44 ++- remote/gogs/helper_test.go | 12 +- remote/gogs/types.go | 6 +- server/server.go | 2 - store/datastore/keys.go | 31 -- store/datastore/keys_test.go | 114 ------- store/store.go | 28 -- template/amber/repo_config.amber | 6 +- web/login.go | 18 +- web/pages.go | 2 - 21 files changed, 797 insertions(+), 696 deletions(-) create mode 100644 remote/gogs/fixtures/handler.go rename remote/gogs/{testdata/hook_push.go => fixtures/hooks.go} (97%) create mode 100644 remote/gogs/gogs_test.go delete mode 100644 store/datastore/keys.go delete mode 100644 store/datastore/keys_test.go diff --git a/api/repo.go b/api/repo.go index c7ed5e2fd..5b631973b 100644 --- a/api/repo.go +++ b/api/repo.go @@ -2,13 +2,11 @@ package api import ( "fmt" - "io/ioutil" "net/http" "github.com/gin-gonic/gin" "github.com/drone/drone/cache" - "github.com/drone/drone/model" "github.com/drone/drone/remote" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/shared/crypto" @@ -72,19 +70,9 @@ func PostRepo(c *gin.Context) { sig, ) - // generate an RSA key and add to the repo - key, err := crypto.GeneratePrivateKey() - if err != nil { - c.String(500, err.Error()) - return - } - keys := new(model.Key) - keys.Public = string(crypto.MarshalPublicKey(&key.PublicKey)) - keys.Private = string(crypto.MarshalPrivateKey(key)) - // activate the repository before we make any // local changes to the database. - err = remote.Activate(user, r, keys, link) + err = remote.Activate(user, r, link) if err != nil { c.String(500, err.Error()) return @@ -96,12 +84,6 @@ func PostRepo(c *gin.Context) { c.String(500, err.Error()) return } - keys.RepoID = r.ID - err = store.CreateKey(c, keys) - if err != nil { - c.String(500, err.Error()) - return - } c.JSON(200, r) } @@ -155,45 +137,6 @@ func GetRepo(c *gin.Context) { c.JSON(http.StatusOK, session.Repo(c)) } -func GetRepoKey(c *gin.Context) { - repo := session.Repo(c) - keys, err := store.GetKey(c, repo) - if err != nil { - c.String(404, "Error fetching repository key") - } else { - c.String(http.StatusOK, keys.Public) - } -} - -func PostRepoKey(c *gin.Context) { - repo := session.Repo(c) - keys, err := store.GetKey(c, repo) - if err != nil { - c.String(404, "Error fetching repository key") - return - } - body, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - c.String(500, "Error reading private key from body. %s", err) - return - } - pkey := crypto.UnmarshalPrivateKey(body) - if pkey == nil { - c.String(500, "Cannot unmarshal private key. Invalid format.") - return - } - - keys.Public = string(crypto.MarshalPublicKey(&pkey.PublicKey)) - keys.Private = string(crypto.MarshalPrivateKey(pkey)) - - err = store.UpdateKey(c, keys) - if err != nil { - c.String(500, "Error updating repository key") - return - } - c.String(201, keys.Public) -} - func DeleteRepo(c *gin.Context) { remote := remote.FromContext(c) repo := session.Repo(c) diff --git a/drone/daemon.go b/drone/daemon.go index cc85e6d77..f2cdf3dd3 100644 --- a/drone/daemon.go +++ b/drone/daemon.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "net/http" "os" "time" @@ -159,6 +158,16 @@ var DaemonCmd = cli.Command{ 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", @@ -205,6 +214,16 @@ var DaemonCmd = cli.Command{ 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", @@ -245,7 +264,11 @@ var DaemonCmd = cli.Command{ 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 // @@ -347,60 +370,70 @@ func setupStore(c *cli.Context) store.Store { } func setupRemote(c *cli.Context) remote.Remote { + var remote remote.Remote + var err error switch { case c.Bool("github"): - return setupGithub(c) + remote, err = setupGithub(c) case c.Bool("gitlab"): - return setupGitlab(c) + remote, err = setupGitlab(c) case c.Bool("bitbucket"): - return setupBitbucket(c) + remote, err = setupBitbucket(c) case c.Bool("stash"): - return setupStash(c) + remote, err = setupStash(c) case c.Bool("gogs"): - return setupGogs(c) + remote, err = setupGogs(c) default: - logrus.Fatalln("version control system not configured") - return nil + err = fmt.Errorf("version control system not configured") } + if err != nil { + logrus.Fatalln(err) + } + return remote } -func setupBitbucket(c *cli.Context) remote.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 { - return gogs.New( - c.String("gogs-server"), - c.Bool("gogs-private-mode"), - c.Bool("gogs-skip-verify"), - ) +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 { - return bitbucketserver.New( - c.String("stash-server"), - c.String("stash-consumer-key"), - c.String("stash-consumer-rsa"), - c.String("stash-git-username"), - c.String("stash-git-password"), - ) +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 { - return gitlab.New( - c.String("gitlab-server"), - c.String("gitlab-client"), - c.String("gitlab-sercret"), - c.Bool("gitlab-private-mode"), - c.Bool("gitlab-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 { - g, err := github.New( +func setupGithub(c *cli.Context) (remote.Remote, error) { + return github.New( c.String("github-server"), c.String("github-client"), c.String("github-sercret"), @@ -409,10 +442,6 @@ func setupGithub(c *cli.Context) remote.Remote { c.Bool("github-skip-verify"), c.BoolT("github-merge-ref"), ) - if err != nil { - log.Fatalln(err) - } - return g } func printSecret(c *cli.Context) error { diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index c7acc38ed..b3e51a97f 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -132,7 +132,6 @@ func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) { accounts = append(accounts, team.Login) } - // for each account, get the list of repos for _, account := range accounts { repos, err := client.ListReposAll(account) if err != nil { @@ -142,7 +141,6 @@ func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) { all = append(all, convertRepoLite(repo)) } } - return all, nil } @@ -168,8 +166,7 @@ func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) { return perms, nil } -// File fetches the file from the Bitbucket repository and returns its contents -// in string format. +// File fetches the file from the Bitbucket repository and returns its contents. func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { config, err := c.newClient(u).FindSource(r.Owner, r.Name, b.Commit, f) if err != nil { diff --git a/remote/bitbucketserver/bitbucketserver.go b/remote/bitbucketserver/bitbucketserver.go index c40a4c7bf..6b73ad136 100644 --- a/remote/bitbucketserver/bitbucketserver.go +++ b/remote/bitbucketserver/bitbucketserver.go @@ -1,23 +1,17 @@ package bitbucketserver -// Requires the following to be set -// REMOTE_DRIVER=bitbucketserver -// REMOTE_CONFIG=https://{servername}?consumer_key={key added on the stash server for oath1}&git_username={username for clone}&git_password={password for clone}&consumer_rsa=/path/to/pem.file&open={not used yet} -// Configure application links in the bitbucket server -- -// application url needs to be the base url to drone -// incoming auth needs to have the consumer key (same as the key in REMOTE_CONFIG) -// set the public key (public key from the private key added to /var/lib/bitbucketserver/private_key.pem name matters) -// consumer call back is the base url to drone plus /authorize/ -// Needs a pem private key added to /var/lib/bitbucketserver/private_key.pem -// After that you should be good to go +// WARNING! This is an work-in-progress patch and does not yet conform to the coding, +// quality or security standards expected of this project. Please use with caution. import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "io/ioutil" "net/http" "net/url" - "strconv" log "github.com/Sirupsen/logrus" "github.com/drone/drone/model" @@ -25,132 +19,132 @@ import ( "github.com/mrjones/oauth" ) -type BitbucketServer struct { +// Opts defines configuration options. +type Opts struct { + URL string // Stash server url. + Username string // Git machine account username. + Password string // Git machine account password. + ConsumerKey string // Oauth1 consumer key. + ConsumerRSA string // Oauth1 consumer key file. + + SkipVerify bool // Skip ssl verification. +} + +// New returns a Remote implementation that integrates with Bitbucket Server, +// the on-premise edition of Bitbucket Cloud, formerly known as Stash. +func New(opts Opts) (remote.Remote, error) { + bb := &client{ + URL: opts.URL, + ConsumerKey: opts.ConsumerKey, + ConsumerRSA: opts.ConsumerRSA, + GitUserName: opts.Username, + GitPassword: opts.Password, + } + + switch { + case bb.GitUserName == "": + return nil, fmt.Errorf("Must have a git machine account username") + case bb.GitPassword == "": + return nil, fmt.Errorf("Must have a git machine account password") + case bb.ConsumerKey == "": + return nil, fmt.Errorf("Must have a oauth1 consumer key") + case bb.ConsumerRSA == "": + return nil, fmt.Errorf("Must have a oauth1 consumer key file") + } + + keyfile, err := ioutil.ReadFile(bb.ConsumerRSA) + if err != nil { + return nil, err + } + block, _ := pem.Decode(keyfile) + bb.PrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + // TODO de-referencing is a bit weird and may not behave as expected, and could + // have race conditions. Instead store the parsed key (I already did this above) + // and then pass the parsed private key when creating the Bitbucket client. + bb.Consumer = *NewClient(bb.ConsumerRSA, bb.ConsumerKey, bb.URL) + return bb, nil +} + +type client struct { URL string ConsumerKey string GitUserName string GitPassword string ConsumerRSA string - Open bool + PrivateKey *rsa.PrivateKey Consumer oauth.Consumer } -func New(url, key, rsa, username, password string) remote.Remote { - bb := &BitbucketServer{ - URL: url, - ConsumerKey: key, - GitUserName: username, - GitPassword: password, - ConsumerRSA: rsa, - } - bb.Consumer = *NewClient(bb.ConsumerRSA, bb.ConsumerKey, bb.URL) - - return bb -} - -func Load(config string) *BitbucketServer { - - url_, err := url.Parse(config) +func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { + requestToken, url, err := c.Consumer.GetRequestTokenAndUrl("oob") if err != nil { - log.Fatalln("unable to parse remote dsn. %s", err) - } - params := url_.Query() - url_.Path = "" - url_.RawQuery = "" - - bitbucketserver := BitbucketServer{} - bitbucketserver.URL = url_.String() - bitbucketserver.GitUserName = params.Get("git_username") - if bitbucketserver.GitUserName == "" { - log.Fatalln("Must have a git_username") - } - bitbucketserver.GitPassword = params.Get("git_password") - if bitbucketserver.GitPassword == "" { - log.Fatalln("Must have a git_password") - } - bitbucketserver.ConsumerKey = params.Get("consumer_key") - if bitbucketserver.ConsumerKey == "" { - log.Fatalln("Must have a consumer_key") - } - bitbucketserver.ConsumerRSA = params.Get("consumer_rsa") - if bitbucketserver.ConsumerRSA == "" { - log.Fatalln("Must have a consumer_rsa") - } - - bitbucketserver.Open, _ = strconv.ParseBool(params.Get("open")) - - bitbucketserver.Consumer = *NewClient(bitbucketserver.ConsumerRSA, bitbucketserver.ConsumerKey, bitbucketserver.URL) - - return &bitbucketserver -} - -func (bs *BitbucketServer) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { - log.Info("Starting to login for bitbucketServer") - - log.Info("getting the requestToken") - requestToken, url, err := bs.Consumer.GetRequestTokenAndUrl("oob") - if err != nil { - log.Error(err) + return nil, err } var code = req.FormValue("oauth_verifier") if len(code) == 0 { - log.Info("redirecting to %s", url) http.Redirect(res, req, url, http.StatusSeeOther) - return nil, false, nil + return nil, nil } - var request_oauth_token = req.FormValue("oauth_token") - requestToken.Token = request_oauth_token - accessToken, err := bs.Consumer.AuthorizeToken(requestToken, code) + requestToken.Token = req.FormValue("oauth_token") + accessToken, err := c.Consumer.AuthorizeToken(requestToken, code) if err != nil { - log.Error(err) + return nil, err } - client, err := bs.Consumer.MakeHttpClient(accessToken) + client, err := c.Consumer.MakeHttpClient(accessToken) if err != nil { - log.Error(err) + return nil, err } - response, err := client.Get(fmt.Sprintf("%s/plugins/servlet/applinks/whoami", bs.URL)) + response, err := client.Get(fmt.Sprintf("%s/plugins/servlet/applinks/whoami", c.URL)) if err != nil { - log.Error(err) + return nil, err } defer response.Body.Close() bits, err := ioutil.ReadAll(response.Body) - userName := string(bits) + if err != nil { + return nil, err + } + login := string(bits) - response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s", bs.URL, userName)) + // TODO errors should never be ignored like this + response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s", c.URL, login)) contents, err := ioutil.ReadAll(response1.Body) defer response1.Body.Close() - var mUser User - json.Unmarshal(contents, &mUser) + var mUser User // TODO prefixing with m* is not a common convention in Go + json.Unmarshal(contents, &mUser) // TODO should not ignore error - user := model.User{} - user.Login = userName - user.Email = mUser.EmailAddress - user.Token = accessToken.Token - - user.Avatar = avatarLink(mUser.EmailAddress) - - return &user, bs.Open, nil + return &model.User{ + Login: login, + Email: mUser.EmailAddress, + Token: accessToken.Token, + Avatar: avatarLink(mUser.EmailAddress), + }, nil } -func (bs *BitbucketServer) Auth(token, secret string) (string, error) { - log.Info("Staring to auth for bitbucketServer. %s", token) - if len(token) == 0 { - return "", fmt.Errorf("Hasn't logged in yet") - } - return token, nil +// Auth is not supported by the Stash driver. +func (*client) Auth(token, secret string) (string, error) { + return "", fmt.Errorf("Not Implemented") } -func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, error) { - log.Info("Staring repo for bitbucketServer with user " + u.Login + " " + owner + " " + name) +// Teams is not supported by the Stash driver. +func (*client) Teams(u *model.User) ([]*model.Team, error) { + var teams []*model.Team + return teams, nil +} - client := NewClientWithToken(&bs.Consumer, u.Token) +func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) { + + client := NewClientWithToken(&c.Consumer, u.Token) + + url := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s", c.URL, owner, name) - url := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s", bs.URL, owner, name) - log.Info("Trying to get " + url) response, err := client.Get(url) if err != nil { log.Error(err) @@ -160,40 +154,36 @@ func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, bsRepo := BSRepo{} json.Unmarshal(contents, &bsRepo) - cloneLink := "" - repoLink := "" + repo := &model.Repo{ + Name: bsRepo.Slug, + Owner: bsRepo.Project.Key, + Branch: "master", + Kind: model.RepoGit, + IsPrivate: !bsRepo.Project.Public, // TODO(josmo) verify this is corrrect + FullName: fmt.Sprintf("%s/%s", bsRepo.Project.Key, bsRepo.Slug), + } for _, item := range bsRepo.Links.Clone { if item.Name == "http" { - cloneLink = item.Href + repo.Clone = item.Href } } for _, item := range bsRepo.Links.Self { if item.Href != "" { - repoLink = item.Href + repo.Link = item.Href } } - //TODO: get the real allow tag+ infomration - repo := &model.Repo{} - repo.Clone = cloneLink - repo.Link = repoLink - repo.Name = bsRepo.Slug - repo.Owner = bsRepo.Project.Key - repo.AllowPush = true - repo.FullName = fmt.Sprintf("%s/%s", bsRepo.Project.Key, bsRepo.Slug) - repo.Branch = "master" - repo.Kind = model.RepoGit return repo, nil } -func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) { - log.Info("Staring repos for bitbucketServer " + u.Login) +func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) { + var repos = []*model.RepoLite{} - client := NewClientWithToken(&bs.Consumer, u.Token) + client := NewClientWithToken(&c.Consumer, u.Token) - response, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/repos?limit=10000", bs.URL)) + response, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/repos?limit=10000", c.URL)) if err != nil { log.Error(err) } @@ -213,10 +203,8 @@ func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error) { return repos, nil } -func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm, error) { - - //TODO: find the real permissions - log.Info("Staring perm for bitbucketServer") +func (c *client) Perm(u *model.User, owner, repo string) (*model.Perm, error) { + // TODO need to fetch real permissions here perms := new(model.Perm) perms.Pull = true perms.Admin = true @@ -224,11 +212,11 @@ func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm, return perms, nil } -func (bs *BitbucketServer) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { +func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { log.Info(fmt.Sprintf("Staring file for bitbucketServer login: %s repo: %s buildevent: %s string: %s", u.Login, r.Name, b.Event, f)) - client := NewClientWithToken(&bs.Consumer, u.Token) - fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", bs.URL, r.Owner, r.Name, f) + client := NewClientWithToken(&c.Consumer, u.Token) + fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", c.URL, r.Owner, r.Name, f) log.Info(fileURL) response, err := client.Get(fileURL) if err != nil { @@ -246,28 +234,26 @@ func (bs *BitbucketServer) File(u *model.User, r *model.Repo, b *model.Build, f return responseBytes, nil } -func (bs *BitbucketServer) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - log.Info("Staring status for bitbucketServer") +// Status is not supported by the Gogs driver. +func (*client) Status(*model.User, *model.Repo, *model.Build, string) error { return nil } -func (bs *BitbucketServer) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { - log.Info("Starting the Netrc lookup") - u, err := url.Parse(bs.URL) +func (c *client) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { + u, err := url.Parse(c.URL) // TODO strip port from url if err != nil { return nil, err } return &model.Netrc{ Machine: u.Host, - Login: bs.GitUserName, - Password: bs.GitPassword, + Login: c.GitUserName, + Password: c.GitPassword, }, nil } -func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - log.Info(fmt.Sprintf("Staring activate for bitbucketServer user: %s repo: %s key: %s link: %s", u.Login, r.Name, k, link)) - client := NewClientWithToken(&bs.Consumer, u.Token) - hook, err := bs.CreateHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) +func (c *client) Activate(u *model.User, r *model.Repo, link string) error { + client := NewClientWithToken(&c.Consumer, u.Token) + hook, err := c.CreateHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) if err != nil { return err } @@ -275,50 +261,38 @@ func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key, return nil } -func (bs *BitbucketServer) Deactivate(u *model.User, r *model.Repo, link string) error { - log.Info(fmt.Sprintf("Staring deactivating for bitbucketServer user: %s repo: %s link: %s", u.Login, r.Name, link)) - client := NewClientWithToken(&bs.Consumer, u.Token) - err := bs.DeleteHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) +func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error { + client := NewClientWithToken(&c.Consumer, u.Token) + err := c.DeleteHook(client, r.Owner, r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook", link) if err != nil { return err } return nil } -func (bs *BitbucketServer) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - log.Info("Staring hook for bitbucketServer") - defer r.Body.Close() - contents, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Info(err) +func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { + hook := new(postHook) + if err := json.NewDecoder(r.Body).Decode(hook); err != nil { + return nil, nil, err } - var hookPost postHook - json.Unmarshal(contents, &hookPost) + build := &model.Build{ + Event: model.EventPush, + Ref: hook.RefChanges[0].RefID, // TODO check for index Values + Author: hook.Changesets.Values[0].ToCommit.Author.EmailAddress, // TODO check for index Values + Commit: hook.RefChanges[0].ToHash, // TODO check for index value + Avatar: avatarLink(hook.Changesets.Values[0].ToCommit.Author.EmailAddress), + } - buildModel := &model.Build{} - buildModel.Event = model.EventPush - buildModel.Ref = hookPost.RefChanges[0].RefID - buildModel.Author = hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress - buildModel.Commit = hookPost.RefChanges[0].ToHash - buildModel.Avatar = avatarLink(hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress) + repo := &model.Repo{ + Name: hook.Repository.Slug, + Owner: hook.Repository.Project.Key, + FullName: fmt.Sprintf("%s/%s", hook.Repository.Project.Key, hook.Repository.Slug), + Branch: "master", + Kind: model.RepoGit, + } - //All you really need is the name and owner. That's what creates the lookup key, so it needs to match the repo info. Just an FYI - repo := &model.Repo{} - repo.Name = hookPost.Repository.Slug - repo.Owner = hookPost.Repository.Project.Key - repo.AllowTag = false - repo.AllowDeploy = false - repo.AllowPull = false - repo.AllowPush = true - repo.FullName = fmt.Sprintf("%s/%s", hookPost.Repository.Project.Key, hookPost.Repository.Slug) - repo.Branch = "master" - repo.Kind = model.RepoGit - - return repo, buildModel, nil -} -func (bs *BitbucketServer) String() string { - return "bitbucketserver" + return repo, build, nil } type HookDetail struct { @@ -336,7 +310,7 @@ type Hook struct { } // Enable hook for named repository -func (bs *BitbucketServer) CreateHook(client *http.Client, project, slug, hook_key, link string) (*Hook, error) { +func (bs *client) CreateHook(client *http.Client, project, slug, hook_key, link string) (*Hook, error) { // Set hook hookBytes := []byte(fmt.Sprintf(`{"hook-url-0":"%s"}`, link)) @@ -351,7 +325,7 @@ func (bs *BitbucketServer) CreateHook(client *http.Client, project, slug, hook_k } // Disable hook for named repository -func (bs *BitbucketServer) DeleteHook(client *http.Client, project, slug, hook_key, link string) error { +func (bs *client) DeleteHook(client *http.Client, project, slug, hook_key, link string) error { enablePath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled", project, slug, hook_key) doDelete(client, bs.URL+enablePath) diff --git a/remote/github/github.go b/remote/github/github.go index 258aa9cb6..ac9b1a6aa 100644 --- a/remote/github/github.go +++ b/remote/github/github.go @@ -272,7 +272,7 @@ func (g *Github) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. -func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { +func (g *Github) Activate(u *model.User, r *model.Repo, link string) error { client := NewClient(g.API, u.Token, g.SkipVerify) _, err := CreateUpdateHook(client, r.Owner, r.Name, link) return err diff --git a/remote/gitlab/gitlab.go b/remote/gitlab/gitlab.go index b5beca6d0..488dbd9d7 100644 --- a/remote/gitlab/gitlab.go +++ b/remote/gitlab/gitlab.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "io/ioutil" + "net" "net/http" "net/url" "strconv" @@ -13,38 +14,59 @@ import ( "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/oauth2" - "github.com/drone/drone/shared/token" "github.com/drone/drone/remote/gitlab/client" ) -const ( - DefaultScope = "api" -) +const DefaultScope = "api" + +// Opts defines configuration options. +type Opts struct { + URL string // Gogs server url. + Client string // Oauth2 client id. + Secret string // Oauth2 client secret. + Username string // Optional machine account username. + Password string // Optional machine account password. + PrivateMode bool // Gogs is running in private mode. + SkipVerify bool // Skip ssl verification. +} + +// New returns a Remote implementation that integrates with Gitlab, an open +// source Git service. See https://gitlab.com +func New(opts Opts) (remote.Remote, error) { + url, err := url.Parse(opts.URL) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(url.Host) + if err == nil { + url.Host = host + } + return &Gitlab{ + URL: opts.URL, + Client: opts.Client, + Secret: opts.Secret, + Machine: url.Host, + Username: opts.Username, + Password: opts.Password, + PrivateMode: opts.PrivateMode, + SkipVerify: opts.SkipVerify, + }, nil +} type Gitlab struct { URL string Client string Secret string - AllowedOrgs []string - CloneMode string - Open bool + Machine string + Username string + Password string PrivateMode bool SkipVerify bool HideArchives bool Search bool } -func New(url, client, secret string, private, skipverify bool) remote.Remote { - return &Gitlab{ - URL: url, - Client: client, - Secret: secret, - PrivateMode: private, - SkipVerify: skipverify, - } -} - func Load(config string) *Gitlab { url_, err := url.Parse(config) if err != nil { @@ -57,17 +79,17 @@ func Load(config string) *Gitlab { gitlab.URL = url_.String() gitlab.Client = params.Get("client_id") gitlab.Secret = params.Get("client_secret") - gitlab.AllowedOrgs = params["orgs"] + // gitlab.AllowedOrgs = params["orgs"] gitlab.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify")) gitlab.HideArchives, _ = strconv.ParseBool(params.Get("hide_archives")) - gitlab.Open, _ = strconv.ParseBool(params.Get("open")) + // gitlab.Open, _ = strconv.ParseBool(params.Get("open")) - switch params.Get("clone_mode") { - case "oauth": - gitlab.CloneMode = "oauth" - default: - gitlab.CloneMode = "token" - } + // switch params.Get("clone_mode") { + // case "oauth": + // gitlab.CloneMode = "oauth" + // default: + // gitlab.CloneMode = "token" + // } // this is a temp workaround gitlab.Search, _ = strconv.ParseBool(params.Get("search")) @@ -77,7 +99,7 @@ func Load(config string) *Gitlab { // Login authenticates the session and returns the // remote user details. -func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { var config = &oauth2.Config{ ClientId: g.Client, @@ -97,41 +119,41 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, var code = req.FormValue("code") if len(code) == 0 { http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther) - return nil, false, nil + return nil, nil } var trans = &oauth2.Transport{Config: config, Transport: trans_} var token_, err = trans.Exchange(code) if err != nil { - return nil, false, fmt.Errorf("Error exchanging token. %s", err) + return nil, fmt.Errorf("Error exchanging token. %s", err) } client := NewClient(g.URL, token_.AccessToken, g.SkipVerify) login, err := client.CurrentUser() if err != nil { - return nil, false, err + return nil, err } - if len(g.AllowedOrgs) != 0 { - groups, err := client.AllGroups() - if err != nil { - return nil, false, fmt.Errorf("Could not check org membership. %s", err) - } - - var member bool - for _, group := range groups { - for _, allowedOrg := range g.AllowedOrgs { - if group.Path == allowedOrg { - member = true - break - } - } - } - - if !member { - return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs) - } - } + // if len(g.AllowedOrgs) != 0 { + // groups, err := client.AllGroups() + // if err != nil { + // return nil, fmt.Errorf("Could not check org membership. %s", err) + // } + // + // var member bool + // for _, group := range groups { + // for _, allowedOrg := range g.AllowedOrgs { + // if group.Path == allowedOrg { + // member = true + // break + // } + // } + // } + // + // if !member { + // return nil, false, fmt.Errorf("User does not belong to correct group. Must belong to %v", g.AllowedOrgs) + // } + // } user := &model.User{} user.Login = login.Username @@ -145,7 +167,7 @@ func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, user.Avatar = g.URL + "/" + login.AvatarUrl } - return user, g.Open, nil + return user, nil } func (g *Gitlab) Auth(token, secret string) (string, error) { @@ -157,6 +179,21 @@ func (g *Gitlab) Auth(token, secret string) (string, error) { return login.Username, nil } +func (g *Gitlab) Teams(u *model.User) ([]*model.Team, error) { + client := NewClient(g.URL, u.Token, g.SkipVerify) + groups, err := client.AllGroups() + if err != nil { + return nil, err + } + var teams []*model.Team + for _, group := range groups { + teams = append(teams, &model.Team{ + Login: group.Name, + }) + } + return teams, nil +} + // Repo fetches the named repository from the remote system. func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) { client := NewClient(g.URL, u.Token, g.SkipVerify) @@ -295,29 +332,47 @@ func (g *Gitlab) Status(u *model.User, repo *model.Repo, b *model.Build, link st // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. -func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - url_, err := url.Parse(g.URL) - if err != nil { - return nil, err - } - netrc := &model.Netrc{} - netrc.Machine = url_.Host +// func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { +// url_, err := url.Parse(g.URL) +// if err != nil { +// return nil, err +// } +// netrc := &model.Netrc{} +// netrc.Machine = url_.Host +// +// switch g.CloneMode { +// case "oauth": +// netrc.Login = "oauth2" +// netrc.Password = u.Token +// case "token": +// t := token.New(token.HookToken, r.FullName) +// netrc.Login = "drone-ci-token" +// netrc.Password, err = t.Sign(r.Hash) +// } +// return netrc, err +// } - switch g.CloneMode { - case "oauth": - netrc.Login = "oauth2" - netrc.Password = u.Token - case "token": - t := token.New(token.HookToken, r.FullName) - netrc.Login = "drone-ci-token" - netrc.Password, err = t.Sign(r.Hash) +// Netrc returns a netrc file capable of authenticating Gitlab requests and +// cloning Gitlab repositories. The netrc will use the global machine account +// when configured. +func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + if g.Password != "" { + return &model.Netrc{ + Login: g.Username, + Password: g.Password, + Machine: g.Machine, + }, nil } - return netrc, err + return &model.Netrc{ + Login: "oauth2", + Password: u.Token, + Machine: g.Machine, + }, nil } // Activate activates a repository by adding a Post-commit hook and // a Public Deploy key, if applicable. -func (g *Gitlab) Activate(user *model.User, repo *model.Repo, k *model.Key, link string) error { +func (g *Gitlab) Activate(user *model.User, repo *model.Repo, link string) error { var client = NewClient(g.URL, user.Token, g.SkipVerify) id, err := GetProjectId(g, client, repo.Owner, repo.Name) if err != nil { diff --git a/remote/gitlab/gitlab_test.go b/remote/gitlab/gitlab_test.go index 944afbb70..6e4f7003f 100644 --- a/remote/gitlab/gitlab_test.go +++ b/remote/gitlab/gitlab_test.go @@ -94,13 +94,13 @@ func Test_Gitlab(t *testing.T) { // Test activate method g.Describe("Activate", func() { g.It("Should be success", func() { - err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test?access_token=token") + err := gitlab.Activate(&user, &repo, "http://example.com/api/hook/test/test?access_token=token") g.Assert(err == nil).IsTrue() }) g.It("Should be failed, when token not given", func() { - err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test") + err := gitlab.Activate(&user, &repo, "http://example.com/api/hook/test/test") g.Assert(err != nil).IsTrue() }) diff --git a/remote/gogs/fixtures/handler.go b/remote/gogs/fixtures/handler.go new file mode 100644 index 000000000..26300644c --- /dev/null +++ b/remote/gogs/fixtures/handler.go @@ -0,0 +1,109 @@ +package fixtures + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Handler returns an http.Handler that is capable of handling a variety of mock +// Bitbucket requests and returning mock responses. +func Handler() http.Handler { + gin.SetMode(gin.TestMode) + + e := gin.New() + e.GET("/api/v1/repos/:owner/:name", getRepo) + e.GET("/api/v1/repos/:owner/:name/raw/:commit/:file", getRepoFile) + e.POST("/api/v1/repos/:owner/:name/hooks", createRepoHook) + e.GET("/api/v1/user/repos", getUserRepos) + + return e +} + +func getRepo(c *gin.Context) { + switch c.Param("name") { + case "repo_not_found": + c.String(404, "") + default: + c.String(200, repoPayload) + } +} + +func getRepoFile(c *gin.Context) { + switch c.Param("file") { + case "file_not_found": + c.String(404, "") + default: + c.String(200, repoFilePayload) + } +} + +func createRepoHook(c *gin.Context) { + in := struct { + Type string `json:"type"` + Conf struct { + Type string `json:"content_type"` + URL string `json:"url"` + } `json:"config"` + }{} + c.BindJSON(&in) + if in.Type != "gogs" || + in.Conf.Type != "json" || + in.Conf.URL != "http://localhost" { + c.String(500, "") + return + } + + c.String(200, "{}") +} + +func getUserRepos(c *gin.Context) { + switch c.Request.Header.Get("Authorization") { + case "token repos_not_found": + c.String(404, "") + default: + c.String(200, userRepoPayload) + } +} + +const repoPayload = ` +{ + "owner": { + "username": "test_name", + "email": "octocat@github.com", + "avatar_url": "https:\/\/secure.gravatar.com\/avatar\/8c58a0be77ee441bb8f8595b7f1b4e87" + }, + "full_name": "test_name\/repo_name", + "private": true, + "html_url": "http:\/\/localhost\/test_name\/repo_name", + "clone_url": "http:\/\/localhost\/test_name\/repo_name.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } +} +` + +const repoFilePayload = `{ platform: linux/amd64 }` + +const userRepoPayload = ` +[ + { + "owner": { + "username": "test_name", + "email": "octocat@github.com", + "avatar_url": "https:\/\/secure.gravatar.com\/avatar\/8c58a0be77ee441bb8f8595b7f1b4e87" + }, + "full_name": "test_name\/repo_name", + "private": true, + "html_url": "http:\/\/localhost\/test_name\/repo_name", + "clone_url": "http:\/\/localhost\/test_name\/repo_name.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } + } +] +` diff --git a/remote/gogs/testdata/hook_push.go b/remote/gogs/fixtures/hooks.go similarity index 97% rename from remote/gogs/testdata/hook_push.go rename to remote/gogs/fixtures/hooks.go index 868f1db6f..f73ced4be 100644 --- a/remote/gogs/testdata/hook_push.go +++ b/remote/gogs/fixtures/hooks.go @@ -1,6 +1,6 @@ -package testdata +package fixtures -var PushHook = ` +var HookPush = ` { "ref": "refs/heads/master", "before": "4b2626259b5a97b6b4eab5e6cca66adb986b672b", diff --git a/remote/gogs/gogs.go b/remote/gogs/gogs.go index 31b4b1604..b4306a827 100644 --- a/remote/gogs/gogs.go +++ b/remote/gogs/gogs.go @@ -12,172 +12,181 @@ import ( "github.com/gogits/go-gogs-client" ) -// Remote defines a remote implementation that integrates with Gogs, an open -// source Git service written in Go. See https://gogs.io/ -type Remote struct { +// Opts defines configuration options. +type Opts struct { + URL string // Gogs server url. + Username string // Optional machine account username. + Password string // Optional machine account password. + PrivateMode bool // Gogs is running in private mode. + SkipVerify bool // Skip ssl verification. +} + +type client struct { URL string - Open bool + Machine string + Username string + Password string PrivateMode bool SkipVerify bool } // New returns a Remote implementation that integrates with Gogs, an open // source Git service written in Go. See https://gogs.io/ -func New(url string, private, skipverify bool) remote.Remote { - return &Remote{ - URL: url, - PrivateMode: private, - SkipVerify: skipverify, +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 &client{ + URL: opts.URL, + Machine: url.Host, + Username: opts.Username, + Password: opts.Password, + PrivateMode: opts.PrivateMode, + SkipVerify: opts.SkipVerify, + }, nil } -// Login authenticates the session and returns the authenticated user. -func (g *Remote) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) { +// Login authenticates an account with Gogs using basic authenticaiton. The +// Gogs account details are returned when the user is successfully authenticated. +func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { var ( username = req.FormValue("username") password = req.FormValue("password") ) - // if the username or password doesn't exist we re-direct - // the user to the login screen. + // if the username or password is empty we re-direct to the login screen. if len(username) == 0 || len(password) == 0 { http.Redirect(res, req, "/login/form", http.StatusSeeOther) - return nil, false, nil + return nil, nil } - client := NewGogsClient(g.URL, "", g.SkipVerify) + client := c.newClient() // try to fetch drone token if it exists var accessToken string tokens, err := client.ListAccessTokens(username, password) - if err != nil { - return nil, false, err - } - for _, token := range tokens { - if token.Name == "drone" { - accessToken = token.Sha1 - break + if err == nil { + for _, token := range tokens { + if token.Name == "drone" { + accessToken = token.Sha1 + break + } } } // if drone token not found, create it if accessToken == "" { - token, err := client.CreateAccessToken(username, password, gogs.CreateAccessTokenOption{Name: "drone"}) - if err != nil { - return nil, false, err + token, terr := client.CreateAccessToken( + username, + password, + gogs.CreateAccessTokenOption{Name: "drone"}, + ) + if terr != nil { + return nil, terr } accessToken = token.Sha1 } - client = NewGogsClient(g.URL, accessToken, g.SkipVerify) - userInfo, err := client.GetUserInfo(username) - if err != nil { - return nil, false, err - } - - user := model.User{} - user.Token = accessToken - user.Login = userInfo.UserName - user.Email = userInfo.Email - user.Avatar = expandAvatar(g.URL, userInfo.AvatarUrl) - return &user, false, nil -} - -// Auth authenticates the session and returns the remote user -// login for the given token and secret -func (g *Remote) Auth(token, secret string) (string, error) { - return "", fmt.Errorf("Method not supported") -} - -// Repo fetches the named repository from the remote system. -func (g *Remote) Repo(u *model.User, owner, name string) (*model.Repo, error) { - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) - repos_, err := client.ListMyRepos() + client = c.newClientToken(accessToken) + account, err := client.GetUserInfo(username) if err != nil { return nil, err } - fullName := owner + "/" + name - for _, repo := range repos_ { - if repo.FullName == fullName { - return toRepo(repo), nil - } - } - - return nil, fmt.Errorf("Not Found") + return &model.User{ + Token: accessToken, + Login: account.UserName, + Email: account.Email, + Avatar: expandAvatar(c.URL, account.AvatarUrl), + }, nil } -// Repos fetches a list of repos from the remote system. -func (g *Remote) Repos(u *model.User) ([]*model.RepoLite, error) { +// Auth is not supported by the Gogs driver. +func (c *client) Auth(token, secret string) (string, error) { + return "", fmt.Errorf("Not Implemented") +} + +// Teams is not supported by the Gogs driver. +func (c *client) Teams(u *model.User) ([]*model.Team, error) { + var empty []*model.Team + return empty, nil +} + +// Repo returns the named Gogs repository. +func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) { + client := c.newClientToken(u.Token) + repo, err := client.GetRepo(owner, name) + if err != nil { + return nil, err + } + return toRepo(repo), nil +} + +// Repos returns a list of all repositories for the Gogs account, including +// organization repositories. +func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) { repos := []*model.RepoLite{} - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) - repos_, err := client.ListMyRepos() + client := c.newClientToken(u.Token) + all, err := client.ListMyRepos() if err != nil { return repos, err } - for _, repo := range repos_ { + for _, repo := range all { repos = append(repos, toRepoLite(repo)) } - return repos, err } -// Perm fetches the named repository permissions from -// the remote system for the specified user. -func (g *Remote) Perm(u *model.User, owner, name string) (*model.Perm, error) { - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) - repos_, err := client.ListMyRepos() +// Perm returns the user permissions for the named Gogs repository. +func (c *client) Perm(u *model.User, owner, name string) (*model.Perm, error) { + client := c.newClientToken(u.Token) + repo, err := client.GetRepo(owner, name) if err != nil { return nil, err } - - fullName := owner + "/" + name - for _, repo := range repos_ { - if repo.FullName == fullName { - return toPerm(repo.Permissions), nil - } - } - - return nil, fmt.Errorf("Not Found") - + return toPerm(repo.Permissions), nil } -// File fetches a file from the remote repository and returns in string format. -func (g *Remote) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) +// File fetches the file from the Gogs repository and returns its contents. +func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { + client := c.newClientToken(u.Token) cfg, err := client.GetFile(r.Owner, r.Name, b.Commit, f) return cfg, err } -// Status sends the commit status to the remote system. -// An example would be the GitHub pull request status. -func (g *Remote) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { - return fmt.Errorf("Not Implemented") +// Status is not supported by the Gogs driver. +func (c *client) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { + return nil } -// Netrc returns a .netrc file that can be used to clone -// private repositories from a remote system. -func (g *Remote) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - url_, err := url.Parse(g.URL) - if err != nil { - return nil, err - } - host, _, err := net.SplitHostPort(url_.Host) - if err == nil { - url_.Host = host +// Netrc returns a netrc file capable of authenticating Gogs requests and +// cloning Gogs repositories. The netrc will use the global machine account +// when configured. +func (c *client) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + if c.Password != "" { + return &model.Netrc{ + Login: c.Username, + Password: c.Password, + Machine: c.Machine, + }, nil } return &model.Netrc{ Login: u.Token, Password: "x-oauth-basic", - Machine: url_.Host, + Machine: c.Machine, }, nil } -// Activate activates a repository by creating the post-commit hook and -// adding the SSH deploy key, if applicable. -func (g *Remote) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { +// Activate activates the repository by registering post-commit hooks with +// the Gogs repository. +func (c *client) Activate(u *model.User, r *model.Repo, link string) error { config := map[string]string{ "url": link, "secret": r.Hash, @@ -189,20 +198,19 @@ func (g *Remote) Activate(u *model.User, r *model.Repo, k *model.Key, link strin Active: true, } - client := NewGogsClient(g.URL, u.Token, g.SkipVerify) + client := c.newClientToken(u.Token) _, err := client.CreateRepoHook(r.Owner, r.Name, hook) return err } -// Deactivate removes a repository by removing all the post-commit hooks -// which are equal to link and removing the SSH deploy key. -func (g *Remote) Deactivate(u *model.User, r *model.Repo, link string) error { - return fmt.Errorf("Not Implemented") +// Deactivate is not supported by the Gogs driver. +func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error { + return nil } -// Hook parses the post-commit hook from the Request body -// and returns the required data in a standard format. -func (g *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { +// Hook parses the incoming Gogs hook and returns the Repository and Build +// details. If the hook is unsupported nil values are returned. +func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { var ( err error repo *model.Repo @@ -211,7 +219,7 @@ func (g *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { switch r.Header.Get("X-Gogs-Event") { case "push": - var push *PushHook + var push *pushHook push, err = parsePush(r.Body) if err == nil { repo = repoFromPush(push) @@ -221,20 +229,20 @@ func (g *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { return repo, build, err } -// NewClient initializes and returns a API client. -func NewGogsClient(url, token string, skipVerify bool) *gogs.Client { - sslClient := &http.Client{} - c := gogs.NewClient(url, token) +// helper function to return the Gogs client +func (c *client) newClient() *gogs.Client { + return c.newClientToken("") +} - if skipVerify { - sslClient.Transport = &http.Transport{ +// helper function to return the Gogs client +func (c *client) newClientToken(token string) *gogs.Client { + client := gogs.NewClient(c.URL, token) + if c.SkipVerify { + httpClient := &http.Client{} + httpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - c.SetHTTPClient(sslClient) + client.SetHTTPClient(httpClient) } - return c -} - -func (g *Remote) String() string { - return "gogs" + return client } diff --git a/remote/gogs/gogs_test.go b/remote/gogs/gogs_test.go new file mode 100644 index 000000000..792935956 --- /dev/null +++ b/remote/gogs/gogs_test.go @@ -0,0 +1,183 @@ +package gogs + +import ( + "net/http/httptest" + "testing" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote/gogs/fixtures" + + "github.com/franela/goblin" + "github.com/gin-gonic/gin" +) + +func Test_gogs(t *testing.T) { + gin.SetMode(gin.TestMode) + + s := httptest.NewServer(fixtures.Handler()) + c, _ := New(Opts{ + URL: s.URL, + SkipVerify: true, + }) + + g := goblin.Goblin(t) + g.Describe("Gogs", func() { + + g.After(func() { + s.Close() + }) + + g.Describe("Creating a remote", func() { + g.It("Should return client with specified options", func() { + remote, _ := New(Opts{ + URL: "http://localhost:8080", + Username: "someuser", + Password: "password", + SkipVerify: true, + PrivateMode: true, + }) + g.Assert(remote.(*client).URL).Equal("http://localhost:8080") + g.Assert(remote.(*client).Machine).Equal("localhost") + g.Assert(remote.(*client).Username).Equal("someuser") + g.Assert(remote.(*client).Password).Equal("password") + g.Assert(remote.(*client).SkipVerify).Equal(true) + g.Assert(remote.(*client).PrivateMode).Equal(true) + }) + g.It("Should handle malformed url", func() { + _, err := New(Opts{URL: "%gh&%ij"}) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Generating a netrc file", func() { + g.It("Should return a netrc with the user token", func() { + remote, _ := New(Opts{ + URL: "http://gogs.com", + }) + netrc, _ := remote.Netrc(fakeUser, nil) + g.Assert(netrc.Machine).Equal("gogs.com") + g.Assert(netrc.Login).Equal(fakeUser.Token) + g.Assert(netrc.Password).Equal("x-oauth-basic") + }) + g.It("Should return a netrc with the machine account", func() { + remote, _ := New(Opts{ + URL: "http://gogs.com", + Username: "someuser", + Password: "password", + }) + netrc, _ := remote.Netrc(nil, nil) + g.Assert(netrc.Machine).Equal("gogs.com") + g.Assert(netrc.Login).Equal("someuser") + g.Assert(netrc.Password).Equal("password") + }) + }) + + g.Describe("Requesting a repository", func() { + g.It("Should return the repository details", func() { + repo, err := c.Repo(fakeUser, fakeRepo.Owner, fakeRepo.Name) + g.Assert(err == nil).IsTrue() + g.Assert(repo.Owner).Equal(fakeRepo.Owner) + g.Assert(repo.Name).Equal(fakeRepo.Name) + g.Assert(repo.FullName).Equal(fakeRepo.Owner + "/" + fakeRepo.Name) + g.Assert(repo.IsPrivate).IsTrue() + g.Assert(repo.Clone).Equal("http://localhost/test_name/repo_name.git") + g.Assert(repo.Link).Equal("http://localhost/test_name/repo_name") + }) + g.It("Should handle a not found error", func() { + _, err := c.Repo(fakeUser, fakeRepoNotFound.Owner, fakeRepoNotFound.Name) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Requesting repository permissions", func() { + g.It("Should return the permission details", func() { + perm, err := c.Perm(fakeUser, fakeRepo.Owner, fakeRepo.Name) + g.Assert(err == nil).IsTrue() + g.Assert(perm.Admin).IsTrue() + g.Assert(perm.Push).IsTrue() + g.Assert(perm.Pull).IsTrue() + }) + g.It("Should handle a not found error", func() { + _, err := c.Perm(fakeUser, fakeRepoNotFound.Owner, fakeRepoNotFound.Name) + g.Assert(err != nil).IsTrue() + }) + }) + + g.Describe("Requesting a repository list", func() { + g.It("Should return the repository list", func() { + repos, err := c.Repos(fakeUser) + g.Assert(err == nil).IsTrue() + g.Assert(repos[0].Owner).Equal(fakeRepo.Owner) + g.Assert(repos[0].Name).Equal(fakeRepo.Name) + g.Assert(repos[0].FullName).Equal(fakeRepo.Owner + "/" + fakeRepo.Name) + }) + g.It("Should handle a not found error", func() { + _, err := c.Repos(fakeUserNoRepos) + g.Assert(err != nil).IsTrue() + }) + }) + + g.It("Should register repositroy hooks", func() { + err := c.Activate(fakeUser, fakeRepo, "http://localhost") + g.Assert(err == nil).IsTrue() + }) + + g.It("Should return a repository file", func() { + raw, err := c.File(fakeUser, fakeRepo, fakeBuild, ".drone.yml") + g.Assert(err == nil).IsTrue() + g.Assert(string(raw)).Equal("{ platform: linux/amd64 }") + }) + + g.Describe("Given an authentication request", func() { + g.It("Should redirect to login form") + g.It("Should create an access token") + g.It("Should handle an access token error") + g.It("Should return the authenticated user") + }) + + g.Describe("Given a repository hook", func() { + g.It("Should skip non-push events") + g.It("Should return push details") + g.It("Should handle a parsing error") + }) + + g.It("Should return no-op for usupporeted features", func() { + _, err1 := c.Auth("octocat", "4vyW6b49Z") + _, err2 := c.Teams(nil) + err3 := c.Status(nil, nil, nil, "") + err4 := c.Deactivate(nil, nil, "") + g.Assert(err1 != nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(err4 == nil).IsTrue() + }) + }) +} + +var ( + fakeUser = &model.User{ + Login: "someuser", + Token: "cfcd2084", + } + + fakeUserNoRepos = &model.User{ + Login: "someuser", + Token: "repos_not_found", + } + + fakeRepo = &model.Repo{ + Owner: "test_name", + Name: "repo_name", + FullName: "test_name/repo_name", + } + + fakeRepoNotFound = &model.Repo{ + Owner: "test_name", + Name: "repo_not_found", + FullName: "test_name/repo_not_found", + } + + fakeBuild = &model.Build{ + Commit: "9ecad50", + } +) diff --git a/remote/gogs/helper.go b/remote/gogs/helper.go index bc698c04d..c6941faa3 100644 --- a/remote/gogs/helper.go +++ b/remote/gogs/helper.go @@ -12,8 +12,7 @@ import ( "github.com/gogits/go-gogs-client" ) -// helper function that converts a Gogs repository -// to a Drone repository. +// helper function that converts a Gogs repository to a Drone repository. func toRepoLite(from *gogs.Repository) *model.RepoLite { name := strings.Split(from.FullName, "/")[1] avatar := expandAvatar( @@ -28,8 +27,7 @@ func toRepoLite(from *gogs.Repository) *model.RepoLite { } } -// helper function that converts a Gogs repository -// to a Drone repository. +// helper function that converts a Gogs repository to a Drone repository. func toRepo(from *gogs.Repository) *model.Repo { name := strings.Split(from.FullName, "/")[1] avatar := expandAvatar( @@ -49,8 +47,7 @@ func toRepo(from *gogs.Repository) *model.Repo { } } -// helper function that converts a Gogs permission -// to a Drone permission. +// helper function that converts a Gogs permission to a Drone permission. func toPerm(from gogs.Permission) *model.Perm { return &model.Perm{ Pull: from.Pull, @@ -59,11 +56,10 @@ func toPerm(from gogs.Permission) *model.Perm { } } -// helper function that extracts the Build data -// from a Gogs push hook -func buildFromPush(hook *PushHook) *model.Build { +// helper function that extracts the Build data from a Gogs push hook +func buildFromPush(hook *pushHook) *model.Build { avatar := expandAvatar( - hook.Repo.Url, + hook.Repo.URL, fixMalformedAvatar(hook.Sender.Avatar), ) return &model.Build{ @@ -79,9 +75,8 @@ func buildFromPush(hook *PushHook) *model.Build { } } -// helper function that extracts the Repository data -// from a Gogs push hook -func repoFromPush(hook *PushHook) *model.Repo { +// helper function that extracts the Repository data from a Gogs push hook +func repoFromPush(hook *pushHook) *model.Repo { fullName := fmt.Sprintf( "%s/%s", hook.Repo.Owner.Username, @@ -91,20 +86,19 @@ func repoFromPush(hook *PushHook) *model.Repo { Name: hook.Repo.Name, Owner: hook.Repo.Owner.Username, FullName: fullName, - Link: hook.Repo.Url, + Link: hook.Repo.URL, } } -// helper function that parses a push hook from -// a read closer. -func parsePush(r io.Reader) (*PushHook, error) { - push := new(PushHook) +// helper function that parses a push hook from a read closer. +func parsePush(r io.Reader) (*pushHook, error) { + push := new(pushHook) err := json.NewDecoder(r).Decode(push) return push, err } -// fixMalformedAvatar is a helper function that fixes -// an avatar url if malformed (known bug with gogs) +// fixMalformedAvatar is a helper function that fixes an avatar url if malformed +// (currently a known bug with gogs) func fixMalformedAvatar(url string) string { index := strings.Index(url, "///") if index != -1 { @@ -117,16 +111,16 @@ func fixMalformedAvatar(url string) string { return url } -// expandAvatar is a helper function that converts -// a relative avatar URL to the abosolute url. +// expandAvatar is a helper function that converts a relative avatar URL to the +// abosolute url. func expandAvatar(repo, rawurl string) string { if !strings.HasPrefix(rawurl, "/avatars/") { return rawurl } - url_, err := url.Parse(repo) + url, err := url.Parse(repo) if err != nil { return rawurl } - url_.Path = rawurl - return url_.String() + url.Path = rawurl + return url.String() } diff --git a/remote/gogs/helper_test.go b/remote/gogs/helper_test.go index 444596e54..9ec3d635b 100644 --- a/remote/gogs/helper_test.go +++ b/remote/gogs/helper_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/drone/drone/model" - "github.com/drone/drone/remote/gogs/testdata" + "github.com/drone/drone/remote/gogs/fixtures" "github.com/franela/goblin" "github.com/gogits/go-gogs-client" @@ -17,7 +17,7 @@ func Test_parse(t *testing.T) { g.Describe("Gogs", func() { g.It("Should parse push hook payload", func() { - buf := bytes.NewBufferString(testdata.PushHook) + buf := bytes.NewBufferString(fixtures.HookPush) hook, err := parsePush(buf) g.Assert(err == nil).IsTrue() g.Assert(hook.Ref).Equal("refs/heads/master") @@ -25,7 +25,7 @@ func Test_parse(t *testing.T) { g.Assert(hook.Before).Equal("4b2626259b5a97b6b4eab5e6cca66adb986b672b") g.Assert(hook.Compare).Equal("http://gogs.golang.org/gordon/hello-world/compare/4b2626259b5a97b6b4eab5e6cca66adb986b672b...ef98532add3b2feb7a137426bba1248724367df5") g.Assert(hook.Repo.Name).Equal("hello-world") - g.Assert(hook.Repo.Url).Equal("http://gogs.golang.org/gordon/hello-world") + g.Assert(hook.Repo.URL).Equal("http://gogs.golang.org/gordon/hello-world") g.Assert(hook.Repo.Owner.Name).Equal("gordon") g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org") g.Assert(hook.Repo.Owner.Username).Equal("gordon") @@ -38,7 +38,7 @@ func Test_parse(t *testing.T) { }) g.It("Should return a Build struct from a push hook", func() { - buf := bytes.NewBufferString(testdata.PushHook) + buf := bytes.NewBufferString(fixtures.HookPush) hook, _ := parsePush(buf) build := buildFromPush(hook) g.Assert(build.Event).Equal(model.EventPush) @@ -53,13 +53,13 @@ func Test_parse(t *testing.T) { }) g.It("Should return a Repo struct from a push hook", func() { - buf := bytes.NewBufferString(testdata.PushHook) + buf := bytes.NewBufferString(fixtures.HookPush) hook, _ := parsePush(buf) repo := repoFromPush(hook) g.Assert(repo.Name).Equal(hook.Repo.Name) g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username) g.Assert(repo.FullName).Equal("gordon/hello-world") - g.Assert(repo.Link).Equal(hook.Repo.Url) + g.Assert(repo.Link).Equal(hook.Repo.URL) }) g.It("Should return a Perm struct from a Gogs Perm", func() { diff --git a/remote/gogs/types.go b/remote/gogs/types.go index ac1947357..2b9f38ed1 100644 --- a/remote/gogs/types.go +++ b/remote/gogs/types.go @@ -1,6 +1,6 @@ package gogs -type PushHook struct { +type pushHook struct { Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` @@ -15,7 +15,7 @@ type PushHook struct { Repo struct { ID int64 `json:"id"` Name string `json:"name"` - Url string `json:"url"` + URL string `json:"url"` Private bool `json:"private"` Owner struct { Name string `json:"name"` @@ -27,7 +27,7 @@ type PushHook struct { Commits []struct { ID string `json:"id"` Message string `json:"message"` - Url string `json:"url"` + URL string `json:"url"` } `json:"commits"` Sender struct { diff --git a/server/server.go b/server/server.go index 477722adb..557b4b756 100644 --- a/server/server.go +++ b/server/server.go @@ -134,8 +134,6 @@ func (s *Server) Handler() http.Handler { repo.Use(session.MustPull) repo.GET("", api.GetRepo) - repo.GET("/key", api.GetRepoKey) - repo.POST("/key", api.PostRepoKey) repo.GET("/builds", api.GetBuilds) repo.GET("/builds/:number", api.GetBuild) repo.GET("/logs/:number/:job", api.GetBuildLogs) diff --git a/store/datastore/keys.go b/store/datastore/keys.go deleted file mode 100644 index ca68b728a..000000000 --- a/store/datastore/keys.go +++ /dev/null @@ -1,31 +0,0 @@ -package datastore - -import ( - "github.com/drone/drone/model" - "github.com/russross/meddler" -) - -func (db *datastore) GetKey(repo *model.Repo) (*model.Key, error) { - var key = new(model.Key) - var err = meddler.QueryRow(db, key, rebind(keyQuery), repo.ID) - return key, err -} - -func (db *datastore) CreateKey(key *model.Key) error { - return meddler.Save(db, keyTable, key) -} - -func (db *datastore) UpdateKey(key *model.Key) error { - return meddler.Save(db, keyTable, key) -} - -func (db *datastore) DeleteKey(key *model.Key) error { - var _, err = db.Exec(rebind(keyDeleteStmt), key.ID) - return err -} - -const keyTable = "keys" - -const keyQuery = "SELECT * FROM `keys` WHERE key_repo_id=? LIMIT 1" - -const keyDeleteStmt = "DELETE FROM `keys` WHERE key_id=?" diff --git a/store/datastore/keys_test.go b/store/datastore/keys_test.go deleted file mode 100644 index 2883ee7f7..000000000 --- a/store/datastore/keys_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package datastore - -import ( - "testing" - - "github.com/drone/drone/model" - "github.com/franela/goblin" -) - -func TestKeys(t *testing.T) { - db := openTest() - defer db.Close() - - s := From(db) - g := goblin.Goblin(t) - g.Describe("Keys", func() { - - // before each test be sure to purge the package - // table data from the database. - g.BeforeEach(func() { - db.Exec(rebind("DELETE FROM `keys`")) - }) - - g.It("Should create a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err := s.CreateKey(&key) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID != 0).IsTrue() - }) - - g.It("Should update a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err := s.CreateKey(&key) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID != 0).IsTrue() - - key.Private = "" - key.Public = "" - - err1 := s.UpdateKey(&key) - getkey, err2 := s.GetKey(&model.Repo{ID: 1}) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - g.Assert(key.ID).Equal(getkey.ID) - g.Assert(key.Public).Equal(getkey.Public) - g.Assert(key.Private).Equal(getkey.Private) - }) - - g.It("Should get a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err := s.CreateKey(&key) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID != 0).IsTrue() - - getkey, err := s.GetKey(&model.Repo{ID: 1}) - g.Assert(err == nil).IsTrue() - g.Assert(key.ID).Equal(getkey.ID) - g.Assert(key.Public).Equal(getkey.Public) - g.Assert(key.Private).Equal(getkey.Private) - }) - - g.It("Should delete a key", func() { - key := model.Key{ - RepoID: 1, - Public: fakePublicKey, - Private: fakePrivateKey, - } - err1 := s.CreateKey(&key) - err2 := s.DeleteKey(&key) - g.Assert(err1 == nil).IsTrue() - g.Assert(err2 == nil).IsTrue() - - _, err := s.GetKey(&model.Repo{ID: 1}) - g.Assert(err == nil).IsFalse() - }) - }) -} - -var fakePublicKey = ` ------BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0 -FPqri0cb2JZfXJ/DgYSF6vUpwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/ -3j+skZ6UtW+5u09lHNsj6tQ51s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQAB ------END PUBLIC KEY----- -` - -var fakePrivateKey = ` - ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp -wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 -1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh -3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 -pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX -GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il -AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF -L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k -X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl -U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ -37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= ------END RSA PRIVATE KEY----- -` diff --git a/store/store.go b/store/store.go index a359a3edd..2d20380aa 100644 --- a/store/store.go +++ b/store/store.go @@ -54,18 +54,6 @@ type Store interface { // DeleteRepo deletes a user repository. DeleteRepo(*model.Repo) error - // GetKey gets a key by unique repository ID. - GetKey(*model.Repo) (*model.Key, error) - - // CreateKey creates a new key. - CreateKey(*model.Key) error - - // UpdateKey updates a user key. - UpdateKey(*model.Key) error - - // DeleteKey deletes a user key. - DeleteKey(*model.Key) error - // GetSecretList gets a list of repository secrets GetSecretList(*model.Repo) ([]*model.Secret, error) @@ -192,22 +180,6 @@ func DeleteRepo(c context.Context, repo *model.Repo) error { return FromContext(c).DeleteRepo(repo) } -func GetKey(c context.Context, repo *model.Repo) (*model.Key, error) { - return FromContext(c).GetKey(repo) -} - -func CreateKey(c context.Context, key *model.Key) error { - return FromContext(c).CreateKey(key) -} - -func UpdateKey(c context.Context, key *model.Key) error { - return FromContext(c).UpdateKey(key) -} - -func DeleteKey(c context.Context, key *model.Key) error { - return FromContext(c).DeleteKey(key) -} - func GetSecretList(c context.Context, r *model.Repo) ([]*model.Secret, error) { return FromContext(c).GetSecretList(r) } diff --git a/template/amber/repo_config.amber b/template/amber/repo_config.amber index be0f3516e..f33c3201a 100644 --- a/template/amber/repo_config.amber +++ b/template/amber/repo_config.amber @@ -65,10 +65,6 @@ block content else input#trusted[type="checkbox"][hidden="hidden"] label.switch[for="trusted"] - div.row - div.col-md-3 Public Key - div.col-md-9 - pre #{Key.Public} #{Repo.Owner}-#{Repo.Name}@drone div.row div.col-md-12 div.alert.alert-danger @@ -78,4 +74,4 @@ block content block append scripts script - var view = new RepoConfigViewModel(#{Repo.FullName}); \ No newline at end of file + var view = new RepoConfigViewModel(#{Repo.FullName}); diff --git a/web/login.go b/web/login.go index dbeb54b5c..4bc03c130 100644 --- a/web/login.go +++ b/web/login.go @@ -23,7 +23,7 @@ func GetLogin(c *gin.Context) { // remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") - tmpuser, open, err := remote.Login(c.Writer, c.Request) + tmpuser, err := remote.Login(c.Writer, c.Request) if err != nil { log.Errorf("cannot authenticate user. %s", err) c.Redirect(303, "/login?error=oauth_error") @@ -35,20 +35,16 @@ func GetLogin(c *gin.Context) { return } + var open = false // TODO get this from context + // get the user from the database u, err := store.GetUserLogin(c, tmpuser.Login) if err != nil { - count, err := store.GetUserCount(c) - if err != nil { - log.Errorf("cannot register %s. %s", tmpuser.Login, err) - c.Redirect(303, "/login?error=internal_error") - return - } // if self-registration is disabled we should // return a notAuthorized error. the only exception // is if no users exist yet in the system we'll proceed. - if !open && count != 0 { + if !open { log.Errorf("cannot register %s. registration closed", tmpuser.Login) c.Redirect(303, "/login?error=access_denied") return @@ -69,12 +65,6 @@ func GetLogin(c *gin.Context) { c.Redirect(303, "/login?error=internal_error") return } - - // if this is the first user, they - // should be an admin. - if count == 0 { - u.Admin = true - } } // update the user meta data and authorization diff --git a/web/pages.go b/web/pages.go index 8480c1e85..7163063ff 100644 --- a/web/pages.go +++ b/web/pages.go @@ -116,7 +116,6 @@ func ShowRepoConf(c *gin.Context) { user := session.User(c) repo := session.Repo(c) - key, _ := store.GetKey(c, repo) token, _ := token.New( token.CsrfToken, @@ -126,7 +125,6 @@ func ShowRepoConf(c *gin.Context) { c.HTML(200, "repo_config.html", gin.H{ "User": user, "Repo": repo, - "Key": key, "Csrf": token, "Link": httputil.GetURL(c.Request), }) From 53eac09f34209d895dcc4c16e03d4e242b9a9143 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Sun, 1 May 2016 17:33:22 -0700 Subject: [PATCH 06/10] added org and open registration --- drone/daemon.go | 28 +++-- server/handler.go | 6 + server/login.go | 161 +++++++++++++++++++++++++ server/server.go | 18 +-- web/login.go | 293 +++++++++++++++++++++++----------------------- 5 files changed, 341 insertions(+), 165 deletions(-) create mode 100644 server/login.go diff --git a/drone/daemon.go b/drone/daemon.go index f2cdf3dd3..bb8e0d898 100644 --- a/drone/daemon.go +++ b/drone/daemon.go @@ -334,16 +334,6 @@ func start(c *cli.Context) error { ) } -func setupConfig(c *cli.Context) *server.Config { - return &server.Config{ - Open: c.Bool("open"), - Yaml: c.String("yaml"), - Secret: c.String("agent-secret"), - Admins: c.StringSlice("admin"), - Orgs: c.StringSlice("orgs"), - } -} - func setupCache(c *cli.Context) cache.Cache { return cache.NewTTL( c.Duration("cache-ttl"), @@ -444,6 +434,24 @@ func setupGithub(c *cli.Context) (remote.Remote, error) { ) } +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 == "" { diff --git a/server/handler.go b/server/handler.go index 25bb1fd5d..fa7b14499 100644 --- a/server/handler.go +++ b/server/handler.go @@ -77,3 +77,9 @@ func HandlerAgent(v string) gin.HandlerFunc { c.Set(k, v) } } + +// ToConfig returns the config from the Context +func ToConfig(c *gin.Context) *Config { + v := c.MustGet("config") + return v.(*Config) +} diff --git a/server/login.go b/server/login.go new file mode 100644 index 000000000..bfd1d2d67 --- /dev/null +++ b/server/login.go @@ -0,0 +1,161 @@ +package server + +import ( + "net/http" + "time" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/drone/drone/shared/crypto" + "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/token" + "github.com/drone/drone/store" + + "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" +) + +func GetLogin(c *gin.Context) { + + // when dealing with redirects we may need to adjust the content type. I + // cannot, however, remember why, so need to revisit this line. + c.Writer.Header().Del("Content-Type") + + tmpuser, err := remote.Login(c, c.Writer, c.Request) + if err != nil { + logrus.Errorf("cannot authenticate user. %s", err) + c.Redirect(303, "/login?error=oauth_error") + return + } + // this will happen when the user is redirected by the remote provider as + // part of the authorization workflow. + if tmpuser == nil { + return + } + config := ToConfig(c) + + // get the user from the database + u, err := store.GetUserLogin(c, tmpuser.Login) + if err != nil { + + // if self-registration is disabled we should return a not authorized error + if !config.Open { + logrus.Errorf("cannot register %s. registration closed", tmpuser.Login) + c.Redirect(303, "/login?error=access_denied") + return + } + + // create the user account + u = &model.User{ + Login: tmpuser.Login, + Token: tmpuser.Token, + Secret: tmpuser.Secret, + Email: tmpuser.Email, + Avatar: tmpuser.Avatar, + Hash: crypto.Rand(), + } + + // insert the user into the database + if err := store.CreateUser(c, u); err != nil { + logrus.Errorf("cannot insert %s. %s", u.Login, err) + c.Redirect(303, "/login?error=internal_error") + return + } + } + + // update the user meta data and authorization data. + u.Token = tmpuser.Token + u.Secret = tmpuser.Secret + u.Email = tmpuser.Email + u.Avatar = tmpuser.Avatar + + if err := store.UpdateUser(c, u); err != nil { + logrus.Errorf("cannot update %s. %s", u.Login, err) + c.Redirect(303, "/login?error=internal_error") + return + } + + if len(config.Orgs) != 0 { + teams, terr := remote.Teams(c, u) + if terr != nil { + logrus.Errorf("cannot verify team membership for %s. %s.", tmpuser.Login, terr) + c.Redirect(303, "/login?error=access_denied") + return + } + var member bool + for _, team := range teams { + if config.Orgs[team.Login] { + member = true + break + } + } + if !member { + logrus.Errorf("cannot verify team membership for %s. %s.", tmpuser.Login, terr) + c.Redirect(303, "/login?error=access_denied") + return + } + } + + exp := time.Now().Add(time.Hour * 72).Unix() + token := token.New(token.SessToken, u.Login) + tokenstr, err := token.SignExpires(u.Hash, exp) + if err != nil { + logrus.Errorf("cannot create token for %s. %s", u.Login, err) + c.Redirect(303, "/login?error=internal_error") + return + } + + httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr) + redirect := httputil.GetCookie(c.Request, "user_last") + if len(redirect) == 0 { + redirect = "/" + } + c.Redirect(303, redirect) + +} + +func GetLogout(c *gin.Context) { + httputil.DelCookie(c.Writer, c.Request, "user_sess") + httputil.DelCookie(c.Writer, c.Request, "user_last") + c.Redirect(303, "/login") +} + +func GetLoginToken(c *gin.Context) { + in := &tokenPayload{} + err := c.Bind(in) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + login, err := remote.Auth(c, in.Access, in.Refresh) + if err != nil { + c.AbortWithError(http.StatusUnauthorized, err) + return + } + + user, err := store.GetUserLogin(c, login) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + + exp := time.Now().Add(time.Hour * 72).Unix() + token := token.New(token.SessToken, user.Login) + tokenstr, err := token.SignExpires(user.Hash, exp) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.IndentedJSON(http.StatusOK, &tokenPayload{ + Access: tokenstr, + Expires: exp - time.Now().Unix(), + }) +} + +type tokenPayload struct { + Access string `json:"access_token,omitempty"` + Refresh string `json:"refresh_token,omitempty"` + Expires int64 `json:"expires_in,omitempty"` +} diff --git a/server/server.go b/server/server.go index 557b4b756..0a3109793 100644 --- a/server/server.go +++ b/server/server.go @@ -27,11 +27,11 @@ import ( // Config defines system configuration parameters. type Config struct { - Open bool // Enables open registration - Yaml string // Customize the Yaml configuration file name - Secret string // Secret token used to authenticate agents - Admins []string // Administrative users - Orgs []string // Organization whitelist + Open bool // Enables open registration + Yaml string // Customize the Yaml configuration file name + Secret string // Secret token used to authenticate agents + Admins map[string]bool // Administrative users + Orgs map[string]bool // Organization whitelist } // Server defines the server configuration. @@ -75,7 +75,7 @@ func (s *Server) Handler() http.Handler { e.GET("/repos", web.ShowAllRepos) e.GET("/login", web.ShowLogin) e.GET("/login/form", web.ShowLoginForm) - e.GET("/logout", web.GetLogout) + e.GET("/logout", GetLogout) // TODO below will Go away with React UI settings := e.Group("/settings") @@ -172,9 +172,9 @@ func (s *Server) Handler() http.Handler { auth := e.Group("/authorize") { - auth.GET("", web.GetLogin) - auth.POST("", web.GetLogin) - auth.POST("/token", web.GetLoginToken) + auth.GET("", GetLogin) + auth.POST("", GetLogin) + auth.POST("/token", GetLoginToken) } queue := e.Group("/api/queue") diff --git a/web/login.go b/web/login.go index 4bc03c130..53e7ab8e1 100644 --- a/web/login.go +++ b/web/login.go @@ -1,148 +1,149 @@ package web -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" - - log "github.com/Sirupsen/logrus" - "github.com/drone/drone/model" - "github.com/drone/drone/remote" - "github.com/drone/drone/shared/crypto" - "github.com/drone/drone/shared/httputil" - "github.com/drone/drone/shared/token" - "github.com/drone/drone/store" -) - -func GetLogin(c *gin.Context) { - remote := remote.FromContext(c) - - // when dealing with redirects we may need - // to adjust the content type. I cannot, however, - // remember why, so need to revisit this line. - c.Writer.Header().Del("Content-Type") - - tmpuser, err := remote.Login(c.Writer, c.Request) - if err != nil { - log.Errorf("cannot authenticate user. %s", err) - c.Redirect(303, "/login?error=oauth_error") - return - } - // this will happen when the user is redirected by - // the remote provide as part of the oauth dance. - if tmpuser == nil { - return - } - - var open = false // TODO get this from context - - // get the user from the database - u, err := store.GetUserLogin(c, tmpuser.Login) - if err != nil { - - // if self-registration is disabled we should - // return a notAuthorized error. the only exception - // is if no users exist yet in the system we'll proceed. - if !open { - log.Errorf("cannot register %s. registration closed", tmpuser.Login) - c.Redirect(303, "/login?error=access_denied") - return - } - - // create the user account - u = &model.User{} - u.Login = tmpuser.Login - u.Token = tmpuser.Token - u.Secret = tmpuser.Secret - u.Email = tmpuser.Email - u.Avatar = tmpuser.Avatar - u.Hash = crypto.Rand() - - // insert the user into the database - if err := store.CreateUser(c, u); err != nil { - log.Errorf("cannot insert %s. %s", u.Login, err) - c.Redirect(303, "/login?error=internal_error") - return - } - } - - // update the user meta data and authorization - // data and cache in the datastore. - u.Token = tmpuser.Token - u.Secret = tmpuser.Secret - u.Email = tmpuser.Email - u.Avatar = tmpuser.Avatar - - if err := store.UpdateUser(c, u); err != nil { - log.Errorf("cannot update %s. %s", u.Login, err) - c.Redirect(303, "/login?error=internal_error") - return - } - - exp := time.Now().Add(time.Hour * 72).Unix() - token := token.New(token.SessToken, u.Login) - tokenstr, err := token.SignExpires(u.Hash, exp) - if err != nil { - log.Errorf("cannot create token for %s. %s", u.Login, err) - c.Redirect(303, "/login?error=internal_error") - return - } - - httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr) - redirect := httputil.GetCookie(c.Request, "user_last") - if len(redirect) == 0 { - redirect = "/" - } - c.Redirect(303, redirect) - -} - -func GetLogout(c *gin.Context) { - - httputil.DelCookie(c.Writer, c.Request, "user_sess") - httputil.DelCookie(c.Writer, c.Request, "user_last") - c.Redirect(303, "/login") -} - -func GetLoginToken(c *gin.Context) { - remote := remote.FromContext(c) - - in := &tokenPayload{} - err := c.Bind(in) - if err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - login, err := remote.Auth(in.Access, in.Refresh) - if err != nil { - c.AbortWithError(http.StatusUnauthorized, err) - return - } - - user, err := store.GetUserLogin(c, login) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - - exp := time.Now().Add(time.Hour * 72).Unix() - token := token.New(token.SessToken, user.Login) - tokenstr, err := token.SignExpires(user.Hash, exp) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - c.IndentedJSON(http.StatusOK, &tokenPayload{ - Access: tokenstr, - Expires: exp - time.Now().Unix(), - }) -} - -type tokenPayload struct { - Access string `json:"access_token,omitempty"` - Refresh string `json:"refresh_token,omitempty"` - Expires int64 `json:"expires_in,omitempty"` -} +// +// import ( +// "net/http" +// "time" +// +// "github.com/drone/drone/model" +// "github.com/drone/drone/remote" +// "github.com/drone/drone/shared/crypto" +// "github.com/drone/drone/shared/httputil" +// "github.com/drone/drone/shared/token" +// "github.com/drone/drone/store" +// +// "github.com/Sirupsen/logrus" +// "github.com/gin-gonic/gin" +// ) +// +// func GetLogin(c *gin.Context) { +// remote := remote.FromContext(c) +// +// // when dealing with redirects we may need +// // to adjust the content type. I cannot, however, +// // remember why, so need to revisit this line. +// c.Writer.Header().Del("Content-Type") +// +// tmpuser, err := remote.Login(c.Writer, c.Request) +// if err != nil { +// logrus.Errorf("cannot authenticate user. %s", err) +// c.Redirect(303, "/login?error=oauth_error") +// return +// } +// // this will happen when the user is redirected by +// // the remote provide as part of the oauth dance. +// if tmpuser == nil { +// return +// } +// +// var open = false // TODO get this from context +// +// // get the user from the database +// u, err := store.GetUserLogin(c, tmpuser.Login) +// if err != nil { +// +// // if self-registration is disabled we should +// // return a notAuthorized error. the only exception +// // is if no users exist yet in the system we'll proceed. +// if !open { +// logrus.Errorf("cannot register %s. registration closed", tmpuser.Login) +// c.Redirect(303, "/login?error=access_denied") +// return +// } +// +// // create the user account +// u = &model.User{} +// u.Login = tmpuser.Login +// u.Token = tmpuser.Token +// u.Secret = tmpuser.Secret +// u.Email = tmpuser.Email +// u.Avatar = tmpuser.Avatar +// u.Hash = crypto.Rand() +// +// // insert the user into the database +// if err := store.CreateUser(c, u); err != nil { +// logrus.Errorf("cannot insert %s. %s", u.Login, err) +// c.Redirect(303, "/login?error=internal_error") +// return +// } +// } +// +// // update the user meta data and authorization +// // data and cache in the datastore. +// u.Token = tmpuser.Token +// u.Secret = tmpuser.Secret +// u.Email = tmpuser.Email +// u.Avatar = tmpuser.Avatar +// +// if err := store.UpdateUser(c, u); err != nil { +// logrus.Errorf("cannot update %s. %s", u.Login, err) +// c.Redirect(303, "/login?error=internal_error") +// return +// } +// +// exp := time.Now().Add(time.Hour * 72).Unix() +// token := token.New(token.SessToken, u.Login) +// tokenstr, err := token.SignExpires(u.Hash, exp) +// if err != nil { +// logrus.Errorf("cannot create token for %s. %s", u.Login, err) +// c.Redirect(303, "/login?error=internal_error") +// return +// } +// +// httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr) +// redirect := httputil.GetCookie(c.Request, "user_last") +// if len(redirect) == 0 { +// redirect = "/" +// } +// c.Redirect(303, redirect) +// +// } +// +// func GetLogout(c *gin.Context) { +// +// httputil.DelCookie(c.Writer, c.Request, "user_sess") +// httputil.DelCookie(c.Writer, c.Request, "user_last") +// c.Redirect(303, "/login") +// } +// +// func GetLoginToken(c *gin.Context) { +// remote := remote.FromContext(c) +// +// in := &tokenPayload{} +// err := c.Bind(in) +// if err != nil { +// c.AbortWithError(http.StatusBadRequest, err) +// return +// } +// +// login, err := remote.Auth(in.Access, in.Refresh) +// if err != nil { +// c.AbortWithError(http.StatusUnauthorized, err) +// return +// } +// +// user, err := store.GetUserLogin(c, login) +// if err != nil { +// c.AbortWithError(http.StatusNotFound, err) +// return +// } +// +// exp := time.Now().Add(time.Hour * 72).Unix() +// token := token.New(token.SessToken, user.Login) +// tokenstr, err := token.SignExpires(user.Hash, exp) +// if err != nil { +// c.AbortWithError(http.StatusInternalServerError, err) +// return +// } +// +// c.IndentedJSON(http.StatusOK, &tokenPayload{ +// Access: tokenstr, +// Expires: exp - time.Now().Unix(), +// }) +// } +// +// type tokenPayload struct { +// Access string `json:"access_token,omitempty"` +// Refresh string `json:"refresh_token,omitempty"` +// Expires int64 `json:"expires_in,omitempty"` +// } From 0fb4aeda3f51addd3989dac3e02cfe193a751162 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 2 May 2016 12:21:25 -0700 Subject: [PATCH 07/10] bump to 0.5 in master --- .gitignore | 2 +- Dockerfile | 2 +- api/swagger/swagger.go | 3 - drone/daemon.go | 315 +++++++++-------- model/config.go | 26 ++ model/user.go | 10 +- remote/cache.go | 85 ----- router/middleware/agent.go | 36 +- router/middleware/bus.go | 17 + router/middleware/cache.go | 24 ++ router/middleware/config.go | 40 +++ router/middleware/queue.go | 17 + router/middleware/remote.go | 102 ++++++ router/middleware/session/agent.go | 22 ++ router/middleware/session/user.go | 4 + router/middleware/store.go | 27 ++ router/middleware/stream.go | 17 + router/middleware/version.go | 12 + router/router.go | 394 +++++++++++----------- {web => server}/badge.go | 2 +- {api => server}/build.go | 21 +- {web => server}/gitlab.go | 2 +- server/handler.go | 85 ----- server/handler_test.go | 1 - {web => server}/hook.go | 20 +- server/login.go | 6 + {web => server}/pages.go | 2 +- {api => server}/queue.go | 2 +- {api => server}/repo.go | 2 +- {api => server}/secret.go | 2 +- server/server.go | 241 ------------- {api => server}/sign.go | 2 +- {web => server}/slack.go | 2 +- {web => server}/stream.go | 2 +- {api => server/swagger}/doc.go | 5 +- {api => server}/swagger/files/swagger.yml | 0 server/swagger/swagger.go | 85 +++++ {api => server}/user.go | 48 +-- {api => server}/users.go | 51 +-- shared/docker/docker.go | 97 ------ web/login.go | 149 -------- yaml/checksum/checksum.go | 70 ---- yaml/checksum/checksum_test.go | 97 ------ 43 files changed, 802 insertions(+), 1347 deletions(-) delete mode 100644 api/swagger/swagger.go create mode 100644 model/config.go delete mode 100644 remote/cache.go create mode 100644 router/middleware/bus.go create mode 100644 router/middleware/cache.go create mode 100644 router/middleware/config.go create mode 100644 router/middleware/queue.go create mode 100644 router/middleware/remote.go create mode 100644 router/middleware/session/agent.go create mode 100644 router/middleware/store.go create mode 100644 router/middleware/stream.go create mode 100644 router/middleware/version.go rename {web => server}/badge.go (99%) rename {api => server}/build.go (95%) rename {web => server}/gitlab.go (99%) delete mode 100644 server/handler.go delete mode 100644 server/handler_test.go rename {web => server}/hook.go (95%) rename {web => server}/pages.go (99%) rename {api => server}/queue.go (99%) rename {api => server}/repo.go (99%) rename {api => server}/secret.go (98%) delete mode 100644 server/server.go rename {api => server}/sign.go (98%) rename {web => server}/slack.go (99%) rename {web => server}/stream.go (99%) rename {api => server/swagger}/doc.go (62%) rename {api => server}/swagger/files/swagger.yml (100%) create mode 100644 server/swagger/swagger.go rename {api => server}/user.go (69%) rename {api => server}/users.go (67%) delete mode 100644 shared/docker/docker.go delete mode 100644 web/login.go delete mode 100644 yaml/checksum/checksum.go delete mode 100644 yaml/checksum/checksum_test.go diff --git a/.gitignore b/.gitignore index 01abb6461..3252b2c16 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ drone/drone .env temp/ -api/swagger/files/* +server/swagger/files/*.json # vendored repositories that we don't actually need # to vendor. so exclude them diff --git a/Dockerfile b/Dockerfile index 9a634564d..363afd3b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ ADD drone/drone /drone #RUN echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf ENTRYPOINT ["/drone"] -CMD ["serve"] +CMD ["daemon"] diff --git a/api/swagger/swagger.go b/api/swagger/swagger.go deleted file mode 100644 index 7921031e5..000000000 --- a/api/swagger/swagger.go +++ /dev/null @@ -1,3 +0,0 @@ -package swagger - -//go:generate go-bindata -pkg swagger -o swagger_gen.go files/ diff --git a/drone/daemon.go b/drone/daemon.go index bb8e0d898..91b271ccb 100644 --- a/drone/daemon.go +++ b/drone/daemon.go @@ -6,20 +6,9 @@ import ( "os" "time" - "github.com/drone/drone/bus" - "github.com/drone/drone/cache" - "github.com/drone/drone/queue" - "github.com/drone/drone/remote" - "github.com/drone/drone/remote/bitbucket" - "github.com/drone/drone/remote/bitbucketserver" - "github.com/drone/drone/remote/github" - "github.com/drone/drone/remote/gitlab" - "github.com/drone/drone/remote/gogs" - "github.com/drone/drone/server" - "github.com/drone/drone/shared/token" - "github.com/drone/drone/store" - "github.com/drone/drone/store/datastore" - "github.com/drone/drone/stream" + "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" @@ -300,22 +289,19 @@ func start(c *cli.Context) error { logrus.SetLevel(logrus.WarnLevel) } - // print the agent secret to the console - // TODO(bradrydzewski) this overall approach should be re-considered - if err := printSecret(c); err != nil { - return err - } - // setup the server and start the listener - server := server.Server{ - Bus: setupBus(c), - Cache: setupCache(c), - Config: setupConfig(c), - Queue: setupQueue(c), - Remote: setupRemote(c), - Stream: setupStream(c), - Store: setupStore(c), - } + 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") != "" { @@ -323,150 +309,151 @@ func start(c *cli.Context) error { c.String("server-addr"), c.String("server-cert"), c.String("server-key"), - server.Handler(), + handler, ) } // start the server without tls enabled return http.ListenAndServe( c.String("server-addr"), - server.Handler(), + 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 -} +// +// 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 = ` --- diff --git a/model/config.go b/model/config.go new file mode 100644 index 000000000..393d5e3ba --- /dev/null +++ b/model/config.go @@ -0,0 +1,26 @@ +package model + +// Config defines system configuration parameters. +type Config struct { + Open bool // Enables open registration + Yaml string // Customize the Yaml configuration file name + Shasum string // Customize the Yaml checksum file name + Secret string // Secret token used to authenticate agents + Admins map[string]bool // Administrative users + Orgs map[string]bool // Organization whitelist +} + +// IsAdmin returns true if the user is a member of the administrator list. +func (c *Config) IsAdmin(user *User) bool { + return c.Admins[user.Login] +} + +// IsMember returns true if the user is a member of the whitelisted teams. +func (c *Config) IsMember(teams []*Team) bool { + for _, team := range teams { + if c.Orgs[team.Login] { + return true + } + } + return false +} diff --git a/model/user.go b/model/user.go index e33d1fc3e..fdd46168a 100644 --- a/model/user.go +++ b/model/user.go @@ -32,11 +32,17 @@ type User struct { Avatar string `json:"avatar_url" meddler:"user_avatar"` // Activate indicates the user is active in the system. - Active bool `json:"active," meddler:"user_active"` + Active bool `json:"active" meddler:"user_active"` // Admin indicates the user is a system administrator. - Admin bool `json:"admin," meddler:"user_admin"` + // + // NOTE: This is sourced from the DRONE_ADMINS environment variable and is no + // longer persisted in the database. + Admin bool `json:"admin,omitempty" meddler:"-"` // Hash is a unique token used to sign tokens. Hash string `json:"-" meddler:"user_hash"` + + // DEPRECATED Admin indicates the user is a system administrator. + XAdmin bool `json:"-" meddler:"user_admin"` } diff --git a/remote/cache.go b/remote/cache.go deleted file mode 100644 index 032edad93..000000000 --- a/remote/cache.go +++ /dev/null @@ -1,85 +0,0 @@ -package remote - -import ( - "time" - - "github.com/drone/drone/model" -) - -// WithCache returns a the parent Remote with a front-end Cache. Remote items -// are cached for duration d. -func WithCache(r Remote, d time.Duration) Remote { - return r -} - -// Cacher implements purge functionality so that we can evict stale data and -// force a refresh. The indended use case is when the repository list is out -// of date and requires manual refresh. -type Cacher interface { - Purge(*model.User) -} - -// Because the cache is so closely tied to the remote we should just include -// them in the same package together. The below code are stubs for merging -// the Cache with the Remote package. - -type cache struct { - Remote -} - -func (c *cache) Repos(u *model.User) ([]*model.RepoLite, error) { - // key := fmt.Sprintf("repos:%s", - // user.Login, - // ) - // // if we fetch from the cache we can return immediately - // val, err := Get(c, key) - // if err == nil { - // return val.([]*model.RepoLite), nil - // } - // // else we try to grab from the remote system and - // // populate our cache. - // repos, err := remote.Repos(c, user) - // if err != nil { - // return nil, err - // } - // - // Set(c, key, repos) - // return repos, nil - return nil, nil -} - -func (c *cache) Perm(u *model.User, owner, repo string) (*model.Perm, error) { - // key := fmt.Sprintf("perms:%s:%s/%s", - // user.Login, - // owner, - // name, - // ) - // // if we fetch from the cache we can return immediately - // val, err := Get(c, key) - // if err == nil { - // return val.(*model.Perm), nil - // } - // // else we try to grab from the remote system and - // // populate our cache. - // perm, err := remote.Perm(c, user, owner, name) - // if err != nil { - // return nil, err - // } - // Set(c, key, perm) - // return perm, nil - return nil, nil -} - -func (c *cache) Purge(*model.User) { - return -} - -func (c *cache) Refresh(u *model.User) (bool, error) { - if r, ok := c.Remote.(Refresher); ok { - return r.Refresh(u) - } - return false, nil -} - -var _ Remote = &cache{} -var _ Refresher = &cache{} diff --git a/router/middleware/agent.go b/router/middleware/agent.go index 0a3265519..f3d884a72 100644 --- a/router/middleware/agent.go +++ b/router/middleware/agent.go @@ -1,45 +1,33 @@ package middleware import ( + "github.com/codegangsta/cli" "github.com/drone/drone/shared/token" "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" - "github.com/ianschenck/envflag" ) -var ( - secret = envflag.String("DRONE_AGENT_SECRET", "", "") - noauth = envflag.Bool("AGENT_NO_AUTH", false, "") -) +const agentKey = "agent" -// Agent is a middleware function that initializes the authorization middleware +// Agents is a middleware function that initializes the authorization middleware // for agents to connect to the queue. -func AgentMust() gin.HandlerFunc { - - if *secret == "" { - logrus.Fatalf("please provide the agent secret to authenticate agent requests") +func Agents(cli *cli.Context) gin.HandlerFunc { + secret := cli.String("agent-secret") + if secret == "" { + logrus.Fatalf("failed to generate token from DRONE_AGENT_SECRET") } - t := token.New(token.AgentToken, "") - s, err := t.Sign(*secret) + t := token.New(secret, "") + s, err := t.Sign(secret) if err != nil { - logrus.Fatalf("invalid agent secret. %s", err) + logrus.Fatalf("failed to generate token from DRONE_AGENT_SECRET. %s", err) } - logrus.Infof("using agent secret %s", *secret) + logrus.Infof("using agent secret %s", secret) logrus.Warnf("agents can connect with token %s", s) return func(c *gin.Context) { - parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { - return *secret, nil - }) - if err != nil { - c.AbortWithError(403, err) - } else if parsed.Kind != token.AgentToken { - c.AbortWithStatus(403) - } else { - c.Next() - } + c.Set(agentKey, secret) } } diff --git a/router/middleware/bus.go b/router/middleware/bus.go new file mode 100644 index 000000000..25665da1d --- /dev/null +++ b/router/middleware/bus.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "github.com/drone/drone/bus" + + "github.com/codegangsta/cli" + "github.com/gin-gonic/gin" +) + +// Bus is a middleware function that initializes the Event Bus and attaches to +// the context of every http.Request. +func Bus(cli *cli.Context) gin.HandlerFunc { + v := bus.New() + return func(c *gin.Context) { + bus.ToContext(c, v) + } +} diff --git a/router/middleware/cache.go b/router/middleware/cache.go new file mode 100644 index 000000000..6f6dec465 --- /dev/null +++ b/router/middleware/cache.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "github.com/drone/drone/cache" + + "github.com/codegangsta/cli" + "github.com/gin-gonic/gin" +) + +// Cache is a middleware function that initializes the Cache and attaches to +// the context of every http.Request. +func Cache(cli *cli.Context) gin.HandlerFunc { + v := setupCache(cli) + return func(c *gin.Context) { + cache.ToContext(c, v) + } +} + +// helper function to create the cache from the CLI context. +func setupCache(c *cli.Context) cache.Cache { + return cache.NewTTL( + c.Duration("cache-ttl"), + ) +} diff --git a/router/middleware/config.go b/router/middleware/config.go new file mode 100644 index 000000000..e2f65dd10 --- /dev/null +++ b/router/middleware/config.go @@ -0,0 +1,40 @@ +package middleware + +import ( + "github.com/drone/drone/model" + + "github.com/codegangsta/cli" + "github.com/gin-gonic/gin" +) + +const configKey = "config" + +// Config is a middleware function that initializes the Configuration and +// attaches to the context of every http.Request. +func Config(cli *cli.Context) gin.HandlerFunc { + v := setupConfig(cli) + return func(c *gin.Context) { + c.Set(configKey, v) + } +} + +// helper function to create the configuration from the CLI context. +func setupConfig(c *cli.Context) *model.Config { + return &model.Config{ + Open: c.Bool("open"), + Yaml: c.String("yaml"), + Shasum: c.String("yaml") + ".sig", + Secret: c.String("agent-secret"), + Admins: sliceToMap(c.StringSlice("admin")), + Orgs: sliceToMap(c.StringSlice("orgs")), + } +} + +// helper function to convert a string slice to a map. +func sliceToMap(s []string) map[string]bool { + v := map[string]bool{} + for _, ss := range s { + v[ss] = true + } + return v +} diff --git a/router/middleware/queue.go b/router/middleware/queue.go new file mode 100644 index 000000000..d2791033e --- /dev/null +++ b/router/middleware/queue.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "github.com/drone/drone/queue" + + "github.com/codegangsta/cli" + "github.com/gin-gonic/gin" +) + +// Queue is a middleware function that initializes the Queue and attaches to +// the context of every http.Request. +func Queue(cli *cli.Context) gin.HandlerFunc { + v := queue.New() + return func(c *gin.Context) { + queue.ToContext(c, v) + } +} diff --git a/router/middleware/remote.go b/router/middleware/remote.go new file mode 100644 index 000000000..58881078f --- /dev/null +++ b/router/middleware/remote.go @@ -0,0 +1,102 @@ +package middleware + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "github.com/drone/drone/remote" + "github.com/drone/drone/remote/bitbucket" + "github.com/drone/drone/remote/bitbucketserver" + "github.com/drone/drone/remote/github" + "github.com/drone/drone/remote/gitlab" + "github.com/drone/drone/remote/gogs" + "github.com/gin-gonic/gin" +) + +// Remote is a middleware function that initializes the Remote and attaches to +// the context of every http.Request. +func Remote(c *cli.Context) gin.HandlerFunc { + v, err := setupRemote(c) + if err != nil { + logrus.Fatalln(err) + } + return func(c *gin.Context) { + remote.ToContext(c, v) + } +} + +// helper function to setup the remote from the CLI arguments. +func setupRemote(c *cli.Context) (remote.Remote, error) { + switch { + case c.Bool("github"): + return setupGithub(c) + case c.Bool("gitlab"): + return setupGitlab(c) + case c.Bool("bitbucket"): + return setupBitbucket(c) + case c.Bool("stash"): + return setupStash(c) + case c.Bool("gogs"): + return setupGogs(c) + default: + return nil, fmt.Errorf("version control system not configured") + } +} + +// helper function to setup the Bitbucket remote from the CLI arguments. +func setupBitbucket(c *cli.Context) (remote.Remote, error) { + return bitbucket.New( + c.String("bitbucket-client"), + c.String("bitbucket-server"), + ), nil +} + +// helper function to setup the Gogs remote from the CLI arguments. +func setupGogs(c *cli.Context) (remote.Remote, error) { + return gogs.New(gogs.Opts{ + URL: c.String("gogs-server"), + Username: c.String("gogs-git-username"), + Password: c.String("gogs-git-password"), + PrivateMode: c.Bool("gogs-private-mode"), + SkipVerify: c.Bool("gogs-skip-verify"), + }) +} + +// helper function to setup the Stash remote from the CLI arguments. +func setupStash(c *cli.Context) (remote.Remote, error) { + return bitbucketserver.New(bitbucketserver.Opts{ + URL: c.String("stash-server"), + Username: c.String("stash-git-username"), + Password: c.String("stash-git-password"), + ConsumerKey: c.String("stash-consumer-key"), + ConsumerRSA: c.String("stash-consumer-rsa"), + SkipVerify: c.Bool("stash-skip-verify"), + }) +} + +// helper function to setup the Gitlab remote from the CLI arguments. +func setupGitlab(c *cli.Context) (remote.Remote, error) { + return gitlab.New(gitlab.Opts{ + URL: c.String("gitlab-server"), + Client: c.String("gitlab-client"), + Secret: c.String("gitlab-sercret"), + Username: c.String("gitlab-git-username"), + Password: c.String("gitlab-git-password"), + PrivateMode: c.Bool("gitlab-private-mode"), + SkipVerify: c.Bool("gitlab-skip-verify"), + }) +} + +// helper function to setup the GitHub remote from the CLI arguments. +func setupGithub(c *cli.Context) (remote.Remote, error) { + return github.New( + c.String("github-server"), + c.String("github-client"), + c.String("github-sercret"), + c.StringSlice("github-scope"), + c.Bool("github-private-mode"), + c.Bool("github-skip-verify"), + c.BoolT("github-merge-ref"), + ) +} diff --git a/router/middleware/session/agent.go b/router/middleware/session/agent.go new file mode 100644 index 000000000..8f8c722b8 --- /dev/null +++ b/router/middleware/session/agent.go @@ -0,0 +1,22 @@ +package session + +import ( + "github.com/drone/drone/shared/token" + "github.com/gin-gonic/gin" +) + +// AuthorizeAgent authorizes requsts from build agents to access the queue. +func AuthorizeAgent(c *gin.Context) { + secret := c.MustGet("agent").(string) + + parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { + return secret, nil + }) + if err != nil { + c.AbortWithError(403, err) + } else if parsed.Kind != token.AgentToken { + c.AbortWithStatus(403) + } else { + c.Next() + } +} diff --git a/router/middleware/session/user.go b/router/middleware/session/user.go index 8453d208b..78f0a16bf 100644 --- a/router/middleware/session/user.go +++ b/router/middleware/session/user.go @@ -44,6 +44,10 @@ func SetUser() gin.HandlerFunc { return user.Hash, err }) if err == nil { + confv := c.MustGet("config") + if conf, ok := confv.(*model.Config); ok { + user.Admin = conf.IsAdmin(user) + } c.Set("user", user) // if this is a session token (ie not the API token) diff --git a/router/middleware/store.go b/router/middleware/store.go new file mode 100644 index 000000000..33b9731bf --- /dev/null +++ b/router/middleware/store.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "github.com/codegangsta/cli" + "github.com/drone/drone/store" + "github.com/drone/drone/store/datastore" + + "github.com/gin-gonic/gin" +) + +// Store is a middleware function that initializes the Datastore and attaches to +// the context of every http.Request. +func Store(cli *cli.Context) gin.HandlerFunc { + v := setupStore(cli) + return func(c *gin.Context) { + store.ToContext(c, v) + c.Next() + } +} + +// helper function to create the datastore from the CLI context. +func setupStore(c *cli.Context) store.Store { + return datastore.New( + c.String("driver"), + c.String("datasource"), + ) +} diff --git a/router/middleware/stream.go b/router/middleware/stream.go new file mode 100644 index 000000000..d78a119c2 --- /dev/null +++ b/router/middleware/stream.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "github.com/drone/drone/stream" + + "github.com/codegangsta/cli" + "github.com/gin-gonic/gin" +) + +// Stream is a middleware function that initializes the Stream and attaches to +// the context of every http.Request. +func Stream(cli *cli.Context) gin.HandlerFunc { + v := stream.New() + return func(c *gin.Context) { + stream.ToContext(c, v) + } +} diff --git a/router/middleware/version.go b/router/middleware/version.go new file mode 100644 index 000000000..20466d8ec --- /dev/null +++ b/router/middleware/version.go @@ -0,0 +1,12 @@ +package middleware + +import ( + "github.com/drone/drone/version" + "github.com/gin-gonic/gin" +) + +// Version is a middleware function that appends the Drone version information +// to the HTTP response. This is intended for debugging and troubleshooting. +func Version(c *gin.Context) { + c.Header("X-DRONE-VERSION", version.Version) +} diff --git a/router/router.go b/router/router.go index 2d62d42b3..3c4a7a44d 100644 --- a/router/router.go +++ b/router/router.go @@ -1,201 +1,199 @@ package router +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "github.com/drone/drone/router/middleware/header" + "github.com/drone/drone/router/middleware/session" + "github.com/drone/drone/router/middleware/token" + "github.com/drone/drone/server" + "github.com/drone/drone/static" + "github.com/drone/drone/template" +) + +func Load(middleware ...gin.HandlerFunc) http.Handler { + + e := gin.New() + e.Use(gin.Recovery()) + + e.SetHTMLTemplate(template.Load()) + e.StaticFS("/static", static.FileSystem()) + + e.Use(header.NoCache) + e.Use(header.Options) + e.Use(header.Secure) + e.Use(middleware...) + e.Use(session.SetUser()) + e.Use(token.Refresh) + + e.GET("/", server.ShowIndex) + e.GET("/repos", server.ShowAllRepos) + e.GET("/login", server.ShowLogin) + e.GET("/login/form", server.ShowLoginForm) + e.GET("/logout", server.GetLogout) + + // TODO below will Go away with React UI + settings := e.Group("/settings") + { + settings.Use(session.MustUser()) + settings.GET("/profile", server.ShowUser) + } + repo := e.Group("/repos/:owner/:name") + { + repo.Use(session.SetRepo()) + repo.Use(session.SetPerm()) + repo.Use(session.MustPull) + + repo.GET("", server.ShowRepo) + repo.GET("/builds/:number", server.ShowBuild) + repo.GET("/builds/:number/:job", server.ShowBuild) + + repo_settings := repo.Group("/settings") + { + repo_settings.GET("", session.MustPush, server.ShowRepoConf) + repo_settings.GET("/encrypt", session.MustPush, server.ShowRepoEncrypt) + repo_settings.GET("/badges", server.ShowRepoBadges) + } + } + // TODO above will Go away with React UI + + user := e.Group("/api/user") + { + user.Use(session.MustUser()) + user.GET("", server.GetSelf) + user.GET("/feed", server.GetFeed) + user.GET("/repos", server.GetRepos) + user.GET("/repos/remote", server.GetRemoteRepos) + user.POST("/token", server.PostToken) + user.DELETE("/token", server.DeleteToken) + } + + users := e.Group("/api/users") + { + users.Use(session.MustAdmin()) + users.GET("", server.GetUsers) + users.POST("", server.PostUser) + users.GET("/:login", server.GetUser) + users.PATCH("/:login", server.PatchUser) + users.DELETE("/:login", server.DeleteUser) + } + + repos := e.Group("/api/repos/:owner/:name") + { + repos.POST("", server.PostRepo) + + repo := repos.Group("") + { + repo.Use(session.SetRepo()) + repo.Use(session.SetPerm()) + repo.Use(session.MustPull) + + repo.GET("", server.GetRepo) + repo.GET("/builds", server.GetBuilds) + repo.GET("/builds/:number", server.GetBuild) + repo.GET("/logs/:number/:job", server.GetBuildLogs) + repo.POST("/sign", session.MustPush, server.Sign) + + repo.POST("/secrets", session.MustPush, server.PostSecret) + repo.DELETE("/secrets/:secret", session.MustPush, server.DeleteSecret) + + // requires push permissions + repo.PATCH("", session.MustPush, server.PatchRepo) + repo.DELETE("", session.MustPush, server.DeleteRepo) + + repo.POST("/builds/:number", session.MustPush, server.PostBuild) + repo.DELETE("/builds/:number/:job", session.MustPush, server.DeleteBuild) + } + } + + badges := e.Group("/api/badges/:owner/:name") + { + badges.GET("/status.svg", server.GetBadge) + badges.GET("/cc.xml", server.GetCC) + } + + e.POST("/hook", server.PostHook) + e.POST("/api/hook", server.PostHook) + + stream := e.Group("/api/stream") + { + stream.Use(session.SetRepo()) + stream.Use(session.SetPerm()) + stream.Use(session.MustPull) + + stream.GET("/:owner/:name", server.GetRepoEvents) + stream.GET("/:owner/:name/:build/:number", server.GetStream) + } + + auth := e.Group("/authorize") + { + auth.GET("", server.GetLogin) + auth.POST("", server.GetLogin) + auth.POST("/token", server.GetLoginToken) + } + + queue := e.Group("/api/queue") + { + queue.Use(session.AuthorizeAgent) + queue.POST("/pull", server.Pull) + queue.POST("/pull/:os/:arch", server.Pull) + queue.POST("/wait/:id", server.Wait) + queue.POST("/stream/:id", server.Stream) + queue.POST("/status/:id", server.Update) + } + + // DELETE THESE + // gitlab := e.Group("/gitlab/:owner/:name") + // { + // gitlab.Use(session.SetRepo()) + // gitlab.GET("/commits/:sha", GetCommit) + // gitlab.GET("/pulls/:number", GetPullRequest) + // + // redirects := gitlab.Group("/redirect") + // { + // redirects.GET("/commits/:sha", RedirectSha) + // redirects.GET("/pulls/:number", RedirectPullRequest) + // } + // } + + // bots := e.Group("/bots") + // { + // bots.Use(session.MustUser()) + // bots.POST("/slack", Slack) + // bots.POST("/slack/:command", Slack) + // } + + return normalize(e) +} + +// THIS HACK JOB IS GOING AWAY SOON. // -// import ( -// "net/http" -// "strings" -// -// "github.com/gin-gonic/gin" -// -// "github.com/drone/drone/api" -// "github.com/drone/drone/router/middleware" -// "github.com/drone/drone/router/middleware/header" -// "github.com/drone/drone/router/middleware/session" -// "github.com/drone/drone/router/middleware/token" -// "github.com/drone/drone/static" -// "github.com/drone/drone/template" -// "github.com/drone/drone/web" -// ) -// -// func Load(middlewares ...gin.HandlerFunc) http.Handler { -// e := gin.New() -// e.Use(gin.Recovery()) -// -// e.SetHTMLTemplate(template.Load()) -// e.StaticFS("/static", static.FileSystem()) -// -// e.Use(header.NoCache) -// e.Use(header.Options) -// e.Use(header.Secure) -// e.Use(middlewares...) -// e.Use(session.SetUser()) -// e.Use(token.Refresh) -// -// e.GET("/", web.ShowIndex) -// e.GET("/repos", web.ShowAllRepos) -// e.GET("/login", web.ShowLogin) -// e.GET("/login/form", web.ShowLoginForm) -// e.GET("/logout", web.GetLogout) -// -// settings := e.Group("/settings") -// { -// settings.Use(session.MustUser()) -// settings.GET("/profile", web.ShowUser) -// } -// repo := e.Group("/repos/:owner/:name") -// { -// repo.Use(session.SetRepo()) -// repo.Use(session.SetPerm()) -// repo.Use(session.MustPull) -// -// repo.GET("", web.ShowRepo) -// repo.GET("/builds/:number", web.ShowBuild) -// repo.GET("/builds/:number/:job", web.ShowBuild) -// -// repo_settings := repo.Group("/settings") -// { -// repo_settings.GET("", session.MustPush, web.ShowRepoConf) -// repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) -// repo_settings.GET("/badges", web.ShowRepoBadges) -// } -// } -// -// user := e.Group("/api/user") -// { -// user.Use(session.MustUser()) -// user.GET("", api.GetSelf) -// user.GET("/feed", api.GetFeed) -// user.GET("/repos", api.GetRepos) -// user.GET("/repos/remote", api.GetRemoteRepos) -// user.POST("/token", api.PostToken) -// user.DELETE("/token", api.DeleteToken) -// } -// -// users := e.Group("/api/users") -// { -// users.Use(session.MustAdmin()) -// users.GET("", api.GetUsers) -// users.POST("", api.PostUser) -// users.GET("/:login", api.GetUser) -// users.PATCH("/:login", api.PatchUser) -// users.DELETE("/:login", api.DeleteUser) -// } -// -// repos := e.Group("/api/repos/:owner/:name") -// { -// repos.POST("", api.PostRepo) -// -// repo := repos.Group("") -// { -// repo.Use(session.SetRepo()) -// repo.Use(session.SetPerm()) -// repo.Use(session.MustPull) -// -// repo.GET("", api.GetRepo) -// repo.GET("/key", api.GetRepoKey) -// repo.POST("/key", api.PostRepoKey) -// repo.GET("/builds", api.GetBuilds) -// repo.GET("/builds/:number", api.GetBuild) -// repo.GET("/logs/:number/:job", api.GetBuildLogs) -// repo.POST("/sign", session.MustPush, api.Sign) -// -// repo.POST("/secrets", session.MustPush, api.PostSecret) -// repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) -// -// // requires authenticated user -// repo.POST("/encrypt", session.MustUser(), api.PostSecure) -// -// // requires push permissions -// repo.PATCH("", session.MustPush, api.PatchRepo) -// repo.DELETE("", session.MustPush, api.DeleteRepo) -// -// repo.POST("/builds/:number", session.MustPush, api.PostBuild) -// repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) -// } -// } -// -// badges := e.Group("/api/badges/:owner/:name") -// { -// badges.GET("/status.svg", web.GetBadge) -// badges.GET("/cc.xml", web.GetCC) -// } -// -// e.POST("/hook", web.PostHook) -// e.POST("/api/hook", web.PostHook) -// -// stream := e.Group("/api/stream") -// { -// stream.Use(session.SetRepo()) -// stream.Use(session.SetPerm()) -// stream.Use(session.MustPull) -// -// 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.GET("", web.GetLogin) -// auth.POST("", web.GetLogin) -// auth.POST("/token", web.GetLoginToken) -// } -// -// queue := e.Group("/api/queue") -// { -// queue.Use(middleware.AgentMust()) -// queue.POST("/pull", api.Pull) -// queue.POST("/pull/:os/:arch", api.Pull) -// queue.POST("/wait/:id", api.Wait) -// queue.POST("/stream/:id", api.Stream) -// queue.POST("/status/:id", api.Update) -// } -// -// gitlab := e.Group("/gitlab/:owner/:name") -// { -// gitlab.Use(session.SetRepo()) -// gitlab.GET("/commits/:sha", web.GetCommit) -// gitlab.GET("/pulls/:number", web.GetPullRequest) -// -// redirects := gitlab.Group("/redirect") -// { -// redirects.GET("/commits/:sha", web.RedirectSha) -// redirects.GET("/pulls/:number", web.RedirectPullRequest) -// } -// } -// -// return normalize(e) -// } -// -// // normalize is a helper function to work around the following -// // issue with gin. https://github.com/gin-gonic/gin/issues/388 -// func normalize(h http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// -// parts := strings.Split(r.URL.Path, "/")[1:] -// switch parts[0] { -// case "settings", "bots", "repos", "api", "login", "logout", "", "authorize", "hook", "static", "gitlab": -// // no-op -// default: -// -// if len(parts) > 2 && parts[2] != "settings" { -// parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) -// } -// -// // prefix the URL with /repo so that it -// // can be effectively routed. -// parts = append([]string{"", "repos"}, parts...) -// -// // reconstruct the path -// r.URL.Path = strings.Join(parts, "/") -// } -// -// h.ServeHTTP(w, r) -// }) -// } +// normalize is a helper function to work around the following +// issue with gin. https://github.com/gin-gonic/gin/issues/388 +func normalize(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + parts := strings.Split(r.URL.Path, "/")[1:] + switch parts[0] { + case "settings", "bots", "repos", "api", "login", "logout", "", "authorize", "hook", "static", "gitlab": + // no-op + default: + + if len(parts) > 2 && parts[2] != "settings" { + parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) + } + + // prefix the URL with /repo so that it + // can be effectively routed. + parts = append([]string{"", "repos"}, parts...) + + // reconstruct the path + r.URL.Path = strings.Join(parts, "/") + } + + h.ServeHTTP(w, r) + }) +} diff --git a/web/badge.go b/server/badge.go similarity index 99% rename from web/badge.go rename to server/badge.go index e987079c5..acece2356 100644 --- a/web/badge.go +++ b/server/badge.go @@ -1,4 +1,4 @@ -package web +package server import ( "fmt" diff --git a/api/build.go b/server/build.go similarity index 95% rename from api/build.go rename to server/build.go index 3ca688a44..1d75fd4df 100644 --- a/api/build.go +++ b/server/build.go @@ -1,10 +1,8 @@ -package api +package server import ( - "fmt" "io" "net/http" - "os" "strconv" "time" @@ -21,18 +19,6 @@ import ( "github.com/drone/drone/router/middleware/session" ) -var ( - droneYml = os.Getenv("BUILD_CONFIG_FILE") - droneSec string -) - -func init() { - if droneYml == "" { - droneYml = ".drone.yml" - } - droneSec = fmt.Sprintf("%s.sig", droneYml) -} - func GetBuilds(c *gin.Context) { repo := session.Repo(c) builds, err := store.GetBuildList(c, repo) @@ -189,7 +175,8 @@ func PostBuild(c *gin.Context) { } // fetch the .drone.yml file from the database - raw, err := remote_.File(user, repo, build, droneYml) + config := ToConfig(c) + raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) @@ -197,7 +184,7 @@ func PostBuild(c *gin.Context) { } // Fetch secrets file but don't exit on error as it's optional - sec, err := remote_.File(user, repo, build, droneSec) + sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } diff --git a/web/gitlab.go b/server/gitlab.go similarity index 99% rename from web/gitlab.go rename to server/gitlab.go index 36364e550..77c504c39 100644 --- a/web/gitlab.go +++ b/server/gitlab.go @@ -1,4 +1,4 @@ -package web +package server import ( "fmt" diff --git a/server/handler.go b/server/handler.go deleted file mode 100644 index fa7b14499..000000000 --- a/server/handler.go +++ /dev/null @@ -1,85 +0,0 @@ -package server - -import ( - "github.com/drone/drone/bus" - "github.com/drone/drone/cache" - "github.com/drone/drone/queue" - "github.com/drone/drone/remote" - "github.com/drone/drone/store" - "github.com/drone/drone/stream" - "github.com/drone/drone/version" - - "github.com/gin-gonic/gin" -) - -// HandlerCache returns a HandlerFunc that passes a Cache to the Context. -func HandlerCache(v cache.Cache) gin.HandlerFunc { - return func(c *gin.Context) { - cache.ToContext(c, v) - } -} - -// HandlerBus returns a HandlerFunc that passes a Bus to the Context. -func HandlerBus(v bus.Bus) gin.HandlerFunc { - return func(c *gin.Context) { - bus.ToContext(c, v) - } -} - -// HandlerStore returns a HandlerFunc that passes a Store to the Context. -func HandlerStore(v store.Store) gin.HandlerFunc { - return func(c *gin.Context) { - store.ToContext(c, v) - } -} - -// HandlerQueue returns a HandlerFunc that passes a Queue to the Context. -func HandlerQueue(v queue.Queue) gin.HandlerFunc { - return func(c *gin.Context) { - queue.ToContext(c, v) - } -} - -// HandlerStream returns a HandlerFunc that passes a Stream to the Context. -func HandlerStream(v stream.Stream) gin.HandlerFunc { - return func(c *gin.Context) { - stream.ToContext(c, v) - } -} - -// HandlerRemote returns a HandlerFunc that passes a Remote to the Context. -func HandlerRemote(v remote.Remote) gin.HandlerFunc { - return func(c *gin.Context) { - remote.ToContext(c, v) - } -} - -// HandlerConfig returns a HandlerFunc that passes server Config to the Context. -func HandlerConfig(v *Config) gin.HandlerFunc { - const k = "config" - return func(c *gin.Context) { - c.Set(k, v) - } -} - -// HandlerVersion returns a HandlerFunc that writes the Version information to -// the http.Response as a the X-Drone-Version header. -func HandlerVersion() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("X-Drone-Version", version.Version) - } -} - -// HandlerAgent returns a HandlerFunc that passes an Agent token to the Context. -func HandlerAgent(v string) gin.HandlerFunc { - const k = "agent" - return func(c *gin.Context) { - c.Set(k, v) - } -} - -// ToConfig returns the config from the Context -func ToConfig(c *gin.Context) *Config { - v := c.MustGet("config") - return v.(*Config) -} diff --git a/server/handler_test.go b/server/handler_test.go deleted file mode 100644 index abb4e431a..000000000 --- a/server/handler_test.go +++ /dev/null @@ -1 +0,0 @@ -package server diff --git a/web/hook.go b/server/hook.go similarity index 95% rename from web/hook.go rename to server/hook.go index b61519949..a2f293f58 100644 --- a/web/hook.go +++ b/server/hook.go @@ -1,8 +1,7 @@ -package web +package server import ( "fmt" - "os" "regexp" "github.com/gin-gonic/gin" @@ -19,18 +18,6 @@ import ( "github.com/drone/drone/yaml" ) -var ( - droneYml = os.Getenv("BUILD_CONFIG_FILE") - droneSec string -) - -func init() { - if droneYml == "" { - droneYml = ".drone.yml" - } - droneSec = fmt.Sprintf("%s.sig", droneYml) -} - var skipRe = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`) func PostHook(c *gin.Context) { @@ -135,13 +122,14 @@ func PostHook(c *gin.Context) { } // fetch the build file from the database - raw, err := remote_.File(user, repo, build, droneYml) + config := ToConfig(c) + raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } - sec, err := remote_.File(user, repo, build, droneSec) + sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) // NOTE we don't exit on failure. The sec file is optional diff --git a/server/login.go b/server/login.go index bfd1d2d67..0934155d1 100644 --- a/server/login.go +++ b/server/login.go @@ -159,3 +159,9 @@ type tokenPayload struct { Refresh string `json:"refresh_token,omitempty"` Expires int64 `json:"expires_in,omitempty"` } + +// ToConfig returns the config from the Context +func ToConfig(c *gin.Context) *model.Config { + v := c.MustGet("config") + return v.(*model.Config) +} diff --git a/web/pages.go b/server/pages.go similarity index 99% rename from web/pages.go rename to server/pages.go index 7163063ff..4d765face 100644 --- a/web/pages.go +++ b/server/pages.go @@ -1,4 +1,4 @@ -package web +package server import ( "net/http" diff --git a/api/queue.go b/server/queue.go similarity index 99% rename from api/queue.go rename to server/queue.go index d18c544c6..2230125a1 100644 --- a/api/queue.go +++ b/server/queue.go @@ -1,4 +1,4 @@ -package api +package server import ( "fmt" diff --git a/api/repo.go b/server/repo.go similarity index 99% rename from api/repo.go rename to server/repo.go index 5b631973b..a5768c8cf 100644 --- a/api/repo.go +++ b/server/repo.go @@ -1,4 +1,4 @@ -package api +package server import ( "fmt" diff --git a/api/secret.go b/server/secret.go similarity index 98% rename from api/secret.go rename to server/secret.go index cc3384552..f39d97680 100644 --- a/api/secret.go +++ b/server/secret.go @@ -1,4 +1,4 @@ -package api +package server import ( "github.com/drone/drone/model" diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 0a3109793..000000000 --- a/server/server.go +++ /dev/null @@ -1,241 +0,0 @@ -package server - -import ( - "net/http" - "strings" - "time" - - "github.com/Sirupsen/logrus" - "github.com/drone/drone/api" - "github.com/drone/drone/bus" - "github.com/drone/drone/cache" - "github.com/drone/drone/queue" - "github.com/drone/drone/remote" - "github.com/drone/drone/router/middleware" - "github.com/drone/drone/router/middleware/header" - "github.com/drone/drone/router/middleware/session" - "github.com/drone/drone/router/middleware/token" - "github.com/drone/drone/static" - "github.com/drone/drone/store" - "github.com/drone/drone/stream" - "github.com/drone/drone/template" - "github.com/drone/drone/web" - - "github.com/gin-gonic/contrib/ginrus" - "github.com/gin-gonic/gin" -) - -// Config defines system configuration parameters. -type Config struct { - Open bool // Enables open registration - Yaml string // Customize the Yaml configuration file name - Secret string // Secret token used to authenticate agents - Admins map[string]bool // Administrative users - Orgs map[string]bool // Organization whitelist -} - -// Server defines the server configuration. -type Server struct { - Bus bus.Bus - Cache cache.Cache - Queue queue.Queue - Remote remote.Remote - Stream stream.Stream - Store store.Store - Config *Config -} - -// Handler returns an http.Handler for servering Drone requests. -func (s *Server) Handler() http.Handler { - - e := gin.New() - e.Use(gin.Recovery()) - - e.SetHTMLTemplate(template.Load()) - e.StaticFS("/static", static.FileSystem()) - - e.Use(header.NoCache) - e.Use(header.Options) - e.Use(header.Secure) - e.Use( - ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), - HandlerVersion(), - HandlerQueue(s.Queue), - HandlerStream(s.Stream), - HandlerBus(s.Bus), - HandlerCache(s.Cache), - HandlerStore(s.Store), - HandlerRemote(s.Remote), - HandlerConfig(s.Config), - ) - e.Use(session.SetUser()) - e.Use(token.Refresh) - - e.GET("/", web.ShowIndex) - e.GET("/repos", web.ShowAllRepos) - e.GET("/login", web.ShowLogin) - e.GET("/login/form", web.ShowLoginForm) - e.GET("/logout", GetLogout) - - // TODO below will Go away with React UI - settings := e.Group("/settings") - { - settings.Use(session.MustUser()) - settings.GET("/profile", web.ShowUser) - } - repo := e.Group("/repos/:owner/:name") - { - repo.Use(session.SetRepo()) - repo.Use(session.SetPerm()) - repo.Use(session.MustPull) - - repo.GET("", web.ShowRepo) - repo.GET("/builds/:number", web.ShowBuild) - repo.GET("/builds/:number/:job", web.ShowBuild) - - repo_settings := repo.Group("/settings") - { - repo_settings.GET("", session.MustPush, web.ShowRepoConf) - repo_settings.GET("/encrypt", session.MustPush, web.ShowRepoEncrypt) - repo_settings.GET("/badges", web.ShowRepoBadges) - } - } - // TODO above will Go away with React UI - - user := e.Group("/api/user") - { - user.Use(session.MustUser()) - user.GET("", api.GetSelf) - user.GET("/feed", api.GetFeed) - user.GET("/repos", api.GetRepos) - user.GET("/repos/remote", api.GetRemoteRepos) - user.POST("/token", api.PostToken) - user.DELETE("/token", api.DeleteToken) - } - - users := e.Group("/api/users") - { - users.Use(session.MustAdmin()) - users.GET("", api.GetUsers) - users.POST("", api.PostUser) - users.GET("/:login", api.GetUser) - users.PATCH("/:login", api.PatchUser) - users.DELETE("/:login", api.DeleteUser) - } - - repos := e.Group("/api/repos/:owner/:name") - { - repos.POST("", api.PostRepo) - - repo := repos.Group("") - { - repo.Use(session.SetRepo()) - repo.Use(session.SetPerm()) - repo.Use(session.MustPull) - - repo.GET("", api.GetRepo) - repo.GET("/builds", api.GetBuilds) - repo.GET("/builds/:number", api.GetBuild) - repo.GET("/logs/:number/:job", api.GetBuildLogs) - repo.POST("/sign", session.MustPush, api.Sign) - - repo.POST("/secrets", session.MustPush, api.PostSecret) - repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret) - - // requires push permissions - repo.PATCH("", session.MustPush, api.PatchRepo) - repo.DELETE("", session.MustPush, api.DeleteRepo) - - repo.POST("/builds/:number", session.MustPush, api.PostBuild) - repo.DELETE("/builds/:number/:job", session.MustPush, api.DeleteBuild) - } - } - - badges := e.Group("/api/badges/:owner/:name") - { - badges.GET("/status.svg", web.GetBadge) - badges.GET("/cc.xml", web.GetCC) - } - - e.POST("/hook", web.PostHook) - e.POST("/api/hook", web.PostHook) - - stream := e.Group("/api/stream") - { - stream.Use(session.SetRepo()) - stream.Use(session.SetPerm()) - stream.Use(session.MustPull) - - stream.GET("/:owner/:name", web.GetRepoEvents) - stream.GET("/:owner/:name/:build/:number", web.GetStream) - } - - auth := e.Group("/authorize") - { - auth.GET("", GetLogin) - auth.POST("", GetLogin) - auth.POST("/token", GetLoginToken) - } - - queue := e.Group("/api/queue") - { - queue.Use(middleware.AgentMust()) - queue.POST("/pull", api.Pull) - queue.POST("/pull/:os/:arch", api.Pull) - queue.POST("/wait/:id", api.Wait) - queue.POST("/stream/:id", api.Stream) - queue.POST("/status/:id", api.Update) - } - - // DELETE THESE - // gitlab := e.Group("/gitlab/:owner/:name") - // { - // gitlab.Use(session.SetRepo()) - // gitlab.GET("/commits/:sha", web.GetCommit) - // gitlab.GET("/pulls/:number", web.GetPullRequest) - // - // redirects := gitlab.Group("/redirect") - // { - // redirects.GET("/commits/:sha", web.RedirectSha) - // redirects.GET("/pulls/:number", web.RedirectPullRequest) - // } - // } - - // bots := e.Group("/bots") - // { - // bots.Use(session.MustUser()) - // bots.POST("/slack", web.Slack) - // bots.POST("/slack/:command", web.Slack) - // } - - return normalize(e) -} - -// THIS HACK JOB IS GOING AWAY SOON. -// -// normalize is a helper function to work around the following -// issue with gin. https://github.com/gin-gonic/gin/issues/388 -func normalize(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - parts := strings.Split(r.URL.Path, "/")[1:] - switch parts[0] { - case "settings", "bots", "repos", "api", "login", "logout", "", "authorize", "hook", "static", "gitlab": - // no-op - default: - - if len(parts) > 2 && parts[2] != "settings" { - parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...) - } - - // prefix the URL with /repo so that it - // can be effectively routed. - parts = append([]string{"", "repos"}, parts...) - - // reconstruct the path - r.URL.Path = strings.Join(parts, "/") - } - - h.ServeHTTP(w, r) - }) -} diff --git a/api/sign.go b/server/sign.go similarity index 98% rename from api/sign.go rename to server/sign.go index 95d13c5ad..df34996e6 100644 --- a/api/sign.go +++ b/server/sign.go @@ -1,4 +1,4 @@ -package api +package server import ( "io/ioutil" diff --git a/web/slack.go b/server/slack.go similarity index 99% rename from web/slack.go rename to server/slack.go index bb03422ce..f6b78c907 100644 --- a/web/slack.go +++ b/server/slack.go @@ -1,4 +1,4 @@ -package web +package server import ( "strings" diff --git a/web/stream.go b/server/stream.go similarity index 99% rename from web/stream.go rename to server/stream.go index cceff005c..aae7bf2fd 100644 --- a/web/stream.go +++ b/server/stream.go @@ -1,4 +1,4 @@ -package web +package server import ( "bufio" diff --git a/api/doc.go b/server/swagger/doc.go similarity index 62% rename from api/doc.go rename to server/swagger/doc.go index 6c00e13ef..2642c47d4 100644 --- a/api/doc.go +++ b/server/swagger/doc.go @@ -11,6 +11,7 @@ // - application/json // // swagger:meta -package api +package swagger -//go:generate swagger generate spec -o swagger/files/swagger.json +//go:generate swagger generate spec -o files/swagger.json +//go:generate go-bindata -pkg swagger -o swagger_gen.go files/ diff --git a/api/swagger/files/swagger.yml b/server/swagger/files/swagger.yml similarity index 100% rename from api/swagger/files/swagger.yml rename to server/swagger/files/swagger.yml diff --git a/server/swagger/swagger.go b/server/swagger/swagger.go new file mode 100644 index 000000000..2e7a006f1 --- /dev/null +++ b/server/swagger/swagger.go @@ -0,0 +1,85 @@ +package swagger + +import ( + "net/http" + + "github.com/drone/drone/model" +) + +// swagger:route GET /users/{login} user getUser +// +// Get the user with the matching login. +// +// Responses: +// 200: user +// +func userFind(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /user user getCurrentUser +// +// Get the currently authenticated user. +// +// Responses: +// 200: user +// +func userCurrent(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /users user getUserList +// +// Get the list of all registered users. +// +// Responses: +// 200: user +// +func userList(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /user/feed user getUserFeed +// +// Get the currently authenticated user's build feed. +// +// Responses: +// 200: feed +// +func userFeed(w http.ResponseWriter, r *http.Request) {} + +// swagger:route DELETE /users/{login} user deleteUserLogin +// +// Delete the user with the matching login. +// +// Responses: +// 200: user +// +func userDelete(w http.ResponseWriter, r *http.Request) {} + +// swagger:route GET /user/repos user getUserRepos +// +// Get the currently authenticated user's active repository list. +// +// Responses: +// 200: repos +// +func repoList(w http.ResponseWriter, r *http.Request) {} + +// swagger:response user +type userResp struct { + // in: body + Body model.User +} + +// swagger:response users +type usersResp struct { + // in: body + Body []model.User +} + +// swagger:response feed +type feedResp struct { + // in: body + Body []model.Feed +} + +// swagger:response repos +type reposResp struct { + // in: body + Body []model.Repo +} diff --git a/api/user.go b/server/user.go similarity index 69% rename from api/user.go rename to server/user.go index 667bf4ed7..210aa7abf 100644 --- a/api/user.go +++ b/server/user.go @@ -1,4 +1,4 @@ -package api +package server import ( "net/http" @@ -6,31 +6,16 @@ import ( "github.com/gin-gonic/gin" "github.com/drone/drone/cache" - "github.com/drone/drone/model" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/shared/crypto" "github.com/drone/drone/shared/token" "github.com/drone/drone/store" ) -// swagger:route GET /user user getUser -// -// Get the currently authenticated user. -// -// Responses: -// 200: user -// func GetSelf(c *gin.Context) { c.JSON(200, session.User(c)) } -// swagger:route GET /user/feed user getUserFeed -// -// Get the currently authenticated user's build feed. -// -// Responses: -// 200: feed -// func GetFeed(c *gin.Context) { repos, err := cache.GetRepos(c, session.User(c)) if err != nil { @@ -46,13 +31,6 @@ func GetFeed(c *gin.Context) { c.JSON(200, feed) } -// swagger:route GET /user/repos user getUserRepos -// -// Get the currently authenticated user's active repository list. -// -// Responses: -// 200: repos -// func GetRepos(c *gin.Context) { repos, err := cache.GetRepos(c, session.User(c)) if err != nil { @@ -105,27 +83,3 @@ func DeleteToken(c *gin.Context) { } c.String(http.StatusOK, tokenstr) } - -// swagger:response user -type userResp struct { - // in: body - Body model.User -} - -// swagger:response users -type usersResp struct { - // in: body - Body []model.User -} - -// swagger:response feed -type feedResp struct { - // in: body - Body []model.Feed -} - -// swagger:response repos -type reposResp struct { - // in: body - Body []model.Repo -} diff --git a/api/users.go b/server/users.go similarity index 67% rename from api/users.go rename to server/users.go index 81756aec4..3661692f2 100644 --- a/api/users.go +++ b/server/users.go @@ -1,4 +1,4 @@ -package api +package server import ( "net/http" @@ -10,13 +10,6 @@ import ( "github.com/drone/drone/store" ) -// swagger:route GET /users user getUserList -// -// Get the list of all registered users. -// -// Responses: -// 200: user -// func GetUsers(c *gin.Context) { users, err := store.GetUserList(c) if err != nil { @@ -26,20 +19,13 @@ func GetUsers(c *gin.Context) { } } -// swagger:route GET /users/{login} user getUserLogin -// -// Get the user with the matching login. -// -// Responses: -// 200: user -// func GetUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.String(404, "Cannot find user. %s", err) - } else { - c.JSON(200, user) + return } + c.JSON(200, user) } func PatchUser(c *gin.Context) { @@ -74,31 +60,20 @@ func PostUser(c *gin.Context) { c.String(http.StatusBadRequest, err.Error()) return } - - user := &model.User{} - user.Login = in.Login - user.Email = in.Email - user.Admin = in.Admin - user.Avatar = in.Avatar - user.Active = true - user.Hash = crypto.Rand() - - err = store.CreateUser(c, user) - if err != nil { + user := &model.User{ + Active: true, + Login: in.Login, + Email: in.Email, + Avatar: in.Avatar, + Hash: crypto.Rand(), + } + if err = store.CreateUser(c, user); err != nil { c.String(http.StatusInternalServerError, err.Error()) return } - c.JSON(http.StatusOK, user) } -// swagger:route DELETE /users/{login} user deleteUserLogin -// -// Delete the user with the matching login. -// -// Responses: -// 200: user -// func DeleteUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { @@ -107,7 +82,7 @@ func DeleteUser(c *gin.Context) { } if err = store.DeleteUser(c, user); err != nil { c.String(500, "Error deleting user. %s", err) - } else { - c.String(200, "") + return } + c.String(200, "") } diff --git a/shared/docker/docker.go b/shared/docker/docker.go deleted file mode 100644 index 6615871e0..000000000 --- a/shared/docker/docker.go +++ /dev/null @@ -1,97 +0,0 @@ -package docker - -import ( - "errors" - - log "github.com/Sirupsen/logrus" - "github.com/samalba/dockerclient" -) - -var ( - LogOpts = &dockerclient.LogOptions{ - Stdout: true, - Stderr: true, - } - - LogOptsTail = &dockerclient.LogOptions{ - Follow: true, - Stdout: true, - Stderr: true, - } -) - -// Run creates the docker container, pulling images if necessary, starts -// the container and blocks until the container exits, returning the exit -// information. -func Run(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { - info, err := RunDaemon(client, conf, name) - if err != nil { - return nil, err - } - - return Wait(client, info.Id) -} - -// RunDaemon creates the docker container, pulling images if necessary, starts -// the container and returns the container information. It does not wait for -// the container to exit. -func RunDaemon(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) { - - // attempts to create the container - id, err := client.CreateContainer(conf, name, nil) - if err != nil { - // and pull the image and re-create if that fails - err = client.PullImage(conf.Image, nil) - if err != nil { - return nil, err - } - id, err = client.CreateContainer(conf, name, nil) - if err != nil { - client.RemoveContainer(id, true, true) - return nil, err - } - } - - // fetches the container information - info, err := client.InspectContainer(id) - if err != nil { - client.RemoveContainer(id, true, true) - return nil, err - } - - // starts the container - err = client.StartContainer(id, &conf.HostConfig) - if err != nil { - client.RemoveContainer(id, true, true) - return nil, err - } - - return info, err -} - -// Wait blocks until the named container exits, returning the exit information. -func Wait(client dockerclient.Client, name string) (*dockerclient.ContainerInfo, error) { - - defer func() { - client.StopContainer(name, 5) - client.KillContainer(name, "9") - }() - - for attempts := 0; attempts < 5; attempts++ { - done := client.Wait(name) - <-done - - info, err := client.InspectContainer(name) - if err != nil { - return nil, err - } - - if !info.State.Running { - return info, nil - } - - log.Debugf("attempting to resume waiting after %d attempts.\n", attempts) - } - - return nil, errors.New("reached maximum wait attempts") -} diff --git a/web/login.go b/web/login.go deleted file mode 100644 index 53e7ab8e1..000000000 --- a/web/login.go +++ /dev/null @@ -1,149 +0,0 @@ -package web - -// -// import ( -// "net/http" -// "time" -// -// "github.com/drone/drone/model" -// "github.com/drone/drone/remote" -// "github.com/drone/drone/shared/crypto" -// "github.com/drone/drone/shared/httputil" -// "github.com/drone/drone/shared/token" -// "github.com/drone/drone/store" -// -// "github.com/Sirupsen/logrus" -// "github.com/gin-gonic/gin" -// ) -// -// func GetLogin(c *gin.Context) { -// remote := remote.FromContext(c) -// -// // when dealing with redirects we may need -// // to adjust the content type. I cannot, however, -// // remember why, so need to revisit this line. -// c.Writer.Header().Del("Content-Type") -// -// tmpuser, err := remote.Login(c.Writer, c.Request) -// if err != nil { -// logrus.Errorf("cannot authenticate user. %s", err) -// c.Redirect(303, "/login?error=oauth_error") -// return -// } -// // this will happen when the user is redirected by -// // the remote provide as part of the oauth dance. -// if tmpuser == nil { -// return -// } -// -// var open = false // TODO get this from context -// -// // get the user from the database -// u, err := store.GetUserLogin(c, tmpuser.Login) -// if err != nil { -// -// // if self-registration is disabled we should -// // return a notAuthorized error. the only exception -// // is if no users exist yet in the system we'll proceed. -// if !open { -// logrus.Errorf("cannot register %s. registration closed", tmpuser.Login) -// c.Redirect(303, "/login?error=access_denied") -// return -// } -// -// // create the user account -// u = &model.User{} -// u.Login = tmpuser.Login -// u.Token = tmpuser.Token -// u.Secret = tmpuser.Secret -// u.Email = tmpuser.Email -// u.Avatar = tmpuser.Avatar -// u.Hash = crypto.Rand() -// -// // insert the user into the database -// if err := store.CreateUser(c, u); err != nil { -// logrus.Errorf("cannot insert %s. %s", u.Login, err) -// c.Redirect(303, "/login?error=internal_error") -// return -// } -// } -// -// // update the user meta data and authorization -// // data and cache in the datastore. -// u.Token = tmpuser.Token -// u.Secret = tmpuser.Secret -// u.Email = tmpuser.Email -// u.Avatar = tmpuser.Avatar -// -// if err := store.UpdateUser(c, u); err != nil { -// logrus.Errorf("cannot update %s. %s", u.Login, err) -// c.Redirect(303, "/login?error=internal_error") -// return -// } -// -// exp := time.Now().Add(time.Hour * 72).Unix() -// token := token.New(token.SessToken, u.Login) -// tokenstr, err := token.SignExpires(u.Hash, exp) -// if err != nil { -// logrus.Errorf("cannot create token for %s. %s", u.Login, err) -// c.Redirect(303, "/login?error=internal_error") -// return -// } -// -// httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr) -// redirect := httputil.GetCookie(c.Request, "user_last") -// if len(redirect) == 0 { -// redirect = "/" -// } -// c.Redirect(303, redirect) -// -// } -// -// func GetLogout(c *gin.Context) { -// -// httputil.DelCookie(c.Writer, c.Request, "user_sess") -// httputil.DelCookie(c.Writer, c.Request, "user_last") -// c.Redirect(303, "/login") -// } -// -// func GetLoginToken(c *gin.Context) { -// remote := remote.FromContext(c) -// -// in := &tokenPayload{} -// err := c.Bind(in) -// if err != nil { -// c.AbortWithError(http.StatusBadRequest, err) -// return -// } -// -// login, err := remote.Auth(in.Access, in.Refresh) -// if err != nil { -// c.AbortWithError(http.StatusUnauthorized, err) -// return -// } -// -// user, err := store.GetUserLogin(c, login) -// if err != nil { -// c.AbortWithError(http.StatusNotFound, err) -// return -// } -// -// exp := time.Now().Add(time.Hour * 72).Unix() -// token := token.New(token.SessToken, user.Login) -// tokenstr, err := token.SignExpires(user.Hash, exp) -// if err != nil { -// c.AbortWithError(http.StatusInternalServerError, err) -// return -// } -// -// c.IndentedJSON(http.StatusOK, &tokenPayload{ -// Access: tokenstr, -// Expires: exp - time.Now().Unix(), -// }) -// } -// -// type tokenPayload struct { -// Access string `json:"access_token,omitempty"` -// Refresh string `json:"refresh_token,omitempty"` -// Expires int64 `json:"expires_in,omitempty"` -// } diff --git a/yaml/checksum/checksum.go b/yaml/checksum/checksum.go deleted file mode 100644 index 4a021a692..000000000 --- a/yaml/checksum/checksum.go +++ /dev/null @@ -1,70 +0,0 @@ -package checksum - -import ( - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "fmt" - "io" - "strings" -) - -// Check is a calculates and verifies a file checksum. This supports the sha1, -// sha256 and sha512 values. -func Check(in, checksum string) bool { - hash, size, _ := split(checksum) - - // if a byte size is provided for the - // Yaml file it must match. - if size > 0 && int64(len(in)) != size { - return false - } - - switch len(hash) { - case 64: - return sha256sum(in) == hash - case 128: - return sha512sum(in) == hash - case 40: - return sha1sum(in) == hash - } - - return false -} - -func sha1sum(in string) string { - h := sha1.New() - io.WriteString(h, in) - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func sha256sum(in string) string { - h := sha256.New() - io.WriteString(h, in) - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func sha512sum(in string) string { - h := sha512.New() - io.WriteString(h, in) - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func split(in string) (string, int64, string) { - var hash string - var name string - var size int64 - - // the checksum might be split into multiple - // sections including the file size and name. - switch strings.Count(in, " ") { - case 1: - fmt.Sscanf(in, "%s %s", &hash, &name) - case 2: - fmt.Sscanf(in, "%s %d %s", &hash, &size, &name) - default: - hash = in - } - - return hash, size, name -} diff --git a/yaml/checksum/checksum_test.go b/yaml/checksum/checksum_test.go deleted file mode 100644 index 9dd6925c7..000000000 --- a/yaml/checksum/checksum_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package checksum - -import ( - "testing" - - "github.com/franela/goblin" -) - -func TestParse(t *testing.T) { - - g := goblin.Goblin(t) - g.Describe("Shasum", func() { - - g.It("Should parse the shasum string", func() { - hash, _, _ := split("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - }) - - g.It("Should parse a two-part shasum string", func() { - hash, _, name := split("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 .drone.yml") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - g.Assert(name).Equal(".drone.yml") - }) - - g.It("Should parse a three-part shasum string", func() { - hash, size, name := split("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 42 .drone.yml") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - g.Assert(name).Equal(".drone.yml") - g.Assert(size).Equal(int64(42)) - }) - - g.It("Should calc a sha1 sum", func() { - hash := sha1sum("foo\n") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - }) - - g.It("Should calc a sha256 sum", func() { - hash := sha256sum("foo\n") - g.Assert(hash).Equal("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c") - }) - - g.It("Should calc a sha512 sum", func() { - hash := sha512sum("foo\n") - g.Assert(hash).Equal("0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6") - }) - - g.It("Should calc a sha1 sum", func() { - hash := sha1sum("foo\n") - g.Assert(hash).Equal("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") - }) - - g.It("Should validate sha1 sum with file size", func() { - ok := Check("foo\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 4 -") - g.Assert(ok).IsTrue() - }) - - g.It("Should validate sha256 sum with file size", func() { - ok := Check("foo\n", "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c 4 -") - g.Assert(ok).IsTrue() - }) - - g.It("Should validate sha512 sum with file size", func() { - ok := Check("foo\n", "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6 4 -") - g.Assert(ok).IsTrue() - }) - - g.It("Should fail validation if incorrect sha1", func() { - ok := Check("bar\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 4 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should fail validation if incorrect sha256", func() { - ok := Check("bar\n", "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c 4 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should fail validation if incorrect sha512", func() { - ok := Check("bar\n", "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6 4 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should return false if file size mismatch", func() { - ok := Check("foo\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 12 -") - g.Assert(ok).IsFalse() - }) - - g.It("Should return false if invalid checksum string", func() { - ok := Check("foo\n", "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15234") - g.Assert(ok).IsFalse() - }) - - g.It("Should return false if empty checksum", func() { - ok := Check("foo\n", "") - g.Assert(ok).IsFalse() - }) - }) -} From 90408f20a5eadb5500abe7b7f4a019e0a679e495 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 2 May 2016 12:27:22 -0700 Subject: [PATCH 08/10] resolve admin error in database --- store/datastore/users_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/store/datastore/users_test.go b/store/datastore/users_test.go index 54e22bb7d..fae9931c0 100644 --- a/store/datastore/users_test.go +++ b/store/datastore/users_test.go @@ -58,7 +58,6 @@ func TestUsers(t *testing.T) { Email: "foo@bar.com", Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", Active: true, - Admin: true, } s.CreateUser(&user) @@ -71,7 +70,6 @@ func TestUsers(t *testing.T) { g.Assert(user.Email).Equal(getuser.Email) g.Assert(user.Avatar).Equal(getuser.Avatar) g.Assert(user.Active).Equal(getuser.Active) - g.Assert(user.Admin).Equal(getuser.Admin) }) g.It("Should Get a User By Login", func() { From d8617cb7f9508fc58ceefcbacf9f69e93a8cf941 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 2 May 2016 12:43:09 -0700 Subject: [PATCH 09/10] fix mock remote to conform to interface --- remote/mock/remote.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/remote/mock/remote.go b/remote/mock/remote.go index 69132d20d..917a85c50 100644 --- a/remote/mock/remote.go +++ b/remote/mock/remote.go @@ -12,13 +12,13 @@ type Remote struct { mock.Mock } -// Activate provides a mock function with given fields: u, r, k, link -func (_m *Remote) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { - ret := _m.Called(u, r, k, link) +// Activate provides a mock function with given fields: u, r, link +func (_m *Remote) Activate(u *model.User, r *model.Repo, link string) error { + ret := _m.Called(u, r, link) var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *model.Key, string) error); ok { - r0 = rf(u, r, k, link) + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { + r0 = rf(u, r, link) } else { r0 = ret.Error(0) } From 78cfd3a0dbdf31e09d2d06d8452169ecc6699771 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 2 May 2016 12:48:47 -0700 Subject: [PATCH 10/10] bump yaml sec [CI SKIP] --- .drone.sec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.sec b/.drone.sec index 975083657..bdc95749e 100644 --- a/.drone.sec +++ b/.drone.sec @@ -1 +1 @@ -eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.Y_yR-WIrz_Shk94TlGh1mhLM5NocQeh1nRTz65Hfn8jo78WtVF9ZDUQsf6z2bpipTp4y0TmSjWHLvQHQf1O_LZ-AxLHkG3_XGqeWl7Jd3lE_xsBeWeOIrC3QKx8dNiyU0FKVCoPwfaMpjpXAFpG4ZesNfpTrxaaoT-0PBYYpbVvAi-lIhh5lqKSDlWRkrzR3pvzLPeY-Pq4yb-DE0wLzAOh5nasde3qIKo9VT2fnRQIPIIC6V4vNyi7EdYDhNcdgx9LCFKIYVkCPRRI1R9mG8tyjpLy47t0gs3y-Gcr8MyNE1lPlToX4JaQ4EKcZZAAf2CVCJ92s1Bp6CZWLiFt9aQ.RoWDoMbR2ICXQPlo.7d_JtqJvLPPAdmIMOoD961rO7KZXwY-Fz8GHx98hdHqr27igkoKm1BXqYqlh043wKkHwVaAy5jNAOUdweJKrb5YJGPdk3Lyh4ZpEDvcAiwPgwsf4_Q1Xhw9A1jSOyapFlaSAR2aKj_370yx0y8htgxTeXj5Nv5n7ZZ4ezYstU8xCF9JxzsBxOfFACqiyZr9ZJcm-47mlftcU1nZy1x9005Vq6oZv9FAk4zwKLpjMC3KC3H252PqbL_U-e9j1i5tlrcDiEkW8gLoMq_RbMj5J6w3j0u4eoUV5e-WfjentG5ZIf_e8c8-oKxXF2A1vdVdswzWhQWoIEs2KDqBWmSPsCNHKHE2t4grnDPxipMJByAUSsEE9_NVWRS2ofjw01YpQd8cRV6v-eKBlF-c1QD4ABqBu0iMYTEuScg.AAAOUFIZbiVDhUdpARCZ9g \ No newline at end of file +eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.RPXRWzmKbZ6KShRzmd1adFofHAofk6weJrzfjEd0-gRoscpoHrbvGy-DOUVOjwfI41H52EBFV8L_-8uXdRKkCsLnzDpWYmbSKAkKLGDMAILlQcGr4-C_6_duP8nK6Lw2MuAeP8KACBnHVXaosks-ARr7bC5hzQBgJLMuxw3O40jkMS7J62T1Oo7CRkSlLpAIu0axmWQ6ZlfRhijyZUMAJTfegykgrMbpL-FlyDsRpHYkCC0Ny5tN2Q4Gocmxwgitv8_uNZqXYuOAxQXEwXTSyLkXhBdIDtNLDXkAmglTdCB7mis-SmOw4lxSEMAU20bFqOrtbOP_5jwIPoLO8FR4Fw.TU_hlTlqCRZdI-Jw.NBZUu2Wv6ZRzoRpcrEyg2V-me_XEAI22HT_BOJ1NS1bhPLWx1zCbu8hfWNI9RBUMvppSzFw6leeUXgFGqOjGquVvAOCBi0pKuPVW5jGkmv3kM43ciUzzR5MErAg_VPqQjKkV5RvsSu7gKHr6PTSOmc8hPU9JyfNNMUh4MGOHJnvv3I-oKJva3oOt-y9KPsuGLo-6hM1WbhYyPvcm3PSiBrKkOZM5f0_2nqcZZzHQ8gvo5BmzxcSAYVmKRo8_rLROMqT1fycnWg_4qsJuD6molP9b-88Vb0vrZ2jpvm-f_Cq2psSPPMQIcxSvlweO-dP7u0WvdLnOsIb-cq6HxqorTxtKSKcsGbqv66gLsThsa8KDivreyRFyHhTiKwTugv8Kw4Fxsfhj6hzbuI6Vy5Utyr6OJ30MRpg8kg.qKp-C7QNFaeU0HZJ6Qwp8g \ No newline at end of file