From cda2cf1e672085f5986368a5e90226f1857d2cb9 Mon Sep 17 00:00:00 2001 From: Seila Gamo Date: Mon, 3 Feb 2025 09:23:42 +0100 Subject: [PATCH] internal/config: walk the graph and merge configuration --- internal/config/config.go | 9 +- internal/config/configgraph.go | 33 +++- internal/config/configgraph_test.go | 146 +++++++++++++++++- internal/config/dag/dag.go | 14 ++ internal/config/testdata/include/common.yaml | 2 + .../config/testdata/include/common_a.yaml | 2 + .../config/testdata/include/no_includes.yaml | 2 +- 7 files changed, 187 insertions(+), 21 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e3a1609..efb90ad 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,14 +91,7 @@ func Parse(path string) (Config, error) { if err != nil { return Config{}, fmt.Errorf("build dag: %w", err) } - if err = graph.Resolve(); err != nil { - return Config{}, fmt.Errorf("resolve dag: %w", err) - } - - cfg, err := graph.Config(path) - if err != nil { - return Config{}, fmt.Errorf("unknown configuration: %w", err) - } + cfg := graph.Resolve() if err = cfg.validate(); err != nil { return Config{}, fmt.Errorf("validate config: %w", err) diff --git a/internal/config/configgraph.go b/internal/config/configgraph.go index b40a775..e2c6666 100644 --- a/internal/config/configgraph.go +++ b/internal/config/configgraph.go @@ -76,16 +76,33 @@ func discoverConfig(url, parent string, d *dag.DAG, configs map[string]Config) e return nil } -// Resolve walks the dag and merge the configuration. -func (d *ConfigGraph) Resolve() error { - // TODO: Walk the dag and merge configurations. - return nil -} - // Config returns a configuration for the given URL. -func (d *ConfigGraph) Config(url string) (Config, error) { - if cfg, ok := d.configs[url]; ok { +func (cg *ConfigGraph) Config(url string) (Config, error) { + if cfg, ok := cg.configs[url]; ok { return cfg, nil } return Config{}, fmt.Errorf("could not find config for %s", url) } + +// Resolve walks the dag and merge the configuration. +func (cg *ConfigGraph) Resolve() Config { + var cfg *Config + cg.dag.DFSWalk(func(vertexID string, vertex interface{}) { + vexCfg, err := cg.Config(vertex.(string)) + if err != nil { + panic(err) + } + + if cfg == nil { + cfg = &vexCfg + return + } + + vexCfg, err = merge(vexCfg, *cfg) + if err != nil { + panic(err) + } + cfg = &vexCfg + }) + return *cfg +} diff --git a/internal/config/configgraph_test.go b/internal/config/configgraph_test.go index 6583bdb..6f04507 100644 --- a/internal/config/configgraph_test.go +++ b/internal/config/configgraph_test.go @@ -27,7 +27,7 @@ func TestNewConfigGraph(t *testing.T) { "testdata/include/no_includes.yaml": { LavaVersion: ptr("v1.0.0"), ChecktypeURLs: []string{ - "checktypes.json", + "checktypes_no_includes.json", }, Targets: []Target{ { @@ -61,7 +61,7 @@ func TestNewConfigGraph(t *testing.T) { "testdata/include/no_includes.yaml": { LavaVersion: ptr("v1.0.0"), ChecktypeURLs: []string{ - "checktypes.json", + "checktypes_no_includes.json", }, Targets: []Target{ { @@ -103,7 +103,7 @@ func TestNewConfigGraph(t *testing.T) { "testdata/include/no_includes.yaml": { LavaVersion: ptr("v1.0.0"), ChecktypeURLs: []string{ - "checktypes.json", + "checktypes_no_includes.json", }, Targets: []Target{ { @@ -136,6 +136,9 @@ func TestNewConfigGraph(t *testing.T) { AssetType: types.DomainName, }, }, + ReportConfig: ReportConfig{ + Severity: ptr(SeverityCritical), + }, }, "testdata/include/common_a.yaml": { Includes: []string{"testdata/include/no_includes.yaml"}, @@ -149,6 +152,9 @@ func TestNewConfigGraph(t *testing.T) { AssetType: types.DomainName, }, }, + ReportConfig: ReportConfig{ + Severity: ptr(SeverityMedium), + }, }, "testdata/include/common_b.yaml": { Includes: []string{"testdata/include/no_includes.yaml"}, @@ -166,7 +172,7 @@ func TestNewConfigGraph(t *testing.T) { "testdata/include/no_includes.yaml": { LavaVersion: ptr("v1.0.0"), ChecktypeURLs: []string{ - "checktypes.json", + "checktypes_no_includes.json", }, Targets: []Target{ { @@ -196,3 +202,135 @@ func TestNewConfigGraph(t *testing.T) { }) } } + +func TestConfigGraph_Resolve(t *testing.T) { + tests := []struct { + name string + URL string + want Config + wantErr bool + }{ + { + name: "no includes", + URL: "testdata/include/no_includes.yaml", + want: Config{ + LavaVersion: ptr("v1.0.0"), + ChecktypeURLs: []string{ + "checktypes_no_includes.json", + }, + Targets: []Target{ + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + }, + }, + wantErr: false, + }, + { + name: "local file", + URL: "testdata/include/local.yaml", + want: Config{ + Includes: []string{"testdata/include/no_includes.yaml"}, + LavaVersion: ptr("v1.0.0"), + ChecktypeURLs: []string{ + "checktypes_no_includes.json", + "checktypes.json", + }, + Targets: []Target{ + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + }, + }, + wantErr: false, + }, + { + name: "duplicated", + URL: "testdata/include/duplicated.yaml", + want: Config{ + Includes: []string{ + "testdata/include/no_includes.yaml", + "testdata/include/no_includes.yaml", + }, + LavaVersion: ptr("v1.0.0"), + ChecktypeURLs: []string{ + "checktypes_no_includes.json", + "checktypes.json", + }, + Targets: []Target{ + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + }, + }, + wantErr: false, + }, + { + name: "common includes", + URL: "testdata/include/common.yaml", + want: Config{ + Includes: []string{ + "testdata/include/no_includes.yaml", + "testdata/include/no_includes.yaml", + "testdata/include/common_a.yaml", + "testdata/include/common_b.yaml", + }, + LavaVersion: ptr("v1.0.0"), + ChecktypeURLs: []string{ + "checktypes.json", + "checktypes_no_includes.json", + "checktypes.json", + "checktypes.json", + }, + Targets: []Target{ + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + { + Identifier: "example.com", + AssetType: types.DomainName, + }, + }, + ReportConfig: ReportConfig{ + Severity: ptr(SeverityCritical), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + graph, err := NewConfigGraph(tt.URL) + if err != nil { + t.Errorf("build dag: %v", err) + } + got := graph.Resolve() + if (err != nil) != tt.wantErr { + t.Errorf("unexpected error: %v", err) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("configs mismatch (-want +got):\n%v", diff) + } + }) + } +} diff --git a/internal/config/dag/dag.go b/internal/config/dag/dag.go index 2138811..ca4feb8 100644 --- a/internal/config/dag/dag.go +++ b/internal/config/dag/dag.go @@ -94,3 +94,17 @@ func (d *DAG) getVertexID(s string) string { } return "" } + +// WalkFunc represents a function that implements the [hdag.Visitor] +// interface. +type WalkFunc func(vertexID string, vertex interface{}) + +// Visit is required by the [hdag.Visitor] interface. +func (fn WalkFunc) Visit(v hdag.Vertexer) { + fn(v.Vertex()) +} + +// DFSWalk walks the [DAG] using a depth first strategy. +func (d *DAG) DFSWalk(fn WalkFunc) { + d.dag.DFSWalk(fn) +} diff --git a/internal/config/testdata/include/common.yaml b/internal/config/testdata/include/common.yaml index fa0a638..b4dd278 100644 --- a/internal/config/testdata/include/common.yaml +++ b/internal/config/testdata/include/common.yaml @@ -7,3 +7,5 @@ checktypes: targets: - identifier: example.com type: DomainName +report: + severity: critical diff --git a/internal/config/testdata/include/common_a.yaml b/internal/config/testdata/include/common_a.yaml index af6d477..30cefd2 100644 --- a/internal/config/testdata/include/common_a.yaml +++ b/internal/config/testdata/include/common_a.yaml @@ -6,3 +6,5 @@ checktypes: targets: - identifier: example.com type: DomainName +report: + severity: medium diff --git a/internal/config/testdata/include/no_includes.yaml b/internal/config/testdata/include/no_includes.yaml index 6618dc0..ec00775 100644 --- a/internal/config/testdata/include/no_includes.yaml +++ b/internal/config/testdata/include/no_includes.yaml @@ -1,6 +1,6 @@ lava: v1.0.0 checktypes: - - checktypes.json + - checktypes_no_includes.json targets: - identifier: example.com type: DomainName