/
secretstruct.go
195 lines (153 loc) · 4.39 KB
/
secretstruct.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package secretstruct
import (
"context"
"errors"
"fmt"
"reflect"
"go.uber.org/multierr"
"gocloud.dev/runtimevar"
"golang.org/x/sync/errgroup"
)
const (
tagName = "secretstruct"
selfTagValue = "self"
stopTagValue = "-"
)
var (
// ErrInvalidInputParamType indicates that an input param is of the wrong type.
ErrInvalidInputParamType = errors.New("input param must be a struct pointer")
// ErrUnsupportedFieldType indicates that a field annotated with `secretstruct` tag is of the wrong type.
// Could be a string or *string.
ErrUnsupportedFieldType = errors.New("unsupported field type")
// ErrInvalidLatestValue indicates that a call to `gocloud.dev/runtimevar#Variable.Latest`
// method returned nil.
ErrInvalidLatestValue = errors.New("latest value is nil")
// ErrTypeMismatch indicates that a call to `gocloud.dev/runtimevar#Variable.Latest`
// method returned something different from []byte or string.
ErrTypeMismatch = errors.New("type mismatch")
)
// Process populates the specified struct based on struct field tags.
func Process(ctx context.Context, in interface{}) error {
infos, err := gatherInfo(in)
if err != nil {
return err
}
g, ctx := errgroup.WithContext(ctx)
for _, info := range infos {
info := info
g.Go(func() error {
if err := processField(ctx, info, getLatest); err != nil {
return fmt.Errorf("processing field %q failed: %w", info.Name, err)
}
return nil
})
}
return g.Wait()
}
// varInfo maintains information about the configuration variable.
type varInfo struct {
Name string
URL string
Field reflect.Value
}
// GatherInfo gathers information about the specified struct.
func gatherInfo(spec interface{}) ([]varInfo, error) {
s := reflect.ValueOf(spec)
if s.Kind() != reflect.Ptr {
return nil, ErrInvalidInputParamType
}
s = s.Elem()
if s.Kind() != reflect.Struct {
return nil, ErrInvalidInputParamType
}
typeOfSpec := s.Type()
var infos []varInfo
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
if !f.CanSet() {
continue
}
structField := typeOfSpec.Field(i)
val, hasTag := structField.Tag.Lookup(tagName)
// if this field is explicitly ignored
if val == stopTagValue {
continue
}
for f.Kind() == reflect.Ptr {
if f.IsNil() {
elemKind := f.Type().Elem().Kind()
if elemKind != reflect.Struct && elemKind != reflect.String {
// nil pointer to a non-struct: leave it alone
break
}
// nil pointer to struct or string: create a zero instance
f.Set(reflect.New(f.Type().Elem()))
}
f = f.Elem()
}
// recursively process inner struct
if f.Kind() == reflect.Struct {
embeddedInfos, err := gatherInfo(f.Addr().Interface())
if err != nil {
return nil, err
}
infos = append(infos, embeddedInfos...)
continue
}
// if this field does not have the tag
if !hasTag {
continue
}
// only strings are supported at the moment. []byte and JSON will be supported later
if f.Kind() != reflect.String {
return nil, fmt.Errorf("field %s (%s): %w", structField.Name, f.Type(), ErrUnsupportedFieldType)
}
// replace "self" special tag value with field current value
if val == selfTagValue {
val = f.String()
}
infos = append(infos, varInfo{
Name: structField.Name,
URL: val,
Field: f,
})
}
return infos, nil
}
type getLatestFunc func(ctx context.Context, v *runtimevar.Variable) (runtimevar.Snapshot, error)
func processField(ctx context.Context, info varInfo, f getLatestFunc) (err error) {
v, err := runtimevar.OpenVariable(ctx, info.URL)
if err != nil {
return err
}
defer func() {
err = multierr.Append(err, v.Close())
}()
latest, err := f(ctx, v)
if err != nil {
return err
}
return setFieldValue(info.Field, latest.Value)
}
func getLatest(ctx context.Context, v *runtimevar.Variable) (runtimevar.Snapshot, error) {
return v.Latest(ctx)
}
func setFieldValue(dst reflect.Value, src interface{}) error {
if src == nil {
return ErrInvalidLatestValue
}
// Default variable decoder is ByteDecoder.
// Try to type assert to both []byte and string.
// This is needed to let users to avoid specifying decoder=string param in URL
// if dst is string and src either string or []byte.
switch data := src.(type) {
case []byte:
dst.SetString(string(data))
case string:
dst.SetString(data)
default:
return fmt.Errorf("can't type assert value of type %T, must be []byte or string: %w",
src, ErrTypeMismatch)
}
return nil
}