Skip to content
Merged
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
107 changes: 107 additions & 0 deletions datastore/changesets/delete_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package changesets

import (
"errors"
"fmt"

cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"

"github.com/smartcontractkit/cld-changesets/datastore/internal/keys"
datastoreseqs "github.com/smartcontractkit/cld-changesets/datastore/sequences"
)

// DeleteResourcesChangeset stages deletions of address refs, contract metadata, and chain metadata
Comment thread
giogam marked this conversation as resolved.
// in a single invocation. Staged deletions are not applied immediately; they are recorded in the
// Datastore and executed during the post-changeset merge phase.
type DeleteResourcesChangeset struct{}
Comment thread
graham-chainlink marked this conversation as resolved.

type DeleteResourcesChangesetInput struct {
AddressRefKeys []keys.AddressRefKey `json:"addressRefKeys"`
ContractMetadataKeys []keys.ContractMetadataKey `json:"contractMetadataKeys"`
ChainMetadataKeys []keys.ChainMetadataKey `json:"chainMetadataKeys"`
}

// VerifyPreconditions ensures the input is valid.
func (DeleteResourcesChangeset) VerifyPreconditions(e cldf.Environment, input DeleteResourcesChangesetInput) error {
if e.DataStore == nil {
return errors.New("missing datastore in environment")
}

if len(input.AddressRefKeys) == 0 && len(input.ContractMetadataKeys) == 0 && len(input.ChainMetadataKeys) == 0 {
return errors.New("at least one resource key slice must be non-empty")
}

for i, key := range input.AddressRefKeys {
fwKey, err := key.ToFrameworkKey()
if err != nil {
return fmt.Errorf("addressRefKeys[%d]: %w", i, err)
}

_, err = e.DataStore.Addresses().Get(fwKey)
if err != nil {
if errors.Is(err, cldfdatastore.ErrAddressRefNotFound) {
return fmt.Errorf("address ref entry for chain selector %v, type %v, version %v and qualifier %q does not exist",
fwKey.ChainSelector(), fwKey.Type(), fwKey.Version(), fwKey.Qualifier())
}

return fmt.Errorf("failed to retrieve address ref entry for chain selector %v, type %v, version %v and qualifier %q: %w",
fwKey.ChainSelector(), fwKey.Type(), fwKey.Version(), fwKey.Qualifier(), err)
}
}

for _, key := range input.ContractMetadataKeys {
fwKey := key.ToFrameworkKey()
_, err := e.DataStore.ContractMetadata().Get(fwKey)
if err != nil {
if errors.Is(err, cldfdatastore.ErrContractMetadataNotFound) {
return fmt.Errorf("contract metadata entry for chain selector %v and address %v does not exist",
fwKey.ChainSelector(), fwKey.Address())
}

return fmt.Errorf("failed to retrieve contract metadata entry for chain selector %v and address %v: %w",
fwKey.ChainSelector(), fwKey.Address(), err)
}
}

for _, key := range input.ChainMetadataKeys {
fwKey := key.ToFrameworkKey()
_, err := e.DataStore.ChainMetadata().Get(fwKey)
if err != nil {
if errors.Is(err, cldfdatastore.ErrChainMetadataNotFound) {
return fmt.Errorf("chain metadata entry for chain selector %v does not exist", fwKey.ChainSelector())
}

return fmt.Errorf("failed to retrieve chain metadata entry for chain selector %v: %w", fwKey.ChainSelector(), err)
}
}

return nil
}

// Apply executes the changeset, staging the resources to be deleted from the Datastore.
func (DeleteResourcesChangeset) Apply(e cldf.Environment, input DeleteResourcesChangesetInput) (cldf.ChangesetOutput, error) {
deps := datastoreseqs.DeleteResourcesSeqDeps{DataStore: e.DataStore}
seqInput := datastoreseqs.DeleteResourcesSeqInput{
AddressRefKeys: input.AddressRefKeys,
ContractMetadataKeys: input.ContractMetadataKeys,
ChainMetadataKeys: input.ChainMetadataKeys,
}

report, err := cldfops.ExecuteSequence(
e.OperationsBundle,
datastoreseqs.DeleteResourcesSeq,
deps,
seqInput,
)
out := cldf.ChangesetOutput{
DataStore: report.Output.DataStore,
Reports: report.ExecutionReports,
}
if err != nil {
return out, err
}

return out, nil
}
216 changes: 216 additions & 0 deletions datastore/changesets/delete_resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package changesets

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/Masterminds/semver/v3"

cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfoperations "github.com/smartcontractkit/chainlink-deployments-framework/operations"
cldflogger "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"

"github.com/smartcontractkit/cld-changesets/datastore/internal/keys"
)

func TestDeleteResourcesChangeset_VerifyPreconditions(t *testing.T) {
t.Parallel()

version := semver.MustParse("1.0.0")
addressRef := cldfdatastore.AddressRef{Address: "0x01", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"}
contractMetadata := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value"}
chainMetadata := cldfdatastore.ChainMetadata{ChainSelector: 5678, Metadata: "chain-value"}

fullDS := func() cldfdatastore.DataStore {
ds := cldfdatastore.NewMemoryDataStore()
require.NoError(t, ds.Addresses().Add(addressRef))
require.NoError(t, ds.ContractMetadata().Add(contractMetadata))
require.NoError(t, ds.ChainMetadata().Add(chainMetadata))

return ds.Seal()
}()

tests := []struct {
name string
env cldf.Environment
input DeleteResourcesChangesetInput
wantErr string
}{
{
name: "success: all three resource types provided",
env: cldf.Environment{DataStore: fullDS},
input: DeleteResourcesChangesetInput{
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
},
},
{
name: "success: only address ref keys",
env: cldf.Environment{DataStore: fullDS},
input: DeleteResourcesChangesetInput{
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
},
},
{
name: "success: only contract metadata keys",
env: cldf.Environment{DataStore: fullDS},
input: DeleteResourcesChangesetInput{
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
},
},
{
name: "success: only chain metadata keys",
env: cldf.Environment{DataStore: fullDS},
input: DeleteResourcesChangesetInput{
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
},
},
{
name: "failure: missing datastore",
env: cldf.Environment{},
input: DeleteResourcesChangesetInput{AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)}},
wantErr: "missing datastore in environment",
},
{
name: "failure: all key slices empty",
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
input: DeleteResourcesChangesetInput{},
wantErr: "at least one resource key slice must be non-empty",
},
{
name: "failure: address ref entry does not exist",
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
input: DeleteResourcesChangesetInput{
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
},
wantErr: fmt.Sprintf("address ref entry for chain selector %v, type %v, version %v and qualifier %q does not exist",
addressRef.ChainSelector, addressRef.Type, addressRef.Version, addressRef.Qualifier),
},
{
name: "failure: contract metadata entry does not exist",
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
input: DeleteResourcesChangesetInput{
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
},
wantErr: fmt.Sprintf("contract metadata entry for chain selector %v and address %v does not exist",
contractMetadata.ChainSelector, contractMetadata.Address),
},
{
name: "failure: chain metadata entry does not exist",
env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()},
input: DeleteResourcesChangesetInput{
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
},
wantErr: fmt.Sprintf("chain metadata entry for chain selector %v does not exist", chainMetadata.ChainSelector),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

err := DeleteResourcesChangeset{}.VerifyPreconditions(tt.env, tt.input)
if tt.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tt.wantErr)
}
})
}
}

func TestDeleteResourcesChangeset_Apply(t *testing.T) {
t.Parallel()

version := semver.MustParse("1.0.0")
addressRef := cldfdatastore.AddressRef{Address: "0x01", ChainSelector: 1234, Type: "MyContract", Version: version, Qualifier: "q1"}
contractMetadata := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value"}
chainMetadata := cldfdatastore.ChainMetadata{ChainSelector: 5678, Metadata: "chain-value"}

fullDS := func() cldfdatastore.DataStore {
ds := cldfdatastore.NewMemoryDataStore()
require.NoError(t, ds.Addresses().Add(addressRef))
require.NoError(t, ds.ContractMetadata().Add(contractMetadata))
require.NoError(t, ds.ChainMetadata().Add(chainMetadata))

return ds.Seal()
}()

bundle := cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter())

tests := []struct {
name string
input DeleteResourcesChangesetInput
wantReportCount int
wantAddressRefDeleted []string
wantContractMetaDeleted []string
wantChainMetaDeleted []string
}{
{
name: "success: deletes only address refs",
input: DeleteResourcesChangesetInput{
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
},
wantReportCount: 4,
wantAddressRefDeleted: []string{addressRef.Key().String()},
wantContractMetaDeleted: []string{},
wantChainMetaDeleted: []string{},
},
{
name: "success: deletes mixed resources",
input: DeleteResourcesChangesetInput{
AddressRefKeys: []keys.AddressRefKey{testAddressRefKey(addressRef)},
ContractMetadataKeys: []keys.ContractMetadataKey{testContractMetadataKey(contractMetadata)},
ChainMetadataKeys: []keys.ChainMetadataKey{testChainMetadataKey(chainMetadata)},
},
wantReportCount: 4,
wantAddressRefDeleted: []string{addressRef.Key().String()},
wantContractMetaDeleted: []string{contractMetadata.Key().String()},
wantChainMetaDeleted: []string{chainMetadata.Key().String()},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

env := cldf.Environment{
DataStore: fullDS,
OperationsBundle: bundle,
}

got, err := DeleteResourcesChangeset{}.Apply(env, tt.input)
require.NoError(t, err)
require.Len(t, got.Reports, tt.wantReportCount)

memDS := got.DataStore.(*cldfdatastore.MemoryDataStore)
require.ElementsMatch(t, tt.wantAddressRefDeleted, memDS.AddressRefStore.DeletedRemoteKeys)
require.ElementsMatch(t, tt.wantContractMetaDeleted, memDS.ContractMetadataStore.DeletedRemoteKeys)
require.ElementsMatch(t, tt.wantChainMetaDeleted, memDS.ChainMetadataStore.DeletedRemoteKeys)
})
}
}

func testAddressRefKey(addressRef cldfdatastore.AddressRef) keys.AddressRefKey {
return keys.AddressRefKey{
ChainSelector: addressRef.ChainSelector,
Type: addressRef.Type,
Version: addressRef.Version,
Qualifier: addressRef.Qualifier,
}
}

func testContractMetadataKey(contractMetadata cldfdatastore.ContractMetadata) keys.ContractMetadataKey {
return keys.ContractMetadataKey{
ChainSelector: contractMetadata.ChainSelector,
Address: contractMetadata.Address,
}
}

func testChainMetadataKey(chainMetadata cldfdatastore.ChainMetadata) keys.ChainMetadataKey {
return keys.ChainMetadataKey{ChainSelector: chainMetadata.ChainSelector}
}
74 changes: 74 additions & 0 deletions datastore/sequences/delete_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package sequences

import (
"fmt"

"github.com/Masterminds/semver/v3"
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations"

"github.com/smartcontractkit/cld-changesets/datastore/internal/keys"
datastoreops "github.com/smartcontractkit/cld-changesets/datastore/operations"
)

// DeleteResourcesSeqDeps holds non-serializable dependencies for the DeleteResourcesSeq sequence.
type DeleteResourcesSeqDeps struct {
DataStore cldfdatastore.DataStore
}

// DeleteResourcesSeqInput is the serializable input of a DeleteResourcesSeq invocation.
type DeleteResourcesSeqInput struct {
AddressRefKeys []keys.AddressRefKey `json:"addressRefKeys"`
ContractMetadataKeys []keys.ContractMetadataKey `json:"contractMetadataKeys"`
ChainMetadataKeys []keys.ChainMetadataKey `json:"chainMetadataKeys"`
}

// DeleteResourcesSeqOutput is the serializable output of a DeleteResourcesSeq invocation.
type DeleteResourcesSeqOutput struct {
DataStore cldfdatastore.MutableDataStore
}

// DeleteResourcesSeq stages deletions across address refs, contract metadata, and chain metadata
// by composing the corresponding per-resource delete operations. Empty slices skip the corresponding
// resource type within each operation. Staged deletions are not applied immediately; they are recorded in the
// Datastore and executed during the post-changeset merge phase.
var DeleteResourcesSeq = cldfops.NewSequence(
"datastore-delete-resources",
semver.MustParse("1.0.0"),
"Stage deletions of address refs, contract metadata, and chain metadata from the Datastore",
func(b cldfops.Bundle, deps DeleteResourcesSeqDeps, input DeleteResourcesSeqInput) (DeleteResourcesSeqOutput, error) {
addressRefReport, err := cldfops.ExecuteOperation(
b,
datastoreops.DeleteAddressRefOp,
datastoreops.DeleteAddressRefDeps{DataStore: deps.DataStore},
datastoreops.DeleteAddressRefInput{AddressRefKeys: input.AddressRefKeys},
)
if err != nil {
return DeleteResourcesSeqOutput{}, fmt.Errorf("failed to delete address refs: %w", err)
}

contractMetadataReport, err := cldfops.ExecuteOperation(
b,
datastoreops.DeleteContractMetadataOp,
datastoreops.DeleteContractMetadataDeps{DataStore: addressRefReport.Output.DataStore.Seal()},
datastoreops.DeleteContractMetadataInput{ContractMetadataKeys: input.ContractMetadataKeys},
)
if err != nil {
return DeleteResourcesSeqOutput{}, fmt.Errorf("failed to delete contract metadata: %w", err)
}

chainMetadataReport, err := cldfops.ExecuteOperation(
b,
datastoreops.DeleteChainMetadataOp,
datastoreops.DeleteChainMetadataDeps{DataStore: contractMetadataReport.Output.DataStore.Seal()},
datastoreops.DeleteChainMetadataInput{ChainMetadataKeys: input.ChainMetadataKeys},
)
if err != nil {
return DeleteResourcesSeqOutput{}, fmt.Errorf("failed to delete chain metadata: %w", err)
}

b.Logger.Infow("Datastore resources successfully staged for deletion")

return DeleteResourcesSeqOutput{DataStore: chainMetadataReport.Output.DataStore}, nil
},
)
Loading
Loading