From 4c32535bfab64956fcaea0018985934983bee20c Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Tue, 19 Nov 2024 12:37:17 +0300 Subject: [PATCH 1/4] initial import for nested configs Signed-off-by: Vasiliy Tolstov --- cmd/servicechecker/main.go | 25 ++++++++++++++++++++++++- pkg/config/config.go | 9 +++++---- pkg/grpcconn/protoset.go | 2 +- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/cmd/servicechecker/main.go b/cmd/servicechecker/main.go index 9be32d7..dce2cd1 100644 --- a/cmd/servicechecker/main.go +++ b/cmd/servicechecker/main.go @@ -3,8 +3,10 @@ package main import ( "context" "fmt" + "io/fs" "os" "os/signal" + "path/filepath" "time" openapi_v3 "github.com/google/gnostic/openapiv3" @@ -73,6 +75,27 @@ func main() { l.Fatal(ctx, "failed to open config", err) } + if cfg.App.ChecksDir != "" { + if !filepath.IsAbs(cfg.App.ChecksDir) { + dir, _ := os.Getwd() + cfg.App.ChecksDir = filepath.Clean(filepath.Join(dir, cfg.App.ChecksDir)) + } + + err = filepath.WalkDir(cfg.App.ChecksDir, func(path string, info fs.DirEntry, err error) error { + if err != nil { + // fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err) + return err + } + if info.IsDir() { + return nil + } + return nil + }) + if err != nil { + l.Fatal(ctx, fmt.Sprintf("error loading config: %s", cfg.App.ChecksDir), err) + } + } + if !cfg.App.MultiUser { m.Init( meter.WriteFDMetrics(true), @@ -153,7 +176,7 @@ func main() { } clients["http"] = hcli - for _, check := range cfg.Checks { + for _, check := range cfg.App.Checks { l.Info(ctx, fmt.Sprintf("check %#+v", check)) if !check.Active { continue diff --git a/pkg/config/config.go b/pkg/config/config.go index a1c9007..c776dd6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,7 +8,9 @@ import ( ) type AppConfig struct { - MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"` + ChecksDir string `json:"checks_dir,omitempty" yaml:"checks_dir,omitempty"` + Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"` + MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"` } type MeterConfig struct { @@ -17,9 +19,8 @@ type MeterConfig struct { } type Config struct { - App *AppConfig `json:"app,omitempty" yaml:"app,omitempty"` - Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"` - Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"` + App *AppConfig `json:"app,omitempty" yaml:"app,omitempty"` + Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"` } type CheckConfig struct { diff --git a/pkg/grpcconn/protoset.go b/pkg/grpcconn/protoset.go index b17b68c..c6ca835 100644 --- a/pkg/grpcconn/protoset.go +++ b/pkg/grpcconn/protoset.go @@ -3,8 +3,8 @@ package grpcconn import ( - "github.com/emicklei/proto" "github.com/jhump/protoreflect/desc" + "google.golang.org/protobuf/proto" ) var protoSets = map[string]*desc.FileDescriptor From ee4c343dee9eb6f17c621523ed09ffa84b3902ad Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Tue, 19 Nov 2024 13:55:37 +0300 Subject: [PATCH 2/4] add empty graphql task Signed-off-by: Vasiliy Tolstov --- cmd/servicechecker/main.go | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/cmd/servicechecker/main.go b/cmd/servicechecker/main.go index dce2cd1..57d34b4 100644 --- a/cmd/servicechecker/main.go +++ b/cmd/servicechecker/main.go @@ -208,6 +208,10 @@ func main() { fn, args, err = newGRPCTask(ctx, l, mtr, check.Name, task) case task.HTTP != nil: fn, args, err = newHTTPTask(ctx, l, mtr, check.Name, task) + case task.GraphQL != nil: + fn, args, err = newGraphQLTask(ctx, l, mtr, check.Name, task) + default: + err = fmt.Errorf("unknown task type") } if err != nil { l.Error(ctx, "failed to create task", err) @@ -424,3 +428,96 @@ func newGRPCTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri return fn, nil, nil } + +func newGraphQLTask(ctx context.Context, l logger.Logger, m meter.Meter, check string, task *config.TaskConfig) (any, []any, error) { + /* + var err error + + c, ok := clients["http"] + if !ok { + err = fmt.Errorf("unknown client http") + l.Error(ctx, "failed to get client", err) + return nil, nil, err + } + + var req interface{} + var rsp interface{} + var treq client.Request + var opts []client.CallOption + var labels []string + + if task.HTTP.OpenAPI != "" { + + openapiBuf, err := os.ReadFile(task.HTTP.OpenAPI) + if err != nil { + l.Error(ctx, "failed to unmarshal openapi file", err) + return nil, nil, err + } + + doc, err := openapi_v3.ParseDocument(openapiBuf) + if err != nil { + l.Error(ctx, "failed to unmarshal openapi file", err) + return nil, nil, err + } + _ = doc + + errmap := make(map[string]interface{}, 1) + errmap["default"] = &codecpb.Frame{} + opts = []client.CallOption{ + httpcli.ErrorMap(errmap), + httpcli.Method(task.HTTP.Method), + httpcli.Path(task.HTTP.Endpoint), + + // client.WithContentType("application/json"), + } + + req = &codecpb.Frame{Data: []byte(task.HTTP.Data)} + rsp = &codecpb.Frame{} + + treq = c.NewRequest(task.Name, task.Name, req) + + labels = []string{"check", check, "task", task.Name, "service", task.Name, "endpoint", task.Name} + } + + fn := func() { + var cerr error + + metadata := make(map[string]string, len(task.HTTP.Metadata)) + var rquid string + for k, v := range task.HTTP.Metadata { + if k == "x-request-id" && v == "generate" { + uid, err := uuid.NewV7() + if err != nil { + l.Error(ctx, "failed to generate x-request-id", err) + uid = uuid.Nil + } else { + v = uid.String() + } + } + metadata[k] = v + rquid = v + } + + l.Info(ctx, fmt.Sprintf("call %s.%s endpoint %s", treq.Service(), treq.Method(), treq.Endpoint()), "x-request-id", rquid) + m.Counter(semconv.ClientRequestInflight, labels...).Inc() + ts := time.Now() + cerr = httpconn.Call(ctx, rquid, l, c, task.HTTP.Addr, time.Duration(task.Timeout), + treq, + rsp, + append(opts, client.WithRequestMetadata(metadata))..., + ) + te := time.Since(ts) + m.Counter(semconv.ClientRequestInflight, labels...).Dec() + + m.Summary(semconv.ClientRequestLatencyMicroseconds, labels...).Update(te.Seconds()) + m.Histogram(semconv.ClientRequestDurationSeconds, labels...).Update(te.Seconds()) + + if cerr != nil { + m.Counter(semconv.ClientRequestTotal, append(labels, "status", "failure")...).Inc() + } else { + m.Counter(semconv.ClientRequestTotal, append(labels, "status", "success")...).Inc() + } + } + */ + return nil, nil, nil +} From 3398ee60f366ed8d739797da9f1a132df2f44d51 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Tue, 19 Nov 2024 18:15:19 +0300 Subject: [PATCH 3/4] complete nested config files Signed-off-by: Vasiliy Tolstov --- cmd/servicechecker/main.go | 38 ++++++++++++++++++++++++++++++++++---- pkg/config/config.go | 2 +- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/cmd/servicechecker/main.go b/cmd/servicechecker/main.go index 57d34b4..4e2ac34 100644 --- a/cmd/servicechecker/main.go +++ b/cmd/servicechecker/main.go @@ -65,17 +65,24 @@ func main() { } meters["default"] = m + + l.Info(ctx, "try to parse config.yaml") f, err := os.Open("config.yaml") if err != nil { l.Fatal(ctx, "failed to open config", err) } - defer f.Close() + cfg := &config.Config{} - if err = cfg.Parse(f); err != nil { + + if err = config.Parse(f, cfg); err != nil { + f.Close() l.Fatal(ctx, "failed to open config", err) } + f.Close() if cfg.App.ChecksDir != "" { + var configFiles []string + if !filepath.IsAbs(cfg.App.ChecksDir) { dir, _ := os.Getwd() cfg.App.ChecksDir = filepath.Clean(filepath.Join(dir, cfg.App.ChecksDir)) @@ -83,17 +90,40 @@ func main() { err = filepath.WalkDir(cfg.App.ChecksDir, func(path string, info fs.DirEntry, err error) error { if err != nil { - // fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err) return err } - if info.IsDir() { + + if info.IsDir() || !info.Type().IsRegular() { return nil } + + if filepath.Ext(info.Name()) != ".yaml" { + return nil + } + + configFiles = append(configFiles, path) + return nil }) if err != nil { l.Fatal(ctx, fmt.Sprintf("error loading config: %s", cfg.App.ChecksDir), err) } + + for _, configFile := range configFiles { + l.Info(ctx, "try to parse "+configFile) + f, err := os.Open(configFile) + if err != nil { + l.Fatal(ctx, "failed to open config", err) + } + + checks := []*config.CheckConfig{} + if err = config.Parse(f, &checks); err != nil { + f.Close() + l.Fatal(ctx, "failed to open config", err) + } + f.Close() + cfg.App.Checks = append(cfg.App.Checks, checks...) + } } if !cfg.App.MultiUser { diff --git a/pkg/config/config.go b/pkg/config/config.go index c776dd6..92640ee 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -66,7 +66,7 @@ type TaskConfig struct { Active bool `json:"active,omitempty" yaml:"active,omitempty"` } -func (cfg *Config) Parse(r io.Reader) error { +func Parse(r io.Reader, cfg interface{}) error { buf, err := io.ReadAll(r) if err != nil { return err From c1103c714a053ca3080cc0b8589e43bdaca50813 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Wed, 20 Nov 2024 00:35:04 +0300 Subject: [PATCH 4/4] allow to work with multiple configs Signed-off-by: Vasiliy Tolstov --- cmd/servicechecker/main.go | 63 ++----------------- go.mod | 4 +- go.sum | 8 ++- pkg/config/config.go | 120 +++++++++++++++++++++++++++++++------ 4 files changed, 116 insertions(+), 79 deletions(-) diff --git a/cmd/servicechecker/main.go b/cmd/servicechecker/main.go index 4e2ac34..0928ac9 100644 --- a/cmd/servicechecker/main.go +++ b/cmd/servicechecker/main.go @@ -3,10 +3,8 @@ package main import ( "context" "fmt" - "io/fs" "os" "os/signal" - "path/filepath" "time" openapi_v3 "github.com/google/gnostic/openapiv3" @@ -64,67 +62,14 @@ func main() { l.Fatal(ctx, "failed to init meter", err) } - meters["default"] = m - - l.Info(ctx, "try to parse config.yaml") - f, err := os.Open("config.yaml") - if err != nil { - l.Fatal(ctx, "failed to open config", err) - } + meters[uuid.Nil.String()] = m cfg := &config.Config{} - if err = config.Parse(f, cfg); err != nil { - f.Close() + l.Info(ctx, "try to load config") + if err := config.Load(config.Filesytem, "config.yaml", cfg); err != nil { l.Fatal(ctx, "failed to open config", err) } - f.Close() - - if cfg.App.ChecksDir != "" { - var configFiles []string - - if !filepath.IsAbs(cfg.App.ChecksDir) { - dir, _ := os.Getwd() - cfg.App.ChecksDir = filepath.Clean(filepath.Join(dir, cfg.App.ChecksDir)) - } - - err = filepath.WalkDir(cfg.App.ChecksDir, func(path string, info fs.DirEntry, err error) error { - if err != nil { - return err - } - - if info.IsDir() || !info.Type().IsRegular() { - return nil - } - - if filepath.Ext(info.Name()) != ".yaml" { - return nil - } - - configFiles = append(configFiles, path) - - return nil - }) - if err != nil { - l.Fatal(ctx, fmt.Sprintf("error loading config: %s", cfg.App.ChecksDir), err) - } - - for _, configFile := range configFiles { - l.Info(ctx, "try to parse "+configFile) - f, err := os.Open(configFile) - if err != nil { - l.Fatal(ctx, "failed to open config", err) - } - - checks := []*config.CheckConfig{} - if err = config.Parse(f, &checks); err != nil { - f.Close() - l.Fatal(ctx, "failed to open config", err) - } - f.Close() - cfg.App.Checks = append(cfg.App.Checks, checks...) - } - } if !cfg.App.MultiUser { m.Init( @@ -214,7 +159,7 @@ func main() { var mtr meter.Meter if !cfg.App.MultiUser { - mtr = meters["default"] + mtr = meters[uuid.Nil.String()] } else { if v, ok := meters[check.User]; ok && v != nil { mtr = v diff --git a/go.mod b/go.mod index 8b63217..e0d33ff 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( go.unistack.org/micro-codec-jsonpb/v3 v3.10.3 go.unistack.org/micro-codec-proto/v3 v3.10.2 go.unistack.org/micro-codec-yaml/v3 v3.10.2 + go.unistack.org/micro-config-file/v3 v3.8.10 go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9 go.unistack.org/micro-proto/v3 v3.4.1 go.unistack.org/micro-server-http/v3 v3.11.34 @@ -20,6 +21,7 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -29,7 +31,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.20.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect google.golang.org/grpc v1.68.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 4082fbf..1e96d97 100644 --- a/go.sum +++ b/go.sum @@ -593,6 +593,8 @@ cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vf cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= @@ -908,6 +910,8 @@ go.unistack.org/micro-codec-proto/v3 v3.10.2 h1:9iUQjBjTsd/RgIqB5rAQMZE0CYWngoW9 go.unistack.org/micro-codec-proto/v3 v3.10.2/go.mod h1:54e1jb6aLL9obJUwJjtVupE5zY4PugTcMSqWDhz9aC4= go.unistack.org/micro-codec-yaml/v3 v3.10.2 h1:02I9XzhaBHqZU8Vd5e2zhf8j4foJ4muPT/x4gdR6E4c= go.unistack.org/micro-codec-yaml/v3 v3.10.2/go.mod h1:A/tYj7x9CRhuin7WxeIvnuo8bMDrZYcJkogVYN8X7rU= +go.unistack.org/micro-config-file/v3 v3.8.10 h1:/IyD/i6I7Ic8jCNq7ZsTpWT8sToNG14gIFkSVPxbNpY= +go.unistack.org/micro-config-file/v3 v3.8.10/go.mod h1:w7uw5KxK3H2OrZwX4p0hQHbp9UzwDODYqJvdofySgxY= go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9 h1:ZXCS0eFiSdvcFYxpxV2Q77gfwAjpIRydwAEI1QBrwuQ= go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9/go.mod h1:xODJQ0Nu/F8k34D/z2ITL91OskI/C674XCkugAxmc3Q= go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q= @@ -1202,8 +1206,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/config/config.go b/pkg/config/config.go index 92640ee..a6ec4c8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,21 +1,22 @@ package config import ( + "fmt" "io" + "io/fs" + "os" + "path/filepath" + "github.com/google/uuid" yamlcodec "go.unistack.org/micro-codec-yaml/v3" mtime "go.unistack.org/micro/v3/util/time" ) -type AppConfig struct { - ChecksDir string `json:"checks_dir,omitempty" yaml:"checks_dir,omitempty"` - Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"` - MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"` -} +var Filesytem fs.FS -type MeterConfig struct { - Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` - Path string `json:"path,omitempty" yaml:"path,omitempty"` +func init() { + dir, _ := os.Getwd() + Filesytem = os.DirFS(dir) } type Config struct { @@ -23,13 +24,25 @@ type Config struct { Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"` } +type AppConfig struct { + ChecksFiles []string `json:"checks_files,omitempty" yaml:"checks_files,omitempty"` + Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"` + MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"` +} + +type MeterConfig struct { + Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` +} + type CheckConfig struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Tasks []*TaskConfig `json:"tasks,omitempty" yaml:"tasks,omitempty"` - Timeout mtime.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` - Interval mtime.Duration `json:"interval,omitempty" yaml:"interval,omitempty"` - Active bool `json:"active,omitempty" yaml:"active,omitempty"` - User string `json:"user,omitempty" yaml:"user,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + User string `json:"user,omitempty" yaml:"user,omitempty"` + Tasks []*TaskConfig `json:"tasks,omitempty" yaml:"tasks,omitempty"` + TasksFiles []string `json:"tasks_files,omitempty" yaml:"tasks_files,omitempty"` + Timeout mtime.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Interval mtime.Duration `json:"interval,omitempty" yaml:"interval,omitempty"` + Active bool `json:"active,omitempty" yaml:"active,omitempty"` } type HTTPConfig struct { @@ -66,11 +79,84 @@ type TaskConfig struct { Active bool `json:"active,omitempty" yaml:"active,omitempty"` } -func Parse(r io.Reader, cfg interface{}) error { - buf, err := io.ReadAll(r) +func Load(fileSystem fs.FS, name string, cfg *Config) error { + if err := load(fileSystem, name, cfg); err != nil { + return err + } + + for _, checksPatternFile := range cfg.App.ChecksFiles { + checkRoot := filepath.Dir(checksPatternFile) + checksFiles := fsWalkDir(fileSystem, checkRoot, checksPatternFile) + for _, checkFile := range checksFiles { + checks := []*CheckConfig{} + if err := load(fileSystem, checkFile, &checks); err != nil { + return err + } + for ckecksIdx := range checks { + for _, tasksPatternFile := range checks[ckecksIdx].TasksFiles { + taskRoot := filepath.Join(filepath.Dir(checksPatternFile), filepath.Dir(tasksPatternFile)) + tasksFiles := fsWalkDir(fileSystem, taskRoot, filepath.Join(filepath.Dir(checksPatternFile), tasksPatternFile)) + for tasksIdx := range tasksFiles { + tasks := []*TaskConfig{} + if err := load(fileSystem, tasksFiles[tasksIdx], &tasks); err != nil { + return err + } + checks[ckecksIdx].Tasks = append(checks[ckecksIdx].Tasks, tasks...) + } + } + cfg.App.Checks = append(cfg.App.Checks, checks[ckecksIdx]) + } + } + } + + if !cfg.App.MultiUser { + for _, check := range cfg.App.Checks { + check.User = uuid.Nil.String() + } + } + + return nil +} + +func fsWalkDir(fileSystem fs.FS, root string, pattern string) []string { + var files []string + fs.WalkDir(fileSystem, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !d.Type().IsRegular() { + return nil + } + var ok bool + if ok, err = filepath.Match(pattern, path); err == nil && ok { + files = append(files, path) + } else { + return err + } + + return nil + }) + return files +} + +func load(fileSystem fs.FS, name string, cfg interface{}) error { + f, err := fileSystem.Open(name) if err != nil { return err } - return yamlcodec.NewCodec().Unmarshal(buf, cfg) + c := yamlcodec.NewCodec() + var buf []byte + + if buf, err = io.ReadAll(f); err == nil { + if err = f.Close(); err == nil { + err = c.Unmarshal(buf, cfg) + } + } + + if err != nil { + return fmt.Errorf("failed to load config %w", err) + } + + return nil }