diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index b8589d8..2706683 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -22,6 +22,7 @@ jobs: uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3 with: version: v1.54 + args: '--timeout=5m' - name: Run Go test coverage run: go test -race -coverprofile=coverage.out -covermode=atomic ./... - name: Upload coverage to Codecov diff --git a/pkg/merger/merge.go b/pkg/merger/merge.go index 51ced7f..a10fa30 100644 --- a/pkg/merger/merge.go +++ b/pkg/merger/merge.go @@ -82,26 +82,32 @@ func (r *mergerResource) export() []*yaml.RNode { } case ConfigMap: - rNode, _ := yaml.FromMap(map[string]interface{}{ + rNode, err := yaml.FromMap(map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]string{ "name": r.Name, }, }) + if err != nil { + log.Fatalf("Error creating ConfigMap: %v", err) + } if err := rNode.LoadMapIntoConfigMapData(r.Output.items); err != nil { log.Fatalf("Error creating ConfigMap data: %v", err) } rlItems = append(rlItems, rNode) case Secret: - rNode, _ := yaml.FromMap(map[string]interface{}{ + rNode, err := yaml.FromMap(map[string]interface{}{ "apiVersion": "v1", "kind": "Secret", "metadata": map[string]string{ "name": r.Name, }, }) + if err != nil { + log.Fatalf("Error creating Secret: %v", err) + } if err := rNode.LoadMapIntoSecretData(r.Output.items); err != nil { log.Fatalf("Error creating Secret data: %v", err) } diff --git a/pkg/merger/merge_test.go b/pkg/merger/merge_test.go index aef5b95..8048636 100644 --- a/pkg/merger/merge_test.go +++ b/pkg/merger/merge_test.go @@ -97,12 +97,19 @@ func TestLoadDestinationFile(t *testing.T) { } } +type testMergeCase struct { + name string + key string + contains string + notContains string +} + func TestMergeReplace(t *testing.T) { tests := []struct { name string desc string actual mergerResource - expected map[string]string + expected []testMergeCase expectedLen int }{ { @@ -124,10 +131,10 @@ func TestMergeReplace(t *testing.T) { Merge: resourceMerge{Strategy: "replace"}, Output: resourceOutput{Format: "raw"}, }, - expected: map[string]string{ - // In the overlay the output key are "resource.input.files.sources[*]". - "dev.yaml": "image: dev:1.0.0", - "stage.yaml": "image: stage:1.0.0", + // In the overlay the output key are "resource.input.files.sources[*]". + expected: []testMergeCase{ + {"src01", "dev.yaml", "image: dev:1.0.0", "image: sidecar"}, + {"src02", "stage.yaml", "image: stage:1.0.0", "image: sidecar"}, }, expectedLen: 2, }, @@ -150,9 +157,179 @@ func TestMergeReplace(t *testing.T) { Merge: resourceMerge{Strategy: "replace"}, Output: resourceOutput{Format: "raw"}, }, - expected: map[string]string{ - // In the patch the output key are "resource.name". - "test-replace-with-patch": "image: stage:1.0.0", + // In the patch the output key are "resource.name". + expected: []testMergeCase{ + {"src", "test-replace-with-patch", "image: stage:1.0.0", "image: sidecar"}, + }, + expectedLen: 1, + }, + } + + for _, tt := range tests { + // Those methods are called in the Default method. + tt.actual.Input.Files.setRoot() + tt.actual.Merge.setStrategy() + tt.actual.Output.items = make(map[string]string) + + // Merge. + tt.actual.merge() + + // Assert. + assert.Equal(t, len(tt.actual.Output.items), tt.expectedLen) + for _, tmc := range tt.expected { + output := tt.actual.Output.items[tmc.key] + assert.Contains(t, output, tmc.contains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.contains, output) + assert.NotContains(t, output, tmc.notContains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.notContains, output) + } + } +} + +func TestMergeAppend(t *testing.T) { + tests := []struct { + name string + desc string + actual mergerResource + expected []testMergeCase + expectedLen int + }{ + { + name: "Test append with overlay", + desc: "The number of the outputs in 'append+overlay' should be the same as input sources", + actual: mergerResource{ + Name: "test-append-with-overlay", + Input: resourceInput{ + Method: "overlay", + Files: resourceInputFiles{ + Root: "../../examples/testdata/", + Sources: []string{ + "input/dev.yaml", + "input/stage.yaml", + }, + Destination: "input/base.yaml", + }, + }, + Merge: resourceMerge{Strategy: "append"}, + Output: resourceOutput{Format: "raw"}, + }, + // In the overlay the output key are "resource.input.files.sources[*]". + expected: []testMergeCase{ + {"src01", "dev.yaml", "image: dev:1.0.0", "image: stage:1.0.0"}, + {"src01", "dev.yaml", "image: sidecar", "image: stage:1.0.0"}, + {"src02", "stage.yaml", "image: stage:1.0.0", "image: dev:1.0.0"}, + {"src02", "stage.yaml", "image: sidecar", "image: dev:1.0.0"}, + }, + expectedLen: 2, + }, + { + name: "Test append with patch", + desc: "The number of the outputs in 'append+patch' should always be one", + actual: mergerResource{ + Name: "test-append-with-patch", + Input: resourceInput{ + Method: "patch", + Files: resourceInputFiles{ + Root: "../../examples/testdata/", + Sources: []string{ + "input/dev.yaml", + "input/stage.yaml", + }, + Destination: "input/base.yaml", + }, + }, + Merge: resourceMerge{Strategy: "append"}, + Output: resourceOutput{Format: "raw"}, + }, + // In the patch the output key are "resource.name". + expected: []testMergeCase{ + {"src", "test-append-with-patch", "image: sidecar", "dummy"}, + {"src", "test-append-with-patch", "image: dev:1.0.0", "dummy"}, + {"src", "test-append-with-patch", "image: stage:1.0.0", "dummy"}, + }, + expectedLen: 1, + }, + } + + for _, tt := range tests { + // Those methods are called in the Default method. + tt.actual.Input.Files.setRoot() + tt.actual.Merge.setStrategy() + tt.actual.Output.items = make(map[string]string) + + // Merge. + tt.actual.merge() + + // Assert. + assert.Equal(t, len(tt.actual.Output.items), tt.expectedLen) + for _, tmc := range tt.expected { + output := tt.actual.Output.items[tmc.key] + assert.Contains(t, output, tmc.contains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.contains, output) + assert.NotContains(t, output, tmc.notContains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.notContains, output) + } + } +} + +func TestMergeCombine(t *testing.T) { + tests := []struct { + name string + desc string + actual mergerResource + expected []testMergeCase + expectedLen int + }{ + { + name: "Test combine with overlay", + desc: "The number of the outputs in 'combine+overlay' should be the same as input sources", + actual: mergerResource{ + Name: "test-combine-with-overlay", + Input: resourceInput{ + Method: "overlay", + Files: resourceInputFiles{ + Root: "../../examples/testdata/", + Sources: []string{ + "input/dev.yaml", + "input/stage.yaml", + }, + Destination: "input/base.yaml", + }, + }, + Merge: resourceMerge{Strategy: "combine"}, + Output: resourceOutput{Format: "raw"}, + }, + // In the overlay the output key are "resource.input.files.sources[*]". + expected: []testMergeCase{ + {"src01.1", "dev.yaml", "image: dev:1.0.0", "image: stage:1.0.0"}, + {"src01.2", "dev.yaml", "image: dev:1.0.0", "image: sidecar"}, + {"src02.1", "stage.yaml", "image: stage:1.0.0", "image: dev:1.0.0"}, + {"src02.2", "stage.yaml", "image: stage:1.0.0", "image: sidecar"}, + }, + expectedLen: 2, + }, + { + name: "Test combine with patch", + desc: "The number of the outputs in 'combine+patch' should always be one", + actual: mergerResource{ + Name: "test-combine-with-patch", + Input: resourceInput{ + Method: "patch", + Files: resourceInputFiles{ + Root: "../../examples/testdata/", + Sources: []string{ + "input/dev.yaml", + "input/stage.yaml", + }, + Destination: "input/base.yaml", + }, + }, + Merge: resourceMerge{Strategy: "combine"}, + Output: resourceOutput{Format: "raw"}, + }, + // In the patch the output key are "resource.name". + expected: []testMergeCase{ + {"src", "test-combine-with-patch", "image: stage:1.0.0", "image: sidecar"}, }, expectedLen: 1, }, @@ -169,10 +346,149 @@ func TestMergeReplace(t *testing.T) { // Assert. assert.Equal(t, len(tt.actual.Output.items), tt.expectedLen) - for k, v := range tt.expected { - output := tt.actual.Output.items[k] - assert.Contains(t, output, v, - "Expected to be %v=%v, but got %v", k, v, output) + for _, tmc := range tt.expected { + output := tt.actual.Output.items[tmc.key] + assert.Contains(t, output, tmc.contains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.contains, output) + assert.NotContains(t, output, tmc.notContains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.notContains, output) + } + } +} + +func TestOutputRaw(t *testing.T) { + tests := []struct { + name string + desc string + actual mergerResource + expected []testMergeCase + expectedLen int + }{ + { + name: "Test Raw output with overlay", + desc: "Raw output is used when the input is a manifest.", + actual: mergerResource{ + Name: "test-configmap", + Output: resourceOutput{ + Format: "raw", + items: map[string]string{ + "iampolicy.yaml": `--- + apiVersion: iam.cnrm.cloud.google.com/v1beta1 + kind: IAMPolicy + metadata: + name: storage-admin-policy + `, + }, + }, + }, + expected: []testMergeCase{ + {"src01.1", "iampolicy.yaml", "kind: IAMPolicy", ""}, + {"src01.2", "iampolicy.yaml", "storage-admin-policy", ""}, + }, + }, + } + + for _, tt := range tests { + // Export. + output := tt.actual.export()[0] + outputManifest, _ := output.String() + + // Assert. + for _, tmc := range tt.expected { + assert.Contains(t, outputManifest, tmc.contains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.contains, outputManifest) + } + } +} + +func TestOutputConfigMap(t *testing.T) { + tests := []struct { + name string + desc string + actual mergerResource + expected []testMergeCase + expectedLen int + }{ + { + name: "Test ConfigMap output with overlay", + desc: "The number of the outputs in 'ConfigMap+overlay' should be the same as input sources", + actual: mergerResource{ + Name: "test-configmap", + Output: resourceOutput{ + Format: "configmap", + items: map[string]string{ + "prom.yaml": ` + global: + scrape_interval: 15s + evaluation_interval: 15s + `, + }, + }, + }, + expected: []testMergeCase{ + {"src01.1", "prom.yaml", "kind: ConfigMap", ""}, + {"src01.2", "prom.yaml", "evaluation_interval", ""}, + }, + }, + } + + for _, tt := range tests { + // Export. + output := tt.actual.export()[0] + outputData := output.GetDataMap() + outputManifest, _ := output.String() + + // Assert. + for _, tmc := range tt.expected { + assert.Contains(t, outputData, tmc.key) + assert.Contains(t, outputManifest, tmc.contains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.contains, outputManifest) + } + } +} + +func TestOutputSecret(t *testing.T) { + tests := []struct { + name string + desc string + actual mergerResource + expected []testMergeCase + expectedLen int + }{ + { + name: "Test Secret output with overlay", + desc: "The number of the outputs in 'Secret+overlay' should be the same as input sources", + actual: mergerResource{ + Name: "test-secret", + Output: resourceOutput{ + Format: "secret", + items: map[string]string{ + "prom.yaml": ` + global: + scrape_interval: 15s + evaluation_interval: 15s + `, + }, + }, + }, + expected: []testMergeCase{ + {"src01.2", "prom.yaml", "kind: Secret", ""}, + {"src01.2", "prom.yaml", "CgkJCQkJCQlnbG9iYWw6CgkJCQkJCQkJc2NyYXBlX2ludGVydmFsOiAxNXMKCQkJCQkJCQ", ""}, + }, + }, + } + + for _, tt := range tests { + // Export. + output := tt.actual.export()[0] + outputData := output.GetDataMap() + outputManifest, _ := output.String() + + // Assert. + for _, tmc := range tt.expected { + assert.Contains(t, outputData, tmc.key) + assert.Contains(t, outputManifest, tmc.contains, + "Expected to be %v %v=%v, but got %v", tmc.name, tmc.key, tmc.contains, outputManifest) } } }