Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading