github.com/Jeffail/benthos/v3@v3.65.0/internal/docs/config.go (about)

     1  package docs
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/Jeffail/benthos/v3/internal/interop/plugins"
    10  	"github.com/Jeffail/gabs/v2"
    11  )
    12  
    13  const labelExpression = `^[a-z0-9_]+$`
    14  
    15  var (
    16  	labelRe = regexp.MustCompile(labelExpression)
    17  
    18  	// ErrBadLabel is returned when creating a component with a bad label.
    19  	ErrBadLabel = fmt.Errorf("should match the regular expression /%v/ and must not start with an underscore", labelExpression)
    20  )
    21  
    22  // ValidateLabel attempts to validate the contents of a component label.
    23  func ValidateLabel(label string) error {
    24  	if strings.HasPrefix(label, "_") {
    25  		return ErrBadLabel
    26  	}
    27  	if !labelRe.MatchString(label) {
    28  		return ErrBadLabel
    29  	}
    30  	return nil
    31  }
    32  
    33  var labelField = FieldString(
    34  	"label", "An optional label to use as an identifier for observability data such as metrics and logging.",
    35  ).OmitWhen(func(field, parent interface{}) (string, bool) {
    36  	gObj := gabs.Wrap(parent)
    37  	if typeStr, exists := gObj.S("type").Data().(string); exists && typeStr == "resource" {
    38  		return "label field should be omitted when pointing to a resource", true
    39  	}
    40  	if resourceStr, exists := gObj.S("resource").Data().(string); exists && resourceStr != "" {
    41  		return "label field should be omitted when pointing to a resource", true
    42  	}
    43  	return "", false
    44  }).AtVersion("3.44.0").Linter(func(ctx LintContext, line, col int, v interface{}) []Lint {
    45  	l, _ := v.(string)
    46  	if l == "" {
    47  		return nil
    48  	}
    49  	if err := ValidateLabel(l); err != nil {
    50  		return []Lint{
    51  			NewLintError(line, fmt.Sprintf("Invalid label '%v': %v", l, err)),
    52  		}
    53  	}
    54  	prevLine, exists := ctx.LabelsToLine[l]
    55  	if exists {
    56  		return []Lint{
    57  			NewLintError(line, fmt.Sprintf("Label '%v' collides with a previously defined label at line %v", l, prevLine)),
    58  		}
    59  	}
    60  	ctx.LabelsToLine[l] = line
    61  	return nil
    62  })
    63  
    64  func reservedFieldsByType(t Type) map[string]FieldSpec {
    65  	m := map[string]FieldSpec{
    66  		"type":   FieldString("type", ""),
    67  		"plugin": FieldCommon("plugin", "").HasType(FieldTypeObject),
    68  	}
    69  	if t == TypeInput || t == TypeOutput {
    70  		m["processors"] = FieldCommon("processors", "").Array().HasType(FieldTypeProcessor).OmitWhen(func(field, _ interface{}) (string, bool) {
    71  			if arr, ok := field.([]interface{}); ok && len(arr) == 0 {
    72  				return "field processors is empty and can be removed", true
    73  			}
    74  			return "", false
    75  		})
    76  	}
    77  	if _, isLabelType := map[Type]struct{}{
    78  		TypeInput:     {},
    79  		TypeProcessor: {},
    80  		TypeOutput:    {},
    81  		TypeCache:     {},
    82  		TypeRateLimit: {},
    83  	}[t]; isLabelType {
    84  		m["label"] = labelField
    85  	}
    86  	return m
    87  }
    88  
    89  // TODO: V4 remove this as it's not needed.
    90  func refreshOldPlugins() {
    91  	plugins.FlushNameTypes(func(nt [2]string) {
    92  		RegisterDocs(ComponentSpec{
    93  			Name:   nt[0],
    94  			Type:   Type(nt[1]),
    95  			Plugin: true,
    96  			Status: StatusExperimental,
    97  		})
    98  	})
    99  }
   100  
   101  // GetInferenceCandidate checks a generic config structure for a component and
   102  // returns either the inferred type name or an error if one cannot be inferred.
   103  func GetInferenceCandidate(docProvider Provider, t Type, defaultType string, raw interface{}) (string, ComponentSpec, error) {
   104  	m, ok := raw.(map[string]interface{})
   105  	if !ok {
   106  		return "", ComponentSpec{}, fmt.Errorf("invalid config value %T, expected object", raw)
   107  	}
   108  
   109  	if tStr, ok := m["type"].(string); ok {
   110  		spec, exists := GetDocs(docProvider, tStr, t)
   111  		if !exists {
   112  			return "", ComponentSpec{}, fmt.Errorf("%v type '%v' was not recognised", string(t), tStr)
   113  		}
   114  		return tStr, spec, nil
   115  	}
   116  
   117  	var keys []string
   118  	for k := range m {
   119  		keys = append(keys, k)
   120  	}
   121  
   122  	return getInferenceCandidateFromList(docProvider, t, defaultType, keys)
   123  }
   124  
   125  func getInferenceCandidateFromList(docProvider Provider, t Type, defaultType string, l []string) (string, ComponentSpec, error) {
   126  	ignore := reservedFieldsByType(t)
   127  
   128  	var candidates []string
   129  	var inferred string
   130  	var inferredSpec ComponentSpec
   131  	for _, k := range l {
   132  		if _, exists := ignore[k]; exists {
   133  			continue
   134  		}
   135  		candidates = append(candidates, k)
   136  		if spec, exists := GetDocs(docProvider, k, t); exists {
   137  			if len(inferred) > 0 {
   138  				candidates = []string{inferred, k}
   139  				sort.Strings(candidates)
   140  				return "", ComponentSpec{}, fmt.Errorf(
   141  					"unable to infer %v type, multiple candidates '%v' and '%v'", string(t), candidates[0], candidates[1],
   142  				)
   143  			}
   144  			inferred = k
   145  			inferredSpec = spec
   146  		}
   147  	}
   148  
   149  	if len(candidates) == 0 && len(defaultType) > 0 {
   150  		// A totally empty component config results in the default.
   151  		// TODO: V4 Disable this
   152  		if spec, exists := GetDocs(docProvider, defaultType, t); exists {
   153  			return defaultType, spec, nil
   154  		}
   155  	}
   156  
   157  	if inferred == "" {
   158  		sort.Strings(candidates)
   159  		return "", ComponentSpec{}, fmt.Errorf("unable to infer %v type, candidates were: %v", string(t), candidates)
   160  	}
   161  	return inferred, inferredSpec, nil
   162  }
   163  
   164  // TODO: V4 Remove this.
   165  func sanitiseConditionConfig(raw interface{}, removeDeprecated bool) error {
   166  	// This is a nasty hack until Benthos v4.
   167  	m, ok := raw.(map[string]interface{})
   168  	if !ok {
   169  		return fmt.Errorf("expected object configuration type, found: %T", raw)
   170  	}
   171  	typeStr, ok := m["type"]
   172  	if !ok {
   173  		return nil
   174  	}
   175  	for k := range m {
   176  		if k == typeStr || k == "type" || k == "plugin" {
   177  			continue
   178  		}
   179  		delete(m, k)
   180  	}
   181  	return nil
   182  }
   183  
   184  // SanitiseComponentConfig reduces a raw component configuration into only the
   185  // fields for the component name configured.
   186  //
   187  // TODO: V4 Remove this
   188  func SanitiseComponentConfig(componentType Type, raw interface{}, filter FieldFilter) error {
   189  	if componentType == "condition" {
   190  		return sanitiseConditionConfig(raw, false)
   191  	}
   192  
   193  	name, spec, err := GetInferenceCandidate(globalProvider, componentType, "", raw)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	m, ok := raw.(map[string]interface{})
   199  	if !ok {
   200  		return fmt.Errorf("expected object configuration type, found: %T", raw)
   201  	}
   202  
   203  	if componentConfRaw, exists := m[name]; exists {
   204  		spec.Config.sanitise(componentConfRaw, filter)
   205  	}
   206  
   207  	reservedFields := reservedFieldsByType(componentType)
   208  	for k, v := range m {
   209  		if k == name {
   210  			continue
   211  		}
   212  		spec, exists := reservedFields[k]
   213  		if !exists {
   214  			delete(m, k)
   215  		}
   216  		if _, omit := spec.shouldOmit(v, m); omit {
   217  			delete(m, k)
   218  		}
   219  	}
   220  
   221  	for name, fieldSpec := range reservedFields {
   222  		fieldSpec.sanitise(m[name], filter)
   223  	}
   224  	return nil
   225  }
   226  
   227  // SanitiseConfig contains fields describing the desired behaviour of the config
   228  // sanitiser such as removing certain fields.
   229  type SanitiseConfig struct {
   230  	RemoveTypeField  bool
   231  	RemoveDeprecated bool
   232  	ForExample       bool
   233  	Filter           FieldFilter
   234  	DocsProvider     Provider
   235  }
   236  
   237  // GetDocs attempts to obtain documentation for a component implementation from
   238  // a docs provider in the config, or if omitted uses the global provider.
   239  func (c SanitiseConfig) GetDocs(name string, ctype Type) (ComponentSpec, bool) {
   240  	return GetDocs(c.DocsProvider, name, ctype)
   241  }