Skip to content
Open
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
110 changes: 110 additions & 0 deletions datastore/changesets/delete_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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,
cldfops.WithSequenceIdempotencyKey[datastoreseqs.DeleteResourcesSeqInput, datastoreseqs.DeleteResourcesSeqDeps](
fmt.Sprintf("%T:%p", e.DataStore, e.DataStore),
),
)
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}
}
Loading
Loading