diff --git a/config/config_keys.go b/config/config_keys.go index 446720b61..08afef6aa 100644 --- a/config/config_keys.go +++ b/config/config_keys.go @@ -28,4 +28,5 @@ const ( KeyTelemetry = "telemetry" KeyCLIId = "cliId" KeySource = "source" + KeyAdditionalMetadata = "additionalMetadata" ) diff --git a/config/contexts.go b/config/contexts.go index 207e31025..13ee63c4d 100644 --- a/config/contexts.go +++ b/config/contexts.go @@ -296,11 +296,8 @@ func setContext(node *yaml.Node, ctx *configtypes.Context) (persist bool, err er return false, errors.New("context name cannot be empty") } - // Get Patch Strategies from config metadata - patchStrategies, err := GetConfigMetadataPatchStrategy() - if err != nil { - patchStrategies = make(map[string]string) - } + // Get Patch Strategies + patchStrategies := constructPatchStrategies() var persistDiscoverySources bool @@ -360,6 +357,22 @@ func setContext(node *yaml.Node, ctx *configtypes.Context) (persist bool, err er return persistDiscoverySources || persist, err } +// Get Patch Strategies from config metadata +// By default; AdditionalMetadata field will be patched in replace strategy if there are no patch strategies +func constructPatchStrategies() map[string]string { + patchStrategies, err := GetConfigMetadataPatchStrategy() + if err != nil { + patchStrategies = map[string]string{ + "contexts.additionalMetadata": "replace", + } + } + // Verify if there are patch strategies defined for `contexts.additionalMetadata` if not set replace by default + if patchStrategies != nil && patchStrategies["contexts.additionalMetadata"] != "merge" { + patchStrategies["contexts.additionalMetadata"] = "replace" + } + return patchStrategies +} + func setCurrentContext(node *yaml.Node, ctx *configtypes.Context) (persist bool, err error) { // Find current context node in the yaml node keys := []nodeutils.Key{ diff --git a/config/contexts_additionalmetadata_test.go b/config/contexts_additionalmetadata_test.go new file mode 100644 index 000000000..5c55cf133 --- /dev/null +++ b/config/contexts_additionalmetadata_test.go @@ -0,0 +1,654 @@ +// Copyright 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" +) + +func TestContextAdditionalMetadataStringToString(t *testing.T) { + // Setup config data + _, cleanUp := setupTestConfig(t, &CfgTestData{cfgNextGen: ``, cfg: ``, cfgMetadata: ``}) + + defer func() { + cleanUp() + }() + + var testcases = []struct { + name string + ctx *configtypes.Context + out map[string]interface{} + errStr string + }{ + + { + name: "should add additional metadata \"issuer1\": \"vmw1\"", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuer1": "vmw1", + }, + }, + out: map[string]interface{}{ + "issuer1": "vmw1", + }, + }, + { + name: "should update additional metadata \"issuer2\": \"vmw2\",", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuer2": "vmw2", + }, + }, + out: map[string]interface{}{ + "issuer2": "vmw2", + }, + }, + + { + name: "should clear additional metadata \"issuer1\": \"\",", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuer1": "", + }, + }, + out: map[string]interface{}{ + "issuer1": "", + }, + }, + + { + name: "should update additional metadata \"issuer1\": \"vmw1\",", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuer1": "vmw1", + }, + }, + out: map[string]interface{}{ + "issuer1": "vmw1", + }, + }, + { + name: "should update additional metadata \"issuer2\": \"vmw\",\n\t\t\t\t\t\"issuer1\": \"vmw\",", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuer2": "vmw", + "issuer1": "vmw", + }, + }, + out: map[string]interface{}{ + "issuer1": "vmw", + "issuer2": "vmw", + }, + }, + + { + name: "should delete all additional metadata", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{}, + }, + out: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // perform test + err := SetContext(tc.ctx, false) + if tc.errStr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.errStr) + } + ctx, err := GetContext(tc.ctx.Name) + assert.NoError(t, err) + + assert.Equal(t, tc.out, ctx.AdditionalMetadata) + }) + } +} + +func TestContextAdditionalMetadataStringToInt(t *testing.T) { + // Setup config data + _, cleanUp := setupTestConfig(t, &CfgTestData{cfgNextGen: ``, cfg: ``, cfgMetadata: ``}) + + defer func() { + cleanUp() + }() + + var testcases = []struct { + name string + ctx *configtypes.Context + out map[string]interface{} + errStr string + }{ + + // Additional metadata of map[string]int + { + name: "should add additional metadata \"contextId\": 0,", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "contextId": 0, + }, + }, + out: map[string]interface{}{ + "contextId": 0, + }, + }, + { + name: "should update additional metadata \"contextId\": 1,", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "contextId": 1, + }, + }, + out: map[string]interface{}{ + "contextId": 1, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // perform test + err := SetContext(tc.ctx, false) + if tc.errStr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.errStr) + } + ctx, err := GetContext(tc.ctx.Name) + assert.NoError(t, err) + + if tc.out != nil { + assert.Equal(t, tc.out, ctx.AdditionalMetadata) + } else { + assert.Equal(t, tc.ctx.AdditionalMetadata, ctx.AdditionalMetadata) + } + }) + } +} + +func TestContextAdditionalMetadataStringToStringArray(t *testing.T) { + // Setup config data + _, cleanUp := setupTestConfig(t, &CfgTestData{cfgNextGen: ``, cfg: ``, cfgMetadata: ``}) + + defer func() { + cleanUp() + }() + + var testcases = []struct { + name string + ctx *configtypes.Context + out map[string]interface{} + errStr string + }{ + + { + name: "should add additional metadata \"issuers\": []string{},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuers": []string{}, + }, + }, + out: map[string]interface{}{ + "issuers": []interface{}{}, + }, + }, + { + name: "should update additional metadata \t\"issuers\": []interface{}{\"vmw1\"},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuers": []interface{}{"vmw1"}, + }, + }, + out: map[string]interface{}{ + "issuers": []interface{}{"vmw1"}, + }, + }, + { + name: "should update additional metadata \"issuers\": []interface{}{\"vmw1\", \"vmw2\"},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuers": []interface{}{"vmw1", "vmw2"}, + }, + }, + out: map[string]interface{}{ + "issuers": []interface{}{"vmw1", "vmw2"}, + }, + }, + { + name: "should update additional metadata \"issuers\": []interface{}{\"vmw1\", \"vmw2\"},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuers": []interface{}{"vmw1", "vmw2"}, + }, + }, + out: map[string]interface{}{ + "issuers": []interface{}{"vmw1", "vmw2"}, + }, + }, + { + name: "should update additional metadata \"issuers\": []interface{}{\"vmw1\", \"vmw2\", \"vmw3\", \"vmw4\"},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "issuers": []interface{}{"vmw1", "vmw2", "vmw3", "vmw4"}, + }, + }, + out: map[string]interface{}{ + "issuers": []interface{}{"vmw1", "vmw2", "vmw3", "vmw4"}, + }, + }, + { + name: "should delete all additional metadata", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{}, + }, + out: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // perform test + err := SetContext(tc.ctx, false) + if tc.errStr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.errStr) + } + ctx, err := GetContext(tc.ctx.Name) + assert.NoError(t, err) + + assert.Equal(t, tc.out, ctx.AdditionalMetadata) + }) + } +} + +func TestContextAdditionalMetadataStringToMap(t *testing.T) { + // Setup config data + _, cleanUp := setupTestConfig(t, &CfgTestData{cfgNextGen: ``, cfg: ``, cfgMetadata: ``}) + + defer func() { + cleanUp() + }() + + var testcases = []struct { + name string + ctx *configtypes.Context + out map[string]interface{} + errStr string + }{ + + { + name: "should add additional metadata \"auth\": map[string]string{\"a1\": \"x\",\n},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "auth": map[string]string{ + "a1": "x", + }, + }, + }, + out: map[string]interface{}{ + "auth": map[string]interface{}{ + "a1": "x", + }, + }, + }, + { + name: "should update additional metadata \"auth\": map[string]string{\n\"a2\": \"y\",\n},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "auth": map[string]string{ + "a2": "y", + }, + }, + }, + out: map[string]interface{}{ + "auth": map[string]interface{}{ + "a2": "y", + }, + }, + }, + { + name: "should update additional metadata \"auth\": map[string]string{\n\"a1\": \"y\",\n},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "auth": map[string]string{ + "a1": "y", + }, + }, + }, + out: map[string]interface{}{ + "auth": map[string]interface{}{ + "a1": "y", + }, + }, + }, + { + name: "should update additional metadata \"auth\": map[string]string{\n\"a1\": \"z\",\n\"a3\": \"z\",\n},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "auth": map[string]string{ + "a1": "z", + "a3": "z", + }, + }, + }, + out: map[string]interface{}{ + "auth": map[string]interface{}{ + "a1": "z", + "a3": "z", + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // perform test + err := SetContext(tc.ctx, false) + if tc.errStr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.errStr) + } + ctx, err := GetContext(tc.ctx.Name) + assert.NoError(t, err) + if tc.out != nil { + assert.Equal(t, tc.out, ctx.AdditionalMetadata) + } else { + assert.Equal(t, tc.ctx.AdditionalMetadata, ctx.AdditionalMetadata) + } + }) + } +} + +func TestContextAdditionalMetadataStringToStruct(t *testing.T) { + // Setup config data + _, cleanUp := setupTestConfig(t, &CfgTestData{cfgNextGen: ``, cfg: ``, cfgMetadata: ``}) + + defer func() { + cleanUp() + }() + + var testcases = []struct { + name string + ctx *configtypes.Context + out map[string]interface{} + errStr string + }{ + + { + name: "should add additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": configtypes.GlobalServerAuth{ + AccessToken: "token1", + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": map[string]interface{}{ + "accessToken": "token1", + }, + }, + }, + { + name: "should update additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": configtypes.GlobalServerAuth{ + AccessToken: "token1", + IDToken: "id-1", + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": map[string]interface{}{ + "accessToken": "token1", + "IDToken": "id-1", + }, + }, + }, + { + name: "should update additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t\tPermissions: []string{\"p1\", \"p2\"},\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": configtypes.GlobalServerAuth{ + AccessToken: "token1", + IDToken: "id-1", + Permissions: []string{"p1", "p2"}, + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": map[string]interface{}{ + "accessToken": "token1", + "IDToken": "id-1", + "permissions": []interface{}{"p1", "p2"}, + }, + }, + }, + + { + name: "should update additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t\tPermissions: []string{\"p1\", \"p2\"},\n\t\t\t\t\t},\n\t\t\t\t\t\"globalAuth2\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t\tPermissions: []string{\"p1\", \"p2\"},\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": configtypes.GlobalServerAuth{ + AccessToken: "token1", + IDToken: "id-1", + Permissions: []string{"p1", "p2"}, + }, + "globalAuth2": configtypes.GlobalServerAuth{ + AccessToken: "token1", + IDToken: "id-1", + Permissions: []string{"p1", "p2"}, + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": map[string]interface{}{ + "accessToken": "token1", + "IDToken": "id-1", + "permissions": []interface{}{"p1", "p2"}, + }, + "globalAuth2": map[string]interface{}{ + "accessToken": "token1", + "IDToken": "id-1", + "permissions": []interface{}{"p1", "p2"}, + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // perform test + err := SetContext(tc.ctx, false) + if tc.errStr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.errStr) + } + ctx, err := GetContext(tc.ctx.Name) + assert.NoError(t, err) + + if tc.out != nil { + assert.Equal(t, tc.out, ctx.AdditionalMetadata) + } else { + assert.Equal(t, tc.ctx.AdditionalMetadata, ctx.AdditionalMetadata) + } + }) + } +} + +func TestContextAdditionalMetadataStringToStructArray(t *testing.T) { + // Setup config data + _, cleanUp := setupTestConfig(t, &CfgTestData{cfgNextGen: ``, cfg: ``, cfgMetadata: ``}) + + defer func() { + cleanUp() + }() + var testcases = []struct { + name string + ctx *configtypes.Context + out map[string]interface{} + errStr string + }{ + + { + name: "should add additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": []*configtypes.GlobalServerAuth{ + { + AccessToken: "token1", + }, + + { + AccessToken: "token2", + }, + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": []interface{}{ + map[string]interface{}{ + "accessToken": "token1", + }, + map[string]interface{}{ + "accessToken": "token2", + }, + }, + }, + }, + + { + name: "should update additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": []*configtypes.GlobalServerAuth{ + { + AccessToken: "token1", + IDToken: "id-1", + }, + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": []interface{}{ + map[string]interface{}{ + "accessToken": "token1", + "IDToken": "id-1", + }, + }, + }, + }, + + { + name: "should update additional metadata \"globalAuth\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t\tPermissions: []string{\"p1\", \"p2\"},\n\t\t\t\t\t},\n\t\t\t\t\t\"globalAuth2\": configtypes.GlobalServerAuth{\n\t\t\t\t\t\tAccessToken: \"token1\",\n\t\t\t\t\t\tIDToken: \"id-1\",\n\t\t\t\t\t\tPermissions: []string{\"p1\", \"p2\"},\n\t\t\t\t\t},", + ctx: &configtypes.Context{ + Name: "test-mc", + Target: configtypes.TargetK8s, + AdditionalMetadata: map[string]interface{}{ + "globalAuth": []*configtypes.GlobalServerAuth{ + { + Permissions: []string{"p1"}, + }, + }, + "globalAuth2": []*configtypes.GlobalServerAuth{ + { + AccessToken: "token2", + Permissions: []string{"p2"}, + }, + }, + }, + }, + out: map[string]interface{}{ + "globalAuth": []interface{}{ + map[string]interface{}{ + "permissions": []interface{}{"p1"}, + }, + }, + "globalAuth2": []interface{}{ + map[string]interface{}{ + "accessToken": "token2", + "permissions": []interface{}{"p2"}, + }, + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // perform test + err := SetContext(tc.ctx, false) + if tc.errStr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.errStr) + } + ctx, err := GetContext(tc.ctx.Name) + assert.NoError(t, err) + if tc.out != nil { + assert.Equal(t, tc.out, ctx.AdditionalMetadata) + } else { + assert.Equal(t, tc.ctx.AdditionalMetadata, ctx.AdditionalMetadata) + } + }) + } +} diff --git a/config/contexts_it_test.go b/config/contexts_it_test.go index 18829d1ea..c2778f170 100644 --- a/config/contexts_it_test.go +++ b/config/contexts_it_test.go @@ -265,6 +265,9 @@ currentContext: name: test bucket: test-bucket-updated manifestPath: test-manifest-path-updated + additionalMetadata: + metaToken: updated-token1 + newToken: optional currentContext: kubernetes: test-mc2 ` @@ -322,6 +325,9 @@ func TestContextsIntegration(t *testing.T) { }, }, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "token1", + }, } err = SetContext(newCtx, true) assert.NoError(t, err) @@ -372,6 +378,10 @@ func TestContextsIntegration(t *testing.T) { }, }, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "updated-token1", + "newToken": "optional", + }, } err = SetContext(updatedCtx, true) assert.NoError(t, err) diff --git a/config/contexts_test.go b/config/contexts_test.go index b146bdb32..131bbcce4 100644 --- a/config/contexts_test.go +++ b/config/contexts_test.go @@ -38,6 +38,9 @@ func TestSetGetDeleteContext(t *testing.T) { }, }, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "token1", + }, } ctx2 := &configtypes.Context{ @@ -57,6 +60,9 @@ func TestSetGetDeleteContext(t *testing.T) { }, }, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "token1", + }, } ctx, err := GetContext("test1") @@ -105,6 +111,8 @@ contexts: ctx-field: new-ctx-field optional: true target: kubernetes + additionalMetadata: + metaToken: token1 clusterOpts: isManagementCluster: true endpoint: old-test-endpoint @@ -151,6 +159,9 @@ contexts: }, }, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "token1", + }, } err := SetContext(ctx, false) @@ -162,6 +173,7 @@ contexts: assert.Equal(t, c.ClusterOpts.Endpoint, "old-test-endpoint") assert.Equal(t, c.ClusterOpts.Path, ctx.ClusterOpts.Path) assert.Equal(t, c.ClusterOpts.Context, ctx.ClusterOpts.Context) + assert.Equal(t, c.AdditionalMetadata, ctx.AdditionalMetadata) } func TestSetContextWithDiscoverySourceWithNewFields(t *testing.T) { @@ -576,6 +588,9 @@ func TestSetContext(t *testing.T) { Context: "test-context", IsManagementCluster: true, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "token1", + }, }, current: true, }, @@ -591,6 +606,9 @@ func TestSetContext(t *testing.T) { Context: "test-context", IsManagementCluster: true, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "token1", + }, }, }, { @@ -625,6 +643,9 @@ func TestSetContext(t *testing.T) { Context: "updated-test-context", IsManagementCluster: true, }, + AdditionalMetadata: map[string]interface{}{ + "metaToken": "updated-token1", + }, }, }, { diff --git a/config/legacy_clientconfig_factory_test.go b/config/legacy_clientconfig_factory_test.go index 2162b9a87..307241aa5 100644 --- a/config/legacy_clientconfig_factory_test.go +++ b/config/legacy_clientconfig_factory_test.go @@ -151,13 +151,16 @@ current: test-mc path: test-context-path context: test-context discoverySources: - - local: - name: test - path: test-local-path - gcp: name: test2 bucket: ctx-test-bucket manifestPath: ctx-test-manifest-path + annotation: one + required: true + contextType: tmc + - local: + name: test + path: test-local-path currentContext: kubernetes: test-mc ` diff --git a/config/nodeutils/helpers.go b/config/nodeutils/helpers.go index b711a6470..92bd18d06 100644 --- a/config/nodeutils/helpers.go +++ b/config/nodeutils/helpers.go @@ -13,9 +13,11 @@ func UniqNodes(nodes []*yaml.Node) []*yaml.Node { mapper := make(map[string]bool) for _, node := range nodes { - if _, ok := mapper[node.Value]; !ok { - mapper[node.Value] = true - uniq = append(uniq, node) + if node.Value != "" { + if _, ok := mapper[node.Value]; !ok { + mapper[node.Value] = true + uniq = append(uniq, node) + } } } diff --git a/config/nodeutils/helpers_test.go b/config/nodeutils/helpers_test.go index 2a9445f90..c141e99ae 100644 --- a/config/nodeutils/helpers_test.go +++ b/config/nodeutils/helpers_test.go @@ -19,6 +19,24 @@ func TestUniqNode(t *testing.T) { { name: "success 2 uniq nodes", count: 2, + nodes: []*yaml.Node{ + { + Kind: yaml.ScalarNode, + Value: "test", + Style: 0, + Tag: "!!str", + }, + { + Kind: yaml.ScalarNode, + Value: "test2", + Style: 0, + Tag: "!!str", + }, + }, + }, + { + name: "success 1 uniq nodes", + count: 1, nodes: []*yaml.Node{ { Kind: yaml.ScalarNode, diff --git a/config/nodeutils/merge_nodes.go b/config/nodeutils/merge_nodes.go index 1bfea7cd6..5722e7052 100644 --- a/config/nodeutils/merge_nodes.go +++ b/config/nodeutils/merge_nodes.go @@ -49,7 +49,10 @@ func mergeNodes(src, dst *yaml.Node) error { } } case yaml.SequenceNode: - setSeqNode(src, dst) + err := setSeqNode(src, dst) + if err != nil { + return errors.Wrap(err, "merge at key "+src.Content[0].Value) + } case yaml.DocumentNode: err := mergeNodes(src.Content[0], dst.Content[0]) if err != nil { @@ -66,9 +69,37 @@ func mergeNodes(src, dst *yaml.Node) error { } // Construct unique sequence nodes for scalar value type -func setSeqNode(src, dst *yaml.Node) { - if dst.Content[0].Kind == yaml.ScalarNode && src.Content[0].Kind == yaml.ScalarNode { - dst.Content = append(dst.Content, src.Content...) - dst.Content = UniqNodes(dst.Content) +func setSeqNode(src, dst *yaml.Node) error { + if len(src.Content) == 0 { + return nil // Nothing to merge + } + + switch src.Content[0].Kind { + case yaml.ScalarNode: + if len(dst.Content) > 0 && dst.Content[0].Kind == yaml.ScalarNode { + dst.Content = append(dst.Content, src.Content...) + dst.Content = UniqNodes(dst.Content) // Ensure uniqueness among scalar nodes + } else { + dst.Content = src.Content + } + case yaml.SequenceNode: + if len(dst.Content) > 0 && dst.Content[0].Kind == yaml.SequenceNode { + if err := mergeNodes(src.Content[0], dst.Content[0]); err != nil { + return errors.New("merge at key " + src.Content[0].Value + " failed with err " + err.Error()) + } + } else { + dst.Content = src.Content + } + + case yaml.MappingNode: + if len(dst.Content) > 0 && dst.Content[0].Kind == yaml.MappingNode { + if err := mergeNodes(src.Content[0], dst.Content[0]); err != nil { + return errors.New("merge at key " + src.Content[0].Value + " failed with err " + err.Error()) + } + } else { + dst.Content = src.Content + } } + + return nil } diff --git a/config/types/clientconfig_types.go b/config/types/clientconfig_types.go index 4335ad913..c81711d08 100644 --- a/config/types/clientconfig_types.go +++ b/config/types/clientconfig_types.go @@ -65,6 +65,9 @@ type Context struct { // ClusterOpts if the context is a kubernetes cluster. ClusterOpts *ClusterServer `json:"clusterOpts,omitempty" yaml:"clusterOpts,omitempty"` + // AdditionalMetadata to provide any additional data that is respective to each context + AdditionalMetadata map[string]interface{} `json:"additionalMetadata,omitempty" yaml:"additionalMetadata,omitempty"` + // DiscoverySources determines from where to discover plugins // associated with this context. // Deprecated: This field is deprecated. It is currently no used.