Skip to content
5 changes: 5 additions & 0 deletions .changeset/tangy-jokes-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

Add top-level address-book and datastore CLI commands
179 changes: 179 additions & 0 deletions engine/cld/legacy/cli/commands/addressbook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package commands

import (
"fmt"

"github.com/spf13/cobra"

"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/legacy/cli"
)

// NewAddressBookCmds creates a new set of commands for address book operations.
func (c Commands) NewAddressBookCmds(domain domain.Domain) *cobra.Command {
addressBookCmd := &cobra.Command{
Use: "address-book",
Short: "Address book operations",
}

addressBookCmd.AddCommand(c.newAddressBookMerge(domain))
addressBookCmd.AddCommand(c.newAddressBookMigrate(domain))
addressBookCmd.AddCommand(c.newAddressBookRemove(domain))

addressBookCmd.PersistentFlags().StringP("environment", "e", "", "Deployment environment (required)")
if err := addressBookCmd.MarkPersistentFlagRequired("environment"); err != nil {
panic(fmt.Sprintf("failed to mark environment flag as required: %v", err))
}

return addressBookCmd
}

var (
addressBookMergeLong = cli.LongDesc(`
Merges the address book artifact of a specific 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.
`)

addressBookMergeExample = cli.Examples(`
# Merge the address book for the 0001_deploy_cap changeset in the ccip staging domain environment
ccip address-book merge --environment staging --name 0001_deploy_cap

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

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

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

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

cmd.Printf("Merged address books for %s %s %s\n",
domain, envKey, name,
)

return nil
},
}

cmd.Flags().StringVarP(&name, "name", "n", "", "name (required)")
cmd.Flags().StringVarP(&timestamp, "timestamp", "t", "", "Durable Pipeline timestamp (optional)")
if err := cmd.MarkFlagRequired("name"); err != nil {
panic(fmt.Sprintf("failed to mark name flag as required: %v", err))
}

return &cmd
}

var (
addressBookMigrateLong = cli.LongDesc(`
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.
`)

addressBookMigrateExample = cli.Examples(`
# Migrate the address book for the ccip staging domain to the new datastore format
ccip address-book migrate --environment staging
`)
)

// newAddressBookMigrate creates a command to convert the address book
// artifact to the new datastore format within a given domain environment.
func (Commands) newAddressBookMigrate(domain domain.Domain) *cobra.Command {
cmd := cobra.Command{
Use: "migrate",
Short: "Migrate address book to the new datastore format",
Long: addressBookMigrateLong,
Example: addressBookMigrateExample,
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\n",
domain, envKey,
)

return nil
},
}

return &cmd
}

var (
addressBookRemoveLong = cli.LongDesc(`
Removes the address book entries introduced by a specific changeset from the main
address book within a given Domain Environment. This can be used to rollback
address-book merge changes.
`)

addressBookRemoveExample = cli.Examples(`
# Remove the address book entries for the 0001_deploy_cap changeset in the ccip staging domain
ccip address-book remove --environment staging --name 0001_deploy_cap
`)
)

// newAddressBookRemove creates a command to remove a changeset's
// address book entries from the main address book within a given domain environment.
func (Commands) newAddressBookRemove(domain domain.Domain) *cobra.Command {
var (
name string
timestamp string
)

cmd := cobra.Command{
Use: "remove",
Short: "Remove changeset address book entries",
Long: addressBookRemoveLong,
Example: addressBookRemoveExample,
RunE: func(cmd *cobra.Command, args []string) error {
envKey, _ := cmd.Flags().GetString("environment")
envDir := domain.EnvDir(envKey)

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

cmd.Printf("Removed address books for %s %s %s\n",
domain, envKey, name,
)

return nil
},
}

cmd.Flags().StringVarP(&name, "name", "n", "", "name (required)")
cmd.Flags().StringVarP(&timestamp, "timestamp", "t", "", "Durable Pipeline timestamp (optional)")
if err := cmd.MarkFlagRequired("name"); err != nil {
panic(fmt.Sprintf("failed to mark name flag as required: %v", err))
}

return &cmd
}
117 changes: 117 additions & 0 deletions engine/cld/legacy/cli/commands/addressbook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package commands

import (
"strings"
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
)

func TestNewAddressBookCmds_Structure(t *testing.T) {
t.Parallel()
c := NewCommands(nil)
var dom domain.Domain
root := c.NewAddressBookCmds(dom)

require.Equal(t, "address-book", root.Use)

subs := root.Commands()
require.Len(t, subs, 3, "expected 3 subcommands under 'address-book'")

uses := make([]string, len(subs))
for i, sc := range subs {
uses[i] = sc.Use
}
require.ElementsMatch(t,
[]string{"merge", "migrate", "remove"},
uses,
)

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

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

tests := []struct {
name string
cmdKey string
wantUse string
wantShort string
wantLongPrefix string
wantExampleContains string
wantFlags []string
}{
{
name: "merge",
cmdKey: "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: "migrate",
cmdKey: "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: "remove",
cmdKey: "remove",
wantUse: "remove",
wantShort: "Remove changeset address book entries",
wantLongPrefix: "Removes the address book entries",
wantExampleContains: "address-book remove --environment staging --name",
wantFlags: []string{
"name", "timestamp",
},
},
}

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

// Give each subtest its own fresh command tree
root := c.NewAddressBookCmds(dom)

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