diff --git a/catalog/changesets/update_chain_metadata.go b/catalog/changesets/update_chain_metadata.go new file mode 100644 index 0000000..3150468 --- /dev/null +++ b/catalog/changesets/update_chain_metadata.go @@ -0,0 +1,68 @@ +package changesets + +import ( + "errors" + "fmt" + + "github.com/samber/lo" + 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" +) + +// UpdateChainMetadataChangeset updates existing chain metadata entries in the Catalog service. +type UpdateChainMetadataChangeset struct{} + +type UpdateChainMetadataChangesetInput struct { + ChainMetadata []cldfdatastore.ChainMetadata `json:"chainMetadata"` +} + +// VerifyPreconditions ensures the input is valid. +func (UpdateChainMetadataChangeset) VerifyPreconditions(e cldf.Environment, input UpdateChainMetadataChangesetInput) error { + if len(input.ChainMetadata) == 0 { + return errors.New("missing chain metadata input") + } + if e.DataStore == nil { + return errors.New("missing datastore in environment") + } + + uniqChainMetadata := lo.UniqBy(input.ChainMetadata, func(cm cldfdatastore.ChainMetadata) cldfdatastore.ChainMetadataKey { + return cm.Key() + }) + if len(uniqChainMetadata) != len(input.ChainMetadata) { + return errors.New("duplicate chain metadata entries found in input") + } + + for _, chainMetadata := range input.ChainMetadata { + _, err := e.DataStore.ChainMetadata().Get(chainMetadata.Key()) + if errors.Is(err, cldfdatastore.ErrChainMetadataNotFound) { + return fmt.Errorf("chain metadata for chain selector %v does not exist", chainMetadata.ChainSelector) + } + if err != nil { + return fmt.Errorf("failed to retrieve chain metadata for chain selector %v: %w", chainMetadata.ChainSelector, err) + } + } + + return nil +} + +// Apply executes the changeset, updating the chain metadata in the Catalog service. +func (UpdateChainMetadataChangeset) Apply( + e cldf.Environment, input UpdateChainMetadataChangesetInput, +) (cldf.ChangesetOutput, error) { + deps := operations.UpdateChainMetadataDeps{DataStore: e.DataStore} + opInput := operations.UpdateChainMetadataInput{ChainMetadata: input.ChainMetadata} + + report, err := cldfops.ExecuteOperation(e.OperationsBundle, operations.UpdateChainMetadataOp, 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/update_chain_metadata_test.go b/catalog/changesets/update_chain_metadata_test.go new file mode 100644 index 0000000..7b214c9 --- /dev/null +++ b/catalog/changesets/update_chain_metadata_test.go @@ -0,0 +1,164 @@ +package changesets + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "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" + + "github.com/smartcontractkit/cld-changesets/catalog/operations" +) + +func TestUpdateChainMetadataChangeset_VerifyPreconditions(t *testing.T) { + t.Parallel() + + chainMetadata1 := cldfdatastore.ChainMetadata{ChainSelector: 1234, Metadata: "value1"} + chainMetadata2 := cldfdatastore.ChainMetadata{ChainSelector: 1234, Metadata: "value2"} + + tests := []struct { + name string + env cldf.Environment + input UpdateChainMetadataChangesetInput + wantErr string + }{ + { + name: "success: valid preconditions", + env: cldf.Environment{DataStore: func() cldfdatastore.DataStore { + ds := cldfdatastore.NewMemoryDataStore() + err := ds.ChainMetadata().Add(chainMetadata1) + require.NoError(t, err) + + return ds.Seal() + }()}, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{chainMetadata1}, + }, + }, + { + name: "failure: missing datastore", + env: cldf.Environment{}, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{{}}, + }, + wantErr: "missing datastore in environment", + }, + { + name: "failure: no chain metadata given", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{}, + }, + wantErr: "missing chain metadata input", + }, + { + name: "failure: duplicate entries", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{chainMetadata1, chainMetadata2}, + }, + wantErr: "duplicate chain metadata entries found in input", + }, + { + name: "failure: chain metadata does not exist", + env: cldf.Environment{DataStore: cldfdatastore.NewMemoryDataStore().Seal()}, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{chainMetadata1}, + }, + wantErr: "chain metadata for chain selector 1234 does not exist", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := UpdateChainMetadataChangeset{}.VerifyPreconditions(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestUpdateChainMetadataChangeset_Apply(t *testing.T) { + t.Parallel() + + chainMetadata1 := cldfdatastore.ChainMetadata{ChainSelector: 1234, Metadata: "value1"} + chainMetadata2 := cldfdatastore.ChainMetadata{ChainSelector: 5678, Metadata: "value2"} + chainMetadata1Updated := cldfdatastore.ChainMetadata{ChainSelector: 1234, Metadata: "updated-value1"} + chainMetadata2Updated := cldfdatastore.ChainMetadata{ChainSelector: 5678, Metadata: "updated-value2"} + + tests := []struct { + name string + env cldf.Environment + input UpdateChainMetadataChangesetInput + want cldf.ChangesetOutput + wantErr string + }{ + { + name: "success: updates two entries in chain metadata", + env: cldf.Environment{ + DataStore: testDataStoreWithChainMetadata(t, chainMetadata1, chainMetadata2).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{chainMetadata1Updated, chainMetadata2Updated}, + }, + want: cldf.ChangesetOutput{ + DataStore: testDataStoreWithChainMetadata(t, chainMetadata1Updated, chainMetadata2Updated), + Reports: []cldfoperations.Report[any, any]{{ + Def: cldfoperations.Definition{ + ID: "catalog-update-chain-metadata", + Version: semver.MustParse("1.0.0"), + Description: "Update chain metadata entries in the Catalog service", + }, + Input: operations.UpdateChainMetadataInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{chainMetadata1Updated, chainMetadata2Updated}, + }, + Output: operations.UpdateChainMetadataOutput{ + DataStore: testDataStoreWithChainMetadata(t, chainMetadata1Updated, chainMetadata2Updated), + }, + }}, + }, + }, + { + name: "failure: fails to update entry that does not exist", + env: cldf.Environment{ + DataStore: testDataStoreWithChainMetadata(t, chainMetadata1).Seal(), + OperationsBundle: cldfoperations.NewBundle(t.Context, cldflogger.Test(t), cldfoperations.NewMemoryReporter()), + }, + input: UpdateChainMetadataChangesetInput{ + ChainMetadata: []cldfdatastore.ChainMetadata{chainMetadata1Updated, chainMetadata2Updated}, + }, + wantErr: "failed to update chain metadata entry 1 in catalog store: " + + "no chain metadata record can be found for the provided key", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := UpdateChainMetadataChangeset{}.Apply(tt.env, tt.input) + + if tt.wantErr == "" { + require.NoError(t, err) + require.Empty(t, + cmp.Diff(tt.want, got, + cmpopts.IgnoreFields(cldfoperations.Report[any, any]{}, "ID", "Timestamp"), + cmpopts.IgnoreUnexported(cldfdatastore.MemoryAddressRefStore{}, cldfdatastore.MemoryChainMetadataStore{}, + cldfdatastore.MemoryContractMetadataStore{}, cldfdatastore.MemoryEnvMetadataStore{}))) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} diff --git a/catalog/operations/update_chain_metadata.go b/catalog/operations/update_chain_metadata.go new file mode 100644 index 0000000..fb153d9 --- /dev/null +++ b/catalog/operations/update_chain_metadata.go @@ -0,0 +1,51 @@ +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" +) + +// UpdateChainMetadataDeps holds non-serializable dependencies for the +// UpdateChainMetadataOp operation. +type UpdateChainMetadataDeps struct { + DataStore cldfdatastore.DataStore +} + +// UpdateChainMetadataInput is the serializable input of an UpdateChainMetadataOp invocation. +type UpdateChainMetadataInput struct { + ChainMetadata []cldfdatastore.ChainMetadata +} + +// UpdateChainMetadataOutput is the serializable output of an UpdateChainMetadataOp invocation. +type UpdateChainMetadataOutput struct { + DataStore cldfdatastore.MutableDataStore +} + +// UpdateChainMetadataOp updates existing chain metadata entries in the Catalog service. +var UpdateChainMetadataOp = cldfops.NewOperation( + "catalog-update-chain-metadata", + semver.MustParse("1.0.0"), + "Update chain metadata entries in the Catalog service", + func(b cldfops.Bundle, deps UpdateChainMetadataDeps, input UpdateChainMetadataInput) (UpdateChainMetadataOutput, error) { + dataStore := cldfdatastore.NewMemoryDataStore() + err := dataStore.Merge(deps.DataStore) + if err != nil { + return UpdateChainMetadataOutput{}, fmt.Errorf("failed to create memory data store: %w", err) + } + + for i, item := range input.ChainMetadata { + err = dataStore.ChainMetadata().Update(item) + if err != nil { + return UpdateChainMetadataOutput{}, fmt.Errorf("failed to update chain metadata entry %d in catalog store: %w", i, err) + } + } + + b.Logger.Infow("Catalog ChainMetadata updated successfully") + + return UpdateChainMetadataOutput{DataStore: dataStore}, nil + }, +)