github.com/observiq/carbon@v0.9.11-0.20200820160507-1b872e368a5e/pipeline/config.go (about)

     1  package pipeline
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/observiq/carbon/errors"
     8  	"github.com/observiq/carbon/operator"
     9  	"github.com/observiq/carbon/operator/helper"
    10  	yaml "gopkg.in/yaml.v2"
    11  )
    12  
    13  // Config is the configuration of a pipeline.
    14  type Config []Params
    15  
    16  // BuildPipeline will build a pipeline from the config.
    17  func (c Config) BuildPipeline(context operator.BuildContext) (*Pipeline, error) {
    18  	operatorConfigs, err := c.buildOperatorConfigs(context.PluginRegistry)
    19  	if err != nil {
    20  		return nil, err
    21  	}
    22  
    23  	operators, err := c.buildOperators(operatorConfigs, context)
    24  	if err != nil {
    25  		return nil, err
    26  	}
    27  
    28  	pipeline, err := NewPipeline(operators)
    29  	if err != nil {
    30  		return nil, err
    31  	}
    32  
    33  	return pipeline, nil
    34  }
    35  
    36  func (c Config) buildOperatorConfigs(pluginRegistry operator.PluginRegistry) ([]operator.Config, error) {
    37  	operatorConfigs := make([]operator.Config, 0, len(c))
    38  
    39  	for i, params := range c {
    40  		if err := params.Validate(); err != nil {
    41  			return nil, errors.Wrap(err, "validate config params")
    42  		}
    43  
    44  		configs, err := params.BuildConfigs(pluginRegistry, "$", c.defaultOutput(i, "$"))
    45  		if err != nil {
    46  			return nil, errors.Wrap(err, "build operator configs")
    47  		}
    48  		operatorConfigs = append(operatorConfigs, configs...)
    49  	}
    50  
    51  	return operatorConfigs, nil
    52  }
    53  
    54  func (c Config) buildOperators(operatorConfigs []operator.Config, context operator.BuildContext) ([]operator.Operator, error) {
    55  	operators := make([]operator.Operator, 0, len(operatorConfigs))
    56  	for _, operatorConfig := range operatorConfigs {
    57  		operator, err := operatorConfig.Build(context)
    58  
    59  		if err != nil {
    60  			return nil, errors.WithDetails(err,
    61  				"operator_id", operatorConfig.ID(),
    62  				"operator_type", operatorConfig.Type(),
    63  			)
    64  		}
    65  
    66  		operators = append(operators, operator)
    67  	}
    68  
    69  	return operators, nil
    70  }
    71  
    72  // default returns an array containing the next operator in the pipeline
    73  // if it exists, where i is the index of the current operator
    74  func (c Config) defaultOutput(i int, namespace string) []string {
    75  	if i+1 < len(c) {
    76  		return []string{helper.AddNamespace(c[i+1].ID(), namespace)}
    77  	}
    78  	return []string{}
    79  }
    80  
    81  // Params is a raw params map that can be converted into an operator config.
    82  type Params map[string]interface{}
    83  
    84  // ID returns the id field in the params map.
    85  func (p Params) ID() string {
    86  	if p.getString("id") == "" {
    87  		return p.getString("type")
    88  	}
    89  	return p.getString("id")
    90  }
    91  
    92  // Type returns the type field in the params map.
    93  func (p Params) Type() string {
    94  	return p.getString("type")
    95  }
    96  
    97  // Outputs returns the output field in the params map.
    98  func (p Params) Outputs() []string {
    99  	return p.getStringArray("output")
   100  }
   101  
   102  // NamespacedID will return the id field with a namespace.
   103  func (p Params) NamespacedID(namespace string) string {
   104  	return helper.AddNamespace(p.ID(), namespace)
   105  }
   106  
   107  // NamespacedOutputs will return the output field with a namespace.
   108  func (p Params) NamespacedOutputs(namespace string) []string {
   109  	outputs := p.Outputs()
   110  	for i, output := range outputs {
   111  		outputs[i] = helper.AddNamespace(output, namespace)
   112  	}
   113  	return outputs
   114  }
   115  
   116  // TemplateInput will return the template input.
   117  func (p Params) TemplateInput(namespace string) string {
   118  	return helper.AddNamespace(p.ID(), namespace)
   119  }
   120  
   121  // TemplateOutput will return the template output.
   122  func (p Params) TemplateOutput(namespace string, defaultOutput []string) string {
   123  	outputs := p.NamespacedOutputs(namespace)
   124  	if len(outputs) == 0 {
   125  		outputs = defaultOutput
   126  	}
   127  	return fmt.Sprintf("[%s]", strings.Join(outputs[:], ", "))
   128  }
   129  
   130  // NamespaceExclusions will return all ids to exclude from namespacing.
   131  func (p Params) NamespaceExclusions(namespace string) []string {
   132  	exclusions := []string{p.NamespacedID(namespace)}
   133  	for _, output := range p.NamespacedOutputs(namespace) {
   134  		exclusions = append(exclusions, output)
   135  	}
   136  	return exclusions
   137  }
   138  
   139  // Validate will validate the basic fields required to make an operator config.
   140  func (p Params) Validate() error {
   141  	if p.Type() == "" {
   142  		return errors.NewError(
   143  			"missing required `type` field for operator config",
   144  			"ensure that all operator configs have a defined type field",
   145  			"id", p.ID(),
   146  		)
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // getString returns a string value from the params block.
   153  func (p Params) getString(key string) string {
   154  	rawValue, ok := p[key]
   155  	if !ok {
   156  		return ""
   157  	}
   158  
   159  	stringValue, ok := rawValue.(string)
   160  	if !ok {
   161  		return ""
   162  	}
   163  
   164  	return stringValue
   165  }
   166  
   167  // getStringArray returns a string array from the params block.
   168  func (p Params) getStringArray(key string) []string {
   169  	rawValue, ok := p[key]
   170  	if !ok {
   171  		return []string{}
   172  	}
   173  
   174  	switch value := rawValue.(type) {
   175  	case string:
   176  		return []string{value}
   177  	case []string:
   178  		return value
   179  	case []interface{}:
   180  		result := []string{}
   181  		for _, x := range value {
   182  			if strValue, ok := x.(string); ok {
   183  				result = append(result, strValue)
   184  			}
   185  		}
   186  		return result
   187  	default:
   188  		return []string{}
   189  	}
   190  }
   191  
   192  // BuildConfigs will build operator configs from a params map.
   193  func (p Params) BuildConfigs(pluginRegistry operator.PluginRegistry, namespace string, defaultOutput []string) ([]operator.Config, error) {
   194  	if operator.IsDefined(p.Type()) {
   195  		return p.buildAsBuiltin(namespace)
   196  	}
   197  
   198  	if pluginRegistry.IsDefined(p.Type()) {
   199  		return p.buildPlugin(pluginRegistry, namespace, defaultOutput)
   200  	}
   201  
   202  	return nil, errors.NewError(
   203  		"unsupported `type` for operator config",
   204  		"ensure that all operators have a supported builtin or plugin type",
   205  		"type", p.Type(),
   206  		"id", p.ID(),
   207  	)
   208  }
   209  
   210  // buildAsBuiltin will build a builtin config from a params map.
   211  func (p Params) buildAsBuiltin(namespace string) ([]operator.Config, error) {
   212  	bytes, err := yaml.Marshal(p)
   213  	if err != nil {
   214  		return nil, errors.NewError(
   215  			"failed to parse config map as yaml",
   216  			"ensure that all config values are supported yaml values",
   217  			"error", err.Error(),
   218  		)
   219  	}
   220  
   221  	var config operator.Config
   222  	if err := yaml.UnmarshalStrict(bytes, &config); err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	config.SetNamespace(namespace)
   227  	return []operator.Config{config}, nil
   228  }
   229  
   230  // buildPlugin will build a plugin config from a params map.
   231  func (p Params) buildPlugin(pluginRegistry operator.PluginRegistry, namespace string, defaultOutput []string) ([]operator.Config, error) {
   232  	templateParams := map[string]interface{}{}
   233  	for key, value := range p {
   234  		templateParams[key] = value
   235  	}
   236  
   237  	templateParams["input"] = p.TemplateInput(namespace)
   238  	templateParams["output"] = p.TemplateOutput(namespace, defaultOutput)
   239  
   240  	config, err := pluginRegistry.Render(p.Type(), templateParams)
   241  	if err != nil {
   242  		return nil, errors.Wrap(err, "failed to render plugin config")
   243  	}
   244  
   245  	exclusions := p.NamespaceExclusions(namespace)
   246  	for _, operatorConfig := range config.Pipeline {
   247  		innerNamespace := p.NamespacedID(namespace)
   248  		operatorConfig.SetNamespace(innerNamespace, append(exclusions, defaultOutput...)...)
   249  	}
   250  
   251  	return config.Pipeline, nil
   252  }
   253  
   254  // UnmarshalYAML will unmarshal yaml bytes into Params
   255  func (p *Params) UnmarshalYAML(unmarshal func(interface{}) error) error {
   256  	var m map[interface{}]interface{}
   257  	err := unmarshal(&m)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	*p = Params(cleanMap(m))
   263  	return nil
   264  }
   265  
   266  func cleanMap(m map[interface{}]interface{}) map[string]interface{} {
   267  	clean := make(map[string]interface{}, len(m))
   268  	for k, v := range m {
   269  		clean[fmt.Sprintf("%v", k)] = cleanValue(v)
   270  	}
   271  	return clean
   272  }
   273  
   274  func cleanValue(v interface{}) interface{} {
   275  	switch v := v.(type) {
   276  	case string, bool, int, int64, int32, float32, float64, nil:
   277  		return v
   278  	case map[interface{}]interface{}:
   279  		return cleanMap(v)
   280  	case []interface{}:
   281  		res := make([]interface{}, 0, len(v))
   282  		for _, arrayVal := range v {
   283  			res = append(res, cleanValue(arrayVal))
   284  		}
   285  		return res
   286  	default:
   287  		return fmt.Sprintf("%v", v)
   288  	}
   289  }