From e5a072d583f9d3e416f55e73ac56580e9803c1a4 Mon Sep 17 00:00:00 2001 From: amit-momin Date: Tue, 24 Mar 2026 12:23:44 -0500 Subject: [PATCH 1/2] Added cap config override to add capability changeset --- .../v2/changeset/add_capabilities.go | 21 +- .../v2/changeset/add_capabilities_test.go | 27 ++ .../changeset/sequences/add_capabilities.go | 13 +- .../changeset/sequences/config_overrides.go | 102 +++++++ .../sequences/config_overrides_test.go | 269 ++++++++++++++++++ 5 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go create mode 100644 deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go index a761a412856..2f859f503d1 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go @@ -27,6 +27,11 @@ type AddCapabilitiesInput struct { DonNames []string `json:"donNames" yaml:"donNames"` // multiple DONs to update CapabilityConfigs []contracts.CapabilityConfig `json:"capabilityConfigs" yaml:"capabilityConfigs"` + // DonCapabilityConfigOverrides maps DON name to a list of config overrides. + // Each override's Config is deep-merged into the base CapabilityConfig.Config for the matching capability. + // If an override's CapabilityID is empty, it applies to all capabilities for that DON. + DonCapabilityConfigOverrides map[string][]sequences.CapabilityConfigOverride `json:"donCapabilityConfigOverrides,omitempty" yaml:"donCapabilityConfigOverrides,omitempty"` + // Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version. // This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option. Force bool `json:"force" yaml:"force"` @@ -48,6 +53,11 @@ func (u AddCapabilities) VerifyPreconditions(_ cldf.Environment, config AddCapab if len(config.CapabilityConfigs) == 0 { return errors.New("capabilityConfigs is required") } + for overrideDon := range config.DonCapabilityConfigOverrides { + if !slices.Contains(donNames, overrideDon) { + return fmt.Errorf("donCapabilityConfigOverrides contains DON name %q which is not in the DON names list", overrideDon) + } + } return nil } @@ -79,11 +89,12 @@ func (u AddCapabilities) Apply(e cldf.Environment, config AddCapabilitiesInput) sequences.AddCapabilities, sequences.AddCapabilitiesDeps{Env: &e, MCMSContracts: mcmsContracts}, sequences.AddCapabilitiesInput{ - RegistryRef: registryRef, - DonNames: u.donNames(config), - CapabilityConfigs: config.CapabilityConfigs, - Force: config.Force, - MCMSConfig: config.MCMSConfig, + RegistryRef: registryRef, + DonNames: u.donNames(config), + CapabilityConfigs: config.CapabilityConfigs, + DonCapabilityConfigOverrides: config.DonCapabilityConfigOverrides, + Force: config.Force, + MCMSConfig: config.MCMSConfig, }, ) if err != nil { diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go index 14f5c419c3f..02423130e2f 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset" "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/pkg" + "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/sequences" crecontracts "github.com/smartcontractkit/chainlink/deployment/cre/contracts" "github.com/smartcontractkit/chainlink/deployment/cre/test" ) @@ -141,6 +142,32 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, }) require.NoError(t, err) + + // Override DON name not in donNames list + err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: "qual", + DonNames: []string{"don-1"}, + CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, + DonCapabilityConfigOverrides: map[string][]sequences.CapabilityConfigOverride{ + "unknown-don": {{Config: map[string]any{"k": "v2"}}}, + }, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown-don") + assert.Contains(t, err.Error(), "not in the DON names list") + + // Valid with overrides + err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: "qual", + DonNames: []string{"don-1", "don-2"}, + CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, + DonCapabilityConfigOverrides: map[string][]sequences.CapabilityConfigOverride{ + "don-2": {{Config: map[string]any{"k": "v2"}}}, + }, + }) + require.NoError(t, err) } func addNewCapability(t *testing.T, fixture *test.EnvWrapperV2, capID string) { diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go index 206b08a15ab..5f3f907c120 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go @@ -32,6 +32,10 @@ type AddCapabilitiesDeps struct { type AddCapabilitiesInput struct { CapabilityConfigs []contracts.CapabilityConfig // if Config subfield is nil, a default config is used + // DonCapabilityConfigOverrides maps DON name to per-DON config overrides that are + // deep-merged into the base CapabilityConfigs. See CapabilityConfigOverride for details. + DonCapabilityConfigOverrides map[string][]CapabilityConfigOverride + // DonNames are the DONs to update. At least one is required. DonNames []string @@ -148,7 +152,12 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti p2pIDs = append(p2pIDs, node.P2pId) } - nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, input.CapabilityConfigs) + donCapConfigs, err := resolveCapabilityConfigsForDON(input.CapabilityConfigs, input.DonCapabilityConfigOverrides[donName]) + if err != nil { + return AddCapabilitiesOutput{}, fmt.Errorf("failed to resolve capability configs for DON %s: %w", donName, err) + } + + nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, donCapConfigs) if err != nil { return AddCapabilitiesOutput{}, fmt.Errorf("failed to build node updates for DON %s: %w", donName, err) } @@ -182,7 +191,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti contracts.UpdateDONInput{ ChainSelector: chainSel, P2PIDs: p2pIDs, - CapabilityConfigs: input.CapabilityConfigs, + CapabilityConfigs: donCapConfigs, MergeCapabilityConfigsWithOnChain: true, DonName: donName, F: don.F, diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go new file mode 100644 index 00000000000..efbb077b96c --- /dev/null +++ b/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go @@ -0,0 +1,102 @@ +package sequences + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" +) + +// CapabilityConfigOverride specifies a per-DON override for capability configs. +// If CapabilityID is empty, the override is applied to all capabilities for that DON. +// Config is deep-merged into the base CapabilityConfig.Config. +type CapabilityConfigOverride struct { + CapabilityID string `json:"capabilityID" yaml:"capabilityID"` + Config map[string]any `json:"config" yaml:"config"` +} + +// resolveCapabilityConfigsForDON returns a copy of baseConfigs with per-DON overrides deep-merged in. +// If an override has an empty CapabilityID, it applies to all capabilities. +// If an override has a CapabilityID, it applies only to the matching capability. +func resolveCapabilityConfigsForDON(baseConfigs []contracts.CapabilityConfig, overrides []CapabilityConfigOverride) ([]contracts.CapabilityConfig, error) { + if len(overrides) == 0 { + return baseConfigs, nil + } + + result := make([]contracts.CapabilityConfig, len(baseConfigs)) + for i, base := range baseConfigs { + result[i] = contracts.CapabilityConfig{ + Capability: base.Capability, + Config: deepCopyMap(base.Config), + } + } + + for _, override := range overrides { + if override.Config == nil { + continue + } + applied := false + for i := range result { + if override.CapabilityID == "" || override.CapabilityID == result[i].Capability.CapabilityID { + result[i].Config = deepMergeMaps(result[i].Config, override.Config) + applied = true + } + } + if override.CapabilityID != "" && !applied { + return nil, fmt.Errorf("override references capability ID %q which does not exist in the base capability configs", override.CapabilityID) + } + } + + return result, nil +} + +// deepMergeMaps recursively merges override into base, returning a new map. +// For nested map[string]any values, it recurses. For all other types the override value wins. +// Neither input is mutated. +func deepMergeMaps(base, override map[string]any) map[string]any { + if base == nil && override == nil { + return nil + } + result := deepCopyMap(base) + if result == nil { + result = make(map[string]any) + } + for k, overrideVal := range override { + baseVal, exists := result[k] + if exists { + baseMap, baseIsMap := baseVal.(map[string]any) + overrideMap, overrideIsMap := overrideVal.(map[string]any) + if baseIsMap && overrideIsMap { + result[k] = deepMergeMaps(baseMap, overrideMap) + continue + } + } + result[k] = deepCopyValue(overrideVal) + } + return result +} + +func deepCopyMap(m map[string]any) map[string]any { + if m == nil { + return nil + } + result := make(map[string]any, len(m)) + for k, v := range m { + result[k] = deepCopyValue(v) + } + return result +} + +func deepCopyValue(v any) any { + switch val := v.(type) { + case map[string]any: + return deepCopyMap(val) + case []any: + cp := make([]any, len(val)) + for i, item := range val { + cp[i] = deepCopyValue(item) + } + return cp + default: + return v + } +} diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go new file mode 100644 index 00000000000..50de1cb1552 --- /dev/null +++ b/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go @@ -0,0 +1,269 @@ +package sequences + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" +) + +func TestDeepMergeMaps(t *testing.T) { + t.Run("both nil", func(t *testing.T) { + result := deepMergeMaps(nil, nil) + assert.Nil(t, result) + }) + + t.Run("nil base", func(t *testing.T) { + override := map[string]any{"key": "value"} + result := deepMergeMaps(nil, override) + assert.Equal(t, map[string]any{"key": "value"}, result) + }) + + t.Run("nil override", func(t *testing.T) { + base := map[string]any{"key": "value"} + result := deepMergeMaps(base, nil) + assert.Equal(t, map[string]any{"key": "value"}, result) + }) + + t.Run("scalar override", func(t *testing.T) { + base := map[string]any{"a": 1, "b": 2} + override := map[string]any{"b": 3} + result := deepMergeMaps(base, override) + assert.Equal(t, map[string]any{"a": 1, "b": 3}, result) + }) + + t.Run("add new key", func(t *testing.T) { + base := map[string]any{"a": 1} + override := map[string]any{"b": 2} + result := deepMergeMaps(base, override) + assert.Equal(t, map[string]any{"a": 1, "b": 2}, result) + }) + + t.Run("nested map merge", func(t *testing.T) { + base := map[string]any{ + "outer": map[string]any{ + "keep": "yes", + "inner": map[string]any{ + "x": 1, + "y": 2, + }, + }, + } + override := map[string]any{ + "outer": map[string]any{ + "inner": map[string]any{ + "y": 99, + }, + }, + } + result := deepMergeMaps(base, override) + expected := map[string]any{ + "outer": map[string]any{ + "keep": "yes", + "inner": map[string]any{ + "x": 1, + "y": 99, + }, + }, + } + assert.Equal(t, expected, result) + }) + + t.Run("does not mutate inputs", func(t *testing.T) { + base := map[string]any{ + "nested": map[string]any{"a": 1}, + } + override := map[string]any{ + "nested": map[string]any{"b": 2}, + } + _ = deepMergeMaps(base, override) + + assert.Equal(t, map[string]any{"nested": map[string]any{"a": 1}}, base) + assert.Equal(t, map[string]any{"nested": map[string]any{"b": 2}}, override) + }) + + t.Run("realistic minResponsesToAggregate override", func(t *testing.T) { + base := map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "registrationRefresh": "20s", + "registrationExpiry": "60s", + "minResponsesToAggregate": 4, + "messageExpiry": "120s", + }, + }, + "BalanceAt": map[string]any{ + "remoteExecutableConfig": map[string]any{ + "requestTimeout": "30s", + }, + }, + }, + } + override := map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "minResponsesToAggregate": 2, + }, + }, + }, + } + result := deepMergeMaps(base, override) + + logTrigger := result["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) + assert.Equal(t, 2, logTrigger["minResponsesToAggregate"]) + assert.Equal(t, "20s", logTrigger["registrationRefresh"]) + assert.Equal(t, "60s", logTrigger["registrationExpiry"]) + assert.Equal(t, "120s", logTrigger["messageExpiry"]) + + balanceAt := result["methodConfigs"].(map[string]any)["BalanceAt"].(map[string]any)["remoteExecutableConfig"].(map[string]any) + assert.Equal(t, "30s", balanceAt["requestTimeout"]) + }) +} + +func TestResolveCapabilityConfigsForDON(t *testing.T) { + baseConfigs := []contracts.CapabilityConfig{ + { + Capability: contracts.Capability{CapabilityID: "cap-a@1.0.0"}, + Config: map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "minResponsesToAggregate": 4, + "registrationRefresh": "20s", + }, + }, + }, + }, + }, + { + Capability: contracts.Capability{CapabilityID: "cap-b@1.0.0"}, + Config: map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "minResponsesToAggregate": 4, + "registrationRefresh": "20s", + }, + }, + }, + }, + }, + } + + t.Run("no overrides returns base", func(t *testing.T) { + result, err := resolveCapabilityConfigsForDON(baseConfigs, nil) + require.NoError(t, err) + assert.Equal(t, baseConfigs, result) + }) + + t.Run("empty overrides returns base", func(t *testing.T) { + result, err := resolveCapabilityConfigsForDON(baseConfigs, []CapabilityConfigOverride{}) + require.NoError(t, err) + assert.Equal(t, baseConfigs, result) + }) + + t.Run("override specific capability", func(t *testing.T) { + overrides := []CapabilityConfigOverride{ + { + CapabilityID: "cap-a@1.0.0", + Config: map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "minResponsesToAggregate": 2, + }, + }, + }, + }, + }, + } + + result, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) + require.NoError(t, err) + require.Len(t, result, 2) + + capAConfig := result[0].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) + assert.Equal(t, 2, capAConfig["minResponsesToAggregate"]) + assert.Equal(t, "20s", capAConfig["registrationRefresh"]) + + capBConfig := result[1].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) + assert.Equal(t, 4, capBConfig["minResponsesToAggregate"]) + }) + + t.Run("override all capabilities with empty ID", func(t *testing.T) { + overrides := []CapabilityConfigOverride{ + { + CapabilityID: "", + Config: map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "minResponsesToAggregate": 2, + }, + }, + }, + }, + }, + } + + result, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) + require.NoError(t, err) + require.Len(t, result, 2) + + for _, cfg := range result { + logTrigger := cfg.Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) + assert.Equal(t, 2, logTrigger["minResponsesToAggregate"]) + assert.Equal(t, "20s", logTrigger["registrationRefresh"]) + } + }) + + t.Run("error on non-existent capability ID", func(t *testing.T) { + overrides := []CapabilityConfigOverride{ + { + CapabilityID: "non-existent@1.0.0", + Config: map[string]any{"foo": "bar"}, + }, + } + + _, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) + require.Error(t, err) + assert.Contains(t, err.Error(), "non-existent@1.0.0") + }) + + t.Run("nil config override is skipped", func(t *testing.T) { + overrides := []CapabilityConfigOverride{ + {CapabilityID: "", Config: nil}, + } + + result, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) + require.NoError(t, err) + assert.Equal(t, 4, result[0].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any)["minResponsesToAggregate"]) + }) + + t.Run("does not mutate base configs", func(t *testing.T) { + overrides := []CapabilityConfigOverride{ + { + CapabilityID: "", + Config: map[string]any{ + "methodConfigs": map[string]any{ + "LogTrigger": map[string]any{ + "remoteTriggerConfig": map[string]any{ + "minResponsesToAggregate": 99, + }, + }, + }, + }, + }, + } + + _, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) + require.NoError(t, err) + + originalVal := baseConfigs[0].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any)["minResponsesToAggregate"] + assert.Equal(t, 4, originalVal) + }) +} From a806d52d04dd844f192c1773ea21cd65808b1ecc Mon Sep 17 00:00:00 2001 From: amit-momin Date: Thu, 2 Apr 2026 16:37:10 -0500 Subject: [PATCH 2/2] Updated add capability changeset to accept map of cap configs --- .../v2/changeset/add_capabilities.go | 56 +--- .../v2/changeset/add_capabilities_test.go | 127 +++------ .../changeset/sequences/add_capabilities.go | 62 ++-- .../changeset/sequences/config_overrides.go | 102 ------- .../sequences/config_overrides_test.go | 269 ------------------ 5 files changed, 89 insertions(+), 527 deletions(-) delete mode 100644 deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go delete mode 100644 deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go index 2f859f503d1..acb8e833a09 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go @@ -3,7 +3,6 @@ package changeset import ( "errors" "fmt" - "slices" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -22,15 +21,10 @@ type AddCapabilitiesInput struct { RegistryChainSel uint64 `json:"registryChainSel" yaml:"registryChainSel"` RegistryQualifier string `json:"registryQualifier" yaml:"registryQualifier"` - MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` - DonName string `json:"donName" yaml:"donName"` // optional if DonNames is set; for backward compatibility - DonNames []string `json:"donNames" yaml:"donNames"` // multiple DONs to update - CapabilityConfigs []contracts.CapabilityConfig `json:"capabilityConfigs" yaml:"capabilityConfigs"` + MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` - // DonCapabilityConfigOverrides maps DON name to a list of config overrides. - // Each override's Config is deep-merged into the base CapabilityConfig.Config for the matching capability. - // If an override's CapabilityID is empty, it applies to all capabilities for that DON. - DonCapabilityConfigOverrides map[string][]sequences.CapabilityConfigOverride `json:"donCapabilityConfigOverrides,omitempty" yaml:"donCapabilityConfigOverrides,omitempty"` + // DonCapabilityConfigs maps DON name to the list of capability configs for that DON. + DonCapabilityConfigs map[string][]contracts.CapabilityConfig `json:"donCapabilityConfigs" yaml:"donCapabilityConfigs"` // Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version. // This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option. @@ -40,34 +34,16 @@ type AddCapabilitiesInput struct { type AddCapabilities struct{} func (u AddCapabilities) VerifyPreconditions(_ cldf.Environment, config AddCapabilitiesInput) error { - if config.DonName != "" && len(config.DonNames) > 0 { - return errors.New("cannot specify both donName and donNames") + if len(config.DonCapabilityConfigs) == 0 { + return errors.New("donCapabilityConfigs must contain at least one DON entry") } - donNames := u.donNames(config) - if len(donNames) == 0 { - return errors.New("must specify donName or donNames") - } - if slices.Contains(donNames, "") { - return errors.New("donName or donNames cannot contain an empty string") - } - if len(config.CapabilityConfigs) == 0 { - return errors.New("capabilityConfigs is required") - } - for overrideDon := range config.DonCapabilityConfigOverrides { - if !slices.Contains(donNames, overrideDon) { - return fmt.Errorf("donCapabilityConfigOverrides contains DON name %q which is not in the DON names list", overrideDon) + for donName, configs := range config.DonCapabilityConfigs { + if donName == "" { + return errors.New("donCapabilityConfigs keys cannot be empty strings") + } + if len(configs) == 0 { + return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName) } - } - return nil -} - -// donNames returns the list of DON names to update (from DonNames or single DonName for backward compatibility). -func (u AddCapabilities) donNames(config AddCapabilitiesInput) []string { - if len(config.DonNames) > 0 { - return config.DonNames - } - if config.DonName != "" { - return []string{config.DonName} } return nil } @@ -89,12 +65,10 @@ func (u AddCapabilities) Apply(e cldf.Environment, config AddCapabilitiesInput) sequences.AddCapabilities, sequences.AddCapabilitiesDeps{Env: &e, MCMSContracts: mcmsContracts}, sequences.AddCapabilitiesInput{ - RegistryRef: registryRef, - DonNames: u.donNames(config), - CapabilityConfigs: config.CapabilityConfigs, - DonCapabilityConfigOverrides: config.DonCapabilityConfigOverrides, - Force: config.Force, - MCMSConfig: config.MCMSConfig, + RegistryRef: registryRef, + DonCapabilityConfigs: config.DonCapabilityConfigs, + Force: config.Force, + MCMSConfig: config.MCMSConfig, }, ) if err != nil { diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go index 02423130e2f..37f11fa52d1 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go @@ -16,7 +16,6 @@ import ( "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset" "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/pkg" - "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/sequences" crecontracts "github.com/smartcontractkit/chainlink/deployment/cre/contracts" "github.com/smartcontractkit/chainlink/deployment/cre/test" ) @@ -75,62 +74,46 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { env := test.SetupEnvV2(t, false) chainSelector := env.RegistrySelector - // Missing donName and donNames - err := cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonNames: nil, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}}, - }) - require.Error(t, err) - assert.Contains(t, err.Error(), "must specify donName or donNames") + capCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}} - // Both donName and donNames set - err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonName: "don-1", - DonNames: []string{"don-2"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}}, + // Empty map + err := cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: "qual", + DonCapabilityConfigs: nil, }) require.Error(t, err) - assert.Contains(t, err.Error(), "cannot specify both donName and donNames") + assert.Contains(t, err.Error(), "donCapabilityConfigs must contain at least one DON entry") - // donNames with empty string + // Empty DON name key err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1", ""}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "": capCfg, + }, }) require.Error(t, err) - assert.Contains(t, err.Error(), "cannot contain an empty string") + assert.Contains(t, err.Error(), "cannot be empty strings") - // Missing capability configs + // Empty config list for a DON err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1"}, - CapabilityConfigs: nil, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": {}, + }, }) require.Error(t, err) - assert.Contains(t, err.Error(), "capabilityConfigs") + assert.Contains(t, err.Error(), "at least one capability config") - // Valid (single DON via donNames) + // Valid (single DON) err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, - }) - require.NoError(t, err) - - // Valid (single DON via donName - backward compatibility) - err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonName: "don-1", - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": capCfg, + }, }) require.NoError(t, err) @@ -138,33 +121,9 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1", "don-2"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, - }) - require.NoError(t, err) - - // Override DON name not in donNames list - err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonNames: []string{"don-1"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, - DonCapabilityConfigOverrides: map[string][]sequences.CapabilityConfigOverride{ - "unknown-don": {{Config: map[string]any{"k": "v2"}}}, - }, - }) - require.Error(t, err) - assert.Contains(t, err.Error(), "unknown-don") - assert.Contains(t, err.Error(), "not in the DON names list") - - // Valid with overrides - err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonNames: []string{"don-1", "don-2"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, - DonCapabilityConfigOverrides: map[string][]sequences.CapabilityConfigOverride{ - "don-2": {{Config: map[string]any{"k": "v2"}}}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": capCfg, + "don-2": capCfg, }, }) require.NoError(t, err) @@ -174,15 +133,16 @@ func addNewCapability(t *testing.T, fixture *test.EnvWrapperV2, capID string) { input := changeset.AddCapabilitiesInput{ RegistryChainSel: fixture.RegistrySelector, RegistryQualifier: test.RegistryQualifier, - DonNames: []string{test.DONName}, - CapabilityConfigs: []contracts.CapabilityConfig{{ - Capability: contracts.Capability{ - CapabilityID: capID, - ConfigurationContract: common.Address{}, - Metadata: newCapMetadata, - }, - Config: newCapConfig, - }}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + test.DONName: {{ + Capability: contracts.Capability{ + CapabilityID: capID, + ConfigurationContract: common.Address{}, + Metadata: newCapMetadata, + }, + Config: newCapConfig, + }}, + }, Force: true, } @@ -273,15 +233,16 @@ func TestAddCapabilities_Apply_MCMS(t *testing.T) { input := changeset.AddCapabilitiesInput{ RegistryChainSel: fixture.RegistrySelector, RegistryQualifier: test.RegistryQualifier, - DonNames: []string{test.DONName}, - CapabilityConfigs: []contracts.CapabilityConfig{{ - Capability: contracts.Capability{ - CapabilityID: newCapID, - ConfigurationContract: common.Address{}, - Metadata: newCapMetadata, - }, - Config: newCapConfig, - }}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + test.DONName: {{ + Capability: contracts.Capability{ + CapabilityID: newCapID, + ConfigurationContract: common.Address{}, + Metadata: newCapMetadata, + }, + Config: newCapConfig, + }}, + }, Force: true, MCMSConfig: &crecontracts.MCMSConfig{ MinDelay: 1 * time.Second, diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go index 5f3f907c120..8239a6439ee 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "slices" "github.com/Masterminds/semver/v3" @@ -30,14 +31,8 @@ type AddCapabilitiesDeps struct { } type AddCapabilitiesInput struct { - CapabilityConfigs []contracts.CapabilityConfig // if Config subfield is nil, a default config is used - - // DonCapabilityConfigOverrides maps DON name to per-DON config overrides that are - // deep-merged into the base CapabilityConfigs. See CapabilityConfigOverride for details. - DonCapabilityConfigOverrides map[string][]CapabilityConfigOverride - - // DonNames are the DONs to update. At least one is required. - DonNames []string + // DonCapabilityConfigs maps DON name to the list of capability configs for that DON. + DonCapabilityConfigs map[string][]contracts.CapabilityConfig // Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version. // This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option. @@ -48,14 +43,16 @@ type AddCapabilitiesInput struct { } func (i *AddCapabilitiesInput) Validate() error { - if len(i.DonNames) == 0 { - return errors.New("must specify at least one DON name") - } - if slices.Contains(i.DonNames, "") { - return errors.New("donNames cannot contain an empty string") + if len(i.DonCapabilityConfigs) == 0 { + return errors.New("donCapabilityConfigs must contain at least one DON entry") } - if len(i.CapabilityConfigs) == 0 { - return errors.New("capabilityConfigs is required") + for donName, configs := range i.DonCapabilityConfigs { + if donName == "" { + return errors.New("donCapabilityConfigs keys cannot be empty strings") + } + if len(configs) == 0 { + return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName) + } } return nil } @@ -94,8 +91,8 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti return AddCapabilitiesOutput{}, fmt.Errorf("failed to create CapabilitiesRegistry: %w", err) } - // Build capabilities list once (registry-level; same for all DONs). - capabilities, err := buildCapabilitiesFromConfigs(input.CapabilityConfigs) + // Build capabilities list once (registry-level; union across all DONs). + capabilities, err := buildCapabilitiesFromAllDONConfigs(input.DonCapabilityConfigs) if err != nil { return AddCapabilitiesOutput{}, err } @@ -141,7 +138,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti var allUpdatedNodes []capabilities_registry_v2.CapabilitiesRegistryNodeParams // Update each DON: get nodes, update node configs, update DON. - for _, donName := range input.DonNames { + for donName, donCapConfigs := range input.DonCapabilityConfigs { don, nodes, err := GetDonNodes(donName, capReg) if err != nil { return AddCapabilitiesOutput{}, fmt.Errorf("failed to get DON %s nodes: %w", donName, err) @@ -152,11 +149,6 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti p2pIDs = append(p2pIDs, node.P2pId) } - donCapConfigs, err := resolveCapabilityConfigsForDON(input.CapabilityConfigs, input.DonCapabilityConfigOverrides[donName]) - if err != nil { - return AddCapabilitiesOutput{}, fmt.Errorf("failed to resolve capability configs for DON %s: %w", donName, err) - } - nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, donCapConfigs) if err != nil { return AddCapabilitiesOutput{}, fmt.Errorf("failed to build node updates for DON %s: %w", donName, err) @@ -242,17 +234,23 @@ func toOpsSlice(opPtrs ...*types.BatchOperation) []types.BatchOperation { return result } -// buildCapabilitiesFromConfigs builds the capability list for RegisterCapabilities (registry-level, no DON). -func buildCapabilitiesFromConfigs(configs []contracts.CapabilityConfig) ([]contracts.RegisterableCapability, error) { - out := make([]contracts.RegisterableCapability, len(configs)) - for i, cfg := range configs { - out[i] = contracts.RegisterableCapability{ - Metadata: cfg.Capability.Metadata, - CapabilityID: cfg.Capability.CapabilityID, - ConfigurationContract: cfg.Capability.ConfigurationContract, +// buildCapabilitiesFromAllDONConfigs collects the unique capabilities across all DONs' configs +// for registry-level registration. +func buildCapabilitiesFromAllDONConfigs(donConfigs map[string][]contracts.CapabilityConfig) ([]contracts.RegisterableCapability, error) { + uniqueCaps := make(map[string]contracts.RegisterableCapability) + for _, configs := range donConfigs { + for _, cfg := range configs { + if _, ok := uniqueCaps[cfg.Capability.CapabilityID]; ok { + continue + } + uniqueCaps[cfg.Capability.CapabilityID] = contracts.RegisterableCapability{ + Metadata: cfg.Capability.Metadata, + CapabilityID: cfg.Capability.CapabilityID, + ConfigurationContract: cfg.Capability.ConfigurationContract, + } } } - return out, nil + return slices.Collect(maps.Values(uniqueCaps)), nil } // buildNodeUpdatesForDON builds node config updates for a DON's nodes (adds the new capabilities to each node). diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go deleted file mode 100644 index efbb077b96c..00000000000 --- a/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides.go +++ /dev/null @@ -1,102 +0,0 @@ -package sequences - -import ( - "fmt" - - "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" -) - -// CapabilityConfigOverride specifies a per-DON override for capability configs. -// If CapabilityID is empty, the override is applied to all capabilities for that DON. -// Config is deep-merged into the base CapabilityConfig.Config. -type CapabilityConfigOverride struct { - CapabilityID string `json:"capabilityID" yaml:"capabilityID"` - Config map[string]any `json:"config" yaml:"config"` -} - -// resolveCapabilityConfigsForDON returns a copy of baseConfigs with per-DON overrides deep-merged in. -// If an override has an empty CapabilityID, it applies to all capabilities. -// If an override has a CapabilityID, it applies only to the matching capability. -func resolveCapabilityConfigsForDON(baseConfigs []contracts.CapabilityConfig, overrides []CapabilityConfigOverride) ([]contracts.CapabilityConfig, error) { - if len(overrides) == 0 { - return baseConfigs, nil - } - - result := make([]contracts.CapabilityConfig, len(baseConfigs)) - for i, base := range baseConfigs { - result[i] = contracts.CapabilityConfig{ - Capability: base.Capability, - Config: deepCopyMap(base.Config), - } - } - - for _, override := range overrides { - if override.Config == nil { - continue - } - applied := false - for i := range result { - if override.CapabilityID == "" || override.CapabilityID == result[i].Capability.CapabilityID { - result[i].Config = deepMergeMaps(result[i].Config, override.Config) - applied = true - } - } - if override.CapabilityID != "" && !applied { - return nil, fmt.Errorf("override references capability ID %q which does not exist in the base capability configs", override.CapabilityID) - } - } - - return result, nil -} - -// deepMergeMaps recursively merges override into base, returning a new map. -// For nested map[string]any values, it recurses. For all other types the override value wins. -// Neither input is mutated. -func deepMergeMaps(base, override map[string]any) map[string]any { - if base == nil && override == nil { - return nil - } - result := deepCopyMap(base) - if result == nil { - result = make(map[string]any) - } - for k, overrideVal := range override { - baseVal, exists := result[k] - if exists { - baseMap, baseIsMap := baseVal.(map[string]any) - overrideMap, overrideIsMap := overrideVal.(map[string]any) - if baseIsMap && overrideIsMap { - result[k] = deepMergeMaps(baseMap, overrideMap) - continue - } - } - result[k] = deepCopyValue(overrideVal) - } - return result -} - -func deepCopyMap(m map[string]any) map[string]any { - if m == nil { - return nil - } - result := make(map[string]any, len(m)) - for k, v := range m { - result[k] = deepCopyValue(v) - } - return result -} - -func deepCopyValue(v any) any { - switch val := v.(type) { - case map[string]any: - return deepCopyMap(val) - case []any: - cp := make([]any, len(val)) - for i, item := range val { - cp[i] = deepCopyValue(item) - } - return cp - default: - return v - } -} diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go deleted file mode 100644 index 50de1cb1552..00000000000 --- a/deployment/cre/capabilities_registry/v2/changeset/sequences/config_overrides_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package sequences - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink/deployment/cre/capabilities_registry/v2/changeset/operations/contracts" -) - -func TestDeepMergeMaps(t *testing.T) { - t.Run("both nil", func(t *testing.T) { - result := deepMergeMaps(nil, nil) - assert.Nil(t, result) - }) - - t.Run("nil base", func(t *testing.T) { - override := map[string]any{"key": "value"} - result := deepMergeMaps(nil, override) - assert.Equal(t, map[string]any{"key": "value"}, result) - }) - - t.Run("nil override", func(t *testing.T) { - base := map[string]any{"key": "value"} - result := deepMergeMaps(base, nil) - assert.Equal(t, map[string]any{"key": "value"}, result) - }) - - t.Run("scalar override", func(t *testing.T) { - base := map[string]any{"a": 1, "b": 2} - override := map[string]any{"b": 3} - result := deepMergeMaps(base, override) - assert.Equal(t, map[string]any{"a": 1, "b": 3}, result) - }) - - t.Run("add new key", func(t *testing.T) { - base := map[string]any{"a": 1} - override := map[string]any{"b": 2} - result := deepMergeMaps(base, override) - assert.Equal(t, map[string]any{"a": 1, "b": 2}, result) - }) - - t.Run("nested map merge", func(t *testing.T) { - base := map[string]any{ - "outer": map[string]any{ - "keep": "yes", - "inner": map[string]any{ - "x": 1, - "y": 2, - }, - }, - } - override := map[string]any{ - "outer": map[string]any{ - "inner": map[string]any{ - "y": 99, - }, - }, - } - result := deepMergeMaps(base, override) - expected := map[string]any{ - "outer": map[string]any{ - "keep": "yes", - "inner": map[string]any{ - "x": 1, - "y": 99, - }, - }, - } - assert.Equal(t, expected, result) - }) - - t.Run("does not mutate inputs", func(t *testing.T) { - base := map[string]any{ - "nested": map[string]any{"a": 1}, - } - override := map[string]any{ - "nested": map[string]any{"b": 2}, - } - _ = deepMergeMaps(base, override) - - assert.Equal(t, map[string]any{"nested": map[string]any{"a": 1}}, base) - assert.Equal(t, map[string]any{"nested": map[string]any{"b": 2}}, override) - }) - - t.Run("realistic minResponsesToAggregate override", func(t *testing.T) { - base := map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "registrationRefresh": "20s", - "registrationExpiry": "60s", - "minResponsesToAggregate": 4, - "messageExpiry": "120s", - }, - }, - "BalanceAt": map[string]any{ - "remoteExecutableConfig": map[string]any{ - "requestTimeout": "30s", - }, - }, - }, - } - override := map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "minResponsesToAggregate": 2, - }, - }, - }, - } - result := deepMergeMaps(base, override) - - logTrigger := result["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) - assert.Equal(t, 2, logTrigger["minResponsesToAggregate"]) - assert.Equal(t, "20s", logTrigger["registrationRefresh"]) - assert.Equal(t, "60s", logTrigger["registrationExpiry"]) - assert.Equal(t, "120s", logTrigger["messageExpiry"]) - - balanceAt := result["methodConfigs"].(map[string]any)["BalanceAt"].(map[string]any)["remoteExecutableConfig"].(map[string]any) - assert.Equal(t, "30s", balanceAt["requestTimeout"]) - }) -} - -func TestResolveCapabilityConfigsForDON(t *testing.T) { - baseConfigs := []contracts.CapabilityConfig{ - { - Capability: contracts.Capability{CapabilityID: "cap-a@1.0.0"}, - Config: map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "minResponsesToAggregate": 4, - "registrationRefresh": "20s", - }, - }, - }, - }, - }, - { - Capability: contracts.Capability{CapabilityID: "cap-b@1.0.0"}, - Config: map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "minResponsesToAggregate": 4, - "registrationRefresh": "20s", - }, - }, - }, - }, - }, - } - - t.Run("no overrides returns base", func(t *testing.T) { - result, err := resolveCapabilityConfigsForDON(baseConfigs, nil) - require.NoError(t, err) - assert.Equal(t, baseConfigs, result) - }) - - t.Run("empty overrides returns base", func(t *testing.T) { - result, err := resolveCapabilityConfigsForDON(baseConfigs, []CapabilityConfigOverride{}) - require.NoError(t, err) - assert.Equal(t, baseConfigs, result) - }) - - t.Run("override specific capability", func(t *testing.T) { - overrides := []CapabilityConfigOverride{ - { - CapabilityID: "cap-a@1.0.0", - Config: map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "minResponsesToAggregate": 2, - }, - }, - }, - }, - }, - } - - result, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) - require.NoError(t, err) - require.Len(t, result, 2) - - capAConfig := result[0].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) - assert.Equal(t, 2, capAConfig["minResponsesToAggregate"]) - assert.Equal(t, "20s", capAConfig["registrationRefresh"]) - - capBConfig := result[1].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) - assert.Equal(t, 4, capBConfig["minResponsesToAggregate"]) - }) - - t.Run("override all capabilities with empty ID", func(t *testing.T) { - overrides := []CapabilityConfigOverride{ - { - CapabilityID: "", - Config: map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "minResponsesToAggregate": 2, - }, - }, - }, - }, - }, - } - - result, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) - require.NoError(t, err) - require.Len(t, result, 2) - - for _, cfg := range result { - logTrigger := cfg.Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any) - assert.Equal(t, 2, logTrigger["minResponsesToAggregate"]) - assert.Equal(t, "20s", logTrigger["registrationRefresh"]) - } - }) - - t.Run("error on non-existent capability ID", func(t *testing.T) { - overrides := []CapabilityConfigOverride{ - { - CapabilityID: "non-existent@1.0.0", - Config: map[string]any{"foo": "bar"}, - }, - } - - _, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) - require.Error(t, err) - assert.Contains(t, err.Error(), "non-existent@1.0.0") - }) - - t.Run("nil config override is skipped", func(t *testing.T) { - overrides := []CapabilityConfigOverride{ - {CapabilityID: "", Config: nil}, - } - - result, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) - require.NoError(t, err) - assert.Equal(t, 4, result[0].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any)["minResponsesToAggregate"]) - }) - - t.Run("does not mutate base configs", func(t *testing.T) { - overrides := []CapabilityConfigOverride{ - { - CapabilityID: "", - Config: map[string]any{ - "methodConfigs": map[string]any{ - "LogTrigger": map[string]any{ - "remoteTriggerConfig": map[string]any{ - "minResponsesToAggregate": 99, - }, - }, - }, - }, - }, - } - - _, err := resolveCapabilityConfigsForDON(baseConfigs, overrides) - require.NoError(t, err) - - originalVal := baseConfigs[0].Config["methodConfigs"].(map[string]any)["LogTrigger"].(map[string]any)["remoteTriggerConfig"].(map[string]any)["minResponsesToAggregate"] - assert.Equal(t, 4, originalVal) - }) -}