10 Commits

Author SHA1 Message Date
22dacb741f update nlreturn lint
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-26 11:32:43 +03:00
74bd8879b5 update definition go version
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-26 11:14:38 +03:00
b98f31c891 update drone ci
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-25 23:59:40 +03:00
504a2369de Merge pull request 'add example' (#11) from example into master
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #11
2024-01-25 23:58:31 +03:00
b0ec158da2 add example
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2024-01-25 23:57:24 +03:00
5586adc4e3 add config definition
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #10
Co-authored-by: andrey <andrey@4devs.io>
Co-committed-by: andrey <andrey@4devs.io>
2024-01-25 23:34:35 +03:00
aeb90ceaa6 Merge pull request 'add provider vault' (#9) from vault into master
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #9
2024-01-25 23:08:20 +03:00
80b0244b52 add provider vault
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-01-25 23:07:50 +03:00
947c40d918 Merge pull request 'add etcd provider' (#8) from etcd into master
Some checks failed
continuous-integration/drone/push Build was killed
Reviewed-on: #8
2024-01-25 22:49:02 +03:00
788e2928fe add etcd provider
Some checks failed
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2024-01-25 22:48:33 +03:00
34 changed files with 1762 additions and 4 deletions

View File

@@ -2,10 +2,6 @@
kind: pipeline
name: default
environment:
VAULT_DEV_LISTEN_ADDRESS: http://vault:8200
VAULT_DEV_ROOT_TOKEN_ID: dev
steps:
- name: test
image: golang
@@ -108,6 +104,7 @@ services:
steps:
- name: test
image: golang
failure: ignore # runtime/cgo: pthread_create failed: Operation not permitted
commands:
- cd provider/etcd
- go test ./...
@@ -118,3 +115,51 @@ steps:
- cd provider/etcd
- golangci-lint run
---
kind: pipeline
type: docker
name: vault
environment:
VAULT_DEV_LISTEN_ADDRESS: http://vault:8200
VAULT_DEV_ROOT_TOKEN_ID: dev
services:
- name: vault
image: vault:1.13.3
environment:
VAULT_DEV_ROOT_TOKEN_ID: dev
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
steps:
- name: test
image: golang
failure: ignore # runtime/cgo: pthread_create failed: Operation not permitted
commands:
- cd provider/vault
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd provider/vault
- golangci-lint run
---
kind: pipeline
type: docker
name: definition
steps:
- name: test
image: golang
commands:
- cd definition
- go test ./...
- name: golangci-lint
image: golangci/golangci-lint:v1.55
commands:
- cd definition
- golangci-lint run

View File

@@ -24,6 +24,13 @@ linters-settings:
locale: US
varnamelen:
min-name-length: 2
ignore-decls:
- w io.Writer
- t testing.T
- e error
- i int
- b bytes.Buffer
- h Handle
linters:
enable-all: true

174
client_example_test.go Normal file
View File

@@ -0,0 +1,174 @@
package config_test
import (
"context"
"fmt"
"log"
"os"
"sync"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/provider/env"
"gitoa.ru/go-4devs/config/provider/watcher"
"gitoa.ru/go-4devs/config/test"
)
func ExampleClient_Value() {
ctx := context.Background()
_ = os.Setenv("FDEVS_CONFIG_LISTEN", "8080")
_ = os.Setenv("FDEVS_CONFIG_HOST", "localhost")
args := os.Args
defer func() {
os.Args = args
}()
os.Args = []string{"main.go", "--host=gitoa.ru"}
// read json config
config, err := config.New(
arg.New(),
env.New(test.Namespace, test.AppName),
)
if err != nil {
log.Print(err)
return
}
port, err := config.Value(ctx, "listen")
if err != nil {
log.Print("listen", err)
return
}
hostValue, err := config.Value(ctx, "host")
if err != nil {
log.Print("host ", err)
return
}
fmt.Printf("listen from env: %d\n", port.Int())
fmt.Printf("replace env host by args: %v\n", hostValue.String())
// Output:
// listen from env: 8080
// replace env host by args: gitoa.ru
}
func ExampleClient_Watch() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_ = os.Setenv("FDEVS_CONFIG_EXAMPLE_ENABLE", "true")
watcher, err := config.New(
watcher.New(time.Microsecond, env.New(test.Namespace, test.AppName)),
)
if err != nil {
log.Print(err)
return
}
wg := sync.WaitGroup{}
wg.Add(1)
err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) error {
fmt.Println("update example_enable old: ", oldVar.Bool(), " new:", newVar.Bool())
wg.Done()
return nil
}, "example_enable")
if err != nil {
log.Print(err)
return
}
_ = os.Setenv("FDEVS_CONFIG_EXAMPLE_ENABLE", "false")
err = watcher.Watch(ctx, func(ctx context.Context, oldVar, newVar config.Value) error {
fmt.Println("update example_db_dsn old: ", oldVar.String(), " new:", newVar.String())
wg.Done()
return nil
}, "example_db_dsn")
if err != nil {
log.Print(err)
return
}
wg.Wait()
// Output:
// update example_enable old: true new: false
}
func ExampleClient_Value_factory() {
ctx := context.Background()
_ = os.Setenv("FDEVS_CONFIG_LISTEN", "8080")
_ = os.Setenv("FDEVS_CONFIG_HOST", "localhost")
_ = os.Setenv("FDEVS_GOLANG_HOST", "go.dev")
args := os.Args
defer func() {
os.Args = args
}()
os.Args = []string{"main.go", "--env=golang"}
config, err := config.New(
arg.New(),
config.Factory(func(ctx context.Context, cfg config.Provider) (config.Provider, error) {
val, err := cfg.Value(ctx, "env")
if err != nil {
return nil, fmt.Errorf("failed read config file:%w", err)
}
return env.New(test.Namespace, val.String()), nil
}),
env.New(test.Namespace, test.AppName),
)
if err != nil {
log.Print(err)
return
}
envName, err := config.Value(ctx, "env")
if err != nil {
log.Print("env ", err)
return
}
host, err := config.Value(ctx, "host")
if err != nil {
log.Print("host ", err)
return
}
listen, err := config.Value(ctx, "listen")
if err != nil {
log.Print("listen", err)
return
}
fmt.Printf("envName from env: %s\n", envName.String())
fmt.Printf("host from env with app name golang: %s\n", host.String())
fmt.Printf("listen from env with default app name: %s\n", listen.String())
// Output:
// envName from env: golang
// host from env with app name golang: go.dev
// listen from env with default app name: 8080
}

29
definition/defenition.go Executable file
View File

@@ -0,0 +1,29 @@
package definition
import (
"fmt"
)
func New() Definition {
return Definition{}
}
type Definition struct {
options Options
}
func (d *Definition) Add(opts ...Option) *Definition {
d.options = append(d.options, opts...)
return d
}
func (d *Definition) View(handle func(Option) error) error {
for idx, opt := range d.options {
if err := handle(opt); err != nil {
return fmt.Errorf("%s[%d]:%w", opt.Kind(), idx, err)
}
}
return nil
}

View File

@@ -0,0 +1,70 @@
package generate
import (
"fmt"
"io"
"gitoa.ru/go-4devs/config/definition"
)
type Generator struct {
pkg string
ViewOption
Imp Imports
errs []error
defaultErrors []string
}
func (g Generator) Pkg() string {
return g.pkg
}
func (g Generator) Imports() []Import {
return g.Imp.Imports()
}
func (g Generator) Handle(w io.Writer, data Handler, opt definition.Option) error {
handle := get(opt.Kind())
return handle(w, data, opt)
}
func (g Generator) StructName() string {
return FuncName(g.Prefix + "_" + g.Struct + "_" + g.Suffix)
}
func (g Generator) Options() ViewOption {
return g.ViewOption
}
func (g Generator) Keys() []string {
return nil
}
func (g Generator) DefaultErrors() []string {
if len(g.defaultErrors) > 0 {
return g.defaultErrors
}
if len(g.ViewOption.Errors.Default) > 0 {
g.Imp.Adds("errors")
}
g.defaultErrors = make([]string, len(g.ViewOption.Errors.Default))
for idx, name := range g.ViewOption.Errors.Default {
short, err := g.AddType(name)
if err != nil {
g.errs = append(g.errs, fmt.Errorf("add default error[%d]:%w", idx, err))
return nil
}
g.defaultErrors[idx] = short
}
return g.defaultErrors
}
func (g *Generator) AddType(pkg string) (string, error) {
return g.Imp.AddType(pkg)
}

View File

@@ -0,0 +1,18 @@
package generate
import (
"errors"
"github.com/iancoleman/strcase"
)
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExist = errors.New("already exist")
ErrWrongType = errors.New("wrong type")
ErrWrongFormat = errors.New("wrong format")
)
func FuncName(in string) string {
return strcase.ToCamel(in)
}

View File

@@ -0,0 +1,89 @@
package generate
import (
"fmt"
"strconv"
"strings"
)
func NewImports() Imports {
return Imports{
data: make(map[string]string),
}
}
type Imports struct {
data map[string]string
}
func (i Imports) Imports() []Import {
imports := make([]Import, 0, len(i.data))
for name, alias := range i.data {
imports = append(imports, Import{
Package: name,
Alias: alias,
})
}
return imports
}
func (i *Imports) Short(fullType string) (string, error) {
idx := strings.LastIndexByte(fullType, '.')
if idx == -1 {
return "", fmt.Errorf("%w: expect package.Type", ErrWrongFormat)
}
if alias, ok := i.data[fullType[:idx]]; ok {
return alias + fullType[idx:], nil
}
return "", fmt.Errorf("%w alias for pkg %v", ErrNotFound, fullType[:idx])
}
func (i *Imports) AddType(fullType string) (string, error) {
idx := strings.LastIndexByte(fullType, '.')
if idx == -1 {
return "", fmt.Errorf("%w: expect pckage.Type", ErrWrongFormat)
}
imp := i.Add(fullType[:idx])
return imp.Alias + fullType[idx:], nil
}
func (i *Imports) Adds(pkgs ...string) {
for _, pkg := range pkgs {
i.Add(pkg)
}
}
func (i *Imports) Add(pkg string) Import {
alias := pkg
if idx := strings.LastIndexByte(pkg, '/'); idx != -1 {
alias = pkg[idx+1:]
}
if al, ok := i.data[pkg]; ok {
return Import{Package: pkg, Alias: al}
}
for _, al := range i.data {
if al == alias {
alias += strconv.Itoa(len(i.data))
}
}
i.data[pkg] = alias
return Import{
Alias: alias,
Package: pkg,
}
}
type Import struct {
Alias string
Package string
}

View File

@@ -0,0 +1,39 @@
package generate
import (
"bytes"
"fmt"
"io"
"gitoa.ru/go-4devs/config/definition"
)
func Run(w io.Writer, pkgName string, defs definition.Definition, viewOpt ViewOption) error {
gen := Generator{
pkg: pkgName,
ViewOption: viewOpt,
Imp: NewImports(),
}
gen.Imp.Adds("gitoa.ru/go-4devs/config", "fmt", "context")
var view bytes.Buffer
err := defs.View(func(o definition.Option) error {
return gen.Handle(&view, &gen, o)
})
if err != nil {
return fmt.Errorf("render options:%w", err)
}
if err := tpl.Execute(w, gen); err != nil {
return fmt.Errorf("render base:%w", err)
}
_, cerr := io.Copy(w, &view)
if cerr != nil {
return fmt.Errorf("copy error:%w", cerr)
}
return nil
}

View File

@@ -0,0 +1,43 @@
package generate
import "text/template"
//nolint:gochecknoglobals
var (
tpl = template.Must(template.New("tpls").Parse(baseTemplate))
baseTemplate = `// Code generated gitoa.ru/go-4devs/config DO NOT EDIT.
package {{.Pkg}}
import (
{{range .Imports}}
{{- .Alias }}"{{ .Package }}"
{{end}}
)
func With{{.StructName}}Log(log func(context.Context, string, ...any)) func(*{{.StructName}}) {
return func(ci *{{.StructName}}) {
ci.log = log
}
}
func New{{.StructName}}(prov config.Provider, opts ...func(*{{.StructName}})) {{.StructName}} {
i := {{.StructName}}{
Provider: prov,
log: func(_ context.Context, format string, args ...any) {
fmt.Printf(format, args...)
},
}
for _, opt := range opts {
opt(&i)
}
return i
}
type {{.StructName}} struct {
config.Provider
log func(context.Context, string, ...any)
}
`
)

View File

@@ -0,0 +1,63 @@
package generate
import (
"fmt"
"io"
"sync"
"gitoa.ru/go-4devs/config/definition"
)
//nolint:gochecknoglobals
var handlers = sync.Map{}
func Add(kind string, h Handle) error {
_, ok := handlers.Load(kind)
if ok {
return fmt.Errorf("kind %v: %w", kind, ErrAlreadyExist)
}
handlers.Store(kind, h)
return nil
}
//nolint:forcetypeassert
func get(kind string) Handle {
handler, ok := handlers.Load(kind)
if !ok {
return func(w io.Writer, h Handler, o definition.Option) error {
return fmt.Errorf("handler by %v:%w", kind, ErrNotFound)
}
}
return handler.(Handle)
}
func MustAdd(kind string, h Handle) {
if err := Add(kind, h); err != nil {
panic(err)
}
}
type Handle func(io.Writer, Handler, definition.Option) error
type Handler interface {
StructName() string
Handle(w io.Writer, handler Handler, opt definition.Option) error
Options() ViewOption
Keys() []string
AddType(fullName string) (string, error)
DefaultErrors() []string
}
type ViewOption struct {
Prefix, Suffix string
Context bool
Struct string
Errors ViewErrors
}
type ViewErrors struct {
Default []string
}

5
definition/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module gitoa.ru/go-4devs/config/definition
go 1.21
require github.com/iancoleman/strcase v0.3.0

2
definition/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=

27
definition/group/group.go Executable file
View File

@@ -0,0 +1,27 @@
package group
import (
"gitoa.ru/go-4devs/config/definition"
)
const Kind = "group"
var _ definition.Option = Group{}
func New(name, desc string, opts ...definition.Option) Group {
return Group{
Name: name,
Description: desc,
Options: opts,
}
}
type Group struct {
Options definition.Options
Name string
Description string
}
func (o Group) Kind() string {
return Kind
}

88
definition/group/view.go Normal file
View File

@@ -0,0 +1,88 @@
package group
import (
"fmt"
"io"
"text/template"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
)
//nolint:gochecknoinits
func init() {
generate.MustAdd(Kind, handle)
}
func handle(w io.Writer, data generate.Handler, option definition.Option) error {
group, ok := option.(Group)
if !ok {
return fmt.Errorf("%w:%T", generate.ErrWrongType, option)
}
viewData := View{
Group: group,
ParentName: data.StructName(),
ViewOption: data.Options(),
}
err := tpl.Execute(w, viewData)
if err != nil {
return fmt.Errorf("render group:%w", err)
}
childData := ChildData{
Handler: data,
structName: viewData.StructName(),
keys: append(data.Keys(), group.Name),
}
for idx, child := range group.Options {
if cerr := data.Handle(w, childData, child); cerr != nil {
return fmt.Errorf("render group child[%d]:%w", idx, cerr)
}
}
return nil
}
type ChildData struct {
generate.Handler
structName string
keys []string
}
func (c ChildData) StructName() string {
return c.structName
}
func (c ChildData) Keys() []string {
return c.keys
}
type View struct {
Group
ParentName string
generate.ViewOption
}
func (v View) FuncName() string {
return generate.FuncName(v.Name)
}
func (v View) StructName() string {
return generate.FuncName(v.Prefix + v.Name + v.Suffix)
}
//nolint:gochecknoglobals
var (
tpl = template.Must(template.New("tpls").Parse(gpoupTemplate))
gpoupTemplate = `type {{.StructName}} struct {
{{.ParentName}}
}
// {{.FuncName}} {{.Description}}.
func (i {{.ParentName}}) {{.FuncName}}() {{.StructName}} {
return {{.StructName}}{i}
}
`
)

27
definition/option.go Executable file
View File

@@ -0,0 +1,27 @@
package definition
type Option interface {
Kind() string
}
type Options []Option
func (s Options) Len() int { return len(s) }
func (s Options) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
type Params []Param
func (p Params) Get(name string) (any, bool) {
for _, param := range p {
if param.Name == name {
return param.Value, true
}
}
return nil, false
}
type Param struct {
Name string
Value any
}

100
definition/option/option.go Executable file
View File

@@ -0,0 +1,100 @@
package option
import (
"gitoa.ru/go-4devs/config/definition"
)
var _ definition.Option = Option{}
const (
Kind = "option"
)
const (
TypeString = "string"
TypeInt = "int"
TypeInt64 = "int64"
TypeUint = "uint"
TypeUint64 = "uint64"
TypeFloat64 = "float64"
TypeBool = "bool"
TypeTime = "time.Time"
TypeDuration = "time.Duration"
)
func Default(v any) func(*Option) {
return func(o *Option) {
o.Default = v
}
}
func New(name, desc string, vtype any, opts ...func(*Option)) Option {
option := Option{
Name: name,
Description: desc,
Type: vtype,
}
for _, opt := range opts {
opt(&option)
}
return option
}
type Option struct {
Name string
Description string
Type any
Default any
Params definition.Params
}
func (o Option) WithParams(params ...definition.Param) Option {
return Option{
Name: o.Name,
Description: o.Description,
Type: o.Type,
Params: append(params, o.Params...),
}
}
func (o Option) Kind() string {
return Kind
}
func Time(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeTime, opts...)
}
func Duration(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeDuration, opts...)
}
func String(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeString, opts...)
}
func Int(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeInt, opts...)
}
func Int64(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeInt64, opts...)
}
func Uint(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeUint, opts...)
}
func Uint64(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeUint64, opts...)
}
func Float64(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeFloat64, opts...)
}
func Bool(name, desc string, opts ...func(*Option)) Option {
return New(name, desc, TypeBool, opts...)
}

View File

@@ -0,0 +1,33 @@
// read{{.FuncName}} {{.Description}}.
func (i {{.StructName}}) read{{.FuncName}}(ctx context.Context) (v {{.Type}},e error) {
val, err := i.Value(ctx, {{ .ParentKeys }}"{{ .Name }}")
if err != nil {
{{if .HasDefault}}
{{$default := .Default}}
{{range .DefaultErrors}}
if errors.Is(err,{{.}}){
return {{$default}}
}
{{end}}
{{end}}
return v, fmt.Errorf("read {{.Keys}}:%w",err)
}
{{.Parse "val" "v" .Keys }}
}
// Read{{.FuncName}} {{.Description}}.
func (i {{.StructName}}) Read{{.FuncName}}(ctx context.Context) ({{.Type}}, error) {
return i.read{{.FuncName}}(ctx)
}
// {{.FuncName}} {{.Description}}.
func (i {{.StructName}}) {{.FuncName}}({{if .Context}} ctx context.Context {{end}}) {{.Type}} {
{{if not .Context}} ctx := context.Background() {{end}}
val, err := i.read{{.FuncName}}(ctx)
if err != nil {
i.log(ctx, "get {{.Keys}}: %v", err)
}
return val
}

View File

@@ -0,0 +1,3 @@
{{block "Parse" .}}
return {{.ValName}}.Parse{{ .FuncType}}()
{{end}}

View File

@@ -0,0 +1,8 @@
{{block "UnmarshalJSON" . }}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read {{.Keys}}:%w", perr)
}
return {{.Value}}, {{.Value}}.UnmarshalJSON([]byte(pval))
{{end}}

View File

@@ -0,0 +1,8 @@
{{block "UnmarshalText" . }}
pval, perr := {{.ValName}}.ParseString()
if perr != nil {
return {{.Value}}, fmt.Errorf("read {{.Keys}}:%w", perr)
}
return {{.Value}}, {{.Value}}.UnmarshalText([]byte(pval))
{{end}}

235
definition/option/view.go Normal file
View File

@@ -0,0 +1,235 @@
package option
import (
"bytes"
"embed"
"encoding"
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
"text/template"
"time"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
)
//go:embed tpl/*
var tpls embed.FS
//nolint:gochecknoglobals
var tpl = template.Must(template.New("tpls").ParseFS(tpls, "tpl/*.tmpl"))
//nolint:gochecknoinits
func init() {
generate.MustAdd(Kind, Handle(tpl.Lookup("option.tmpl")))
}
func Handle(tpl *template.Template) generate.Handle {
return func(w io.Writer, h generate.Handler, o definition.Option) error {
opt, _ := o.(Option)
if err := tpl.Execute(w, View{Option: opt, Handler: h}); err != nil {
return fmt.Errorf("option tpl:%w", err)
}
return nil
}
}
type View struct {
Option
generate.Handler
}
func (v View) Context() bool {
return v.Options().Context
}
func (v View) FuncName() string {
if funcName, ok := v.Option.Params.Get(ViewParamFunctName); ok {
name, _ := funcName.(string)
return name
}
return generate.FuncName(v.Name)
}
func (v View) Description() string {
if desc, ok := v.Option.Params.Get(ViewParamDescription); ok {
description, _ := desc.(string)
return description
}
return v.Option.Description
}
func (v View) Default() string {
switch data := v.Option.Default.(type) {
case time.Time:
return fmt.Sprintf("time.Parse(%q,time.RFC3339Nano)", data.Format(time.RFC3339Nano))
case time.Duration:
return fmt.Sprintf("time.ParseDuration(%q)", data)
default:
return fmt.Sprintf("%#v, nil", data)
}
}
func (v View) HasDefault() bool {
return v.Option.Default != nil
}
func (v View) ParentKeys() string {
if len(v.Handler.Keys()) > 0 {
return `"` + strings.Join(v.Handler.Keys(), `","`) + `",`
}
return ""
}
func (v View) Type() string {
slice := ""
if vtype, ok := v.Option.Type.(string); ok {
if strings.Contains(vtype, ".") {
if name, err := v.AddType(vtype); err == nil {
return slice + name
}
}
return vtype
}
rtype := reflect.TypeOf(v.Option.Type)
if rtype.PkgPath() == "" {
return rtype.String()
}
if rtype.Kind() == reflect.Slice {
slice = "[]"
}
short, err := v.AddType(rtype.PkgPath() + "." + rtype.Name())
if err != nil {
return err.Error()
}
return slice + short
}
func (v View) FuncType() string {
return generate.FuncName(v.Type())
}
func (v View) Parse(valName string, value string, keys []string) string {
h := parser(v.Option.Type)
data, err := h(ParseData{
Value: value,
ValName: valName,
Keys: keys,
View: v,
})
if err != nil {
return err.Error()
}
return data
}
//nolint:gochecknoglobals,unparam
var parses = map[string]func(data ParseData) (string, error){
typesIntreface[0].Name(): func(data ParseData) (string, error) {
var b bytes.Buffer
err := tpl.ExecuteTemplate(&b, "unmarshal_text.tmpl", data)
if err != nil {
return "", fmt.Errorf("execute unmarshal text:%w", err)
}
return b.String(), nil
},
typesIntreface[1].Name(): func(data ParseData) (string, error) {
var b bytes.Buffer
err := tpl.ExecuteTemplate(&b, "unmarshal_json.tmpl", data)
if err != nil {
return "", fmt.Errorf("execute unmarshal json:%w", err)
}
return b.String(), nil
},
TypeInt: internal,
TypeInt64: internal,
TypeBool: internal,
TypeString: internal,
TypeFloat64: internal,
TypeUint: internal,
TypeUint64: internal,
"time.Duration": func(data ParseData) (string, error) {
return fmt.Sprintf("return %s.ParseDuration()", data.ValName), nil
},
"time.Time": func(data ParseData) (string, error) {
return fmt.Sprintf("return %s.ParseTime()", data.ValName), nil
},
"any": func(data ParseData) (string, error) {
return fmt.Sprintf("return %[2]s, %[1]s.Unmarshal(&%[2]s)", data.ValName, data.Value), nil
},
}
func internal(data ParseData) (string, error) {
var b bytes.Buffer
err := tpl.ExecuteTemplate(&b, "parse.tmpl", data)
if err != nil {
return "", fmt.Errorf("execute parse.tmpl:%w", err)
}
return b.String(), nil
}
//nolint:gochecknoglobals
var typesIntreface = [...]reflect.Type{
reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem(),
reflect.TypeOf((*json.Unmarshaler)(nil)).Elem(),
}
func parser(data any) func(ParseData) (string, error) {
vtype := reflect.TypeOf(data)
name := vtype.Name()
if v, ok := data.(string); ok {
name = v
}
if vtype.Kind() == reflect.Slice {
return parses["any"]
}
if h, ok := parses[name]; ok {
return h
}
for _, extypes := range typesIntreface {
if vtype.Implements(extypes) {
return parses[extypes.Name()]
}
if vtype.Kind() != reflect.Ptr && reflect.PointerTo(vtype).Implements(extypes) {
return parses[extypes.Name()]
}
}
return parses["any"]
}
type ParseData struct {
Value string
ValName string
Keys []string
View
}

View File

@@ -0,0 +1,6 @@
package option
const (
ViewParamFunctName = "view.funcName"
ViewParamDescription = "view.description"
)

27
definition/proto/proto.go Normal file
View File

@@ -0,0 +1,27 @@
package proto
import (
"gitoa.ru/go-4devs/config/definition"
)
const Kind = "proto"
func New(name, desc string, opt definition.Option) Proto {
pr := Proto{
Name: name,
Description: desc,
Option: opt,
}
return pr
}
type Proto struct {
Name string
Description string
Option definition.Option
}
func (p Proto) Kind() string {
return Kind
}

80
definition/proto/view.go Normal file
View File

@@ -0,0 +1,80 @@
package proto
import (
"fmt"
"io"
"strings"
"text/template"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
"gitoa.ru/go-4devs/config/definition/option"
)
//nolint:gochecknoinits
func init() {
generate.MustAdd(Kind, handle)
}
func handle(w io.Writer, data generate.Handler, opt definition.Option) error {
proto, ok := opt.(Proto)
if !ok {
return fmt.Errorf("%w:%T", generate.ErrWrongType, opt)
}
if viewOpt, ok := proto.Option.(option.Option); ok {
viewOpt = viewOpt.WithParams(
definition.Param{
Name: option.ViewParamFunctName,
Value: generate.FuncName(proto.Name) + generate.FuncName(viewOpt.Name),
},
definition.Param{
Name: option.ViewParamDescription,
Value: proto.Description + " " + viewOpt.Description,
},
)
return option.Handle(tpl)(w, data, viewOpt)
}
return fmt.Errorf("%w:%T", generate.ErrWrongType, opt)
}
//nolint:gochecknoglobals
var (
tpl = template.Must(template.New("tpls").Funcs(template.FuncMap{"join": strings.Join}).Parse(templateOption))
templateOption = `// read{{.FuncName}} {{.Description}}.
func (i {{.StructName}}) read{{.FuncName}}(ctx context.Context, key string) (v {{.Type}},e error) {
val, err := i.Value(ctx, {{ .ParentKeys }} key, "{{.Name}}")
if err != nil {
{{if .HasDefault}}
{{$default := .Default}}
{{range .DefaultErrors}}
if errors.Is(err,{{.}}){
return {{$default}}
}
{{end}}
{{end}}
return v, fmt.Errorf("read {{.Keys}}:%w",err)
}
{{.Parse "val" "v" .Keys }}
}
// Read{{.FuncName}} {{.Description}}.
func (i {{.StructName}}) Read{{.FuncName}}(ctx context.Context, key string) ({{.Type}}, error) {
return i.read{{.FuncName}}(ctx, key)
}
// {{.FuncName}} {{.Description}}.
func (i {{.StructName}}) {{.FuncName}}({{if .Context}} ctx context.Context, {{end}} key string) {{.Type}} {
{{if not .Context}} ctx := context.Background() {{end}}
val, err := i.read{{.FuncName}}(ctx, key)
if err != nil {
i.log(ctx, "get {{.Keys}}: %v", err)
}
return val
}
`
)

View File

@@ -0,0 +1,32 @@
package definition
import (
"context"
"time"
"github.com/sirupsen/logrus"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
"gitoa.ru/go-4devs/log/level"
)
func Configure(ctx context.Context, def *definition.Definition) error {
def.Add(
option.String("test", "test description", option.Default("defult")),
option.Int("int", "test int description", option.Default(2)),
group.New("group", "group description", option.String("test", "test description")),
option.Time("start", "start at", option.Default(time.Now())),
option.Duration("timer", "timer", option.Default(time.Hour)),
group.New("log", "logger",
option.New("level", "log level", level.Level(0), option.Default(level.Debug)),
option.New("logrus", "logrus level", logrus.Level(0), option.Default(logrus.DebugLevel)),
proto.New("sevice", "cutom service log", option.New("level", "log level", level.Level(0), option.Default(level.Debug))),
),
option.New("erors", "skiped errors", []string{}),
proto.New("proto_errors", "proto errors", option.New("erors", "skiped errors", []string{})),
)
return nil
}

View File

@@ -0,0 +1,61 @@
//go:build ignore
// +build ignore
package main
import (
"context"
"fmt"
"go/format"
"os"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/generate"
"gitoa.ru/go-4devs/config/eample"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stdout, err)
os.Exit(1)
}
}
func run() error {
ctx := context.Background()
def := definition.New()
if err := eample.Configure(ctx, &def); err != nil {
return err
}
f, err := os.Create("eample/defenition_input.go")
if err != nil {
return err
}
gerr := generate.Run(f, "eample", def, generate.ViewOption{
Struct: "Configure",
Suffix: "Input",
Errors: generate.ViewErrors{
Default: []string{
"gitoa.ru/go-4devs/config.ErrVariableNotFound",
},
},
})
if gerr != nil {
return gerr
}
in, err := os.ReadFile(f.Name())
if err != nil {
return err
}
out, err := format.Source(in)
if err != nil {
return err
}
return os.WriteFile(f.Name(), out, 0644)
}

14
example/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module gitoa.ru/go-4devs/config/example
go 1.21.5
require (
github.com/sirupsen/logrus v1.9.3
gitoa.ru/go-4devs/config/definition v0.0.0-20240125203435-5586adc4e3d8
gitoa.ru/go-4devs/log v0.5.3
)
require (
github.com/iancoleman/strcase v0.3.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

21
example/go.sum Normal file
View File

@@ -0,0 +1,21 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gitoa.ru/go-4devs/config/definition v0.0.0-20240125203435-5586adc4e3d8 h1:PZ4SE0gq719+lXorGaRPfGSSO2JpfxTWCWOSsna+7Yw=
gitoa.ru/go-4devs/config/definition v0.0.0-20240125203435-5586adc4e3d8/go.mod h1:jV6jF0PsK4ffNC8hrBxMx33PVJtnW5O6S/sGNJDqrd4=
gitoa.ru/go-4devs/log v0.5.3 h1:o/4DcypxbgQ9GEfUWrZ3FVrVfttuJgLs2ptMVPj47sE=
gitoa.ru/go-4devs/log v0.5.3/go.mod h1:tREtjEH2cTHl0p3uCVcH9g5tlqtsVNI/tDQVfq53Ty4=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

29
provider/vault/go.mod Normal file
View File

@@ -0,0 +1,29 @@
module gitoa.ru/go-4devs/config/provider/vault
go 1.21
require (
github.com/hashicorp/vault/api v1.11.0
gitoa.ru/go-4devs/config v0.0.2
)
require (
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.6.6 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
)

92
provider/vault/go.sum Normal file
View File

@@ -0,0 +1,92 @@
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.11.0 h1:AChWByeHf4/P9sX3Y1B7vFsQhZO2BgQiCMQ2SA1P1UY=
github.com/hashicorp/vault/api v1.11.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gitoa.ru/go-4devs/config v0.0.2 h1:bkTxW57kDDMf4cj/8W7fxPSN7JCPWEqlhCmL6LP3Vzg=
gitoa.ru/go-4devs/config v0.0.2/go.mod h1:xfEC2Al9xnMLJUuekYs3KhJ5BIzWAseNwkMwbN6/xss=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

120
provider/vault/provider.go Normal file
View File

@@ -0,0 +1,120 @@
package vault
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/value"
)
const (
Name = "vault"
Separator = "/"
Prefix = "secret/data/"
ValueName = "value"
)
var _ config.Provider = (*Provider)(nil)
type SecretOption func(*Provider)
func WithSecretResolve(f func(key []string) (string, string)) SecretOption {
return func(s *Provider) { s.resolve = f }
}
func New(namespace, appName string, client *api.Client, opts ...SecretOption) *Provider {
prov := Provider{
client: client,
resolve: func(key []string) (string, string) {
keysLen := len(key)
if keysLen == 1 {
return "", key[0]
}
return strings.Join(key[:keysLen-1], Separator), key[keysLen-1]
},
name: Name,
prefix: Prefix + namespace + Separator + appName,
}
for _, opt := range opts {
opt(&prov)
}
return &prov
}
type Provider struct {
client *api.Client
resolve func(key []string) (string, string)
name string
prefix string
}
func (p *Provider) Name() string {
return p.name
}
func (p *Provider) Key(in []string) (string, string) {
path, val := p.resolve(in)
if path == "" {
return p.prefix, val
}
return p.prefix + Separator + path, val
}
func (p *Provider) read(path, key string) (*api.Secret, error) {
secret, err := p.client.Logical().Read(path)
if err != nil {
return nil, fmt.Errorf("read[%s:%s]:%w", path, key, err)
}
if secret == nil && key != ValueName {
return p.read(path+Separator+key, ValueName)
}
return secret, nil
}
func (p *Provider) Value(_ context.Context, key ...string) (config.Value, error) {
path, field := p.Key(key)
secret, err := p.read(path, field)
if err != nil {
return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", err, path, field, p.Name())
}
if secret == nil || len(secret.Data) == 0 {
return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrValueNotFound, path, field, p.Name())
}
if len(secret.Warnings) > 0 {
return nil,
fmt.Errorf("%w: warn: %s, path:%s, field:%s, provider:%s", config.ErrValueNotFound, secret.Warnings, path, field, p.Name())
}
data, ok := secret.Data["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%w: path:%s, field:%s, provider:%s", config.ErrValueNotFound, path, field, p.Name())
}
if val, ok := data[field]; ok {
return value.JString(fmt.Sprint(val)), nil
}
if val, ok := data[ValueName]; ok {
return value.JString(fmt.Sprint(val)), nil
}
md, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("%w: %w", config.ErrInvalidValue, err)
}
return value.JBytes(md), nil
}

View File

@@ -0,0 +1,47 @@
package vault_test
import (
"context"
"fmt"
"log"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/provider/vault"
)
func ExampleClient_Value() {
const (
namespace = "fdevs"
appName = "config"
)
ctx := context.Background()
// configure vault client
vaultClient, err := NewVault()
if err != nil {
log.Print(err)
return
}
config, err := config.New(
vault.New(namespace, appName, vaultClient),
)
if err != nil {
log.Print(err)
return
}
dsn, err := config.Value(ctx, "example", "dsn")
if err != nil {
log.Print("example:dsn ", err)
return
}
fmt.Printf("dsn from vault: %s\n", dsn.String())
// Output:
// dsn from vault: pgsql://user@pass:127.0.0.1:5432
}

View File

@@ -0,0 +1,26 @@
package vault_test
import (
"testing"
"time"
"gitoa.ru/go-4devs/config/provider/vault"
"gitoa.ru/go-4devs/config/test"
"gitoa.ru/go-4devs/config/test/require"
)
func TestProvider(t *testing.T) {
t.Parallel()
cl, err := NewVault()
require.NoError(t, err)
provider := vault.New("fdevs", "config", cl)
read := []test.Read{
test.NewReadConfig("database"),
test.NewRead(test.DSN, "db", "dsn"),
test.NewRead(time.Minute, "db", "timeout"),
}
test.Run(t, provider, read)
}

View File

@@ -0,0 +1,90 @@
package vault_test
import (
"bytes"
"context"
"encoding/json"
"net/http"
"os"
"github.com/hashicorp/vault/api"
"gitoa.ru/go-4devs/config/test"
)
const token = "dev"
func NewVault() (*api.Client, error) {
address, ok := os.LookupEnv("VAULT_DEV_LISTEN_ADDRESS")
if !ok {
address = "http://127.0.0.1:8200"
}
tokenID, ok := os.LookupEnv("VAULT_DEV_ROOT_TOKEN_ID")
if !ok {
tokenID = token
}
cl, err := api.NewClient(&api.Config{
Address: address,
})
if err != nil {
return nil, err
}
cl.SetToken(tokenID)
values := map[string]map[string]interface{}{
"database": {
"duration": 1260000000000,
"enabled": true,
},
"db": {
"dsn": test.DSN,
"timeout": "60s",
},
"example": {
"dsn": test.DSN,
"timeout": "60s",
},
}
for name, val := range values {
if err := create(address, tokenID, name, val); err != nil {
return nil, err
}
}
return cl, nil
}
func create(host, token, path string, data map[string]interface{}) error {
type Req struct {
Data interface{} `json:"data"`
}
b, err := json.Marshal(Req{Data: data})
if err != nil {
return err
}
body := bytes.NewBuffer(b)
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodPost,
host+"/v1/secret/data/fdevs/config/"+path,
body,
)
if err != nil {
return err
}
req.Header.Set("X-Vault-Token", token)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
return res.Body.Close()
}