Skip to content

Commit 1cad199

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

6 files changed

Lines changed: 652 additions & 144 deletions

File tree

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

0 commit comments

Comments
 (0)