Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/orange-planets-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

feat(cli): clone addressbook/datastore merge to dp cli
162 changes: 161 additions & 1 deletion engine/cld/legacy/cli/commands/durable-pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,27 @@ func (c Commands) NewDurablePipelineCmds(
Short: "Durable Pipeline commands",
}

addressBookCmd := &cobra.Command{
Use: "address-book",
Short: "Address book operations",
}
addressBookCmd.AddCommand(c.newDurablePipelineAddressBookMerge(domain))
addressBookCmd.AddCommand(c.newDurablePipelineAddressBookMigrate(domain))

datastoreCmd := &cobra.Command{
Use: "datastore",
Short: "Datastore operations",
}
datastoreCmd.AddCommand(c.newDurablePipelineDataStoreMerge(domain))

evmCmd.AddCommand(
c.newDurablePipelineRun(domain, loadMigration, decodeProposalCtxProvider, loadConfigResolvers),
c.newDurablePipelineInputGenerate(domain, loadMigration, loadConfigResolvers),
c.newDurablePipelineListBuild(domain, loadMigration, loadConfigResolvers),
c.newDurablePipelineTemplateInput(domain, loadMigration, loadConfigResolvers))
c.newDurablePipelineTemplateInput(domain, loadMigration, loadConfigResolvers),
addressBookCmd,
datastoreCmd,
)

evmCmd.PersistentFlags().StringP("environment", "e", "", "Deployment environment (required)")
_ = evmCmd.MarkPersistentFlagRequired("environment")
Expand Down Expand Up @@ -677,3 +693,147 @@ func (c Commands) newDurablePipelineTemplateInput(

return &cmd
}

var (
durablePipelineMergeAddressBookLong = `
Merges the address book artifact of a specific durable pipeline changeset to the main address book within a
given Domain Environment. This is to ensure that the address book is up-to-date with the
latest changeset changes.
`

durablePipelineMergeAddressBookExample = `
# Merge the address book for the 0001_deploy_cap changeset in the staging environment
ccip durable-pipeline address-book merge --environment staging --name 0001_deploy_cap

# Merge with a specific timestamp
ccip durable-pipeline address-book merge --environment staging --name 0001_deploy_cap --timestamp 1234567890
`
)

// newDurablePipelineAddressBookMerge creates a command to merge the address books for a durable pipeline changeset to
// the main address book within a given domain environment.
func (Commands) newDurablePipelineAddressBookMerge(domain dom.Domain) *cobra.Command {
var (
changesetName string
timestamp string
)

cmd := cobra.Command{
Use: "merge",
Short: "Merge the address book",
Long: durablePipelineMergeAddressBookLong,
Example: durablePipelineMergeAddressBookExample,
RunE: func(cmd *cobra.Command, args []string) error {
envKey, _ := cmd.Flags().GetString("environment")
envDir := domain.EnvDir(envKey)

if err := envDir.MergeMigrationAddressBook(changesetName, timestamp); err != nil {
return fmt.Errorf("error during address book merge for %s %s %s: %w",
domain, envKey, changesetName, err,
)
}

cmd.Printf("Merged address books for %s %s %s",
domain, envKey, changesetName,
)

return nil
},
}

cmd.Flags().StringVarP(&changesetName, "name", "n", "", "name (required)")
cmd.Flags().StringVarP(&timestamp, "timestamp", "t", "", "Durable Pipeline timestamp (optional)")

_ = cmd.MarkFlagRequired("name")

return &cmd
}

var (
durablePipelineMigrateAddressBookLong = `
Converts the address book artifact format to the new datastore schema within a
given Domain Environment. This updates your on-chain address book to the latest storage format.
`

durablePipelineMigrateAddressBookExample = `
# Migrate the address book for the staging domain to the new datastore format
ccip durable-pipeline address-book migrate --environment staging
`
)

// newDurablePipelineAddressBookMigrate creates a command to convert the address book
// artifact to the new datastore format within a given domain environment.
func (Commands) newDurablePipelineAddressBookMigrate(domain dom.Domain) *cobra.Command {
cmd := cobra.Command{
Use: "migrate",
Short: "Migrate address book to the new datastore format",
Long: durablePipelineMigrateAddressBookLong,
Example: durablePipelineMigrateAddressBookExample,
RunE: func(cmd *cobra.Command, args []string) error {
envKey, _ := cmd.Flags().GetString("environment")
envDir := domain.EnvDir(envKey)

if err := envDir.MigrateAddressBook(); err != nil {
return fmt.Errorf("error during address book conversion for %s %s: %w",
domain, envKey, err,
)
}

cmd.Printf("Address book for %s %s successfully migrated to the new datastore format",
domain, envKey,
)

return nil
},
}

return &cmd
}

var (
durablePipelineDataStoreMergeExample = `
# Merge the data store for the 0001_deploy_cap changeset in the staging domain
ccip durable-pipeline datastore merge --environment staging --name 0001_deploy_cap

# Merge with a specific timestamp
ccip durable-pipeline datastore merge --environment staging --name 0001_deploy_cap --timestamp 1234567890
`
)

// newDurablePipelineDataStoreMerge creates a command to merge the data store for a durable pipeline changeset
func (Commands) newDurablePipelineDataStoreMerge(domain dom.Domain) *cobra.Command {
var (
changesetName string
timestamp string
)

cmd := cobra.Command{
Use: "merge",
Short: "Merge data stores",
Long: "Merge the data store for a changeset to the main data store",
Example: durablePipelineDataStoreMergeExample,
RunE: func(cmd *cobra.Command, args []string) error {
envKey, _ := cmd.Flags().GetString("environment")
envDir := domain.EnvDir(envKey)

if err := envDir.MergeMigrationDataStore(changesetName, timestamp); err != nil {
return fmt.Errorf("error during data store merge for %s %s %s: %w",
domain, envKey, changesetName, err,
)
}

cmd.Printf("Merged data stores for %s %s %s",
domain, envKey, changesetName,
)

return nil
},
}

cmd.Flags().StringVarP(&changesetName, "name", "n", "", "name (required)")
cmd.Flags().StringVarP(&timestamp, "timestamp", "t", "", "Durable Pipeline timestamp (optional)")

_ = cmd.MarkFlagRequired("name")

return &cmd
}
133 changes: 133 additions & 0 deletions engine/cld/legacy/cli/commands/durable-pipelines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"

fresolvers "github.com/smartcontractkit/chainlink-deployments-framework/changeset/resolvers"
Expand Down Expand Up @@ -1821,3 +1822,135 @@ changesets:
require.Error(t, err)
require.Contains(t, err.Error(), "--changeset-index can only be used with array format YAML files")
}

func TestNewDurablePipelineCmds_Structure(t *testing.T) {
t.Parallel()
c := NewCommands(nil)
var testDomain domain.Domain
root := c.NewDurablePipelineCmds(testDomain, fakeLoadRegistry, fakeDecodeCtx, nil)

require.Equal(t, "durable-pipeline", root.Use)

subs := root.Commands()
require.Len(t, subs, 6, "expected 6 subcommands under 'durable-pipeline'")

uses := make([]string, len(subs))
for i, sc := range subs {
uses[i] = sc.Use
}
require.ElementsMatch(t,
[]string{"run", "input-generate", "list", "template-input", "address-book", "datastore"},
uses,
)

// The "environment" flag is persistent on root
flag := root.PersistentFlags().Lookup("environment")
require.NotNil(t, flag, "persistent flag 'environment' should exist")

// address-book group
abIdx := indexOf(subs, "address-book")
require.NotEqual(t, -1, abIdx)
abSubs := subs[abIdx].Commands()
abUses := make([]string, len(abSubs))
for i, sc := range abSubs {
abUses[i] = sc.Use
}
require.ElementsMatch(t,
[]string{"merge", "migrate"},
abUses,
)

// datastore group
dsIdx := indexOf(subs, "datastore")
require.NotEqual(t, -1, dsIdx)
dsSubs := subs[dsIdx].Commands()
dsUses := make([]string, len(dsSubs))
for i, sc := range dsSubs {
dsUses[i] = sc.Use
}
require.ElementsMatch(t,
[]string{"merge"},
dsUses,
)
}

func TestDurablePipelineCommandMetadata(t *testing.T) {
t.Parallel()
c := NewCommands(nil)
testDomain := domain.Domain{}

tests := []struct {
name string
cmdKey string
wantUse string
wantShort string
wantLongPrefix string
wantExampleContains string
wantFlags []string
}{
{
name: "address-book merge",
cmdKey: "address-book merge",
wantUse: "merge",
wantShort: "Merge the address book",
wantLongPrefix: "Merges the address book artifact",
wantExampleContains: "address-book merge --environment staging --name",
wantFlags: []string{
"name", "timestamp",
},
},
{
name: "address-book migrate",
cmdKey: "address-book migrate",
wantUse: "migrate",
wantShort: "Migrate address book to the new datastore format",
wantLongPrefix: "Converts the address book artifact format",
wantExampleContains: "address-book migrate --environment staging",
wantFlags: []string{},
},
{
name: "datastore merge",
cmdKey: "datastore merge",
wantUse: "merge",
wantShort: "Merge data stores",
wantLongPrefix: "Merge the data store for a changeset",
wantExampleContains: "datastore merge --environment staging --name",
wantFlags: []string{
"name", "timestamp",
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Give each subtest its own fresh command tree
root := c.NewDurablePipelineCmds(testDomain, fakeLoadRegistry, fakeDecodeCtx, nil)

t.Parallel()

parts := strings.Split(tc.cmdKey, " ")
cmd, _, err := root.Find(parts)
require.NoError(t, err)
require.NotNil(t, cmd, "command not found: %s", tc.cmdKey)

require.Equal(t, tc.wantUse, cmd.Use)
require.Contains(t, cmd.Short, tc.wantShort)
require.Contains(t, cmd.Long, tc.wantLongPrefix)
require.Contains(t, cmd.Example, tc.wantExampleContains)

for _, flagName := range tc.wantFlags {
var flag *pflag.Flag
if flagName == "environment" {
// persistent flag lives on root
flag = root.PersistentFlags().Lookup("environment")
} else {
flag = cmd.Flags().Lookup(flagName)
if flag == nil {
flag = cmd.PersistentFlags().Lookup(flagName)
}
}
require.NotNil(t, flag, "flag %q not found on %s", flagName, tc.name)
}
})
}
}
Loading