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

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	"github.com/Jeffail/benthos/v3/internal/bundle"
    11  	"github.com/Jeffail/benthos/v3/internal/docs"
    12  	ifilepath "github.com/Jeffail/benthos/v3/internal/filepath"
    13  	"github.com/Jeffail/benthos/v3/lib/cache"
    14  	"github.com/Jeffail/benthos/v3/lib/condition"
    15  	"github.com/Jeffail/benthos/v3/lib/config"
    16  	"github.com/Jeffail/benthos/v3/lib/input"
    17  	"github.com/Jeffail/benthos/v3/lib/manager"
    18  	"github.com/Jeffail/benthos/v3/lib/output"
    19  	"github.com/Jeffail/benthos/v3/lib/processor"
    20  	"github.com/Jeffail/benthos/v3/lib/ratelimit"
    21  	"gopkg.in/yaml.v3"
    22  )
    23  
    24  type resourceFileInfo struct {
    25  	configFileInfo
    26  
    27  	// Need to track the resource that came from the previous read as their
    28  	// absence in an update means they need to be removed.
    29  	inputs     map[string]*input.Config
    30  	conditions map[string]*condition.Config
    31  	processors map[string]*processor.Config
    32  	outputs    map[string]*output.Config
    33  	caches     map[string]*cache.Config
    34  	rateLimits map[string]*ratelimit.Config
    35  }
    36  
    37  func resInfoFromConfig(conf *manager.ResourceConfig) resourceFileInfo {
    38  	resInfo := resourceFileInfo{
    39  		inputs:     map[string]*input.Config{},
    40  		conditions: map[string]*condition.Config{},
    41  		processors: map[string]*processor.Config{},
    42  		outputs:    map[string]*output.Config{},
    43  		caches:     map[string]*cache.Config{},
    44  		rateLimits: map[string]*ratelimit.Config{},
    45  	}
    46  
    47  	// This is an unlikely race condition, see readMain for more info.
    48  	resInfo.updatedAt = time.Now()
    49  
    50  	// Old style
    51  	for k, v := range conf.Manager.Inputs {
    52  		resInfo.inputs[k] = &v
    53  	}
    54  	for k, v := range conf.Manager.Conditions {
    55  		resInfo.conditions[k] = &v
    56  	}
    57  	for k, v := range conf.Manager.Processors {
    58  		resInfo.processors[k] = &v
    59  	}
    60  	for k, v := range conf.Manager.Outputs {
    61  		resInfo.outputs[k] = &v
    62  	}
    63  	for k, v := range conf.Manager.Caches {
    64  		resInfo.caches[k] = &v
    65  	}
    66  	for k, v := range conf.Manager.RateLimits {
    67  		resInfo.rateLimits[k] = &v
    68  	}
    69  
    70  	// New style
    71  	for _, c := range conf.ResourceInputs {
    72  		resInfo.inputs[c.Label] = &c
    73  	}
    74  	for _, c := range conf.ResourceProcessors {
    75  		resInfo.processors[c.Label] = &c
    76  	}
    77  	for _, c := range conf.ResourceOutputs {
    78  		resInfo.outputs[c.Label] = &c
    79  	}
    80  	for _, c := range conf.ResourceCaches {
    81  		resInfo.caches[c.Label] = &c
    82  	}
    83  	for _, c := range conf.ResourceRateLimits {
    84  		resInfo.rateLimits[c.Label] = &c
    85  	}
    86  
    87  	return resInfo
    88  }
    89  
    90  func (r *Reader) readResources(conf *manager.ResourceConfig) (lints []string, err error) {
    91  	resourcesPaths, err := ifilepath.Globs(r.resourcePaths)
    92  	if err != nil {
    93  		return nil, fmt.Errorf("failed to resolve resource glob pattern: %w", err)
    94  	}
    95  	for _, path := range resourcesPaths {
    96  		rconf := manager.NewResourceConfig()
    97  		var rLints []string
    98  		if rLints, err = readResource(path, &rconf); err != nil {
    99  			return
   100  		}
   101  		lints = append(lints, rLints...)
   102  
   103  		if err = conf.AddFrom(&rconf); err != nil {
   104  			err = fmt.Errorf("%v: %w", path, err)
   105  			return
   106  		}
   107  		r.resourceFileInfo[filepath.Clean(path)] = resInfoFromConfig(&rconf)
   108  	}
   109  	return
   110  }
   111  
   112  func readResource(path string, conf *manager.ResourceConfig) (lints []string, err error) {
   113  	defer func() {
   114  		if err != nil {
   115  			err = fmt.Errorf("%v: %w", path, err)
   116  		}
   117  	}()
   118  
   119  	var confBytes []byte
   120  	if confBytes, lints, err = config.ReadWithJSONPointersLinted(path, true); err != nil {
   121  		return
   122  	}
   123  
   124  	var rawNode yaml.Node
   125  	if err = yaml.Unmarshal(confBytes, &rawNode); err != nil {
   126  		return
   127  	}
   128  	if !bytes.HasPrefix(confBytes, []byte("# BENTHOS LINT DISABLE")) {
   129  		allowTest := append(docs.FieldSpecs{
   130  			config.TestsField,
   131  		}, manager.Spec()...)
   132  		for _, lint := range allowTest.LintYAML(docs.NewLintContext(), &rawNode) {
   133  			lints = append(lints, fmt.Sprintf("resource file %v: line %v: %v", path, lint.Line, lint.What))
   134  		}
   135  	}
   136  
   137  	err = rawNode.Decode(conf)
   138  	return
   139  }
   140  
   141  func (r *Reader) reactResourceUpdate(mgr bundle.NewManagement, strict bool, path string) bool {
   142  	r.resourceFileInfoMut.Lock()
   143  	defer r.resourceFileInfoMut.Unlock()
   144  
   145  	if _, exists := r.resourceFileInfo[path]; !exists {
   146  		mgr.Logger().Warnf("Skipping resource update for unknown path: %v", path)
   147  		return true
   148  	}
   149  
   150  	mgr.Logger().Infof("Resource %v config updated, attempting to update resources.", path)
   151  
   152  	newResConf := manager.NewResourceConfig()
   153  	lints, err := readResource(path, &newResConf)
   154  	if err != nil {
   155  		mgr.Logger().Errorf("Failed to read updated resources config: %v", err)
   156  		return true
   157  	}
   158  
   159  	lintlog := mgr.Logger().NewModule(".linter")
   160  	for _, lint := range lints {
   161  		lintlog.Infoln(lint)
   162  	}
   163  	if strict && len(lints) > 0 {
   164  		mgr.Logger().Errorln("Rejecting updated resource config due to linter errors, to allow linting errors run Benthos with --chilled")
   165  		return true
   166  	}
   167  
   168  	// TODO: Should we error out if the new config is missing some resources?
   169  	// (as they will continue to exist). Also, we could avoid restarting
   170  	// resources where the config hasn't changed.
   171  
   172  	newInfo := resInfoFromConfig(&newResConf)
   173  	if !newInfo.applyChanges(mgr) {
   174  		return false
   175  	}
   176  
   177  	r.resourceFileInfo[path] = newInfo
   178  	return true
   179  }
   180  
   181  func (i *resourceFileInfo) applyChanges(mgr bundle.NewManagement) bool {
   182  	// Kind of arbitrary, but I feel better about having some sort of timeout.
   183  	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
   184  	defer cancel()
   185  
   186  	// WARNING: The order here is actually kind of important, we want to start
   187  	// with components that could be dependencies of other components. This is
   188  	// a "best attempt", so not all edge cases need to be accounted for.
   189  	for k, v := range i.rateLimits {
   190  		if err := mgr.StoreRateLimit(ctx, k, *v); err != nil {
   191  			mgr.Logger().Errorf("Failed to update resource %v: %v", k, err)
   192  			return false
   193  		}
   194  		mgr.Logger().Infof("Updated resource %v config from file.", k)
   195  	}
   196  	for k, v := range i.caches {
   197  		if err := mgr.StoreCache(ctx, k, *v); err != nil {
   198  			mgr.Logger().Errorf("Failed to update resource %v: %v", k, err)
   199  			return false
   200  		}
   201  		mgr.Logger().Infof("Updated resource %v config from file.", k)
   202  	}
   203  	for k, v := range i.processors {
   204  		if err := mgr.StoreProcessor(ctx, k, *v); err != nil {
   205  			mgr.Logger().Errorf("Failed to update resource %v: %v", k, err)
   206  			return false
   207  		}
   208  		mgr.Logger().Infof("Updated resource %v config from file.", k)
   209  	}
   210  	for k, v := range i.inputs {
   211  		if err := mgr.StoreInput(ctx, k, *v); err != nil {
   212  			mgr.Logger().Errorf("Failed to update resource %v: %v", k, err)
   213  			return false
   214  		}
   215  		mgr.Logger().Infof("Updated resource %v config from file.", k)
   216  	}
   217  	for k, v := range i.outputs {
   218  		if err := mgr.StoreOutput(ctx, k, *v); err != nil {
   219  			mgr.Logger().Errorf("Failed to update resource %v: %v", k, err)
   220  			return false
   221  		}
   222  		mgr.Logger().Infof("Updated resource %v config from file.", k)
   223  	}
   224  
   225  	return true
   226  }