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 }