Merge pull request #1825 from donny-dont/feature/global_secrets

Implementing global secrets
This commit is contained in:
Brad Rydzewski 2016-10-25 00:03:20 +02:00 committed by GitHub
commit 58f84ced84
17 changed files with 423 additions and 216 deletions

View file

@ -69,6 +69,15 @@ type Client interface {
// TeamSecretDel deletes a named team secret. // TeamSecretDel deletes a named team secret.
TeamSecretDel(string, string) error 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 returns a repository build by number.
Build(string, string, int) (*model.Build, error) Build(string, string, int) (*model.Build, error)

View file

@ -24,28 +24,30 @@ const (
pathLogs = "%s/api/queue/logs/%d" pathLogs = "%s/api/queue/logs/%d"
pathLogsAuth = "%s/api/queue/logs/%d?access_token=%s" pathLogsAuth = "%s/api/queue/logs/%d?access_token=%s"
pathSelf = "%s/api/user" pathSelf = "%s/api/user"
pathFeed = "%s/api/user/feed" pathFeed = "%s/api/user/feed"
pathRepos = "%s/api/user/repos" pathRepos = "%s/api/user/repos"
pathRepo = "%s/api/repos/%s/%s" pathRepo = "%s/api/repos/%s/%s"
pathChown = "%s/api/repos/%s/%s/chown" pathChown = "%s/api/repos/%s/%s/chown"
pathEncrypt = "%s/api/repos/%s/%s/encrypt" pathEncrypt = "%s/api/repos/%s/%s/encrypt"
pathBuilds = "%s/api/repos/%s/%s/builds" pathBuilds = "%s/api/repos/%s/%s/builds"
pathBuild = "%s/api/repos/%s/%s/builds/%v" pathBuild = "%s/api/repos/%s/%s/builds/%v"
pathJob = "%s/api/repos/%s/%s/builds/%d/%d" pathJob = "%s/api/repos/%s/%s/builds/%d/%d"
pathLog = "%s/api/repos/%s/%s/logs/%d/%d" pathLog = "%s/api/repos/%s/%s/logs/%d/%d"
pathKey = "%s/api/repos/%s/%s/key" pathKey = "%s/api/repos/%s/%s/key"
pathSign = "%s/api/repos/%s/%s/sign" pathSign = "%s/api/repos/%s/%s/sign"
pathRepoSecrets = "%s/api/repos/%s/%s/secrets" pathRepoSecrets = "%s/api/repos/%s/%s/secrets"
pathRepoSecret = "%s/api/repos/%s/%s/secrets/%s" pathRepoSecret = "%s/api/repos/%s/%s/secrets/%s"
pathTeamSecrets = "%s/api/teams/%s/secrets" pathTeamSecrets = "%s/api/teams/%s/secrets"
pathTeamSecret = "%s/api/teams/%s/secrets/%s" pathTeamSecret = "%s/api/teams/%s/secrets/%s"
pathNodes = "%s/api/nodes" pathGlobalSecrets = "%s/api/global/secrets"
pathNode = "%s/api/nodes/%d" pathGlobalSecret = "%s/api/global/secrets/%s"
pathUsers = "%s/api/users" pathNodes = "%s/api/nodes"
pathUser = "%s/api/users/%s" pathNode = "%s/api/nodes/%d"
pathBuildQueue = "%s/api/builds" pathUsers = "%s/api/users"
pathAgent = "%s/api/agents" pathUser = "%s/api/users/%s"
pathBuildQueue = "%s/api/builds"
pathAgent = "%s/api/agents"
) )
type client struct { type client struct {
@ -280,7 +282,7 @@ func (c *client) SecretDel(owner, name, secret string) error {
return c.delete(uri) 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) { func (c *client) TeamSecretList(team string) ([]*model.Secret, error) {
var out []*model.Secret var out []*model.Secret
uri := fmt.Sprintf(pathTeamSecrets, c.base, team) uri := fmt.Sprintf(pathTeamSecrets, c.base, team)
@ -288,18 +290,38 @@ func (c *client) TeamSecretList(team string) ([]*model.Secret, error) {
return out, err 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 { func (c *client) TeamSecretPost(team string, secret *model.Secret) error {
uri := fmt.Sprintf(pathTeamSecrets, c.base, team) uri := fmt.Sprintf(pathTeamSecrets, c.base, team)
return c.post(uri, secret, nil) 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 { func (c *client) TeamSecretDel(team, secret string) error {
uri := fmt.Sprintf(pathTeamSecret, c.base, team, secret) uri := fmt.Sprintf(pathTeamSecret, c.base, team, secret)
return c.delete(uri) 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. // Sign returns a cryptographic signature for the input string.
func (c *client) Sign(owner, name string, in []byte) ([]byte, error) { func (c *client) Sign(owner, name string, in []byte) ([]byte, error) {
uri := fmt.Sprintf(pathSign, c.base, owner, name) uri := fmt.Sprintf(pathSign, c.base, owner, name)

11
drone/global.go Normal file
View file

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

13
drone/global_secret.go Normal file
View file

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

View file

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

View file

@ -0,0 +1 @@
package main

View file

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

33
drone/global_secret_rm.go Normal file
View file

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

View file

@ -43,6 +43,7 @@ func main() {
repoCmd, repoCmd,
userCmd, userCmd,
orgCmd, orgCmd,
globalCmd,
} }
app.Run(os.Args) app.Run(os.Args)

View file

@ -1,13 +1,9 @@
package main package main
import ( import (
"fmt"
"io/ioutil"
"log" "log"
"strings"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/drone/drone/model"
) )
var orgSecretAddCmd = cli.Command{ var orgSecretAddCmd = cli.Command{
@ -19,26 +15,7 @@ var orgSecretAddCmd = cli.Command{
log.Fatalln(err) log.Fatalln(err)
} }
}, },
Flags: []cli.Flag{ Flags: secretAddFlags(),
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",
},
},
} }
func orgSecretAdd(c *cli.Context) error { func orgSecretAdd(c *cli.Context) error {
@ -51,27 +28,9 @@ func orgSecretAdd(c *cli.Context) error {
name := c.Args().Get(1) name := c.Args().Get(1)
value := c.Args().Get(2) value := c.Args().Get(2)
secret := &model.Secret{} secret, err := secretParseCmd(name, value, c)
secret.Name = name if err != nil {
secret.Value = value return err
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)
} }
client, err := newClient(c) client, err := newClient(c)

View file

@ -2,9 +2,6 @@ package main
import ( import (
"log" "log"
"os"
"strings"
"text/template"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
) )
@ -17,21 +14,7 @@ var orgSecretListCmd = cli.Command{
log.Fatalln(err) log.Fatalln(err)
} }
}, },
Flags: []cli.Flag{ Flags: secretListFlags(),
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",
},
},
} }
func orgSecretList(c *cli.Context) error { func orgSecretList(c *cli.Context) error {
@ -53,35 +36,5 @@ func orgSecretList(c *cli.Context) error {
return err return err
} }
tmpl, err := template.New("_").Funcs(orgSecretFuncMap).Parse(c.String("format") + "\n") return secretDisplayList(secrets, c)
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, ", ")
},
} }

View file

@ -1,6 +1,15 @@
package main 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{ var secretCmd = cli.Command{
Name: "secret", Name: "secret",
@ -11,3 +20,112 @@ var secretCmd = cli.Command{
secretListCmd, 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, ", ")
},
}

View file

@ -1,13 +1,9 @@
package main package main
import ( import (
"fmt"
"io/ioutil"
"log" "log"
"strings"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/drone/drone/model"
) )
var secretAddCmd = cli.Command{ var secretAddCmd = cli.Command{
@ -19,26 +15,7 @@ var secretAddCmd = cli.Command{
log.Fatalln(err) log.Fatalln(err)
} }
}, },
Flags: []cli.Flag{ Flags: secretAddFlags(),
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",
},
},
} }
func secretAdd(c *cli.Context) error { func secretAdd(c *cli.Context) error {
@ -54,27 +31,9 @@ func secretAdd(c *cli.Context) error {
return nil return nil
} }
secret := &model.Secret{} secret, err := secretParseCmd(tail[0], tail[1], c)
secret.Name = tail[0] if err != nil {
secret.Value = tail[1] return err
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)
} }
client, err := newClient(c) client, err := newClient(c)

View file

@ -2,9 +2,6 @@ package main
import ( import (
"log" "log"
"os"
"strings"
"text/template"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
) )
@ -17,21 +14,7 @@ var secretListCmd = cli.Command{
log.Fatalln(err) log.Fatalln(err)
} }
}, },
Flags: []cli.Flag{ Flags: secretListFlags(),
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 secretList(c *cli.Context) error { func secretList(c *cli.Context) error {
@ -53,35 +36,5 @@ func secretList(c *cli.Context) error {
return err return err
} }
tmpl, err := template.New("_").Funcs(secretFuncMap).Parse(c.String("format") + "\n") return secretDisplayList(secrets, c)
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, ", ")
},
} }

View file

@ -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 := e.Group("/api/repos/:owner/:name")
{ {
repos.POST("", server.PostRepo) repos.POST("", server.PostRepo)

62
server/global_secret.go Normal file
View file

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

View file

@ -146,6 +146,8 @@ type Store interface {
DeleteAgent(*model.Agent) error DeleteAgent(*model.Agent) error
} }
const globalTeamName = "__global__"
// GetUser gets a user by unique ID. // GetUser gets a user by unique ID.
func GetUser(c context.Context, id int64) (*model.User, error) { func GetUser(c context.Context, id int64) (*model.User, error) {
return FromContext(c).GetUser(id) return FromContext(c).GetUser(id)
@ -246,12 +248,30 @@ func DeleteTeamSecret(c context.Context, s *model.TeamSecret) error {
return FromContext(c).DeleteTeamSecret(s) 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) { func GetMergedSecretList(c context.Context, r *model.Repo) ([]*model.Secret, error) {
var ( var (
secrets []*model.Secret secrets []*model.Secret
) )
repoSecs, err := FromContext(c).GetSecretList(r) repoSecs, err := GetSecretList(c, r)
if err != nil { if err != nil {
return nil, err return nil, err
@ -261,7 +281,7 @@ func GetMergedSecretList(c context.Context, r *model.Repo) ([]*model.Secret, err
secrets = append(secrets, secret.Secret()) secrets = append(secrets, secret.Secret())
} }
teamSecs, err := FromContext(c).GetTeamSecretList(r.Owner) teamSecs, err := GetTeamSecretList(c, r.Owner)
if err != nil { if err != nil {
return nil, err return nil, err
@ -271,6 +291,16 @@ func GetMergedSecretList(c context.Context, r *model.Repo) ([]*model.Secret, err
secrets = append(secrets, secret.Secret()) 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 return secrets, nil
} }