forked from lukasjarosch/skipper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
data.go
254 lines (222 loc) · 6.66 KB
/
data.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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package skipper
import (
"fmt"
"reflect"
"strconv"
"gopkg.in/yaml.v3"
)
// Data is an arbitrary map of values which makes up the inventory.
type Data map[string]interface{}
// NewData attempts to convert any given interface{} into [Data].
// This is done by first using `yaml.Marshal` and then `yaml.Unmarshal`.
// If the given interface is compatible with [Data], these steps will succeed.
func NewData(input interface{}) (Data, error) {
outBytes, err := yaml.Marshal(input)
if err != nil {
return nil, err
}
var data Data
err = yaml.Unmarshal(outBytes, &data)
if err != nil {
return nil, err
}
return data, nil
}
// String returns the string result of `yaml.Marshal`.
// Can be useful for debugging or just dumping the inventory.
func (d Data) String() string {
out, _ := yaml.Marshal(d)
return string(out)
}
// Bytes returns a `[]byte` representation of Data
func (d Data) Bytes() []byte {
return []byte(d.String())
}
// HasKey returns true if Data[key] exists.
// Note that his function does not support paths like `HasKey("foo.bar.baz")`.
// For that you can use [GetPath]
func (d Data) HasKey(key string) bool {
if _, ok := d[key]; ok {
return true
}
return false
}
// Get returns the value at `Data[key]` as [Data].
// Note that his function does not support paths like `HasKey("foo.bar.baz")`.
// For that you can use [GetPath]
func (d Data) Get(key string) Data {
if d[key] == nil {
return nil
}
return d[key].(Data)
}
// GetPath allows path based indexing into Data.
// A path is a slice of interfaces which are used as keys in order.
// Supports array indexing (arrays start at 0)
// Examples of valid paths:
// - ["foo", "bar"]
// - ["foo", "bar", 0]
func (d Data) GetPath(path ...interface{}) (tree interface{}, err error) {
tree = d
for i, el := range path {
switch node := tree.(type) {
case Data:
key, ok := el.(string)
if !ok {
return nil, fmt.Errorf("unexpected string key in map[string]interface '%T' at index %d", el, i)
}
tree, ok = node[key]
if !ok {
return nil, fmt.Errorf("key not found: %v", el)
}
case map[interface{}]interface{}:
var ok bool
tree, ok = node[el]
if !ok {
return nil, fmt.Errorf("key not found: %v", el)
}
case []interface{}:
index, ok := el.(int)
if !ok {
index, err = strconv.Atoi(fmt.Sprint(el))
if err != nil {
return nil, fmt.Errorf("unexpected integer path element '%v' (%T)", el, el)
}
}
if index < 0 || index >= len(node) {
return nil, fmt.Errorf("path index out of range: %d", index)
}
tree = node[index]
default:
return nil, fmt.Errorf("unexpected node type %T at index %d", node, i)
}
}
return tree, nil
}
// SetPath uses the same path slices as [GetPath], only that it can set the value at the given path.
// Supports array indexing (arrays start at 0)
func (d *Data) SetPath(value interface{}, path ...interface{}) (err error) {
var tree interface{}
tree = (*d)
if len(path) == 0 {
return fmt.Errorf("path cannot be empty")
}
i := len(path) - 1
if len(path) > 1 {
var tmp interface{}
tmp, err = tree.(Data).GetPath(path[:i]...)
if err != nil {
return err
}
tree = tmp
}
element := path[i]
switch node := tree.(type) {
case Data:
key, ok := element.(string)
if !ok {
return fmt.Errorf("unexpected string key in map[string]interface '%T' at index %d", element, i)
}
node[key] = value
case []interface{}:
index, ok := element.(int)
if !ok {
index, err = strconv.Atoi(fmt.Sprint(element))
if err != nil {
return fmt.Errorf("unexpected integer path element '%v (%T)'", element, element)
}
}
if index < 0 || index >= len(node) {
return fmt.Errorf("path index out of range: %d", index)
}
node[index] = value
default:
return fmt.Errorf("unexpected node type %T at index %d", node, i)
}
return nil
}
// MergeReplace merges the existing Data with the given.
// If a key already exists, the passed data has precedence and it's value will be used.
func (d Data) MergeReplace(data Data) Data {
out := make(Data, len(d))
for k, v := range d {
out[k] = v
}
for k, v := range data {
if v, ok := v.(Data); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(Data); ok {
out[k] = bv.MergeReplace(v)
continue
}
}
}
if v, ok := v.([]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.([]interface{}); ok {
out[k] = append(bv, v...)
continue
}
}
}
out[k] = v
}
return out
}
// FindValueFunc is a callback used to find values inside a Data map.
// `value` is the actual found value; `path` are the path segments which point to that value
// The function returns the extracted value and an error (if any).
type FindValueFunc func(value string, path []interface{}) (interface{}, error)
// FindValues can be used to find specific 'leaf' nodes, aka values.
// The Data is iterated recursively and once a plain value is found, the given FindValueFunc is called.
// It's the responsibility of the FindValueFunc to determine if the value is what is searched for.
// The FindValueFunc can return any data, which is aggregated and written into the passed `*[]interface{}`.
// The callee is then responsible of handling the returned value and ensuring the correct types were returned.
func (d Data) FindValues(valueFunc FindValueFunc, target *[]interface{}) (err error) {
// newPath is used to copy an existing []interface and hard-copy it.
// This is required because Go wants to optimize slice usage by reusing memory.
// Most of the time, this is totally fine, but in this case it would mess up the slice
// by changing the path []interface of already found secrets.
newPath := func(path []interface{}, appendValue interface{}) []interface{} {
tmp := make([]interface{}, len(path))
copy(tmp, path)
tmp = append(tmp, appendValue)
return tmp
}
var walk func(reflect.Value, []interface{}) error
walk = func(v reflect.Value, path []interface{}) error {
// fix indirects through pointers and interfaces
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
v = v.Elem()
}
switch v.Kind() {
case reflect.Array, reflect.Slice:
for i := 0; i < v.Len(); i++ {
err := walk(v.Index(i), newPath(path, i))
if err != nil {
return err
}
}
case reflect.Map:
for _, key := range v.MapKeys() {
if v.MapIndex(key).IsNil() {
break
}
err := walk(v.MapIndex(key), newPath(path, key.String()))
if err != nil {
return err
}
}
default:
// at this point we have found a value and give off control to the given valueFunc
value, err := valueFunc(v.String(), path)
if err != nil {
return err
}
(*target) = append(*target, value)
}
return nil
}
err = walk(reflect.ValueOf(d), nil)
return err
}