From 082570fb5bf17a69a022ddb4f59351e200a4e192 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Fri, 29 Apr 2016 12:39:56 -0700 Subject: [PATCH] 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) + }) +}