diff --git a/client/client.go b/client/client.go index 8a423aad..456683d8 100644 --- a/client/client.go +++ b/client/client.go @@ -70,6 +70,15 @@ type Client interface { // TeamSecretDel deletes a named team secret. TeamSecretDel(string, string) error + // GlobalSecretList returns a list of global secrets. + GlobalSecretList() ([]*model.Secret, error) + + // GlobalSecretPost create or updates a global secret. + GlobalSecretPost(secret *model.Secret) error + + // GlobalSecretDel deletes a named global secret. + GlobalSecretDel(secret string) error + // Build returns a repository build by number. Build(string, string, int) (*model.Build, error) diff --git a/client/client_impl.go b/client/client_impl.go index ca661a88..e46c2947 100644 --- a/client/client_impl.go +++ b/client/client_impl.go @@ -28,28 +28,30 @@ const ( pathLogs = "%s/api/queue/logs/%d" pathLogsAuth = "%s/api/queue/logs/%d?access_token=%s" - pathSelf = "%s/api/user" - pathFeed = "%s/api/user/feed" - pathRepos = "%s/api/user/repos" - pathRepo = "%s/api/repos/%s/%s" - pathChown = "%s/api/repos/%s/%s/chown" - 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" - pathRepoSecrets = "%s/api/repos/%s/%s/secrets" - pathRepoSecret = "%s/api/repos/%s/%s/secrets/%s" - pathTeamSecrets = "%s/api/teams/%s/secrets" - pathTeamSecret = "%s/api/teams/%s/secrets/%s" - pathNodes = "%s/api/nodes" - pathNode = "%s/api/nodes/%d" - pathUsers = "%s/api/users" - pathUser = "%s/api/users/%s" - pathBuildQueue = "%s/api/builds" - pathAgent = "%s/api/agents" + pathSelf = "%s/api/user" + pathFeed = "%s/api/user/feed" + pathRepos = "%s/api/user/repos" + pathRepo = "%s/api/repos/%s/%s" + pathChown = "%s/api/repos/%s/%s/chown" + 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" + pathRepoSecrets = "%s/api/repos/%s/%s/secrets" + pathRepoSecret = "%s/api/repos/%s/%s/secrets/%s" + pathTeamSecrets = "%s/api/teams/%s/secrets" + pathTeamSecret = "%s/api/teams/%s/secrets/%s" + pathGlobalSecrets = "%s/api/global/secrets" + pathGlobalSecret = "%s/api/global/secrets/%s" + pathNodes = "%s/api/nodes" + pathNode = "%s/api/nodes/%d" + pathUsers = "%s/api/users" + pathUser = "%s/api/users/%s" + pathBuildQueue = "%s/api/builds" + pathAgent = "%s/api/agents" ) type client struct { @@ -284,7 +286,7 @@ func (c *client) SecretDel(owner, name, secret string) error { return c.delete(uri) } -// TeamSecretList returns a list of a repository secrets. +// TeamSecretList returns a list of organizational secrets. func (c *client) TeamSecretList(team string) ([]*model.Secret, error) { var out []*model.Secret uri := fmt.Sprintf(pathTeamSecrets, c.base, team) @@ -292,18 +294,38 @@ func (c *client) TeamSecretList(team string) ([]*model.Secret, error) { return out, err } -// TeamSecretPost create or updates a repository secret. +// TeamSecretPost create or updates a organizational secret. func (c *client) TeamSecretPost(team string, secret *model.Secret) error { uri := fmt.Sprintf(pathTeamSecrets, c.base, team) return c.post(uri, secret, nil) } -// TeamSecretDel deletes a named repository secret. +// TeamSecretDel deletes a named orgainization secret. func (c *client) TeamSecretDel(team, secret string) error { uri := fmt.Sprintf(pathTeamSecret, c.base, team, secret) return c.delete(uri) } +// GlobalSecretList returns a list of global secrets. +func (c *client) GlobalSecretList() ([]*model.Secret, error) { + var out []*model.Secret + uri := fmt.Sprintf(pathGlobalSecrets, c.base) + err := c.get(uri, &out) + return out, err +} + +// GlobalSecretPost create or updates a global secret. +func (c *client) GlobalSecretPost(secret *model.Secret) error { + uri := fmt.Sprintf(pathGlobalSecrets, c.base) + return c.post(uri, secret, nil) +} + +// GlobalSecretDel deletes a named global secret. +func (c *client) GlobalSecretDel(secret string) error { + uri := fmt.Sprintf(pathGlobalSecret, c.base, 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) diff --git a/drone/global.go b/drone/global.go new file mode 100644 index 00000000..bb6de1f4 --- /dev/null +++ b/drone/global.go @@ -0,0 +1,11 @@ +package main + +import "github.com/codegangsta/cli" + +var globalCmd = cli.Command{ + Name: "global", + Usage: "manage global state", + Subcommands: []cli.Command{ + globalSecretCmd, + }, +} diff --git a/drone/global_secret.go b/drone/global_secret.go new file mode 100644 index 00000000..e6b2f068 --- /dev/null +++ b/drone/global_secret.go @@ -0,0 +1,13 @@ +package main + +import "github.com/codegangsta/cli" + +var globalSecretCmd = cli.Command{ + Name: "secret", + Usage: "manage secrets", + Subcommands: []cli.Command{ + globalSecretAddCmd, + globalSecretRemoveCmd, + globalSecretListCmd, + }, +} diff --git a/drone/global_secret_add.go b/drone/global_secret_add.go new file mode 100644 index 00000000..747ca34c --- /dev/null +++ b/drone/global_secret_add.go @@ -0,0 +1,41 @@ +package main + +import ( + "log" + + "github.com/codegangsta/cli" +) + +var globalSecretAddCmd = cli.Command{ + Name: "add", + Usage: "adds a secret", + ArgsUsage: "[key] [value]", + Action: func(c *cli.Context) { + if err := globalSecretAdd(c); err != nil { + log.Fatalln(err) + } + }, + Flags: secretAddFlags(), +} + +func globalSecretAdd(c *cli.Context) error { + if len(c.Args()) != 2 { + cli.ShowSubcommandHelp(c) + return nil + } + + name := c.Args().First() + value := c.Args().Get(1) + + secret, err := secretParseCmd(name, value, c) + if err != nil { + return err + } + + client, err := newClient(c) + if err != nil { + return err + } + + return client.GlobalSecretPost(secret) +} diff --git a/drone/global_secret_info.go b/drone/global_secret_info.go new file mode 100644 index 00000000..06ab7d0f --- /dev/null +++ b/drone/global_secret_info.go @@ -0,0 +1 @@ +package main diff --git a/drone/global_secret_list.go b/drone/global_secret_list.go new file mode 100644 index 00000000..5af148d1 --- /dev/null +++ b/drone/global_secret_list.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + + "github.com/codegangsta/cli" +) + +var globalSecretListCmd = cli.Command{ + Name: "ls", + Usage: "list all secrets", + Action: func(c *cli.Context) { + if err := globalSecretList(c); err != nil { + log.Fatalln(err) + } + }, + Flags: secretListFlags(), +} + +func globalSecretList(c *cli.Context) error { + client, err := newClient(c) + if err != nil { + return err + } + + secrets, err := client.GlobalSecretList() + + if err != nil || len(secrets) == 0 { + return err + } + + return secretDisplayList(secrets, c) +} diff --git a/drone/global_secret_rm.go b/drone/global_secret_rm.go new file mode 100644 index 00000000..47ceec8a --- /dev/null +++ b/drone/global_secret_rm.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + + "github.com/codegangsta/cli" +) + +var globalSecretRemoveCmd = cli.Command{ + Name: "rm", + Usage: "remove a secret", + Action: func(c *cli.Context) { + if err := globalSecretRemove(c); err != nil { + log.Fatalln(err) + } + }, +} + +func globalSecretRemove(c *cli.Context) error { + if len(c.Args()) != 1 { + cli.ShowSubcommandHelp(c) + return nil + } + + secret := c.Args().First() + + client, err := newClient(c) + if err != nil { + return err + } + + return client.GlobalSecretDel(secret) +} diff --git a/drone/main.go b/drone/main.go index 738382f0..d4867da1 100644 --- a/drone/main.go +++ b/drone/main.go @@ -43,6 +43,7 @@ func main() { repoCmd, userCmd, orgCmd, + globalCmd, } app.Run(os.Args) diff --git a/drone/org_secret_add.go b/drone/org_secret_add.go index c9145fb2..07570a03 100644 --- a/drone/org_secret_add.go +++ b/drone/org_secret_add.go @@ -1,13 +1,9 @@ package main import ( - "fmt" - "io/ioutil" "log" - "strings" "github.com/codegangsta/cli" - "github.com/drone/drone/model" ) var orgSecretAddCmd = cli.Command{ @@ -19,26 +15,7 @@ var orgSecretAddCmd = cli.Command{ 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{}, - }, - cli.StringFlag{ - Name: "input", - Usage: "input secret value from a file", - }, - }, + Flags: secretAddFlags(), } func orgSecretAdd(c *cli.Context) error { @@ -51,27 +28,9 @@ func orgSecretAdd(c *cli.Context) error { name := c.Args().Get(1) value := c.Args().Get(2) - secret := &model.Secret{} - secret.Name = name - secret.Value = value - secret.Images = c.StringSlice("image") - secret.Events = c.StringSlice("event") - - if len(secret.Images) == 0 { - return fmt.Errorf("Please specify the --image parameter") - } - - // TODO(bradrydzewski) below we use an @ sybmol to denote that the secret - // value should be loaded from a file (inspired by curl). I'd prefer to use - // a --input flag to explicitly specify a filepath instead. - - if strings.HasPrefix(secret.Value, "@") { - path := secret.Value[1:] - out, ferr := ioutil.ReadFile(path) - if ferr != nil { - return ferr - } - secret.Value = string(out) + secret, err := secretParseCmd(name, value, c) + if err != nil { + return err } client, err := newClient(c) diff --git a/drone/org_secret_list.go b/drone/org_secret_list.go index 550224d0..1163a3d2 100644 --- a/drone/org_secret_list.go +++ b/drone/org_secret_list.go @@ -2,9 +2,6 @@ package main import ( "log" - "os" - "strings" - "text/template" "github.com/codegangsta/cli" ) @@ -17,21 +14,7 @@ var orgSecretListCmd = cli.Command{ log.Fatalln(err) } }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "format", - Usage: "format output", - Value: tmplOrgSecretList, - }, - cli.StringFlag{ - Name: "image", - Usage: "filter by image", - }, - cli.StringFlag{ - Name: "event", - Usage: "filter by event", - }, - }, + Flags: secretListFlags(), } func orgSecretList(c *cli.Context) error { @@ -53,35 +36,5 @@ func orgSecretList(c *cli.Context) error { return err } - tmpl, err := template.New("_").Funcs(orgSecretFuncMap).Parse(c.String("format") + "\n") - - if err != nil { - return err - } - - for _, secret := range secrets { - if c.String("image") != "" && !stringInSlice(c.String("image"), secret.Images) { - continue - } - - if c.String("event") != "" && !stringInSlice(c.String("event"), secret.Events) { - continue - } - - tmpl.Execute(os.Stdout, secret) - } - - return nil -} - -// template for secret list items -var tmplOrgSecretList = "\x1b[33m{{ .Name }} \x1b[0m" + ` -Images: {{ list .Images }} -Events: {{ list .Events }} -` - -var orgSecretFuncMap = template.FuncMap{ - "list": func(s []string) string { - return strings.Join(s, ", ") - }, + return secretDisplayList(secrets, c) } diff --git a/drone/secret.go b/drone/secret.go index d39e5b86..89087f02 100644 --- a/drone/secret.go +++ b/drone/secret.go @@ -1,6 +1,15 @@ package main -import "github.com/codegangsta/cli" +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "text/template" + + "github.com/codegangsta/cli" + "github.com/drone/drone/model" +) var secretCmd = cli.Command{ Name: "secret", @@ -11,3 +20,112 @@ var secretCmd = cli.Command{ secretListCmd, }, } + +func secretAddFlags() []cli.Flag { + return []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{}, + }, + cli.StringFlag{ + Name: "input", + Usage: "input secret value from a file", + }, + cli.BoolFlag{ + Name: "skip-verify", + Usage: "skip verification for the secret", + }, + } +} + +func secretListFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: "format", + Usage: "format output", + Value: tmplSecretList, + }, + cli.StringFlag{ + Name: "image", + Usage: "filter by image", + }, + cli.StringFlag{ + Name: "event", + Usage: "filter by event", + }, + } +} + +func secretParseCmd(name string, value string, c *cli.Context) (*model.Secret, error) { + secret := &model.Secret{} + secret.Name = name + secret.Value = value + secret.Images = c.StringSlice("image") + secret.Events = c.StringSlice("event") + secret.SkipVerify = c.Bool("skip-verify") + + if len(secret.Images) == 0 { + return nil, fmt.Errorf("Please specify the --image parameter") + } + + // TODO(bradrydzewski) below we use an @ sybmol to denote that the secret + // value should be loaded from a file (inspired by curl). I'd prefer to use + // a --input flag to explicitly specify a filepath instead. + + if strings.HasPrefix(secret.Value, "@") { + path := secret.Value[1:] + out, ferr := ioutil.ReadFile(path) + if ferr != nil { + return nil, ferr + } + secret.Value = string(out) + } + + return secret, nil +} + +func secretDisplayList(secrets []*model.Secret, c *cli.Context) error { + tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(c.String("format") + "\n") + + if err != nil { + return err + } + + for _, secret := range secrets { + if c.String("image") != "" && !stringInSlice(c.String("image"), secret.Images) { + continue + } + + if c.String("event") != "" && !stringInSlice(c.String("event"), secret.Events) { + continue + } + + tmpl.Execute(os.Stdout, secret) + } + + return nil + +} + +// template for secret list items +var tmplSecretList = "\x1b[33m{{ .Name }} \x1b[0m" + ` +Images: {{ list .Images }} +Events: {{ list .Events }} +SkipVerify: {{ .SkipVerify }} +` + +var secretFuncMap = template.FuncMap{ + "list": func(s []string) string { + return strings.Join(s, ", ") + }, +} diff --git a/drone/secret_add.go b/drone/secret_add.go index 4ab115f8..fe8a9197 100644 --- a/drone/secret_add.go +++ b/drone/secret_add.go @@ -1,13 +1,9 @@ package main import ( - "fmt" - "io/ioutil" "log" - "strings" "github.com/codegangsta/cli" - "github.com/drone/drone/model" ) var secretAddCmd = cli.Command{ @@ -19,26 +15,7 @@ var secretAddCmd = cli.Command{ 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{}, - }, - cli.StringFlag{ - Name: "input", - Usage: "input secret value from a file", - }, - }, + Flags: secretAddFlags(), } func secretAdd(c *cli.Context) error { @@ -54,27 +31,9 @@ func secretAdd(c *cli.Context) error { 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") - } - - // TODO(bradrydzewski) below we use an @ sybmol to denote that the secret - // value should be loaded from a file (inspired by curl). I'd prefer to use - // a --input flag to explicitly specify a filepath instead. - - if strings.HasPrefix(secret.Value, "@") { - path := secret.Value[1:] - out, ferr := ioutil.ReadFile(path) - if ferr != nil { - return ferr - } - secret.Value = string(out) + secret, err := secretParseCmd(tail[0], tail[1], c) + if err != nil { + return err } client, err := newClient(c) diff --git a/drone/secret_list.go b/drone/secret_list.go index e875e556..2fc6c33e 100644 --- a/drone/secret_list.go +++ b/drone/secret_list.go @@ -2,9 +2,6 @@ package main import ( "log" - "os" - "strings" - "text/template" "github.com/codegangsta/cli" ) @@ -17,21 +14,7 @@ var secretListCmd = cli.Command{ log.Fatalln(err) } }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "format", - Usage: "format output", - Value: tmplSecretList, - }, - cli.StringFlag{ - Name: "image", - Usage: "filter by image", - }, - cli.StringFlag{ - Name: "event", - Usage: "filter by event", - }, - }, + Flags: secretListFlags(), } func secretList(c *cli.Context) error { @@ -53,35 +36,5 @@ func secretList(c *cli.Context) error { return err } - tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(c.String("format") + "\n") - - if err != nil { - return err - } - - for _, secret := range secrets { - if c.String("image") != "" && !stringInSlice(c.String("image"), secret.Images) { - continue - } - - if c.String("event") != "" && !stringInSlice(c.String("event"), secret.Events) { - continue - } - - tmpl.Execute(os.Stdout, secret) - } - - return nil -} - -// template for secret list items -var tmplSecretList = "\x1b[33m{{ .Name }} \x1b[0m" + ` -Images: {{ list .Images }} -Events: {{ list .Events }} -` - -var secretFuncMap = template.FuncMap{ - "list": func(s []string) string { - return strings.Join(s, ", ") - }, + return secretDisplayList(secrets, c) } diff --git a/router/router.go b/router/router.go index d3c8590e..e9fba78c 100644 --- a/router/router.go +++ b/router/router.go @@ -74,6 +74,15 @@ func Load(middleware ...gin.HandlerFunc) http.Handler { } } + global := e.Group("/api/global") + { + global.Use(session.MustAdmin()) + + global.GET("/secrets", server.GetGlobalSecrets) + global.POST("/secrets", server.PostGlobalSecret) + global.DELETE("/secrets/:secret", server.DeleteGlobalSecret) + } + repos := e.Group("/api/repos/:owner/:name") { repos.POST("", server.PostRepo) diff --git a/server/global_secret.go b/server/global_secret.go new file mode 100644 index 00000000..ba989899 --- /dev/null +++ b/server/global_secret.go @@ -0,0 +1,62 @@ +package server + +import ( + "net/http" + + "github.com/drone/drone/model" + "github.com/drone/drone/store" + + "github.com/gin-gonic/gin" +) + +func GetGlobalSecrets(c *gin.Context) { + secrets, err := store.GetGlobalSecretList(c) + + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + var list []*model.TeamSecret + + for _, s := range secrets { + list = append(list, s.Clone()) + } + + c.JSON(http.StatusOK, list) +} + +func PostGlobalSecret(c *gin.Context) { + in := &model.TeamSecret{} + err := c.Bind(in) + if err != nil { + c.String(http.StatusBadRequest, "Invalid JSON input. %s", err.Error()) + return + } + in.ID = 0 + + err = store.SetGlobalSecret(c, in) + if err != nil { + c.String(http.StatusInternalServerError, "Unable to persist global secret. %s", err.Error()) + return + } + + c.String(http.StatusOK, "") +} + +func DeleteGlobalSecret(c *gin.Context) { + name := c.Param("secret") + + secret, err := store.GetGlobalSecret(c, name) + if err != nil { + c.String(http.StatusNotFound, "Cannot find secret %s.", name) + return + } + err = store.DeleteGlobalSecret(c, secret) + if err != nil { + c.String(http.StatusInternalServerError, "Unable to delete global secret. %s", err.Error()) + return + } + + c.String(http.StatusOK, "") +} diff --git a/store/store.go b/store/store.go index e531054e..5a751242 100644 --- a/store/store.go +++ b/store/store.go @@ -146,6 +146,8 @@ type Store interface { DeleteAgent(*model.Agent) error } +const globalTeamName = "__global__" + // GetUser gets a user by unique ID. func GetUser(c context.Context, id int64) (*model.User, error) { return FromContext(c).GetUser(id) @@ -246,12 +248,30 @@ func DeleteTeamSecret(c context.Context, s *model.TeamSecret) error { return FromContext(c).DeleteTeamSecret(s) } +func GetGlobalSecretList(c context.Context) ([]*model.TeamSecret, error) { + return GetTeamSecretList(c, globalTeamName) +} + +func GetGlobalSecret(c context.Context, name string) (*model.TeamSecret, error) { + return GetTeamSecret(c, globalTeamName, name) +} + +func SetGlobalSecret(c context.Context, s *model.TeamSecret) error { + s.Key = globalTeamName + return SetTeamSecret(c, s) +} + +func DeleteGlobalSecret(c context.Context, s *model.TeamSecret) error { + s.Key = globalTeamName + return DeleteTeamSecret(c, s) +} + func GetMergedSecretList(c context.Context, r *model.Repo) ([]*model.Secret, error) { var ( secrets []*model.Secret ) - repoSecs, err := FromContext(c).GetSecretList(r) + repoSecs, err := GetSecretList(c, r) if err != nil { return nil, err @@ -261,7 +281,7 @@ func GetMergedSecretList(c context.Context, r *model.Repo) ([]*model.Secret, err secrets = append(secrets, secret.Secret()) } - teamSecs, err := FromContext(c).GetTeamSecretList(r.Owner) + teamSecs, err := GetTeamSecretList(c, r.Owner) if err != nil { return nil, err @@ -271,6 +291,16 @@ func GetMergedSecretList(c context.Context, r *model.Repo) ([]*model.Secret, err secrets = append(secrets, secret.Secret()) } + globalSecs, err := GetGlobalSecretList(c) + + if err != nil { + return nil, err + } + + for _, secret := range globalSecs { + secrets = append(secrets, secret.Secret()) + } + return secrets, nil }