enable periodic tokens
This commit is contained in:
parent
580fe9abb7
commit
6d61e57e93
4 changed files with 372 additions and 0 deletions
26
plugins/secrets/vault/opts.go
Normal file
26
plugins/secrets/vault/opts.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2018 Drone.IO Inc
|
||||
// Use of this software is governed by the Drone Enterpise License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package vault
|
||||
|
||||
import "time"
|
||||
|
||||
// Opts sets custom options for the vault client.
|
||||
type Opts func(v *vault)
|
||||
|
||||
// WithTTL returns an options that sets a TTL used to
|
||||
// refresh periodic tokens.
|
||||
func WithTTL(d time.Duration) Opts {
|
||||
return func(v *vault) {
|
||||
v.ttl = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithRenewal returns an options that sets the renewal
|
||||
// period used to refresh periodic tokens
|
||||
func WithRenewal(d time.Duration) Opts {
|
||||
return func(v *vault) {
|
||||
v.renew = d
|
||||
}
|
||||
}
|
28
plugins/secrets/vault/opts_test.go
Normal file
28
plugins/secrets/vault/opts_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2018 Drone.IO Inc
|
||||
// Use of this software is governed by the Drone Enterpise License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWithTTL(t *testing.T) {
|
||||
v := new(vault)
|
||||
opt := WithTTL(time.Hour)
|
||||
opt(v)
|
||||
if got, want := v.ttl, time.Hour; got != want {
|
||||
t.Errorf("Want ttl %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithRenewal(t *testing.T) {
|
||||
v := new(vault)
|
||||
opt := WithRenewal(time.Hour)
|
||||
opt(v)
|
||||
if got, want := v.renew, time.Hour; got != want {
|
||||
t.Errorf("Want renewal %v, got %v", want, got)
|
||||
}
|
||||
}
|
219
plugins/secrets/vault/vault.go
Normal file
219
plugins/secrets/vault/vault.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2018 Drone.IO Inc
|
||||
// Use of this software is governed by the Drone Enterpise License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/drone/drone/extras/secrets"
|
||||
"github.com/drone/drone/model"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// yaml configuration representation
|
||||
//
|
||||
// secrets:
|
||||
// docker_username:
|
||||
// file: path/to/docker/username
|
||||
// docker_password:
|
||||
// file: path/to/docker/password
|
||||
//
|
||||
type vaultConfig struct {
|
||||
Secrets map[string]struct {
|
||||
Path string
|
||||
File string
|
||||
Vault string
|
||||
}
|
||||
}
|
||||
|
||||
type vault struct {
|
||||
store model.ConfigStore
|
||||
client *api.Client
|
||||
ttl time.Duration
|
||||
renew time.Duration
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// New returns a new store with secrets loaded from vault.
|
||||
func New(store model.ConfigStore, opts ...Opts) (secrets.Plugin, error) {
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := &vault{
|
||||
store: store,
|
||||
client: client,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(v)
|
||||
}
|
||||
v.start() // start the refresh process.
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v *vault) SecretListBuild(repo *model.Repo, build *model.Build) ([]*model.Secret, error) {
|
||||
return v.list(repo, build)
|
||||
}
|
||||
|
||||
func (v *vault) list(repo *model.Repo, build *model.Build) ([]*model.Secret, error) {
|
||||
conf, err := v.store.ConfigLoad(build.ConfigID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
in = []byte(conf.Data)
|
||||
out = new(vaultConfig)
|
||||
|
||||
secrets []*model.Secret
|
||||
)
|
||||
err = yaml.Unmarshal(in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, val := range out.Secrets {
|
||||
var path string
|
||||
switch {
|
||||
case val.Path != "":
|
||||
path = val.Path
|
||||
case val.File != "":
|
||||
path = val.File
|
||||
case val.Vault != "":
|
||||
path = val.Vault
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
vaultSecret, err := v.get(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vaultSecret == nil {
|
||||
continue
|
||||
}
|
||||
if !vaultSecret.Match(repo.FullName) {
|
||||
continue
|
||||
}
|
||||
|
||||
secrets = append(secrets, &model.Secret{
|
||||
Name: key,
|
||||
Value: vaultSecret.Value,
|
||||
Events: vaultSecret.Event,
|
||||
Images: vaultSecret.Image,
|
||||
})
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func (v *vault) get(path string) (*vaultSecret, error) {
|
||||
secret, err := v.client.Logical().Read(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return parseVaultSecret(secret.Data), nil
|
||||
}
|
||||
|
||||
// start starts the renewal loop.
|
||||
func (v *vault) start() {
|
||||
if v.renew == 0 || v.ttl == 0 {
|
||||
logrus.Debugf("vault: token renewal disabled")
|
||||
return
|
||||
}
|
||||
if v.done != nil {
|
||||
close(v.done)
|
||||
}
|
||||
logrus.Debugf("vault: token renewal enabled: renew every %v", v.renew)
|
||||
v.done = make(chan struct{})
|
||||
if v.renew != 0 {
|
||||
go v.renewLoop()
|
||||
}
|
||||
}
|
||||
|
||||
// stop stops the renewal loop.
|
||||
func (v *vault) stop() {
|
||||
close(v.done)
|
||||
}
|
||||
|
||||
func (v *vault) renewLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-time.After(v.renew):
|
||||
incr := int(v.ttl / time.Second)
|
||||
|
||||
logrus.Debugf("vault: refreshing token: increment %v", v.ttl)
|
||||
_, err := v.client.Auth().Token().RenewSelf(incr)
|
||||
if err != nil {
|
||||
logrus.Errorf("vault: refreshing token failed: %s", err)
|
||||
} else {
|
||||
logrus.Debugf("vault: refreshing token succeeded")
|
||||
}
|
||||
case <-v.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type vaultSecret struct {
|
||||
Value string
|
||||
Image []string
|
||||
Event []string
|
||||
Repo []string
|
||||
}
|
||||
|
||||
func parseVaultSecret(data map[string]interface{}) *vaultSecret {
|
||||
secret := new(vaultSecret)
|
||||
|
||||
if vvalue, ok := data["value"]; ok {
|
||||
if svalue, ok := vvalue.(string); ok {
|
||||
secret.Value = svalue
|
||||
}
|
||||
}
|
||||
if vimage, ok := data["image"]; ok {
|
||||
if simage, ok := vimage.(string); ok {
|
||||
secret.Image = strings.Split(simage, ",")
|
||||
}
|
||||
}
|
||||
if vevent, ok := data["event"]; ok {
|
||||
if sevent, ok := vevent.(string); ok {
|
||||
secret.Event = strings.Split(sevent, ",")
|
||||
}
|
||||
}
|
||||
if vrepo, ok := data["repo"]; ok {
|
||||
if srepo, ok := vrepo.(string); ok {
|
||||
secret.Repo = strings.Split(srepo, ",")
|
||||
}
|
||||
}
|
||||
if secret.Event == nil {
|
||||
secret.Event = []string{}
|
||||
}
|
||||
if secret.Image == nil {
|
||||
secret.Image = []string{}
|
||||
}
|
||||
if secret.Repo == nil {
|
||||
secret.Repo = []string{}
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
func (v *vaultSecret) Match(name string) bool {
|
||||
if len(v.Repo) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, pattern := range v.Repo {
|
||||
if ok, _ := path.Match(pattern, name); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
99
plugins/secrets/vault/vault_test.go
Normal file
99
plugins/secrets/vault/vault_test.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2018 Drone.IO Inc
|
||||
// Use of this software is governed by the Drone Enterpise License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/kr/pretty"
|
||||
)
|
||||
|
||||
// Use the following snippet to spin up a local vault
|
||||
// server for integration testing:
|
||||
//
|
||||
// docker run --cap-add=IPC_LOCK -e 'VAULT_DEV_ROOT_TOKEN_ID=dummy' -p 8200:8200 vault
|
||||
// export VAULT_ADDR=http://127.0.0.1:8200
|
||||
// export VAULT_TOKEN=dummy
|
||||
|
||||
func TestVaultGet(t *testing.T) {
|
||||
if os.Getenv("VAULT_TOKEN") == "" {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = client.Logical().Write("secret/testing/drone/a", map[string]interface{}{
|
||||
"value": "hello",
|
||||
"image": "golang",
|
||||
"event": "push,pull_request",
|
||||
"repo": "octocat/hello-world,github/*",
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
plugin := vault{client: client}
|
||||
secret, err := plugin.get("secret/testing/drone/a")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if got, want := secret.Value, "hello"; got != want {
|
||||
t.Errorf("Expect secret value %s, got %s", want, got)
|
||||
}
|
||||
|
||||
secret, err = plugin.get("secret/testing/drone/404")
|
||||
if err != nil {
|
||||
t.Errorf("Expect silent failure when secret does not exist, got %s", err)
|
||||
}
|
||||
if secret != nil {
|
||||
t.Errorf("Expect nil secret when path does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultSecretParse(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"value": "password",
|
||||
"event": "push,tag",
|
||||
"image": "plugins/s3,plugins/ec2",
|
||||
"repo": "octocat/hello-world,github/*",
|
||||
}
|
||||
want := vaultSecret{
|
||||
Value: "password",
|
||||
Event: []string{"push", "tag"},
|
||||
Image: []string{"plugins/s3", "plugins/ec2"},
|
||||
Repo: []string{"octocat/hello-world", "github/*"},
|
||||
}
|
||||
got := parseVaultSecret(data)
|
||||
if !reflect.DeepEqual(want, *got) {
|
||||
t.Errorf("Failed read Secret.Data")
|
||||
pretty.Fdiff(os.Stderr, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultSecretMatch(t *testing.T) {
|
||||
secret := vaultSecret{
|
||||
Repo: []string{"octocat/hello-world", "github/*"},
|
||||
}
|
||||
if secret.Match("octocat/*") {
|
||||
t.Errorf("Expect octocat/* does not match")
|
||||
}
|
||||
if !secret.Match("octocat/hello-world") {
|
||||
t.Errorf("Expect octocat/hello-world does match")
|
||||
}
|
||||
if !secret.Match("github/hello-world") {
|
||||
t.Errorf("Expect github/hello-world does match wildcard")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue