-
Notifications
You must be signed in to change notification settings - Fork 113
/
validate.go
723 lines (641 loc) · 22.1 KB
/
validate.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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package fields
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/Masterminds/semver"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/elastic/elastic-package/internal/common"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/multierror"
"github.com/elastic/elastic-package/internal/packages"
"github.com/elastic/elastic-package/internal/packages/buildmanifest"
)
var semver2_0_0 = semver.MustParse("2.0.0")
// Validator is responsible for fields validation.
type Validator struct {
// Schema contains definition records.
Schema []FieldDefinition
// FieldDependencyManager resolves references to external fields
FieldDependencyManager *DependencyManager
// SpecVersion contains the version of the spec used by the package.
specVersion semver.Version
// expectedDataset contains the value expected for dataset fields.
expectedDataset string
defaultNumericConversion bool
numericKeywordFields map[string]struct{}
disabledDependencyManagement bool
enabledAllowedIPCheck bool
allowedCIDRs []*net.IPNet
}
// ValidatorOption represents an optional flag that can be passed to CreateValidatorForDirectory.
type ValidatorOption func(*Validator) error
// WithSpecVersion enables validation dependant of the spec version used by the package.
func WithSpecVersion(version string) ValidatorOption {
return func(v *Validator) error {
sv, err := semver.NewVersion(version)
if err != nil {
return fmt.Errorf("invalid version %q: %v", version, err)
}
v.specVersion = *sv
return nil
}
}
// WithDefaultNumericConversion configures the validator to accept defined keyword (or constant_keyword) fields as numeric-type.
func WithDefaultNumericConversion() ValidatorOption {
return func(v *Validator) error {
v.defaultNumericConversion = true
return nil
}
}
// WithNumericKeywordFields configures the validator to accept specific fields to have numeric-type
// while defined as keyword or constant_keyword.
func WithNumericKeywordFields(fields []string) ValidatorOption {
return func(v *Validator) error {
v.numericKeywordFields = make(map[string]struct{}, len(fields))
for _, field := range fields {
v.numericKeywordFields[field] = struct{}{}
}
return nil
}
}
// WithDisabledDependencyManagement configures the validator to ignore external fields and won't follow dependencies.
func WithDisabledDependencyManagement() ValidatorOption {
return func(v *Validator) error {
v.disabledDependencyManagement = true
return nil
}
}
// WithEnabledAllowedIPCheck configures the validator to perform check on the IP values against an allowed list.
func WithEnabledAllowedIPCheck() ValidatorOption {
return func(v *Validator) error {
v.enabledAllowedIPCheck = true
return nil
}
}
// WithExpectedDataset configures the validator to check if the dataset fields have the expected values.
func WithExpectedDataset(dataset string) ValidatorOption {
return func(v *Validator) error {
v.expectedDataset = dataset
return nil
}
}
// CreateValidatorForDirectory function creates a validator for the directory.
func CreateValidatorForDirectory(fieldsParentDir string, opts ...ValidatorOption) (v *Validator, err error) {
v = new(Validator)
for _, opt := range opts {
if err := opt(v); err != nil {
return nil, err
}
}
v.allowedCIDRs = initializeAllowedCIDRsList()
fieldsDir := filepath.Join(fieldsParentDir, "fields")
v.Schema, err = loadFieldsFromDir(fieldsDir)
if err != nil {
return nil, errors.Wrapf(err, "can't load fields from directory (path: %s)", fieldsDir)
}
if v.disabledDependencyManagement {
return v, nil
}
packageRoot, found, err := packages.FindPackageRoot()
if err != nil {
return nil, errors.Wrap(err, "can't find package root")
}
// As every command starts with approximating where is the package root, it isn't required to return an error in case the root is missing.
// This is also useful for testing purposes, where we don't have a real package, but just "fields" directory. The package root is always absent.
if !found {
logger.Debug("Package root not found, dependency management will be disabled.")
v.disabledDependencyManagement = true
return v, nil
}
bm, ok, err := buildmanifest.ReadBuildManifest(packageRoot)
if err != nil {
return nil, errors.Wrap(err, "can't read build manifest")
}
if !ok {
v.disabledDependencyManagement = true
return v, nil
}
fdm, err := CreateFieldDependencyManager(bm.Dependencies)
if err != nil {
return nil, errors.Wrap(err, "can't create field dependency manager")
}
v.FieldDependencyManager = fdm
return v, nil
}
//go:embed _static/allowed_geo_ips.txt
var allowedGeoIPs string
func initializeAllowedCIDRsList() (cidrs []*net.IPNet) {
s := bufio.NewScanner(strings.NewReader(allowedGeoIPs))
for s.Scan() {
_, cidr, err := net.ParseCIDR(s.Text())
if err != nil {
panic("invalid ip in _static/allowed_geo_ips.txt: " + s.Text())
}
cidrs = append(cidrs, cidr)
}
return cidrs
}
func loadFieldsFromDir(fieldsDir string) ([]FieldDefinition, error) {
files, err := filepath.Glob(filepath.Join(fieldsDir, "*.yml"))
if err != nil {
return nil, errors.Wrapf(err, "reading directory with fields failed (path: %s)", fieldsDir)
}
var fields []FieldDefinition
for _, file := range files {
body, err := os.ReadFile(file)
if err != nil {
return nil, errors.Wrap(err, "reading fields file failed")
}
var u []FieldDefinition
err = yaml.Unmarshal(body, &u)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling field body failed")
}
fields = append(fields, u...)
}
return fields, nil
}
// ValidateDocumentBody validates the provided document body.
func (v *Validator) ValidateDocumentBody(body json.RawMessage) multierror.Error {
var c common.MapStr
err := json.Unmarshal(body, &c)
if err != nil {
var errs multierror.Error
errs = append(errs, errors.Wrap(err, "unmarshalling document body failed"))
return errs
}
return v.ValidateDocumentMap(c)
}
// ValidateDocumentMap validates the provided document as common.MapStr.
func (v *Validator) ValidateDocumentMap(body common.MapStr) multierror.Error {
errs := v.validateDocumentValues(body)
errs = append(errs, v.validateMapElement("", body, body)...)
if len(errs) == 0 {
return nil
}
return errs
}
var datasetFieldNames = []string{
"event.dataset",
"data_stream.dataset",
}
func (v *Validator) validateDocumentValues(body common.MapStr) multierror.Error {
var errs multierror.Error
if !v.specVersion.LessThan(semver2_0_0) && v.expectedDataset != "" {
for _, datasetField := range datasetFieldNames {
value, err := body.GetValue(datasetField)
if err == common.ErrKeyNotFound {
continue
}
str, ok := value.(string)
if !ok || str != v.expectedDataset {
err := errors.Errorf("field %q should have value %q, it has \"%v\"",
datasetField, v.expectedDataset, value)
errs = append(errs, err)
}
}
}
return errs
}
func (v *Validator) validateMapElement(root string, elem common.MapStr, doc common.MapStr) multierror.Error {
var errs multierror.Error
for name, val := range elem {
key := strings.TrimLeft(root+"."+name, ".")
switch val := val.(type) {
case []map[string]interface{}:
for _, m := range val {
err := v.validateMapElement(key, m, doc)
if err != nil {
errs = append(errs, err...)
}
}
case map[string]interface{}:
if isFieldTypeFlattened(key, v.Schema) {
// Do not traverse into objects with flattened data types
// because the entire object is mapped as a single field.
continue
}
err := v.validateMapElement(key, val, doc)
if err != nil {
errs = append(errs, err...)
}
default:
err := v.validateScalarElement(key, val, doc)
if err != nil {
errs = append(errs, err)
}
}
}
return errs
}
func (v *Validator) validateScalarElement(key string, val interface{}, doc common.MapStr) error {
if key == "" {
return nil // root key is always valid
}
definition := FindElementDefinition(key, v.Schema)
if definition == nil && skipValidationForField(key) {
return nil // generic field, let's skip validation for now
}
if definition == nil {
return fmt.Errorf(`field "%s" is undefined`, key)
}
if !v.disabledDependencyManagement && definition.External != "" {
def, err := v.FieldDependencyManager.ImportField(definition.External, key)
if err != nil {
return errors.Wrapf(err, "can't import field (field: %s)", key)
}
definition = &def
}
// Convert numeric keyword fields to string for validation.
_, found := v.numericKeywordFields[key]
if (found || v.defaultNumericConversion) && isNumericKeyword(*definition, val) {
val = fmt.Sprintf("%q", val)
}
err := v.validateExpectedNormalization(*definition, val)
if err != nil {
return errors.Wrapf(err, "field %q is not normalized as expected", key)
}
err = v.parseElementValue(key, *definition, val, doc)
if err != nil {
return errors.Wrap(err, "parsing field value failed")
}
return nil
}
func isNumericKeyword(definition FieldDefinition, val interface{}) bool {
_, isNumber := val.(float64)
return isNumber && (definition.Type == "keyword" || definition.Type == "constant_keyword")
}
// skipValidationForField skips field validation (field presence) of special fields. The special fields are present
// in every (most?) documents collected by Elastic Agent, but aren't defined in any integration in `fields.yml` files.
// FIXME https://github.com/elastic/elastic-package/issues/147
func skipValidationForField(key string) bool {
return isFieldFamilyMatching("agent", key) ||
isFieldFamilyMatching("elastic_agent", key) ||
isFieldFamilyMatching("cloud", key) || // too many common fields
isFieldFamilyMatching("event", key) || // too many common fields
isFieldFamilyMatching("host", key) || // too many common fields
isFieldFamilyMatching("metricset", key) || // field is deprecated
isFieldFamilyMatching("event.module", key) // field is deprecated
}
func isFieldFamilyMatching(family, key string) bool {
return key == family || strings.HasPrefix(key, family+".")
}
func isFieldTypeFlattened(key string, fieldDefinitions []FieldDefinition) bool {
definition := FindElementDefinition(key, fieldDefinitions)
return definition != nil && definition.Type == "flattened"
}
func findElementDefinitionForRoot(root, searchedKey string, FieldDefinitions []FieldDefinition) *FieldDefinition {
for _, def := range FieldDefinitions {
key := strings.TrimLeft(root+"."+def.Name, ".")
if compareKeys(key, def, searchedKey) {
return &def
}
if len(def.Fields) == 0 {
continue
}
fd := findElementDefinitionForRoot(key, searchedKey, def.Fields)
if fd != nil {
return fd
}
}
return nil
}
// FindElementDefinition is a helper function used to find the fields definition in the schema.
func FindElementDefinition(searchedKey string, fieldDefinitions []FieldDefinition) *FieldDefinition {
return findElementDefinitionForRoot("", searchedKey, fieldDefinitions)
}
// compareKeys checks if `searchedKey` matches with the given `key`. `key` can contain
// wildcards (`*`), that match any sequence of characters in `searchedKey` different to dots.
func compareKeys(key string, def FieldDefinition, searchedKey string) bool {
// Loop over every byte in `key` to find if there is a matching byte in `searchedKey`.
var j int
for _, k := range []byte(key) {
if j >= len(searchedKey) {
// End of searched key reached before maching all characters in the key.
return false
}
switch k {
case searchedKey[j]:
// Match, continue.
j++
case '*':
// Wildcard, match everything till next dot.
switch idx := strings.IndexByte(searchedKey[j:], '.'); idx {
default:
// Jump till next dot.
j += idx
case -1:
// No dots, wildcard matches with the rest of the searched key.
j = len(searchedKey)
case 0:
// Empty name on wildcard, this is not permitted (e.g. `example..foo`).
return false
}
default:
// No match.
return false
}
}
// If everything matched, searched key has been found.
if len(searchedKey) == j {
return true
}
// Workaround for potential subfields of certain types as geo_point or histogram.
if len(searchedKey) > j {
extraPart := searchedKey[j:]
if validSubField(def, extraPart) {
return true
}
}
return false
}
func (v *Validator) validateExpectedNormalization(definition FieldDefinition, val interface{}) error {
// Validate expected normalization starting with packages following spec v2 format.
if v.specVersion.LessThan(semver2_0_0) {
return nil
}
for _, normalize := range definition.Normalize {
switch normalize {
case "array":
if _, isArray := val.([]interface{}); val != nil && !isArray {
return fmt.Errorf("expected array, found %q (%T)", val, val)
}
}
}
return nil
}
// validSubField checks if the extra part that didn't match with any field definition,
// matches with the possible sub field of complex fields like geo_point or histogram.
func validSubField(def FieldDefinition, extraPart string) bool {
fieldType := def.Type
if def.Type == "object" && def.ObjectType != "" {
fieldType = def.ObjectType
}
subFields := []string{".lat", ".lon", ".values", ".counts"}
perType := map[string][]string{
"geo_point": subFields[0:2],
"histogram": subFields[2:4],
}
allowed, found := perType[fieldType]
if !found {
if def.External != "" {
// An unresolved external field could be anything.
allowed = subFields
} else {
return false
}
}
for _, a := range allowed {
if a == extraPart {
return true
}
}
return false
}
// parseElementValue checks that the value stored in a field matches the field definition. For
// arrays it checks it for each Element.
func (v *Validator) parseElementValue(key string, definition FieldDefinition, val interface{}, doc common.MapStr) error {
err := v.parseAllElementValues(key, definition, val, doc)
if err != nil {
return err
}
return forEachElementValue(key, definition, val, doc, v.parseSingleElementValue)
}
// parseAllElementValues performs validations that must be done for all elements at once in
// case that there are multiple values.
func (v *Validator) parseAllElementValues(key string, definition FieldDefinition, val interface{}, doc common.MapStr) error {
switch definition.Type {
case "constant_keyword", "keyword", "text":
if !v.specVersion.LessThan(semver2_0_0) {
strings, err := valueToStringsSlice(val)
if err != nil {
return fmt.Errorf("field %q value \"%v\" (%T): %w", key, val, val, err)
}
if err := ensureExpectedEventType(key, strings, definition, doc); err != nil {
return err
}
}
}
return nil
}
// parseSingeElementValue performs validations on individual values of each element.
func (v *Validator) parseSingleElementValue(key string, definition FieldDefinition, val interface{}, doc common.MapStr) error {
invalidTypeError := func() error {
return fmt.Errorf("field %q's Go type, %T, does not match the expected field type: %s (field value: %v)", key, val, definition.Type, val)
}
switch definition.Type {
// Constant keywords can define a value in the definition, if they do, all
// values stored in this field should be this one.
// If a pattern is provided, it checks if the value matches.
case "constant_keyword":
valStr, valid := val.(string)
if !valid {
return invalidTypeError()
}
if err := ensureConstantKeywordValueMatches(key, valStr, definition.Value); err != nil {
return err
}
if err := ensurePatternMatches(key, valStr, definition.Pattern); err != nil {
return err
}
if err := ensureAllowedValues(key, valStr, definition); err != nil {
return err
}
// Normal text fields should be of type string.
// If a pattern is provided, it checks if the value matches.
case "keyword", "text":
valStr, valid := val.(string)
if !valid {
return invalidTypeError()
}
if err := ensurePatternMatches(key, valStr, definition.Pattern); err != nil {
return err
}
if err := ensureAllowedValues(key, valStr, definition); err != nil {
return err
}
// Dates are expected to be formatted as strings or as seconds or milliseconds
// since epoch.
// If it is a string and a pattern is provided, it checks if the value matches.
case "date":
switch val := val.(type) {
case string:
if err := ensurePatternMatches(key, val, definition.Pattern); err != nil {
return err
}
case float64:
// date as seconds or milliseconds since epoch
if definition.Pattern != "" {
return fmt.Errorf("numeric date in field %q, but pattern defined", key)
}
default:
return invalidTypeError()
}
// IP values should be actual IPs, included in the ranges of IPs available
// in the geoip test database.
// If a pattern is provided, it checks if the value matches.
case "ip":
valStr, valid := val.(string)
if !valid {
return invalidTypeError()
}
if err := ensurePatternMatches(key, valStr, definition.Pattern); err != nil {
return err
}
if v.enabledAllowedIPCheck && !v.isAllowedIPValue(valStr) {
return fmt.Errorf("the IP %q is not one of the allowed test IPs (see: https://github.com/elastic/elastic-package/blob/main/internal/fields/_static/allowed_geo_ips.txt)", valStr)
}
// Groups should only contain nested fields, not single values.
case "group":
switch val.(type) {
case map[string]interface{}:
// TODO: This is probably an element from an array of objects,
// even if not recommended, it should be validated.
default:
return fmt.Errorf("field %q is a group of fields, it cannot store values", key)
}
// Numbers should have been parsed as float64, otherwise they are not numbers.
case "float", "long", "double":
if _, valid := val.(float64); !valid {
return invalidTypeError()
}
// All other types are considered valid not blocking validation.
default:
return nil
}
return nil
}
// isAllowedIPValue checks if the provided IP is allowed for testing
// The set of allowed IPs are:
// - private IPs as described in RFC 1918 & RFC 4193
// - public IPs allowed by MaxMind for testing
// - 0.0.0.0 and 255.255.255.255 for IPv4
// - 0:0:0:0:0:0:0:0 and ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff for IPv6
func (v *Validator) isAllowedIPValue(s string) bool {
ip := net.ParseIP(s)
if ip == nil {
return false
}
for _, allowedCIDR := range v.allowedCIDRs {
if allowedCIDR.Contains(ip) {
return true
}
}
if ip.IsUnspecified() ||
ip.IsPrivate() ||
ip.IsLoopback() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsMulticast() ||
ip.Equal(net.IPv4bcast) {
return true
}
return false
}
// forEachElementValue visits a function for each element in the given value if
// it is an array. If it is not an array, it calls the function with it.
func forEachElementValue(key string, definition FieldDefinition, val interface{}, doc common.MapStr, fn func(string, FieldDefinition, interface{}, common.MapStr) error) error {
arr, isArray := val.([]interface{})
if !isArray {
return fn(key, definition, val, doc)
}
for _, element := range arr {
err := fn(key, definition, element, doc)
if err != nil {
return err
}
}
return nil
}
// ensurePatternMatches validates the document's field value matches the field
// definitions regular expression pattern.
func ensurePatternMatches(key, value, pattern string) error {
if pattern == "" {
return nil
}
valid, err := regexp.MatchString(pattern, value)
if err != nil {
return errors.Wrap(err, "invalid pattern")
}
if !valid {
return fmt.Errorf("field %q's value, %s, does not match the expected pattern: %s", key, value, pattern)
}
return nil
}
// ensureConstantKeywordValueMatches validates the document's field value
// matches the definition's constant_keyword value.
func ensureConstantKeywordValueMatches(key, value, constantKeywordValue string) error {
if constantKeywordValue == "" {
return nil
}
if value != constantKeywordValue {
return fmt.Errorf("field %q's value %q does not match the declared constant_keyword value %q", key, value, constantKeywordValue)
}
return nil
}
// ensureAllowedValues validates that the document's field value
// is one of the allowed values.
func ensureAllowedValues(key, value string, definition FieldDefinition) error {
if !definition.AllowedValues.IsAllowed(value) {
return fmt.Errorf("field %q's value %q is not one of the allowed values (%s)", key, value, strings.Join(definition.AllowedValues.Values(), ", "))
}
if e := definition.ExpectedValues; len(e) > 0 && !common.StringSliceContains(e, value) {
return fmt.Errorf("field %q's value %q is not one of the expected values (%s)", key, value, strings.Join(e, ", "))
}
return nil
}
// ensureExpectedEventType validates that the document's `event.type` field is one of the expected
// one for the given value.
func ensureExpectedEventType(key string, values []string, definition FieldDefinition, doc common.MapStr) error {
eventTypeVal, _ := doc.GetValue("event.type")
eventTypes, err := valueToStringsSlice(eventTypeVal)
if err != nil {
return fmt.Errorf("field \"event.type\" value \"%v\" (%T): %w", eventTypeVal, eventTypeVal, err)
}
var expected []string
for _, value := range values {
expectedForValue := definition.AllowedValues.ExpectedEventTypes(value)
expected = common.StringSlicesUnion(expected, expectedForValue)
}
if len(expected) == 0 {
// No restrictions defined for this value, all good to go.
return nil
}
for _, eventType := range eventTypes {
if !common.StringSliceContains(expected, eventType) {
return fmt.Errorf("field \"event.type\" value %q is not one of the expected values (%s) for any of the values of %q (%s)", eventType, strings.Join(expected, ", "), key, strings.Join(values, ", "))
}
}
return nil
}
func valueToStringsSlice(value interface{}) ([]string, error) {
switch v := value.(type) {
case nil:
return nil, nil
case string:
return []string{v}, nil
case []interface{}:
var values []string
for _, e := range v {
s, ok := e.(string)
if !ok {
return nil, fmt.Errorf("expected string or array of strings")
}
values = append(values, s)
}
return values, nil
default:
return nil, fmt.Errorf("expected string or array of strings")
}
}