Skip to content

Commit cb61521

Browse files
committed
feat: add DeleteResourcesSeq and DeleteResourcesChangeset for batched multi-resource datastore deletion
1 parent 72744a0 commit cb61521

4 files changed

Lines changed: 479 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)