Skip to content

Commit f33c8f4

Browse files
authored
feat: add DeleteResourcesSeq and DeleteResourcesChangeset (#78)
Adds a new changeset and sequence that stage deletions across address refs, contract metadata, and chain metadata in a single invocation.
1 parent 2960ced commit f33c8f4

6 files changed

Lines changed: 619 additions & 60 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package changesets
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
8+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
9+
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
10+
11+
"github.com/smartcontractkit/cld-changesets/datastore/internal/keys"
12+
datastoreseqs "github.com/smartcontractkit/cld-changesets/datastore/sequences"
13+
)
14+
15+
// DeleteResourcesChangeset stages deletions of address refs, contract metadata, and chain metadata
16+
// in a single invocation. Staged deletions are not applied immediately; they are recorded in the
17+
// Datastore and executed during the post-changeset merge phase.
18+
type DeleteResourcesChangeset struct{}
19+
20+
type DeleteResourcesChangesetInput struct {
21+
AddressRefKeys []keys.AddressRefKey `json:"addressRefKeys"`
22+
ContractMetadataKeys []keys.ContractMetadataKey `json:"contractMetadataKeys"`
23+
ChainMetadataKeys []keys.ChainMetadataKey `json:"chainMetadataKeys"`
24+
}
25+
26+
// VerifyPreconditions ensures the input is valid.
27+
func (DeleteResourcesChangeset) VerifyPreconditions(e cldf.Environment, input DeleteResourcesChangesetInput) error {
28+
if e.DataStore == nil {
29+
return errors.New("missing datastore in environment")
30+
}
31+
32+
if len(input.AddressRefKeys) == 0 && len(input.ContractMetadataKeys) == 0 && len(input.ChainMetadataKeys) == 0 {
33+
return errors.New("at least one resource key slice must be non-empty")
34+
}
35+
36+
for i, key := range input.AddressRefKeys {
37+
fwKey, err := key.ToFrameworkKey()
38+
if err != nil {
39+
return fmt.Errorf("addressRefKeys[%d]: %w", i, err)
40+
}
41+
42+
_, err = e.DataStore.Addresses().Get(fwKey)
43+
if err != nil {
44+
if errors.Is(err, cldfdatastore.ErrAddressRefNotFound) {
45+
return fmt.Errorf("address ref entry for chain selector %v, type %v, version %v and qualifier %q does not exist",
46+
fwKey.ChainSelector(), fwKey.Type(), fwKey.Version(), fwKey.Qualifier())
47+
}
48+
49+
return fmt.Errorf("failed to retrieve address ref entry for chain selector %v, type %v, version %v and qualifier %q: %w",
50+
fwKey.ChainSelector(), fwKey.Type(), fwKey.Version(), fwKey.Qualifier(), err)
51+
}
52+
}
53+
54+
for _, key := range input.ContractMetadataKeys {
55+
fwKey := key.ToFrameworkKey()
56+
_, err := e.DataStore.ContractMetadata().Get(fwKey)
57+
if err != nil {
58+
if errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) {
59+
return fmt.Errorf("contract metadata entry for chain selector %v and address %v does not exist",
60+
fwKey.ChainSelector(), fwKey.Address())
61+
}
62+
63+
return fmt.Errorf("failed to retrieve contract metadata entry for chain selector %v and address %v: %w",
64+
fwKey.ChainSelector(), fwKey.Address(), err)
65+
}
66+
}
67+
68+
for _, key := range input.ChainMetadataKeys {
69+
fwKey := key.ToFrameworkKey()
70+
_, err := e.DataStore.ChainMetadata().Get(fwKey)
71+
if err != nil {
72+
if errors.Is(err, cldfdatastore.ErrChainMetadataNotFound) {
73+
return fmt.Errorf("chain metadata entry for chain selector %v does not exist", fwKey.ChainSelector())
74+
}
75+
76+
return fmt.Errorf("failed to retrieve chain metadata entry for chain selector %v: %w", fwKey.ChainSelector(), err)
77+
}
78+
}
79+
80+
return nil
81+
}
82+
83+
// Apply executes the changeset, staging the resources to be deleted from the Datastore.
84+
func (DeleteResourcesChangeset) Apply(e cldf.Environment, input DeleteResourcesChangesetInput) (cldf.ChangesetOutput, error) {
85+
deps := datastoreseqs.DeleteResourcesSeqDeps{DataStore: e.DataStore}
86+
seqInput := datastoreseqs.DeleteResourcesSeqInput{
87+
AddressRefKeys: input.AddressRefKeys,
88+
ContractMetadataKeys: input.ContractMetadataKeys,
89+
ChainMetadataKeys: input.ChainMetadataKeys,
90+
}
91+
92+
report, err := cldfops.ExecuteSequence(
93+
e.OperationsBundle,
94+
datastoreseqs.DeleteResourcesSeq,
95+
deps,
96+
seqInput,
97+
)
98+
out := cldf.ChangesetOutput{
99+
DataStore: report.Output.DataStore,
100+
Reports: report.ExecutionReports,
101+
}
102+
if err != nil {
103+
return out, err
104+
}
105+
106+
return out, nil
107+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package changesets
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/Masterminds/semver/v3"
10+
11+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
12+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
cldfoperations "github.com/smartcontractkit/chainlink-deployments-framework/operations"
14+
cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
15+
16+
"github.com/smartcontractkit/cld-changesets/datastore/internal/keys"
17+
)
18+
19+
func TestDeleteResourcesChangeset_VerifyPreconditions(t *testing.T) {
20+
t.Parallel()
21+
22+
version := semver.MustParse("1.0.0")
23+
addressRef := cldfdatastore.AddressRef{Address: "0x01", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"}
24+
contractMetadata := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value"}
25+
chainMetadata := cldfdatastore.ChainMetadata{ChainSelector: 5678, Metadata: "chain-value"}
26+
27+
fullDS := func() cldfdatastore.DataStore {
28+
ds := cldfdatastore.NewMemoryDataStore()
29+
require.NoError(t, ds.Addresses().Add(addressRef))
30+
require.NoError(t, ds.ContractMetadata().Add(contractMetadata))
31+
require.NoError(t, ds.ChainMetadata().Add(chainMetadata))
32+
33+
return ds.Seal()
34+
}()
35+
36+
tests := []struct {
37+
name string
38+
env cldf.Environment
39+
input DeleteResourcesChangesetInput
40+
wantErr string
41+
}{
42+
{
43+
name: "success: all three resource types provided",
44+
env: cldf.Environment{DataStore: fullDS},
45+
input: DeleteResourcesChangesetInput{
46+
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
47+
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
48+
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
49+
},
50+
},
51+
{
52+
name: "success: only address ref keys",
53+
env: cldf.Environment{DataStore: fullDS},
54+
input: DeleteResourcesChangesetInput{
55+
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
56+
},
57+
},
58+
{
59+
name: "success: only contract metadata keys",
60+
env: cldf.Environment{DataStore: fullDS},
61+
input: DeleteResourcesChangesetInput{
62+
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
63+
},
64+
},
65+
{
66+
name: "success: only chain metadata keys",
67+
env: cldf.Environment{DataStore: fullDS},
68+
input: DeleteResourcesChangesetInput{
69+
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
70+
},
71+
},
72+
{
73+
name: "failure: missing datastore",
74+
env: cldf.Environment{},
75+
input: DeleteResourcesChangesetInput{AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)}},
76+
wantErr: "missing datastore in environment",
77+
},
78+
{
79+
name: "failure: all key slices empty",
80+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
81+
input: DeleteResourcesChangesetInput{},
82+
wantErr: "at least one resource key slice must be non-empty",
83+
},
84+
{
85+
name: "failure: address ref entry does not exist",
86+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
87+
input: DeleteResourcesChangesetInput{
88+
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
89+
},
90+
wantErr: fmt.Sprintf("address ref entry for chain selector %v, type %v, version %v and qualifier %q does not exist",
91+
addressRef.ChainSelector, addressRef.Type, addressRef.Version, addressRef.Qualifier),
92+
},
93+
{
94+
name: "failure: contract metadata entry does not exist",
95+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
96+
input: DeleteResourcesChangesetInput{
97+
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
98+
},
99+
wantErr: fmt.Sprintf("contract metadata entry for chain selector %v and address %v does not exist",
100+
contractMetadata.ChainSelector, contractMetadata.Address),
101+
},
102+
{
103+
name: "failure: chain metadata entry does not exist",
104+
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
105+
input: DeleteResourcesChangesetInput{
106+
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
107+
},
108+
wantErr: fmt.Sprintf("chain metadata entry for chain selector %v does not exist", chainMetadata.ChainSelector),
109+
},
110+
}
111+
112+
for _, tt := range tests {
113+
t.Run(tt.name, func(t *testing.T) {
114+
t.Parallel()
115+
116+
err := DeleteResourcesChangeset{}.VerifyPreconditions(tt.env, tt.input)
117+
if tt.wantErr == "" {
118+
require.NoError(t, err)
119+
} else {
120+
require.ErrorContains(t, err, tt.wantErr)
121+
}
122+
})
123+
}
124+
}
125+
126+
func TestDeleteResourcesChangeset_Apply(t *testing.T) {
127+
t.Parallel()
128+
129+
version := semver.MustParse("1.0.0")
130+
addressRef := cldfdatastore.AddressRef{Address: "0x01", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"}
131+
contractMetadata := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value"}
132+
chainMetadata := cldfdatastore.ChainMetadata{ChainSelector: 5678, Metadata: "chain-value"}
133+
134+
fullDS := func() cldfdatastore.DataStore {
135+
ds := cldfdatastore.NewMemoryDataStore()
136+
require.NoError(t, ds.Addresses().Add(addressRef))
137+
require.NoError(t, ds.ContractMetadata().Add(contractMetadata))
138+
require.NoError(t, ds.ChainMetadata().Add(chainMetadata))
139+
140+
return ds.Seal()
141+
}()
142+
143+
bundle := cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter())
144+
145+
tests := []struct {
146+
name string
147+
input DeleteResourcesChangesetInput
148+
wantReportCount int
149+
wantAddressRefDeleted []string
150+
wantContractMetaDeleted []string
151+
wantChainMetaDeleted []string
152+
}{
153+
{
154+
name: "success: deletes only address refs",
155+
input: DeleteResourcesChangesetInput{
156+
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
157+
},
158+
wantReportCount: 4,
159+
wantAddressRefDeleted: []string{addressRef.Key().String()},
160+
wantContractMetaDeleted: []string{},
161+
wantChainMetaDeleted: []string{},
162+
},
163+
{
164+
name: "success: deletes mixed resources",
165+
input: DeleteResourcesChangesetInput{
166+
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
167+
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
168+
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
169+
},
170+
wantReportCount: 4,
171+
wantAddressRefDeleted: []string{addressRef.Key().String()},
172+
wantContractMetaDeleted: []string{contractMetadata.Key().String()},
173+
wantChainMetaDeleted: []string{chainMetadata.Key().String()},
174+
},
175+
}
176+
177+
for _, tt := range tests {
178+
t.Run(tt.name, func(t *testing.T) {
179+
t.Parallel()
180+
181+
env := cldf.Environment{
182+
DataStore: fullDS,
183+
OperationsBundle: bundle,
184+
}
185+
186+
got, err := DeleteResourcesChangeset{}.Apply(env, tt.input)
187+
require.NoError(t, err)
188+
require.Len(t, got.Reports, tt.wantReportCount)
189+
190+
memDS := got.DataStore.(*cldfdatastore.MemoryDataStore)
191+
require.ElementsMatch(t, tt.wantAddressRefDeleted, memDS.AddressRefStore.DeletedRemoteKeys)
192+
require.ElementsMatch(t, tt.wantContractMetaDeleted, memDS.ContractMetadataStore.DeletedRemoteKeys)
193+
require.ElementsMatch(t, tt.wantChainMetaDeleted, memDS.ChainMetadataStore.DeletedRemoteKeys)
194+
})
195+
}
196+
}
197+
198+
func testAddressRefKey(addressRef cldfdatastore.AddressRef) keys.AddressRefKey {
199+
return keys.AddressRefKey{
200+
ChainSelector: addressRef.ChainSelector,
201+
Type: addressRef.Type,
202+
Version: addressRef.Version,
203+
Qualifier: addressRef.Qualifier,
204+
}
205+
}
206+
207+
func testContractMetadataKey(contractMetadata cldfdatastore.ContractMetadata) keys.ContractMetadataKey {
208+
return keys.ContractMetadataKey{
209+
ChainSelector: contractMetadata.ChainSelector,
210+
Address: contractMetadata.Address,
211+
}
212+
}
213+
214+
func testChainMetadataKey(chainMetadata cldfdatastore.ChainMetadata) keys.ChainMetadataKey {
215+
return keys.ChainMetadataKey{ChainSelector: chainMetadata.ChainSelector}
216+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package sequences
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Masterminds/semver/v3"
7+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
8+
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
9+
10+
"github.com/smartcontractkit/cld-changesets/datastore/internal/keys"
11+
datastoreops "github.com/smartcontractkit/cld-changesets/datastore/operations"
12+
)
13+
14+
// DeleteResourcesSeqDeps holds non-serializable dependencies for the DeleteResourcesSeq sequence.
15+
type DeleteResourcesSeqDeps struct {
16+
DataStore cldfdatastore.DataStore
17+
}
18+
19+
// DeleteResourcesSeqInput is the serializable input of a DeleteResourcesSeq invocation.
20+
type DeleteResourcesSeqInput struct {
21+
AddressRefKeys []keys.AddressRefKey `json:"addressRefKeys"`
22+
ContractMetadataKeys []keys.ContractMetadataKey `json:"contractMetadataKeys"`
23+
ChainMetadataKeys []keys.ChainMetadataKey `json:"chainMetadataKeys"`
24+
}
25+
26+
// DeleteResourcesSeqOutput is the serializable output of a DeleteResourcesSeq invocation.
27+
type DeleteResourcesSeqOutput struct {
28+
DataStore cldfdatastore.MutableDataStore
29+
}
30+
31+
// DeleteResourcesSeq stages deletions across address refs, contract metadata, and chain metadata
32+
// by composing the corresponding per-resource delete operations. Empty slices skip the corresponding
33+
// resource type within each operation. Staged deletions are not applied immediately; they are recorded in the
34+
// Datastore and executed during the post-changeset merge phase.
35+
var DeleteResourcesSeq = cldfops.NewSequence(
36+
"datastore-delete-resources",
37+
semver.MustParse("1.0.0"),
38+
"Stage deletions of address refs, contract metadata, and chain metadata from the Datastore",
39+
func(b cldfops.Bundle, deps DeleteResourcesSeqDeps, input DeleteResourcesSeqInput) (DeleteResourcesSeqOutput, error) {
40+
addressRefReport, err := cldfops.ExecuteOperation(
41+
b,
42+
datastoreops.DeleteAddressRefOp,
43+
datastoreops.DeleteAddressRefDeps{DataStore: deps.DataStore},
44+
datastoreops.DeleteAddressRefInput{AddressRefKeys: input.AddressRefKeys},
45+
)
46+
if err != nil {
47+
return DeleteResourcesSeqOutput{}, fmt.Errorf("failed to delete address refs: %w", err)
48+
}
49+
50+
contractMetadataReport, err := cldfops.ExecuteOperation(
51+
b,
52+
datastoreops.DeleteContractMetadataOp,
53+
datastoreops.DeleteContractMetadataDeps{DataStore: addressRefReport.Output.DataStore.Seal()},
54+
datastoreops.DeleteContractMetadataInput{ContractMetadataKeys: input.ContractMetadataKeys},
55+
)
56+
if err != nil {
57+
return DeleteResourcesSeqOutput{}, fmt.Errorf("failed to delete contract metadata: %w", err)
58+
}
59+
60+
chainMetadataReport, err := cldfops.ExecuteOperation(
61+
b,
62+
datastoreops.DeleteChainMetadataOp,
63+
datastoreops.DeleteChainMetadataDeps{DataStore: contractMetadataReport.Output.DataStore.Seal()},
64+
datastoreops.DeleteChainMetadataInput{ChainMetadataKeys: input.ChainMetadataKeys},
65+
)
66+
if err != nil {
67+
return DeleteResourcesSeqOutput{}, fmt.Errorf("failed to delete chain metadata: %w", err)
68+
}
69+
70+
b.Logger.Infow("Datastore resources successfully staged for deletion")
71+
72+
return DeleteResourcesSeqOutput{DataStore: chainMetadataReport.Output.DataStore}, nil
73+
},
74+
)

0 commit comments

Comments
 (0)