diff --git a/api/build.go b/api/build.go index 22583735..d0bd3738 100644 --- a/api/build.go +++ b/api/build.go @@ -35,7 +35,7 @@ func init() { } droneSec = fmt.Sprintf("%s.sec", strings.TrimSuffix(droneYml, filepath.Ext(droneYml))) if os.Getenv("CANARY") == "true" { - droneSec = fmt.Sprintf("%s.sig", strings.TrimSuffix(droneYml, filepath.Ext(droneYml))) + droneSec = fmt.Sprintf("%s.sig", droneYml) } } @@ -291,7 +291,10 @@ func PostBuild(c *gin.Context) { // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) - secs, _ := store.GetSecretList(c, repo) + secs, err := store.GetSecretList(c, repo) + if err != nil { + log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) + } // IMPORTANT. PLEASE READ // @@ -305,14 +308,24 @@ func PostBuild(c *gin.Context) { var verified bool signature, err := jose.ParseSigned(string(sec)) - if err == nil && len(sec) != 0 { + if err != nil { + log.Debugf("cannot parse .drone.yml.sig file. %s", err) + } else if len(sec) == 0 { + log.Debugf("cannot parse .drone.yml.sig file. empty file") + } else { signed = true - output, err := signature.Verify(repo.Hash) - if err == nil && string(output) == string(raw) { + output, err := signature.Verify([]byte(repo.Hash)) + if err != nil { + log.Debugf("cannot verify .drone.yml.sig file. %s", err) + } else if string(output) != string(raw) { + log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) + } else { verified = true } } + log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) + bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) for _, job := range jobs { queue.Publish(c, &queue.Work{ diff --git a/client/client.go b/client/client.go index 783c2c93..33957fdb 100644 --- a/client/client.go +++ b/client/client.go @@ -3,11 +3,21 @@ package client import ( "io" + "github.com/drone/drone/model" "github.com/drone/drone/queue" ) // Client is used to communicate with a Drone server. type Client interface { + // Sign returns a cryptographic signature for the input string. + Sign(string, string, []byte) ([]byte, error) + + // SecretPost create or updates a repository secret. + SecretPost(string, string, *model.Secret) error + + // SecretDel deletes a named repository secret. + SecretDel(string, string, string) error + // Pull pulls work from the server queue. Pull(os, arch string) (*queue.Work, error) diff --git a/client/client_impl.go b/client/client_impl.go index 307a4e74..c1fecaa8 100644 --- a/client/client_impl.go +++ b/client/client_impl.go @@ -2,6 +2,7 @@ package client import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -22,6 +23,24 @@ const ( pathWait = "%s/api/queue/wait/%d" pathStream = "%s/api/queue/stream/%d" pathPush = "%s/api/queue/status/%d" + + pathSelf = "%s/api/user" + pathFeed = "%s/api/user/feed" + pathRepos = "%s/api/user/repos" + pathRepo = "%s/api/repos/%s/%s" + pathEncrypt = "%s/api/repos/%s/%s/encrypt" + pathBuilds = "%s/api/repos/%s/%s/builds" + pathBuild = "%s/api/repos/%s/%s/builds/%v" + pathJob = "%s/api/repos/%s/%s/builds/%d/%d" + pathLog = "%s/api/repos/%s/%s/logs/%d/%d" + pathKey = "%s/api/repos/%s/%s/key" + pathSign = "%s/api/repos/%s/%s/sign" + pathSecrets = "%s/api/repos/%s/%s/secrets" + pathSecret = "%s/api/repos/%s/%s/secrets/%s" + pathNodes = "%s/api/nodes" + pathNode = "%s/api/nodes/%d" + pathUsers = "%s/api/users" + pathUser = "%s/api/users/%s" ) type client struct { @@ -34,14 +53,50 @@ func NewClient(uri string) Client { return &client{http.DefaultClient, uri} } -// NewClientToken returns a client at the specified url that -// authenticates all outbound requests with the given token. +// NewClientToken returns a client at the specified url that authenticates all +// outbound requests with the given token. func NewClientToken(uri, token string) Client { config := new(oauth2.Config) auther := config.Client(oauth2.NoContext, &oauth2.Token{AccessToken: token}) return &client{auther, uri} } +// NewClientTokenTLS returns a client at the specified url that authenticates +// all outbound requests with the given token and tls.Config if provided. +func NewClientTokenTLS(uri, token string, c *tls.Config) Client { + config := new(oauth2.Config) + auther := config.Client(oauth2.NoContext, &oauth2.Token{AccessToken: token}) + if c != nil { + if trans, ok := auther.Transport.(*oauth2.Transport); ok { + trans.Base = &http.Transport{TLSClientConfig: c} + } + } + return &client{auther, uri} +} + +// SecretPost create or updates a repository secret. +func (c *client) SecretPost(owner, name string, secret *model.Secret) error { + uri := fmt.Sprintf(pathSecrets, c.base, owner, name) + return c.post(uri, secret, nil) +} + +// SecretDel deletes a named repository secret. +func (c *client) SecretDel(owner, name, secret string) error { + uri := fmt.Sprintf(pathSecret, c.base, owner, name, secret) + return c.delete(uri) +} + +// Sign returns a cryptographic signature for the input string. +func (c *client) Sign(owner, name string, in []byte) ([]byte, error) { + uri := fmt.Sprintf(pathSign, c.base, owner, name) + rc, err := stream(c.client, uri, "POST", in, nil) + if err != nil { + return nil, err + } + defer rc.Close() + return ioutil.ReadAll(rc) +} + // Pull pulls work from the server queue. func (c *client) Pull(os, arch string) (*queue.Work, error) { out := new(queue.Work) diff --git a/client/http.go b/client/http.go new file mode 100644 index 00000000..d62da4b0 --- /dev/null +++ b/client/http.go @@ -0,0 +1,61 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" +) + +// helper function to stream an http request +func stream(client *http.Client, rawurl, method string, in, out interface{}) (io.ReadCloser, error) { + uri, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + // if we are posting or putting data, we need to + // write it to the body of the request. + var buf io.ReadWriter + if in == nil { + // nothing + } else if rw, ok := in.(io.ReadWriter); ok { + buf = rw + } else if b, ok := in.([]byte); ok { + buf = new(bytes.Buffer) + buf.Write(b) + } else { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(in) + if err != nil { + return nil, err + } + } + + // creates a new http request to bitbucket. + req, err := http.NewRequest(method, uri.String(), buf) + if err != nil { + return nil, err + } + if in == nil { + // nothing + } else if _, ok := in.(io.ReadWriter); ok { + req.Header.Set("Content-Type", "plain/text") + } else { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode > http.StatusPartialContent { + defer resp.Body.Close() + out, _ := ioutil.ReadAll(resp.Body) + return nil, fmt.Errorf(string(out)) + } + return resp.Body, nil +} diff --git a/drone/agent/agent.go b/drone/agent/agent.go index cfe7ae19..cae875f1 100644 --- a/drone/agent/agent.go +++ b/drone/agent/agent.go @@ -83,7 +83,16 @@ var AgentCmd = cli.Command{ EnvVar: "DRONE_PLUGIN_NETRC", Name: "netrc-plugin", Usage: "plugins that receive the netrc file", - Value: &cli.StringSlice{"git", "hg"}, + Value: &cli.StringSlice{ + "git", + "git:*", + "hg", + "hg:*", + "plugins/hg", + "plugins/hg:*", + "plugins/git", + "plugins/git:*", + }, }, cli.StringSliceFlag{ EnvVar: "DRONE_PLUGIN_PRIVILEGED", diff --git a/drone/agent/exec.go b/drone/agent/exec.go index 0bab31c2..b0ded68f 100644 --- a/drone/agent/exec.go +++ b/drone/agent/exec.go @@ -66,25 +66,29 @@ func (r *pipeline) run() error { secrets = append(secrets, &model.Secret{ Name: "DRONE_NETRC_USERNAME", Value: w.Netrc.Login, - Images: []string{"git", "hg"}, // TODO(bradrydzewski) use the command line parameters here + Images: r.config.netrc, // TODO(bradrydzewski) use the command line parameters here Events: []string{model.EventDeploy, model.EventPull, model.EventPush, model.EventTag}, }) secrets = append(secrets, &model.Secret{ Name: "DRONE_NETRC_PASSWORD", Value: w.Netrc.Password, - Images: []string{"git", "hg"}, + Images: r.config.netrc, Events: []string{model.EventDeploy, model.EventPull, model.EventPush, model.EventTag}, }) secrets = append(secrets, &model.Secret{ Name: "DRONE_NETRC_MACHINE", Value: w.Netrc.Machine, - Images: []string{"git", "hg"}, + Images: r.config.netrc, Events: []string{model.EventDeploy, model.EventPull, model.EventPush, model.EventTag}, }) } + for _, secret := range secrets { + fmt.Printf("SECRET %s %s\n", secret.Name, secret.Value) + } + trans := []compiler.Transform{ - builtin.NewCloneOp("plugins/git:latest", true), + builtin.NewCloneOp("git", true), builtin.NewCacheOp( "plugins/cache:latest", "/var/lib/drone/cache/"+w.Repo.FullName, diff --git a/drone/drone.go b/drone/drone.go index cd1f8f81..2a92660d 100644 --- a/drone/drone.go +++ b/drone/drone.go @@ -19,10 +19,25 @@ func main2() { app.Name = "drone" app.Version = version.Version app.Usage = "command line utility" - + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "t, token", + Value: "", + Usage: "server auth token", + EnvVar: "DRONE_TOKEN", + }, + cli.StringFlag{ + Name: "s, server", + Value: "", + Usage: "server location", + EnvVar: "DRONE_SERVER", + }, + } app.Commands = []cli.Command{ agent.AgentCmd, server.ServeCmd, + SignCmd, + SecretCmd, } app.Run(os.Args) diff --git a/drone/secret.go b/drone/secret.go new file mode 100644 index 00000000..a4ab0d7d --- /dev/null +++ b/drone/secret.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "log" + + "github.com/drone/drone/model" + + "github.com/codegangsta/cli" +) + +// SecretCmd is the exported command for managing secrets. +var SecretCmd = cli.Command{ + Name: "secret", + Usage: "manage secrets", + Subcommands: []cli.Command{ + // Secret Add + { + Name: "add", + Usage: "add a secret", + ArgsUsage: "[repo] [key] [value]", + UsageText: "foo", + Action: func(c *cli.Context) { + if err := secretAdd(c); err != nil { + log.Fatalln(err) + } + }, + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "event", + Usage: "inject the secret for these event types", + Value: &cli.StringSlice{ + model.EventPush, + model.EventTag, + model.EventDeploy, + }, + }, + cli.StringSliceFlag{ + Name: "image", + Usage: "inject the secret for these image types", + Value: &cli.StringSlice{}, + }, + }, + }, + // Secret Delete + { + Name: "rm", + Usage: "remove a secret", + Action: func(c *cli.Context) { + if err := secretDel(c); err != nil { + log.Fatalln(err) + } + }, + }, + }, +} + +func secretAdd(c *cli.Context) error { + + repo := c.Args().First() + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + tail := c.Args().Tail() + if len(tail) != 2 { + cli.ShowSubcommandHelp(c) + return nil + } + + secret := &model.Secret{} + secret.Name = tail[0] + secret.Value = tail[1] + secret.Images = c.StringSlice("image") + secret.Events = c.StringSlice("event") + + if len(secret.Images) == 0 { + return fmt.Errorf("Please specify the --image parameter") + } + + client, err := newClient(c) + if err != nil { + return err + } + + return client.SecretPost(owner, name, secret) +} + +func secretDel(c *cli.Context) error { + repo := c.Args().First() + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + secret := c.Args().Get(1) + + client, err := newClient(c) + if err != nil { + return err + } + return client.SecretDel(owner, name, secret) +} diff --git a/drone/sign.go b/drone/sign.go new file mode 100644 index 00000000..03c9a5f0 --- /dev/null +++ b/drone/sign.go @@ -0,0 +1,56 @@ +package main + +import ( + "io/ioutil" + "log" + + "github.com/codegangsta/cli" +) + +// SignCmd is the exported command for signing the yaml. +var SignCmd = cli.Command{ + Name: "sign", + Usage: "creates a secure yaml file", + Action: func(c *cli.Context) { + if err := sign(c); err != nil { + log.Fatalln(err) + } + }, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "in", + Usage: "input file", + Value: ".drone.yml", + }, + cli.StringFlag{ + Name: "out", + Usage: "output file signature", + Value: ".drone.yml.sig", + }, + }, +} + +func sign(c *cli.Context) error { + repo := c.Args().First() + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + in, err := readInput(c.String("in")) + if err != nil { + return err + } + + client, err := newClient(c) + if err != nil { + return err + } + + sig, err := client.Sign(owner, name, in) + if err != nil { + return err + } + + return ioutil.WriteFile(c.String("out"), sig, 0664) +} diff --git a/drone/util.go b/drone/util.go new file mode 100644 index 00000000..f52cff63 --- /dev/null +++ b/drone/util.go @@ -0,0 +1,53 @@ +package main + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/drone/drone/client" + + "github.com/codegangsta/cli" + "github.com/jackspirou/syscerts" +) + +func newClient(c *cli.Context) (client.Client, error) { + var token = c.GlobalString("token") + var server = c.GlobalString("server") + + // if no server url is provided we can default + // to the hosted Drone service. + if len(server) == 0 { + return nil, fmt.Errorf("Error: you must provide the Drone server address.") + } + if len(token) == 0 { + return nil, fmt.Errorf("Error: you must provide your Drone access token.") + } + + // attempt to find system CA certs + certs := syscerts.SystemRootsPool() + tlsConfig := &tls.Config{RootCAs: certs} + + // create the drone client with TLS options + return client.NewClientTokenTLS(server, token, tlsConfig), nil +} + +func parseRepo(str string) (user, repo string, err error) { + var parts = strings.Split(str, "/") + if len(parts) != 2 { + err = fmt.Errorf("Error: Invalid or missing repository. eg octocat/hello-world.") + return + } + user = parts[0] + repo = parts[1] + return +} + +func readInput(in string) ([]byte, error) { + if in == "-" { + return ioutil.ReadAll(os.Stdin) + } + return ioutil.ReadFile(in) +} diff --git a/engine/compiler/builtin/args.go b/engine/compiler/builtin/args.go index d0d471c0..835a1ed4 100644 --- a/engine/compiler/builtin/args.go +++ b/engine/compiler/builtin/args.go @@ -79,7 +79,7 @@ func argsToEnv(from map[string]interface{}, to map[string]string) error { } else { out, err = json.YAMLToJSON(out) if err != nil { - println(err.Error()) + // return err TODO(bradrydzewski) unit test coverage for possible errors } to[k] = string(out) } diff --git a/queue/types.go b/queue/types.go index 85b87b7b..48fc4194 100644 --- a/queue/types.go +++ b/queue/types.go @@ -16,6 +16,6 @@ type Work struct { Netrc *model.Netrc `json:"netrc"` Keys *model.Key `json:"keys"` System *model.System `json:"system"` - Secrets []*model.Secret `json:"secret"` + Secrets []*model.Secret `json:"secrets"` User *model.User `json:"user"` } diff --git a/store/datastore/repos_test.go b/store/datastore/repos_test.go index accb256e..71f9519c 100644 --- a/store/datastore/repos_test.go +++ b/store/datastore/repos_test.go @@ -33,10 +33,7 @@ func TestRepos(t *testing.T) { err1 := s.CreateRepo(&repo) err2 := s.UpdateRepo(&repo) getrepo, err3 := s.GetRepo(repo.ID) - if err3 != nil { - println("Get Repo Error") - println(err3.Error()) - } + g.Assert(err1 == nil).IsTrue() g.Assert(err2 == nil).IsTrue() g.Assert(err3 == nil).IsTrue() diff --git a/stream/reader_test.go b/stream/reader_test.go new file mode 100644 index 00000000..e113cc4a --- /dev/null +++ b/stream/reader_test.go @@ -0,0 +1,7 @@ +package stream + +import "testing" + +func TetsReader(t *testing.T) { + t.Skip() //TODO(bradrydzewski) implement reader tests +} diff --git a/stream/stream_impl_test.go b/stream/stream_impl_test.go new file mode 100644 index 00000000..fdc29fce --- /dev/null +++ b/stream/stream_impl_test.go @@ -0,0 +1,7 @@ +package stream + +import "testing" + +func TetsStream(t *testing.T) { + t.Skip() //TODO(bradrydzewski) implement stream tests +} diff --git a/stream/writer_test.go b/stream/writer_test.go new file mode 100644 index 00000000..c0c757e1 --- /dev/null +++ b/stream/writer_test.go @@ -0,0 +1,7 @@ +package stream + +import "testing" + +func TetsWriter(t *testing.T) { + t.Skip() //TODO(bradrydzewski) implement writer tests +} diff --git a/web/hook.go b/web/hook.go index 1f9c896b..710b67a0 100644 --- a/web/hook.go +++ b/web/hook.go @@ -33,7 +33,7 @@ func init() { } droneSec = fmt.Sprintf("%s.sec", strings.TrimSuffix(droneYml, filepath.Ext(droneYml))) if os.Getenv("CANARY") == "true" { - droneSec = fmt.Sprintf("%s.sig", strings.TrimSuffix(droneYml, filepath.Ext(droneYml))) + droneSec = fmt.Sprintf("%s.sig", droneYml) } } @@ -209,7 +209,10 @@ func PostHook(c *gin.Context) { // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) - secs, _ := store.GetSecretList(c, repo) + secs, err := store.GetSecretList(c, repo) + if err != nil { + log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) + } // IMPORTANT. PLEASE READ // @@ -223,14 +226,24 @@ func PostHook(c *gin.Context) { var verified bool signature, err := jose.ParseSigned(string(sec)) - if err == nil && len(sec) != 0 { + if err != nil { + log.Debugf("cannot parse .drone.yml.sig file. %s", err) + } else if len(sec) == 0 { + log.Debugf("cannot parse .drone.yml.sig file. empty file") + } else { signed = true - output, err := signature.Verify(repo.Hash) - if err == nil && string(output) == string(raw) { + output, err := signature.Verify([]byte(repo.Hash)) + if err != nil { + log.Debugf("cannot verify .drone.yml.sig file. %s", err) + } else if string(output) != string(raw) { + log.Debugf("cannot verify .drone.yml.sig file. no match") + } else { verified = true } } + log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) + bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) for _, job := range jobs { queue.Publish(c, &queue.Work{