Skip to content

Commit 9f6a430

Browse files
authored
Add capability config override to add capability changeset (#21676)
* Added cap config override to add capability changeset * Updated add capability changeset to accept map of cap configs
1 parent 0e5dbb7 commit 9f6a430

3 files changed

Lines changed: 94 additions & 114 deletions

File tree

deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package changeset
33
import (
44
"errors"
55
"fmt"
6-
"slices"
76

87
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
98
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
@@ -22,10 +21,10 @@ type AddCapabilitiesInput struct {
2221
RegistryChainSel uint64 `json:"registryChainSel" yaml:"registryChainSel"`
2322
RegistryQualifier string `json:"registryQualifier" yaml:"registryQualifier"`
2423

25-
MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"`
26-
DonName string `json:"donName" yaml:"donName"` // optional if DonNames is set; for backward compatibility
27-
DonNames []string `json:"donNames" yaml:"donNames"` // multiple DONs to update
28-
CapabilityConfigs []contracts.CapabilityConfig `json:"capabilityConfigs" yaml:"capabilityConfigs"`
24+
MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"`
25+
26+
// DonCapabilityConfigs maps DON name to the list of capability configs for that DON.
27+
DonCapabilityConfigs map[string][]contracts.CapabilityConfig `json:"donCapabilityConfigs" yaml:"donCapabilityConfigs"`
2928

3029
// Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version.
3130
// This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option.
@@ -35,29 +34,16 @@ type AddCapabilitiesInput struct {
3534
type AddCapabilities struct{}
3635

3736
func (u AddCapabilities) VerifyPreconditions(_ cldf.Environment, config AddCapabilitiesInput) error {
38-
if config.DonName != "" && len(config.DonNames) > 0 {
39-
return errors.New("cannot specify both donName and donNames")
40-
}
41-
donNames := u.donNames(config)
42-
if len(donNames) == 0 {
43-
return errors.New("must specify donName or donNames")
44-
}
45-
if slices.Contains(donNames, "") {
46-
return errors.New("donName or donNames cannot contain an empty string")
47-
}
48-
if len(config.CapabilityConfigs) == 0 {
49-
return errors.New("capabilityConfigs is required")
50-
}
51-
return nil
52-
}
53-
54-
// donNames returns the list of DON names to update (from DonNames or single DonName for backward compatibility).
55-
func (u AddCapabilities) donNames(config AddCapabilitiesInput) []string {
56-
if len(config.DonNames) > 0 {
57-
return config.DonNames
37+
if len(config.DonCapabilityConfigs) == 0 {
38+
return errors.New("donCapabilityConfigs must contain at least one DON entry")
5839
}
59-
if config.DonName != "" {
60-
return []string{config.DonName}
40+
for donName, configs := range config.DonCapabilityConfigs {
41+
if donName == "" {
42+
return errors.New("donCapabilityConfigs keys cannot be empty strings")
43+
}
44+
if len(configs) == 0 {
45+
return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName)
46+
}
6147
}
6248
return nil
6349
}
@@ -79,11 +65,10 @@ func (u AddCapabilities) Apply(e cldf.Environment, config AddCapabilitiesInput)
7965
sequences.AddCapabilities,
8066
sequences.AddCapabilitiesDeps{Env: &e, MCMSContracts: mcmsContracts},
8167
sequences.AddCapabilitiesInput{
82-
RegistryRef: registryRef,
83-
DonNames: u.donNames(config),
84-
CapabilityConfigs: config.CapabilityConfigs,
85-
Force: config.Force,
86-
MCMSConfig: config.MCMSConfig,
68+
RegistryRef: registryRef,
69+
DonCapabilityConfigs: config.DonCapabilityConfigs,
70+
Force: config.Force,
71+
MCMSConfig: config.MCMSConfig,
8772
},
8873
)
8974
if err != nil {

deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go

Lines changed: 45 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -74,71 +74,57 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) {
7474
env := test.SetupEnvV2(t, false)
7575
chainSelector := env.RegistrySelector
7676

77-
// Missing donName and donNames
78-
err := cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
79-
RegistryChainSel: chainSelector,
80-
RegistryQualifier: "qual",
81-
DonNames: nil,
82-
CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}},
83-
})
84-
require.Error(t, err)
85-
assert.Contains(t, err.Error(), "must specify donName or donNames")
77+
capCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}
8678

87-
// Both donName and donNames set
88-
err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
89-
RegistryChainSel: chainSelector,
90-
RegistryQualifier: "qual",
91-
DonName: "don-1",
92-
DonNames: []string{"don-2"},
93-
CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}},
79+
// Empty map
80+
err := cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
81+
RegistryChainSel: chainSelector,
82+
RegistryQualifier: "qual",
83+
DonCapabilityConfigs: nil,
9484
})
9585
require.Error(t, err)
96-
assert.Contains(t, err.Error(), "cannot specify both donName and donNames")
86+
assert.Contains(t, err.Error(), "donCapabilityConfigs must contain at least one DON entry")
9787

98-
// donNames with empty string
88+
// Empty DON name key
9989
err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
10090
RegistryChainSel: chainSelector,
10191
RegistryQualifier: "qual",
102-
DonNames: []string{"don-1", ""},
103-
CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}},
92+
DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{
93+
"": capCfg,
94+
},
10495
})
10596
require.Error(t, err)
106-
assert.Contains(t, err.Error(), "cannot contain an empty string")
97+
assert.Contains(t, err.Error(), "cannot be empty strings")
10798

108-
// Missing capability configs
99+
// Empty config list for a DON
109100
err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
110101
RegistryChainSel: chainSelector,
111102
RegistryQualifier: "qual",
112-
DonNames: []string{"don-1"},
113-
CapabilityConfigs: nil,
103+
DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{
104+
"don-1": {},
105+
},
114106
})
115107
require.Error(t, err)
116-
assert.Contains(t, err.Error(), "capabilityConfigs")
108+
assert.Contains(t, err.Error(), "at least one capability config")
117109

118-
// Valid (single DON via donNames)
110+
// Valid (single DON)
119111
err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
120112
RegistryChainSel: chainSelector,
121113
RegistryQualifier: "qual",
122-
DonNames: []string{"don-1"},
123-
CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}},
124-
})
125-
require.NoError(t, err)
126-
127-
// Valid (single DON via donName - backward compatibility)
128-
err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
129-
RegistryChainSel: chainSelector,
130-
RegistryQualifier: "qual",
131-
DonName: "don-1",
132-
CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}},
114+
DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{
115+
"don-1": capCfg,
116+
},
133117
})
134118
require.NoError(t, err)
135119

136120
// Valid (multiple DONs)
137121
err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{
138122
RegistryChainSel: chainSelector,
139123
RegistryQualifier: "qual",
140-
DonNames: []string{"don-1", "don-2"},
141-
CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}},
124+
DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{
125+
"don-1": capCfg,
126+
"don-2": capCfg,
127+
},
142128
})
143129
require.NoError(t, err)
144130
}
@@ -147,15 +133,16 @@ func addNewCapability(t *testing.T, fixture *test.EnvWrapperV2, capID string) {
147133
input := changeset.AddCapabilitiesInput{
148134
RegistryChainSel: fixture.RegistrySelector,
149135
RegistryQualifier: test.RegistryQualifier,
150-
DonNames: []string{test.DONName},
151-
CapabilityConfigs: []contracts.CapabilityConfig{{
152-
Capability: contracts.Capability{
153-
CapabilityID: capID,
154-
ConfigurationContract: common.Address{},
155-
Metadata: newCapMetadata,
156-
},
157-
Config: newCapConfig,
158-
}},
136+
DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{
137+
test.DONName: {{
138+
Capability: contracts.Capability{
139+
CapabilityID: capID,
140+
ConfigurationContract: common.Address{},
141+
Metadata: newCapMetadata,
142+
},
143+
Config: newCapConfig,
144+
}},
145+
},
159146
Force: true,
160147
}
161148

@@ -246,15 +233,16 @@ func TestAddCapabilities_Apply_MCMS(t *testing.T) {
246233
input := changeset.AddCapabilitiesInput{
247234
RegistryChainSel: fixture.RegistrySelector,
248235
RegistryQualifier: test.RegistryQualifier,
249-
DonNames: []string{test.DONName},
250-
CapabilityConfigs: []contracts.CapabilityConfig{{
251-
Capability: contracts.Capability{
252-
CapabilityID: newCapID,
253-
ConfigurationContract: common.Address{},
254-
Metadata: newCapMetadata,
255-
},
256-
Config: newCapConfig,
257-
}},
236+
DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{
237+
test.DONName: {{
238+
Capability: contracts.Capability{
239+
CapabilityID: newCapID,
240+
ConfigurationContract: common.Address{},
241+
Metadata: newCapMetadata,
242+
},
243+
Config: newCapConfig,
244+
}},
245+
},
258246
Force: true,
259247
MCMSConfig: &crecontracts.MCMSConfig{
260248
MinDelay: 1 * time.Second,

deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"maps"
78
"slices"
89

910
"github.com/Masterminds/semver/v3"
@@ -30,10 +31,8 @@ type AddCapabilitiesDeps struct {
3031
}
3132

3233
type AddCapabilitiesInput struct {
33-
CapabilityConfigs []contracts.CapabilityConfig // if Config subfield is nil, a default config is used
34-
35-
// DonNames are the DONs to update. At least one is required.
36-
DonNames []string
34+
// DonCapabilityConfigs maps DON name to the list of capability configs for that DON.
35+
DonCapabilityConfigs map[string][]contracts.CapabilityConfig
3736

3837
// Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version.
3938
// This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option.
@@ -44,14 +43,16 @@ type AddCapabilitiesInput struct {
4443
}
4544

4645
func (i *AddCapabilitiesInput) Validate() error {
47-
if len(i.DonNames) == 0 {
48-
return errors.New("must specify at least one DON name")
49-
}
50-
if slices.Contains(i.DonNames, "") {
51-
return errors.New("donNames cannot contain an empty string")
46+
if len(i.DonCapabilityConfigs) == 0 {
47+
return errors.New("donCapabilityConfigs must contain at least one DON entry")
5248
}
53-
if len(i.CapabilityConfigs) == 0 {
54-
return errors.New("capabilityConfigs is required")
49+
for donName, configs := range i.DonCapabilityConfigs {
50+
if donName == "" {
51+
return errors.New("donCapabilityConfigs keys cannot be empty strings")
52+
}
53+
if len(configs) == 0 {
54+
return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName)
55+
}
5556
}
5657
return nil
5758
}
@@ -90,8 +91,8 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti
9091
return AddCapabilitiesOutput{}, fmt.Errorf("failed to create CapabilitiesRegistry: %w", err)
9192
}
9293

93-
// Build capabilities list once (registry-level; same for all DONs).
94-
capabilities, err := buildCapabilitiesFromConfigs(input.CapabilityConfigs)
94+
// Build capabilities list once (registry-level; union across all DONs).
95+
capabilities, err := buildCapabilitiesFromAllDONConfigs(input.DonCapabilityConfigs)
9596
if err != nil {
9697
return AddCapabilitiesOutput{}, err
9798
}
@@ -137,7 +138,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti
137138
var allUpdatedNodes []capabilities_registry_v2.CapabilitiesRegistryNodeParams
138139

139140
// Update each DON: get nodes, update node configs, update DON.
140-
for _, donName := range input.DonNames {
141+
for donName, donCapConfigs := range input.DonCapabilityConfigs {
141142
don, nodes, err := GetDonNodes(donName, capReg)
142143
if err != nil {
143144
return AddCapabilitiesOutput{}, fmt.Errorf("failed to get DON %s nodes: %w", donName, err)
@@ -148,7 +149,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti
148149
p2pIDs = append(p2pIDs, node.P2pId)
149150
}
150151

151-
nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, input.CapabilityConfigs)
152+
nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, donCapConfigs)
152153
if err != nil {
153154
return AddCapabilitiesOutput{}, fmt.Errorf("failed to build node updates for DON %s: %w", donName, err)
154155
}
@@ -182,7 +183,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti
182183
contracts.UpdateDONInput{
183184
ChainSelector: chainSel,
184185
P2PIDs: p2pIDs,
185-
CapabilityConfigs: input.CapabilityConfigs,
186+
CapabilityConfigs: donCapConfigs,
186187
MergeCapabilityConfigsWithOnChain: true,
187188
DonName: donName,
188189
F: don.F,
@@ -233,17 +234,23 @@ func toOpsSlice(opPtrs ...*types.BatchOperation) []types.BatchOperation {
233234
return result
234235
}
235236

236-
// buildCapabilitiesFromConfigs builds the capability list for RegisterCapabilities (registry-level, no DON).
237-
func buildCapabilitiesFromConfigs(configs []contracts.CapabilityConfig) ([]contracts.RegisterableCapability, error) {
238-
out := make([]contracts.RegisterableCapability, len(configs))
239-
for i, cfg := range configs {
240-
out[i] = contracts.RegisterableCapability{
241-
Metadata: cfg.Capability.Metadata,
242-
CapabilityID: cfg.Capability.CapabilityID,
243-
ConfigurationContract: cfg.Capability.ConfigurationContract,
237+
// buildCapabilitiesFromAllDONConfigs collects the unique capabilities across all DONs' configs
238+
// for registry-level registration.
239+
func buildCapabilitiesFromAllDONConfigs(donConfigs map[string][]contracts.CapabilityConfig) ([]contracts.RegisterableCapability, error) {
240+
uniqueCaps := make(map[string]contracts.RegisterableCapability)
241+
for _, configs := range donConfigs {
242+
for _, cfg := range configs {
243+
if _, ok := uniqueCaps[cfg.Capability.CapabilityID]; ok {
244+
continue
245+
}
246+
uniqueCaps[cfg.Capability.CapabilityID] = contracts.RegisterableCapability{
247+
Metadata: cfg.Capability.Metadata,
248+
CapabilityID: cfg.Capability.CapabilityID,
249+
ConfigurationContract: cfg.Capability.ConfigurationContract,
250+
}
244251
}
245252
}
246-
return out, nil
253+
return slices.Collect(maps.Values(uniqueCaps)), nil
247254
}
248255

249256
// buildNodeUpdatesForDON builds node config updates for a DON's nodes (adds the new capabilities to each node).

0 commit comments

Comments
 (0)