From 13ffca926c86d5f054bb56bc5b706c97061cd0e7 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 | 12 +- internal/config/configgraph.go | 37 ++++- internal/config/configgraph_test.go | 147 +++++++++++++++++- internal/config/dag/dag.go | 8 +- internal/config/testdata/include/common.yaml | 2 + .../config/testdata/include/common_a.yaml | 2 + .../config/testdata/include/no_includes.yaml | 2 +- 7 files changed, 184 insertions(+), 26 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e3a1609..5f6a854 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,20 +91,14 @@ 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) - } + graph.Resolve() + cfg := graph.cfg if err = cfg.validate(); err != nil { return Config{}, fmt.Errorf("validate config: %w", err) } - return cfg, nil + return *cfg, nil } // Decode decodes from a slice of bytes to a [Config] structure. diff --git a/internal/config/configgraph.go b/internal/config/configgraph.go index b40a775..1d086fb 100644 --- a/internal/config/configgraph.go +++ b/internal/config/configgraph.go @@ -7,6 +7,9 @@ import ( "fmt" "io" "log/slog" + "os" + + hdag "github.com/heimdalr/dag" "github.com/adevinta/lava/internal/config/dag" "github.com/adevinta/lava/internal/urlutil" @@ -16,6 +19,7 @@ import ( type ConfigGraph struct { dag *dag.DAG configs map[string]Config + cfg *Config } // NewConfigGraph creates a new [ConfigGraph] that represents the whole configuration. @@ -76,16 +80,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) } + +// Visit executes the merge when a vertex is visited. +func (cg *ConfigGraph) Visit(v hdag.Vertexer) { + _, cfg := v.Vertex() + config, err := cg.Config(cfg.(string)) + if err != nil { + os.Exit(1) + } + if cg.cfg == nil { + cg.cfg = &config + } else { + mergedConfig, err := merge(config, *cg.cfg) + if err != nil { + os.Exit(1) + } + cg.cfg = &mergedConfig + } +} + +// Resolve walks the dag and merge the configuration. +func (cg *ConfigGraph) Resolve() { + cg.dag.Dag.DFSWalk(cg) +} diff --git a/internal/config/configgraph_test.go b/internal/config/configgraph_test.go index 6583bdb..d13e4b6 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", + "commonchecktypes.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", + "commonchecktypes.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", + "commonchecktypes.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", + "commonchecktypes.json", }, Targets: []Target{ { @@ -196,3 +202,136 @@ 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{ + "commonchecktypes.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{ + "commonchecktypes.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{ + "commonchecktypes.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", + "commonchecktypes.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) + } + graph.Resolve() + got := graph.cfg + if (err != nil) != tt.wantErr { + t.Errorf("unexpected error: want: %v, got: %v", tt.wantErr, 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..48b11e6 100644 --- a/internal/config/dag/dag.go +++ b/internal/config/dag/dag.go @@ -29,14 +29,14 @@ var ( // DAG represents a Direct Acyclic Graph object. type DAG struct { - dag *hdag.DAG + Dag *hdag.DAG vertices map[string]string } // New returns a Directed Acyclic Graph object. func New() *DAG { return &DAG{ - dag: hdag.NewDAG(), + Dag: hdag.NewDAG(), vertices: make(map[string]string), } } @@ -44,7 +44,7 @@ func New() *DAG { // AddVertex adds the vertex x to the DAG. AddVertex returns an error if s is // nil or s is already part of the graph. func (d *DAG) AddVertex(s string) (string, error) { - id, err := d.dag.AddVertex(s) + id, err := d.Dag.AddVertex(s) if err != nil { if errors.As(err, &hdag.VertexDuplicateError{}) { return id, fmt.Errorf("%w: %s", ErrDuplicatedVertex, s) @@ -66,7 +66,7 @@ func (d *DAG) AddEdge(from, to string) error { if toID == "" { return fmt.Errorf("%w: %s", ErrUnknownVertex, to) } - err := d.dag.AddEdge(fromID, toID) + err := d.Dag.AddEdge(fromID, toID) if err != nil { if errors.As(err, &hdag.EdgeDuplicateError{}) { return fmt.Errorf("%w: from %s to %s", ErrDuplicatedEdge, from, to) diff --git a/internal/config/testdata/include/common.yaml b/internal/config/testdata/include/common.yaml index fa0a638..9374887 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 \ No newline at end of file diff --git a/internal/config/testdata/include/common_a.yaml b/internal/config/testdata/include/common_a.yaml index af6d477..38d1f1f 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 \ No newline at end of file diff --git a/internal/config/testdata/include/no_includes.yaml b/internal/config/testdata/include/no_includes.yaml index 6618dc0..2c36635 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 + - commonchecktypes.json targets: - identifier: example.com type: DomainName