add vault/etcd
This commit is contained in:
		
							
								
								
									
										51
									
								
								config/source/etcd/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								config/source/etcd/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | # Etcd Source | ||||||
|  |  | ||||||
|  | The etcd source reads config from etcd key/values | ||||||
|  |  | ||||||
|  | This source supports etcd version 3 and beyond. | ||||||
|  |  | ||||||
|  | ## Etcd Format | ||||||
|  |  | ||||||
|  | The etcd source expects keys under the default prefix `/micro/config` (prefix can be changed) | ||||||
|  |  | ||||||
|  | Values are expected to be JSON | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | // set database | ||||||
|  | etcdctl put /micro/config/database '{"address": "10.0.0.1", "port": 3306}' | ||||||
|  | // set cache | ||||||
|  | etcdctl put /micro/config/cache '{"address": "10.0.0.2", "port": 6379}' | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Keys are split on `/` so access becomes | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | conf.Get("micro", "config", "database") | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## New Source | ||||||
|  |  | ||||||
|  | Specify source with data | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | etcdSource := etcd.NewSource( | ||||||
|  | 	// optionally specify etcd address; default to localhost:8500 | ||||||
|  | 	etcd.WithAddress("10.0.0.10:8500"), | ||||||
|  | 	// optionally specify prefix; defaults to /micro/config | ||||||
|  | 	etcd.WithPrefix("/my/prefix"), | ||||||
|  | 	// optionally strip the provided prefix from the keys, defaults to false | ||||||
|  | 	etcd.StripPrefix(true), | ||||||
|  | ) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Load Source | ||||||
|  |  | ||||||
|  | Load the source into config | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | // Create new config | ||||||
|  | conf := config.NewConfig() | ||||||
|  |  | ||||||
|  | // Load file source | ||||||
|  | conf.Load(etcdSource) | ||||||
|  | ``` | ||||||
							
								
								
									
										134
									
								
								config/source/etcd/etcd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								config/source/etcd/etcd.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | |||||||
|  | package etcd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | 	cetcd "go.etcd.io/etcd/clientv3" | ||||||
|  | 	"go.etcd.io/etcd/mvcc/mvccpb" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Currently a single etcd reader | ||||||
|  | type etcd struct { | ||||||
|  | 	prefix      string | ||||||
|  | 	stripPrefix string | ||||||
|  | 	opts        source.Options | ||||||
|  | 	client      *cetcd.Client | ||||||
|  | 	cerr        error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	DefaultPrefix = "/micro/config/" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *etcd) Read() (*source.ChangeSet, error) { | ||||||
|  | 	if c.cerr != nil { | ||||||
|  | 		return nil, c.cerr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rsp, err := c.client.Get(context.Background(), c.prefix, cetcd.WithPrefix()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if rsp == nil || len(rsp.Kvs) == 0 { | ||||||
|  | 		return nil, fmt.Errorf("source not found: %s", c.prefix) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var kvs []*mvccpb.KeyValue | ||||||
|  | 	for _, v := range rsp.Kvs { | ||||||
|  | 		kvs = append(kvs, (*mvccpb.KeyValue)(v)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data := makeMap(c.opts.Encoder, kvs, c.stripPrefix) | ||||||
|  |  | ||||||
|  | 	b, err := c.opts.Encoder.Encode(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error reading source: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cs := &source.ChangeSet{ | ||||||
|  | 		Timestamp: time.Now(), | ||||||
|  | 		Source:    c.String(), | ||||||
|  | 		Data:      b, | ||||||
|  | 		Format:    c.opts.Encoder.String(), | ||||||
|  | 	} | ||||||
|  | 	cs.Checksum = cs.Sum() | ||||||
|  |  | ||||||
|  | 	return cs, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *etcd) String() string { | ||||||
|  | 	return "etcd" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *etcd) Watch() (source.Watcher, error) { | ||||||
|  | 	if c.cerr != nil { | ||||||
|  | 		return nil, c.cerr | ||||||
|  | 	} | ||||||
|  | 	cs, err := c.Read() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return newWatcher(c.prefix, c.stripPrefix, c.client.Watcher, cs, c.opts) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewSource(opts ...source.Option) source.Source { | ||||||
|  | 	options := source.NewOptions(opts...) | ||||||
|  |  | ||||||
|  | 	var endpoints []string | ||||||
|  |  | ||||||
|  | 	// check if there are any addrs | ||||||
|  | 	addrs, ok := options.Context.Value(addressKey{}).([]string) | ||||||
|  | 	if ok { | ||||||
|  | 		for _, a := range addrs { | ||||||
|  | 			addr, port, err := net.SplitHostPort(a) | ||||||
|  | 			if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { | ||||||
|  | 				port = "2379" | ||||||
|  | 				addr = a | ||||||
|  | 				endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) | ||||||
|  | 			} else if err == nil { | ||||||
|  | 				endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(endpoints) == 0 { | ||||||
|  | 		endpoints = []string{"localhost:2379"} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	config := cetcd.Config{ | ||||||
|  | 		Endpoints: endpoints, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u, ok := options.Context.Value(authKey{}).(*authCreds) | ||||||
|  | 	if ok { | ||||||
|  | 		config.Username = u.Username | ||||||
|  | 		config.Password = u.Password | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// use default config | ||||||
|  | 	client, err := cetcd.New(config) | ||||||
|  |  | ||||||
|  | 	prefix := DefaultPrefix | ||||||
|  | 	sp := "" | ||||||
|  | 	f, ok := options.Context.Value(prefixKey{}).(string) | ||||||
|  | 	if ok { | ||||||
|  | 		prefix = f | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if b, ok := options.Context.Value(stripPrefixKey{}).(bool); ok && b { | ||||||
|  | 		sp = prefix | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &etcd{ | ||||||
|  | 		prefix:      prefix, | ||||||
|  | 		stripPrefix: sp, | ||||||
|  | 		opts:        options, | ||||||
|  | 		client:      client, | ||||||
|  | 		cerr:        err, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								config/source/etcd/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								config/source/etcd/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | package etcd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type addressKey struct{} | ||||||
|  | type prefixKey struct{} | ||||||
|  | type stripPrefixKey struct{} | ||||||
|  | type authKey struct{} | ||||||
|  |  | ||||||
|  | type authCreds struct { | ||||||
|  | 	Username string | ||||||
|  | 	Password string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithAddress sets the consul address | ||||||
|  | func WithAddress(a ...string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, addressKey{}, a) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithPrefix sets the key prefix to use | ||||||
|  | func WithPrefix(p string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, prefixKey{}, p) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StripPrefix indicates whether to remove the prefix from config entries, or leave it in place. | ||||||
|  | func StripPrefix(strip bool) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		o.Context = context.WithValue(o.Context, stripPrefixKey{}, strip) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Auth allows you to specify username/password | ||||||
|  | func Auth(username, password string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, authKey{}, &authCreds{Username: username, Password: password}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										89
									
								
								config/source/etcd/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								config/source/etcd/util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | package etcd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/config/encoder" | ||||||
|  | 	"go.etcd.io/etcd/clientv3" | ||||||
|  | 	"go.etcd.io/etcd/mvcc/mvccpb" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func makeEvMap(e encoder.Encoder, data map[string]interface{}, kv []*clientv3.Event, stripPrefix string) map[string]interface{} { | ||||||
|  | 	if data == nil { | ||||||
|  | 		data = make(map[string]interface{}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, v := range kv { | ||||||
|  | 		switch mvccpb.Event_EventType(v.Type) { | ||||||
|  | 		case mvccpb.DELETE: | ||||||
|  | 			data = update(e, data, (*mvccpb.KeyValue)(v.Kv), "delete", stripPrefix) | ||||||
|  | 		default: | ||||||
|  | 			data = update(e, data, (*mvccpb.KeyValue)(v.Kv), "insert", stripPrefix) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return data | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func makeMap(e encoder.Encoder, kv []*mvccpb.KeyValue, stripPrefix string) map[string]interface{} { | ||||||
|  | 	data := make(map[string]interface{}) | ||||||
|  |  | ||||||
|  | 	for _, v := range kv { | ||||||
|  | 		data = update(e, data, v, "put", stripPrefix) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return data | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func update(e encoder.Encoder, data map[string]interface{}, v *mvccpb.KeyValue, action, stripPrefix string) map[string]interface{} { | ||||||
|  | 	// remove prefix if non empty, and ensure leading / is removed as well | ||||||
|  | 	vkey := strings.TrimPrefix(strings.TrimPrefix(string(v.Key), stripPrefix), "/") | ||||||
|  | 	// split on prefix | ||||||
|  | 	haveSplit := strings.Contains(vkey, "/") | ||||||
|  | 	keys := strings.Split(vkey, "/") | ||||||
|  |  | ||||||
|  | 	var vals interface{} | ||||||
|  | 	e.Decode(v.Value, &vals) | ||||||
|  |  | ||||||
|  | 	if !haveSplit && len(keys) == 1 { | ||||||
|  | 		switch action { | ||||||
|  | 		case "delete": | ||||||
|  | 			data = make(map[string]interface{}) | ||||||
|  | 		default: | ||||||
|  | 			v, ok := vals.(map[string]interface{}) | ||||||
|  | 			if ok { | ||||||
|  | 				data = v | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return data | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// set data for first iteration | ||||||
|  | 	kvals := data | ||||||
|  | 	// iterate the keys and make maps | ||||||
|  | 	for i, k := range keys { | ||||||
|  | 		kval, ok := kvals[k].(map[string]interface{}) | ||||||
|  | 		if !ok { | ||||||
|  | 			// create next map | ||||||
|  | 			kval = make(map[string]interface{}) | ||||||
|  | 			// set it | ||||||
|  | 			kvals[k] = kval | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// last key: write vals | ||||||
|  | 		if l := len(keys) - 1; i == l { | ||||||
|  | 			switch action { | ||||||
|  | 			case "delete": | ||||||
|  | 				delete(kvals, k) | ||||||
|  | 			default: | ||||||
|  | 				kvals[k] = vals | ||||||
|  | 			} | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// set kvals for next iterator | ||||||
|  | 		kvals = kval | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return data | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								config/source/etcd/watcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								config/source/etcd/watcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | package etcd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | 	cetcd "go.etcd.io/etcd/clientv3" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type watcher struct { | ||||||
|  | 	opts        source.Options | ||||||
|  | 	name        string | ||||||
|  | 	stripPrefix string | ||||||
|  |  | ||||||
|  | 	sync.RWMutex | ||||||
|  | 	cs *source.ChangeSet | ||||||
|  |  | ||||||
|  | 	ch   chan *source.ChangeSet | ||||||
|  | 	exit chan bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newWatcher(key, strip string, wc cetcd.Watcher, cs *source.ChangeSet, opts source.Options) (source.Watcher, error) { | ||||||
|  | 	w := &watcher{ | ||||||
|  | 		opts:        opts, | ||||||
|  | 		name:        "etcd", | ||||||
|  | 		stripPrefix: strip, | ||||||
|  | 		cs:          cs, | ||||||
|  | 		ch:          make(chan *source.ChangeSet), | ||||||
|  | 		exit:        make(chan bool), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ch := wc.Watch(context.Background(), key, cetcd.WithPrefix()) | ||||||
|  |  | ||||||
|  | 	go w.run(wc, ch) | ||||||
|  |  | ||||||
|  | 	return w, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *watcher) handle(evs []*cetcd.Event) { | ||||||
|  | 	w.RLock() | ||||||
|  | 	data := w.cs.Data | ||||||
|  | 	w.RUnlock() | ||||||
|  |  | ||||||
|  | 	var vals map[string]interface{} | ||||||
|  |  | ||||||
|  | 	// unpackage existing changeset | ||||||
|  | 	if err := w.opts.Encoder.Decode(data, &vals); err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// update base changeset | ||||||
|  | 	d := makeEvMap(w.opts.Encoder, vals, evs, w.stripPrefix) | ||||||
|  |  | ||||||
|  | 	// pack the changeset | ||||||
|  | 	b, err := w.opts.Encoder.Encode(d) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// create new changeset | ||||||
|  | 	cs := &source.ChangeSet{ | ||||||
|  | 		Timestamp: time.Now(), | ||||||
|  | 		Source:    w.name, | ||||||
|  | 		Data:      b, | ||||||
|  | 		Format:    w.opts.Encoder.String(), | ||||||
|  | 	} | ||||||
|  | 	cs.Checksum = cs.Sum() | ||||||
|  |  | ||||||
|  | 	// set base change set | ||||||
|  | 	w.Lock() | ||||||
|  | 	w.cs = cs | ||||||
|  | 	w.Unlock() | ||||||
|  |  | ||||||
|  | 	// send update | ||||||
|  | 	w.ch <- cs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *watcher) run(wc cetcd.Watcher, ch cetcd.WatchChan) { | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case rsp, ok := <-ch: | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			w.handle(rsp.Events) | ||||||
|  | 		case <-w.exit: | ||||||
|  | 			wc.Close() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *watcher) Next() (*source.ChangeSet, error) { | ||||||
|  | 	select { | ||||||
|  | 	case cs := <-w.ch: | ||||||
|  | 		return cs, nil | ||||||
|  | 	case <-w.exit: | ||||||
|  | 		return nil, errors.New("watcher stopped") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *watcher) Stop() error { | ||||||
|  | 	select { | ||||||
|  | 	case <-w.exit: | ||||||
|  | 		return nil | ||||||
|  | 	default: | ||||||
|  | 		close(w.exit) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								config/source/vault/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								config/source/vault/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | # Vault Source | ||||||
|  |  | ||||||
|  | The vault source reads config from different secret engines in a Vault server. For example: | ||||||
|  | ``` | ||||||
|  | kv: secret/data/<my/secret> | ||||||
|  | database credentials: database/creds/<my-db-role> | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## New Source | ||||||
|  |  | ||||||
|  | Specify source with data | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | vaultSource := vault.NewSource( | ||||||
|  | 	// mandatory: it specifies server address.  | ||||||
|  | 	// It could have different formats: | ||||||
|  | 	// 127.0.0.1 -> https://127.0.0.1:8200 | ||||||
|  | 	// http://127.0.0.1 -> http://127.0.0.1:8200 | ||||||
|  | 	// http://127.0.0.1:2233 | ||||||
|  | 	vault.WithAddress("http://127.0.0.1:8200"), | ||||||
|  | 	// mandatory: it specifies a resource to been access | ||||||
|  | 	vault.WithResourcePath("secret/data/my/secret"), | ||||||
|  |     // mandatory: it specifies a resource to been access | ||||||
|  | 	vault.WithToken("<my-token>"), | ||||||
|  | 	// optional: path to store my secret. | ||||||
|  | 	// By default use resourcePath value  | ||||||
|  | 	vault.WithSecretName("my/secret"), | ||||||
|  | 	// optional: namespace. | ||||||
|  |     vault.WithNameSpace("myNameSpace"), | ||||||
|  | ) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Load Source | ||||||
|  |  | ||||||
|  | Load the source into config | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | // Create new config | ||||||
|  | conf := config.NewConfig() | ||||||
|  |  | ||||||
|  | // Load file source | ||||||
|  | conf.Load(vaultSource) | ||||||
|  | ``` | ||||||
							
								
								
									
										63
									
								
								config/source/vault/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								config/source/vault/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | package vault | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type addressKey struct{} | ||||||
|  | type resourcePath struct{} | ||||||
|  | type nameSpace struct{} | ||||||
|  | type tokenKey struct{} | ||||||
|  | type secretName struct{} | ||||||
|  |  | ||||||
|  | // WithAddress sets the server address | ||||||
|  | func WithAddress(a string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, addressKey{}, a) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithResourcePath sets the resource that will be access | ||||||
|  | func WithResourcePath(p string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, resourcePath{}, p) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithNameSpace sets the namespace that its going to be access | ||||||
|  | func WithNameSpace(n string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, nameSpace{}, n) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithToken sets the key token to use | ||||||
|  | func WithToken(t string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, tokenKey{}, t) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithSecretName sets the name of the secret to wrap in on a map | ||||||
|  | func WithSecretName(t string) source.Option { | ||||||
|  | 	return func(o *source.Options) { | ||||||
|  | 		if o.Context == nil { | ||||||
|  | 			o.Context = context.Background() | ||||||
|  | 		} | ||||||
|  | 		o.Context = context.WithValue(o.Context, secretName{}, t) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								config/source/vault/testdata/vault_init_commands
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								config/source/vault/testdata/vault_init_commands
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | vault kv put secret/data/db/auth user=myuser password=mypassword2 host=128.23.33.21 port=3307 | ||||||
							
								
								
									
										98
									
								
								config/source/vault/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								config/source/vault/util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | package vault | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | 	"net" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func makeMap(kv map[string]interface{}, secretName string) (map[string]interface{}, error) { | ||||||
|  | 	data := make(map[string]interface{}) | ||||||
|  |  | ||||||
|  | 	// if secret version included | ||||||
|  | 	if kv["data"] != nil && kv["metadata"] != nil { | ||||||
|  | 		kv = kv["data"].(map[string]interface{}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	target := data | ||||||
|  |  | ||||||
|  | 	// if secretName defined, wrap secrets under a map | ||||||
|  | 	if secretName != "" { | ||||||
|  | 		path := strings.Split(secretName, "/") | ||||||
|  | 		// find (or create) the location we want to put this value at | ||||||
|  | 		for i, dir := range path { | ||||||
|  | 			if _, ok := target[dir]; !ok { | ||||||
|  | 				target[dir] = make(map[string]interface{}) | ||||||
|  | 			} | ||||||
|  | 			if i < len(path)-1 { | ||||||
|  | 				target = target[dir].(map[string]interface{}) | ||||||
|  | 			} else { | ||||||
|  | 				target[dir] = kv | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return data, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getAddress(options source.Options) string { | ||||||
|  | 	// check if there are any addrs | ||||||
|  | 	a, ok := options.Context.Value(addressKey{}).(string) | ||||||
|  | 	if ok { | ||||||
|  | 		// check if http protocol is defined | ||||||
|  | 		if a[0] != 'h' { | ||||||
|  | 			addr, port, err := net.SplitHostPort(a) | ||||||
|  | 			if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { | ||||||
|  | 				port = "8200" | ||||||
|  | 				addr = a | ||||||
|  | 				return fmt.Sprintf("https://%s:%s", addr, port) | ||||||
|  | 			} else if err == nil { | ||||||
|  | 				return fmt.Sprintf("https://%s:%s", addr, port) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			u, _ := url.Parse(a) | ||||||
|  |  | ||||||
|  | 			if host, port, _ := net.SplitHostPort(u.Host); host == "" { | ||||||
|  | 				port = "8200" | ||||||
|  | 				return fmt.Sprintf("%s://%s:%s", u.Scheme, u.Host, port) | ||||||
|  | 			} else { | ||||||
|  | 				return fmt.Sprintf("%s://%s", u.Scheme, u.Host) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getToken(options source.Options) string { | ||||||
|  | 	token, ok := options.Context.Value(tokenKey{}).(string) | ||||||
|  | 	if ok { | ||||||
|  | 		return token | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getResourcePath(options source.Options) string { | ||||||
|  | 	path, ok := options.Context.Value(resourcePath{}).(string) | ||||||
|  | 	if ok { | ||||||
|  | 		return path | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getNameSpace(options source.Options) string { | ||||||
|  | 	ns, ok := options.Context.Value(nameSpace{}).(string) | ||||||
|  | 	if ok { | ||||||
|  | 		return ns | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getSecretName(options source.Options) string { | ||||||
|  | 	ns, ok := options.Context.Value(secretName{}).(string) | ||||||
|  | 	if ok { | ||||||
|  | 		return ns | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
							
								
								
									
										96
									
								
								config/source/vault/vault.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								config/source/vault/vault.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | package vault | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/hashicorp/vault/api" | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Currently a single vault reader | ||||||
|  | type vault struct { | ||||||
|  | 	secretPath string | ||||||
|  | 	secretName string | ||||||
|  | 	opts       source.Options | ||||||
|  | 	client     *api.Client | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *vault) Read() (*source.ChangeSet, error) { | ||||||
|  | 	secret, err := c.client.Logical().Read(c.secretPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if secret == nil { | ||||||
|  | 		return nil, fmt.Errorf("source not found: %s", c.secretPath) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if secret.Data == nil && secret.Warnings != nil { | ||||||
|  | 		return nil, fmt.Errorf("source: %s errors: %v", c.secretPath, secret.Warnings) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := makeMap(secret.Data, c.secretName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error reading data: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	b, err := c.opts.Encoder.Encode(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error reading source: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cs := &source.ChangeSet{ | ||||||
|  | 		Timestamp: time.Now(), | ||||||
|  | 		Format:    c.opts.Encoder.String(), | ||||||
|  | 		Source:    c.String(), | ||||||
|  | 		Data:      b, | ||||||
|  | 	} | ||||||
|  | 	cs.Checksum = cs.Sum() | ||||||
|  |  | ||||||
|  | 	return cs, nil | ||||||
|  | 	//return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *vault) String() string { | ||||||
|  | 	return "vault" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *vault) Watch() (source.Watcher, error) { | ||||||
|  | 	w := newWatcher(c.client) | ||||||
|  |  | ||||||
|  | 	return w, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewSource creates a new vault source | ||||||
|  | func NewSource(opts ...source.Option) source.Source { | ||||||
|  | 	options := source.NewOptions(opts...) | ||||||
|  |  | ||||||
|  | 	// create the client | ||||||
|  | 	client, _ := api.NewClient(api.DefaultConfig()) | ||||||
|  |  | ||||||
|  | 	// get and set options | ||||||
|  | 	if address := getAddress(options); address != "" { | ||||||
|  | 		_ = client.SetAddress(address) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if nameSpace := getNameSpace(options); nameSpace != "" { | ||||||
|  | 		client.SetNamespace(nameSpace) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if token := getToken(options); token != "" { | ||||||
|  | 		client.SetToken(token) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	path := getResourcePath(options) | ||||||
|  | 	name := getSecretName(options) | ||||||
|  | 	if name == "" { | ||||||
|  | 		name = path | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &vault{ | ||||||
|  | 		opts:       options, | ||||||
|  | 		client:     client, | ||||||
|  | 		secretPath: path, | ||||||
|  | 		secretName: name, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										133
									
								
								config/source/vault/vault_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								config/source/vault/vault_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | package vault | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/micro/go-micro/config" | ||||||
|  | 	"os" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestVaultMakeMap(t *testing.T) { | ||||||
|  | 	tt := []struct { | ||||||
|  | 		name       string | ||||||
|  | 		expected   []byte | ||||||
|  | 		input      []byte | ||||||
|  | 		secretName string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:       "simple valid data 1", | ||||||
|  | 			secretName: "my/secret", | ||||||
|  | 			input:      []byte(`{"data":{"bar":"bazz", "tar":"par"}, "metadata":{"version":1, "destroyed": false}}`), | ||||||
|  | 			expected:   []byte(`{"my":{"secret":{"bar":"bazz", "tar":"par"}}}`), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:       "simple valid data 2", | ||||||
|  | 			secretName: "my/secret", | ||||||
|  | 			input:      []byte(`{"bar":"bazz", "tar":"par"}`), | ||||||
|  | 			expected:   []byte(`{"my":{"secret":{"bar":"bazz", "tar":"par"}}}`), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range tt { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			var input map[string]interface{} | ||||||
|  | 			var expected map[string]interface{} | ||||||
|  |  | ||||||
|  | 			_ = json.Unmarshal(tc.input, &input) | ||||||
|  | 			_ = json.Unmarshal(tc.expected, &expected) | ||||||
|  |  | ||||||
|  | 			out, _ := makeMap(input, tc.secretName) | ||||||
|  |  | ||||||
|  | 			if eq := reflect.DeepEqual(out, expected); !eq { | ||||||
|  | 				fmt.Println(eq) | ||||||
|  | 				t.Fatalf("expected %v and got %v", expected, out) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestVault_Read(t *testing.T) { | ||||||
|  | 	if tr := os.Getenv("TRAVIS"); len(tr) > 0 { | ||||||
|  | 		t.Skip() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		address = "http://127.0.0.1" | ||||||
|  | 		resource = "secret/data/db/auth" | ||||||
|  | 		token = "s.Q4Zi0CSowXZl7sh0z96ijcT4" | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	data := []byte(`{"secret":{"data":{"db":{"auth":{"host":"128.23.33.21","password":"mypassword","port":"3306","user":"myuser"}}}}}`) | ||||||
|  |  | ||||||
|  | 	tt := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		addr     string | ||||||
|  | 		resource string | ||||||
|  | 		token string | ||||||
|  | 	}{ | ||||||
|  | 		{name: "read data basic", addr: address, resource: resource, token: token}, | ||||||
|  | 		{name: "read data without token", addr: address, resource: resource, token: ""}, | ||||||
|  | 		{name: "read data full address format", addr: "http://127.0.0.1:8200", resource: resource, token: token}, | ||||||
|  | 		{name: "read data wrong resource path", addr: address, resource: "secrets/data/db/auth", token: token}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range tt { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			source := NewSource( | ||||||
|  | 				WithAddress(tc.addr), | ||||||
|  | 				WithResourcePath(tc.resource), | ||||||
|  | 				WithToken(tc.token), | ||||||
|  | 			) | ||||||
|  |  | ||||||
|  | 			r, err := source.Read() | ||||||
|  | 			if err != nil { | ||||||
|  | 				if tc.token == "" { | ||||||
|  | 					return | ||||||
|  | 				} else if strings.Compare(err.Error(), "source not found: secrets/data/db/auth") == 0 { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				t.Errorf("%s: not able to read the config values because: %v", tc.name, err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if string(r.Data) != string(data) { | ||||||
|  | 				t.Logf("data expected: %v", string(data)) | ||||||
|  | 				t.Logf("data got from configmap: %v", string(r.Data)) | ||||||
|  | 				t.Errorf("data from configmap does not match.") | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestVault_String(t *testing.T) { | ||||||
|  | 	source := NewSource() | ||||||
|  |  | ||||||
|  | 	if source.String() != "vault" { | ||||||
|  | 		t.Errorf("expecting to get %v and instead got %v", "vault", source) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestVaultNewSource(t *testing.T) { | ||||||
|  | 	if tr := os.Getenv("TRAVIS"); len(tr) > 0 { | ||||||
|  | 		t.Skip() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	conf := config.NewConfig() | ||||||
|  |  | ||||||
|  | 	_ = conf.Load(NewSource( | ||||||
|  | 		WithAddress("http://127.0.0.1"), | ||||||
|  | 		WithResourcePath("secret/data/db/auth"), | ||||||
|  | 		WithToken("s.Q4Zi0CSowXZl7sh0z96ijcT4"), | ||||||
|  | 	)) | ||||||
|  |  | ||||||
|  | 	if user := conf.Get("secret", "data", "db", "auth", "user").String("user"); user != "myuser" { | ||||||
|  | 		t.Errorf("expected %v and got %v", "myuser", user) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if addr := conf.Get("secret", "data", "db", "auth", "host").String("host"); addr != "128.23.33.21" { | ||||||
|  | 		t.Errorf("expected %v and got %v", "128.23.33.21", addr) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								config/source/vault/watcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								config/source/vault/watcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package vault | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"github.com/hashicorp/vault/api" | ||||||
|  | 	"github.com/micro/go-micro/config/source" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type watcher struct { | ||||||
|  | 	c    *api.Client | ||||||
|  | 	exit chan bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newWatcher(c *api.Client) *watcher { | ||||||
|  | 	return &watcher{ | ||||||
|  | 		c:    c, | ||||||
|  | 		exit: make(chan bool), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *watcher) Next() (*source.ChangeSet, error) { | ||||||
|  | 	<-w.exit | ||||||
|  | 	return nil, errors.New("url watcher stopped") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *watcher) Stop() error { | ||||||
|  | 	select { | ||||||
|  | 	case <-w.exit: | ||||||
|  | 	default: | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user