diff --git a/catalog/changesets/delete_contract_metadata.go b/catalog/changesets/delete_contract_metadata.go new file mode 100644 index 0000000..7955268 --- /dev/null +++ b/catalog/changesets/delete_contract_metadata.go @@ -0,0 +1,61 @@ +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/catalog/operations" +) + +// DeleteContractMetadataChangeset deletes contract metadata entries from the Catalog service. +type DeleteContractMetadataChangeset struct{} + +type DeleteContractMetadataChangesetInput struct { + ContractMetadataKeys []cldfdatastore.ContractMetadataKey `json:"contractMetadataKeys"` +} + +// VerifyPreconditions ensures the input is valid. +func (DeleteContractMetadataChangeset) VerifyPreconditions(e cldf.Environment, input DeleteContractMetadataChangesetInput) error { + if len(input.ContractMetadataKeys) == 0 { + return errors.New("missing contract metadata keys input") + } + if e.DataStore == nil { + return errors.New("missing datastore in environment") + } + + for _, key := range input.ContractMetadataKeys { + _, err := e.DataStore.ContractMetadata().Get(key) + 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", + key.ChainSelector(), key.Address()) + } + + return fmt.Errorf("failed to retrieve contract metadata entry for chain selector %v and address %v: %w", + key.ChainSelector(), key.Address(), err) + } + } + + return nil +} + +// Apply executes the changeset, staging the contract metadata entries to be deleted from the Catalog service or local datastore files. +func (DeleteContractMetadataChangeset) Apply(e cldf.Environment, input DeleteContractMetadataChangesetInput) (cldf.ChangesetOutput, error) { + deps := operations.DeleteContractMetadataDeps{DataStore: e.DataStore} + opInput := operations.DeleteContractMetadataInput{ContractMetadataKeys: input.ContractMetadataKeys} + + report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.DeleteContractMetadataOp, deps, opInput) + out := cldf.ChangesetOutput{ + DataStore: report.Output.DataStore, + Reports: []cldfops.Report[any, any]{report.ToGenericReport()}, + } + if err != nil { + return out, err + } + + return out, nil +} diff --git a/catalog/changesets/delete_contract_metadata_test.go b/catalog/changesets/delete_contract_metadata_test.go new file mode 100644 index 0000000..41d9dd7 --- /dev/null +++ b/catalog/changesets/delete_contract_metadata_test.go @@ -0,0 +1,118 @@ +package changesets + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + 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" +) + +func TestDeleteContractMetadataChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 1234, Metadata: "value2"} + + tests := []struct { + name string + env cldf.Environment + input DeleteContractMetadataChangesetInput + wantErr string + }{ + { + name: "success: valid preconditions", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2).Seal(), + }, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{contractMetadata1.Key(), contractMetadata2.Key()}, + }, + }, + { + name: "failure: missing datastore", + env: cldf.Environment{}, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{contractMetadata1.Key()}, + }, + wantErr: "missing datastore in environment", + }, + { + name: "failure: no contract metadata keys given", + env: cldf.Environment{ + DataStore: cldfdatastore.NewMemoryDataStore().Seal(), + }, + input: DeleteContractMetadataChangesetInput{ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{}}, + wantErr: "missing contract metadata keys input", + }, + { + name: "failure: contract metadata entry does not exist", + env: cldf.Environment{ + DataStore: cldfdatastore.NewMemoryDataStore().Seal(), + }, + input: DeleteContractMetadataChangesetInput{ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{contractMetadata2.Key()}}, + wantErr: fmt.Sprintf("contract metadata entry for chain selector %v and address %v does not exist", contractMetadata2.ChainSelector, contractMetadata2.Address), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := DeleteContractMetadataChangeset{}.VerifyPreconditions(tt.env, tt.input) + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestDeleteContractMetadataChangeset_Apply(t *testing.T) { + t.Parallel() + + contractMetadata1 := cldfdatastore.ContractMetadata{Address: "0x01", ChainSelector: 1234, Metadata: "value1"} + contractMetadata2 := cldfdatastore.ContractMetadata{Address: "0x02", ChainSelector: 5678, Metadata: "value2"} + + tests := []struct { + name string + env cldf.Environment + input DeleteContractMetadataChangesetInput + wantDeletedKeys []string + wantErr string + }{ + { + name: "success: stages two contract metadata entries for deletion", + env: cldf.Environment{ + DataStore: testDataStoreWithContractMetadata(t, contractMetadata1, contractMetadata2).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: DeleteContractMetadataChangesetInput{ + ContractMetadataKeys: []cldfdatastore.ContractMetadataKey{contractMetadata1.Key(), contractMetadata2.Key()}, + }, + wantDeletedKeys: []string{contractMetadata1.Key().String(), contractMetadata2.Key().String()}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := DeleteContractMetadataChangeset{}.Apply(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + require.Len(t, got.Reports, 1) + memDS := got.DataStore.(*cldfdatastore.MemoryDataStore) + require.ElementsMatch(t, tt.wantDeletedKeys, memDS.ContractMetadataStore.DeletedRemoteKeys) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} diff --git a/catalog/operations/delete_contract_metadata.go b/catalog/operations/delete_contract_metadata.go new file mode 100644 index 0000000..43c2280 --- /dev/null +++ b/catalog/operations/delete_contract_metadata.go @@ -0,0 +1,49 @@ +package operations + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldfops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// DeleteContractMetadataDeps holds non-serializable dependencies for the DeleteContractMetadataOp operation. +type DeleteContractMetadataDeps struct { + DataStore cldfdatastore.DataStore +} + +// DeleteContractMetadataInput is the serializable input of a DeleteContractMetadataOp invocation. +type DeleteContractMetadataInput struct { + ContractMetadataKeys []cldfdatastore.ContractMetadataKey +} + +// DeleteContractMetadataOutput is the serializable output of a DeleteContractMetadataOp invocation. +type DeleteContractMetadataOutput struct { + DataStore cldfdatastore.MutableDataStore +} + +// DeleteContractMetadataOp deletes contract metadata entries from the Catalog service or local datastore files. +var DeleteContractMetadataOp = cldfops.NewOperation( + "datastore-delete-contract-metadata", + semver.MustParse("1.0.0"), + "Delete contract metadata entries from the Catalog service or local datastore files", + func(b cldfops.Bundle, deps DeleteContractMetadataDeps, input DeleteContractMetadataInput) (DeleteContractMetadataOutput, error) { + dataStore := cldfdatastore.NewMemoryDataStore() + err := dataStore.Merge(deps.DataStore) + if err != nil { + return DeleteContractMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err) + } + + for i, key := range input.ContractMetadataKeys { + err = dataStore.ContractMetadata().RemoteDelete(key) + if err != nil { + return DeleteContractMetadataOutput{}, fmt.Errorf("failed to delete contract metadata entry %d in datastore: %w", i, err) + } + } + + b.Logger.Infow("Catalog ContractMetadata successfully staged for deletion") + + return DeleteContractMetadataOutput{DataStore: dataStore}, nil + }, +)