1package main
2
3import (
4	"flag"
5	"fmt"
6	"reflect"
7	"regexp"
8	"strings"
9	"unicode"
10
11	"github.com/grafana/dskit/flagext"
12	"github.com/pkg/errors"
13	"github.com/prometheus/common/model"
14	"github.com/weaveworks/common/logging"
15)
16
17var (
18	yamlFieldNameParser   = regexp.MustCompile("^[^,]+")
19	yamlFieldInlineParser = regexp.MustCompile("^[^,]*,inline$")
20)
21
22type configBlock struct {
23	name          string
24	desc          string
25	entries       []*configEntry
26	flagsPrefix   string
27	flagsPrefixes []string
28}
29
30func (b *configBlock) Add(entry *configEntry) {
31	b.entries = append(b.entries, entry)
32}
33
34type configEntry struct {
35	kind     string
36	name     string
37	required bool
38
39	// In case the kind is "block"
40	block     *configBlock
41	blockDesc string
42	root      bool
43
44	// In case the kind is "field"
45	fieldFlag    string
46	fieldDesc    string
47	fieldType    string
48	fieldDefault string
49}
50
51type rootBlock struct {
52	name       string
53	desc       string
54	structType reflect.Type
55}
56
57func parseFlags(cfg flagext.Registerer) map[uintptr]*flag.Flag {
58	fs := flag.NewFlagSet("", flag.PanicOnError)
59	cfg.RegisterFlags(fs)
60
61	flags := map[uintptr]*flag.Flag{}
62	fs.VisitAll(func(f *flag.Flag) {
63		// Skip deprecated flags
64		if f.Value.String() == "deprecated" {
65			return
66		}
67
68		ptr := reflect.ValueOf(f.Value).Pointer()
69		flags[ptr] = f
70	})
71
72	return flags
73}
74
75func parseConfig(block *configBlock, cfg interface{}, flags map[uintptr]*flag.Flag) ([]*configBlock, error) {
76	blocks := []*configBlock{}
77
78	// If the input block is nil it means we're generating the doc for the top-level block
79	if block == nil {
80		block = &configBlock{}
81		blocks = append(blocks, block)
82	}
83
84	// The input config is expected to be addressable.
85	if reflect.TypeOf(cfg).Kind() != reflect.Ptr {
86		t := reflect.TypeOf(cfg)
87		return nil, fmt.Errorf("%s is a %s while a %s is expected", t, t.Kind(), reflect.Ptr)
88	}
89
90	// The input config is expected to be a pointer to struct.
91	v := reflect.ValueOf(cfg).Elem()
92	t := v.Type()
93
94	if v.Kind() != reflect.Struct {
95		return nil, fmt.Errorf("%s is a %s while a %s is expected", v, v.Kind(), reflect.Struct)
96	}
97
98	for i := 0; i < t.NumField(); i++ {
99		field := t.Field(i)
100		fieldValue := v.FieldByIndex(field.Index)
101
102		// Skip fields explicitly marked as "hidden" in the doc
103		if isFieldHidden(field) {
104			continue
105		}
106
107		// Skip fields not exported via yaml (unless they're inline)
108		fieldName := getFieldName(field)
109		if fieldName == "" && !isFieldInline(field) {
110			continue
111		}
112
113		// Skip field types which are non configurable
114		if field.Type.Kind() == reflect.Func {
115			continue
116		}
117
118		// Skip deprecated fields we're still keeping for backward compatibility
119		// reasons (by convention we prefix them by UnusedFlag)
120		if strings.HasPrefix(field.Name, "UnusedFlag") {
121			continue
122		}
123
124		// Handle custom fields in vendored libs upon which we have no control.
125		fieldEntry, err := getCustomFieldEntry(field, fieldValue, flags)
126		if err != nil {
127			return nil, err
128		}
129		if fieldEntry != nil {
130			block.Add(fieldEntry)
131			continue
132		}
133
134		// Recursively re-iterate if it's a struct
135		if field.Type.Kind() == reflect.Struct {
136			// Check whether the sub-block is a root config block
137			rootName, rootDesc, isRoot := isRootBlock(field.Type)
138
139			// Since we're going to recursively iterate, we need to create a new sub
140			// block and pass it to the doc generation function.
141			var subBlock *configBlock
142
143			if !isFieldInline(field) {
144				var blockName string
145				var blockDesc string
146
147				if isRoot {
148					blockName = rootName
149					blockDesc = rootDesc
150				} else {
151					blockName = fieldName
152					blockDesc = getFieldDescription(field, "")
153				}
154
155				subBlock = &configBlock{
156					name: blockName,
157					desc: blockDesc,
158				}
159
160				block.Add(&configEntry{
161					kind:      "block",
162					name:      fieldName,
163					required:  isFieldRequired(field),
164					block:     subBlock,
165					blockDesc: blockDesc,
166					root:      isRoot,
167				})
168
169				if isRoot {
170					blocks = append(blocks, subBlock)
171				}
172			} else {
173				subBlock = block
174			}
175
176			// Recursively generate the doc for the sub-block
177			otherBlocks, err := parseConfig(subBlock, fieldValue.Addr().Interface(), flags)
178			if err != nil {
179				return nil, err
180			}
181
182			blocks = append(blocks, otherBlocks...)
183			continue
184		}
185
186		fieldType, err := getFieldType(field.Type)
187		if err != nil {
188			return nil, errors.Wrapf(err, "config=%s.%s", t.PkgPath(), t.Name())
189		}
190
191		fieldFlag, err := getFieldFlag(field, fieldValue, flags)
192		if err != nil {
193			return nil, errors.Wrapf(err, "config=%s.%s", t.PkgPath(), t.Name())
194		}
195		if fieldFlag == nil {
196			block.Add(&configEntry{
197				kind:      "field",
198				name:      fieldName,
199				required:  isFieldRequired(field),
200				fieldDesc: getFieldDescription(field, ""),
201				fieldType: fieldType,
202			})
203			continue
204		}
205
206		block.Add(&configEntry{
207			kind:         "field",
208			name:         fieldName,
209			required:     isFieldRequired(field),
210			fieldFlag:    fieldFlag.Name,
211			fieldDesc:    getFieldDescription(field, fieldFlag.Usage),
212			fieldType:    fieldType,
213			fieldDefault: fieldFlag.DefValue,
214		})
215	}
216
217	return blocks, nil
218}
219
220func getFieldName(field reflect.StructField) string {
221	name := field.Name
222	tag := field.Tag.Get("yaml")
223
224	// If the tag is not specified, then an exported field can be
225	// configured via the field name (lowercase), while an unexported
226	// field can't be configured.
227	if tag == "" {
228		if unicode.IsLower(rune(name[0])) {
229			return ""
230		}
231
232		return strings.ToLower(name)
233	}
234
235	// Parse the field name
236	fieldName := yamlFieldNameParser.FindString(tag)
237	if fieldName == "-" {
238		return ""
239	}
240
241	return fieldName
242}
243
244func getFieldType(t reflect.Type) (string, error) {
245	// Handle custom data types used in the config
246	switch t.String() {
247	case "*url.URL":
248		return "url", nil
249	case "time.Duration":
250		return "duration", nil
251	case "cortex.moduleName":
252		return "string", nil
253	case "flagext.StringSliceCSV":
254		return "string", nil
255	case "flagext.CIDRSliceCSV":
256		return "string", nil
257	case "[]*relabel.Config":
258		return "relabel_config...", nil
259	}
260
261	// Fallback to auto-detection of built-in data types
262	switch t.Kind() {
263	case reflect.Bool:
264		return "boolean", nil
265
266	case reflect.Int:
267		fallthrough
268	case reflect.Int8:
269		fallthrough
270	case reflect.Int16:
271		fallthrough
272	case reflect.Int32:
273		fallthrough
274	case reflect.Int64:
275		fallthrough
276	case reflect.Uint:
277		fallthrough
278	case reflect.Uint8:
279		fallthrough
280	case reflect.Uint16:
281		fallthrough
282	case reflect.Uint32:
283		fallthrough
284	case reflect.Uint64:
285		return "int", nil
286
287	case reflect.Float32:
288		fallthrough
289	case reflect.Float64:
290		return "float", nil
291
292	case reflect.String:
293		return "string", nil
294
295	case reflect.Slice:
296		// Get the type of elements
297		elemType, err := getFieldType(t.Elem())
298		if err != nil {
299			return "", err
300		}
301
302		return "list of " + elemType, nil
303
304	case reflect.Map:
305		return fmt.Sprintf("map of %s to %s", t.Key(), t.Elem().String()), nil
306
307	default:
308		return "", fmt.Errorf("unsupported data type %s", t.Kind())
309	}
310}
311
312func getFieldFlag(field reflect.StructField, fieldValue reflect.Value, flags map[uintptr]*flag.Flag) (*flag.Flag, error) {
313	if isAbsentInCLI(field) {
314		return nil, nil
315	}
316	fieldPtr := fieldValue.Addr().Pointer()
317	fieldFlag, ok := flags[fieldPtr]
318	if !ok {
319		return nil, fmt.Errorf("unable to find CLI flag for '%s' config entry", field.Name)
320	}
321
322	return fieldFlag, nil
323}
324
325func getCustomFieldEntry(field reflect.StructField, fieldValue reflect.Value, flags map[uintptr]*flag.Flag) (*configEntry, error) {
326	if field.Type == reflect.TypeOf(logging.Level{}) || field.Type == reflect.TypeOf(logging.Format{}) {
327		fieldFlag, err := getFieldFlag(field, fieldValue, flags)
328		if err != nil {
329			return nil, err
330		}
331
332		return &configEntry{
333			kind:         "field",
334			name:         getFieldName(field),
335			required:     isFieldRequired(field),
336			fieldFlag:    fieldFlag.Name,
337			fieldDesc:    fieldFlag.Usage,
338			fieldType:    "string",
339			fieldDefault: fieldFlag.DefValue,
340		}, nil
341	}
342	if field.Type == reflect.TypeOf(flagext.URLValue{}) {
343		fieldFlag, err := getFieldFlag(field, fieldValue, flags)
344		if err != nil {
345			return nil, err
346		}
347
348		return &configEntry{
349			kind:         "field",
350			name:         getFieldName(field),
351			required:     isFieldRequired(field),
352			fieldFlag:    fieldFlag.Name,
353			fieldDesc:    fieldFlag.Usage,
354			fieldType:    "url",
355			fieldDefault: fieldFlag.DefValue,
356		}, nil
357	}
358	if field.Type == reflect.TypeOf(flagext.Secret{}) {
359		fieldFlag, err := getFieldFlag(field, fieldValue, flags)
360		if err != nil {
361			return nil, err
362		}
363
364		return &configEntry{
365			kind:         "field",
366			name:         getFieldName(field),
367			required:     isFieldRequired(field),
368			fieldFlag:    fieldFlag.Name,
369			fieldDesc:    fieldFlag.Usage,
370			fieldType:    "string",
371			fieldDefault: fieldFlag.DefValue,
372		}, nil
373	}
374	if field.Type == reflect.TypeOf(model.Duration(0)) {
375		fieldFlag, err := getFieldFlag(field, fieldValue, flags)
376		if err != nil {
377			return nil, err
378		}
379
380		return &configEntry{
381			kind:         "field",
382			name:         getFieldName(field),
383			required:     isFieldRequired(field),
384			fieldFlag:    fieldFlag.Name,
385			fieldDesc:    fieldFlag.Usage,
386			fieldType:    "duration",
387			fieldDefault: fieldFlag.DefValue,
388		}, nil
389	}
390	if field.Type == reflect.TypeOf(flagext.Time{}) {
391		fieldFlag, err := getFieldFlag(field, fieldValue, flags)
392		if err != nil {
393			return nil, err
394		}
395
396		return &configEntry{
397			kind:         "field",
398			name:         getFieldName(field),
399			required:     isFieldRequired(field),
400			fieldFlag:    fieldFlag.Name,
401			fieldDesc:    fieldFlag.Usage,
402			fieldType:    "time",
403			fieldDefault: fieldFlag.DefValue,
404		}, nil
405	}
406
407	return nil, nil
408}
409
410func isFieldHidden(f reflect.StructField) bool {
411	return getDocTagFlag(f, "hidden")
412}
413
414func isAbsentInCLI(f reflect.StructField) bool {
415	return getDocTagFlag(f, "nocli")
416}
417
418func isFieldRequired(f reflect.StructField) bool {
419	return getDocTagFlag(f, "required")
420}
421
422func isFieldInline(f reflect.StructField) bool {
423	return yamlFieldInlineParser.MatchString(f.Tag.Get("yaml"))
424}
425
426func getFieldDescription(f reflect.StructField, fallback string) string {
427	if desc := getDocTagValue(f, "description"); desc != "" {
428		return desc
429	}
430
431	return fallback
432}
433
434func isRootBlock(t reflect.Type) (string, string, bool) {
435	for _, rootBlock := range rootBlocks {
436		if t == rootBlock.structType {
437			return rootBlock.name, rootBlock.desc, true
438		}
439	}
440
441	return "", "", false
442}
443
444func getDocTagFlag(f reflect.StructField, name string) bool {
445	cfg := parseDocTag(f)
446	_, ok := cfg[name]
447	return ok
448}
449
450func getDocTagValue(f reflect.StructField, name string) string {
451	cfg := parseDocTag(f)
452	return cfg[name]
453}
454
455func parseDocTag(f reflect.StructField) map[string]string {
456	cfg := map[string]string{}
457	tag := f.Tag.Get("doc")
458
459	if tag == "" {
460		return cfg
461	}
462
463	for _, entry := range strings.Split(tag, "|") {
464		parts := strings.SplitN(entry, "=", 2)
465
466		switch len(parts) {
467		case 1:
468			cfg[parts[0]] = ""
469		case 2:
470			cfg[parts[0]] = parts[1]
471		}
472	}
473
474	return cfg
475}
476