Skip to content
Merged
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
106 changes: 20 additions & 86 deletions engine/cld/legacy/cli/commands/state.go
Original file line number Diff line number Diff line change
@@ -1,100 +1,34 @@
package commands

import (
"context"
"fmt"
"os"
"time"

"github.com/spf13/cobra"

"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/environment"
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/commands/state"
)

// NewStateCmds creates a new set of commands for state environment.
func (c Commands) NewStateCmds(dom domain.Domain, config StateConfig) *cobra.Command {
stateCmd := &cobra.Command{
Use: "state",
Short: "State commands",
}
stateCmd.AddCommand(c.newStateGenerate(dom, config))

stateCmd.PersistentFlags().
StringP("environment", "e", "", "Deployment environment (required)")
_ = stateCmd.MarkPersistentFlagRequired("environment")

return stateCmd
}

// StateConfig holds configuration for state commands.
// Deprecated: Use commands.StateConfig directly for new integrations.
type StateConfig struct {
ViewState deployment.ViewStateV2
}

func (c Commands) newStateGenerate(dom domain.Domain, cfg StateConfig) *cobra.Command {
var (
persist bool
outputPath string
prevStatePath string
)

cmd := cobra.Command{
Use: "generate",
Short: "Generate latest state. Nodes must be present in the `nodes.json` to be included.",
RunE: func(cmd *cobra.Command, args []string) error {
envKey, _ := cmd.Flags().GetString("environment")
envdir := dom.EnvDir(envKey)
viewTimeout := 10 * time.Minute

cmd.Printf("Generate latest state for %s in environment: %s\n", dom, envKey)
cmd.Printf("This command may take a while to complete, please be patient. Timeout set to %v\n", viewTimeout)
ctx, cancel := context.WithTimeout(cmd.Context(), viewTimeout)
defer cancel()

env, err := environment.Load(ctx, dom, envKey, environment.WithLogger(c.lggr))
if err != nil {
return fmt.Errorf("failed to load environment %w", err)
}

prevState, err := envdir.LoadState()
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to load previous state: %w", err)
}

state, err := cfg.ViewState(env, prevState)
if err != nil {
return fmt.Errorf("unable to snapshot state: %w", err)
}

b, err := state.MarshalJSON()
if err != nil {
return fmt.Errorf("unable to marshal state: %w", err)
}

if persist {
// Save the state to the outputPath if defined, otherwise save it with the default
// path in the product and environment directory with the default file name.
if outputPath != "" {
err = domain.SaveViewState(outputPath, state)
} else {
err = envdir.SaveViewState(state)
}

if err != nil {
return fmt.Errorf("failed to save state: %w", err)
}
}

cmd.Println(string(b))

return nil
},
}

cmd.Flags().BoolVarP(&persist, "persist", "p", false, "Persist state to disk")
cmd.Flags().StringVarP(&outputPath, "outputPath", "o", "", "Output path. Default is <product>/<environment>/state.json")
cmd.Flags().StringVarP(&prevStatePath, "previousState", "s", "", "Previous state's path. Default is <product>/<environment>/state.json")

return &cmd
// NewStateCmds creates a new set of commands for state environment.
// This method delegates to the modular state package for backward compatibility.
//
// Deprecated: Use the modular commands package for new integrations:
//
// import "github.com/smartcontractkit/chainlink-deployments-framework/pkg/commands"
//
// cmds := commands.New(lggr)
// rootCmd.AddCommand(cmds.State(myDomain, commands.StateConfig{
// ViewState: myViewStateFunc,
// }))
func (c Commands) NewStateCmds(dom domain.Domain, config StateConfig) *cobra.Command {
return state.NewCommand(state.Config{
Logger: c.lggr,
Domain: dom,
ViewState: config.ViewState,
})
}
61 changes: 37 additions & 24 deletions engine/cld/legacy/cli/commands/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"github.com/stretchr/testify/require"

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

func TestNewStateCmds_Structure(t *testing.T) {
t.Parallel()
c := Commands{}

c := NewCommands(logger.Nop())
dom := domain.NewDomain("/tmp", "foo")
var cfg StateConfig
root := c.NewStateCmds(dom, cfg)
Expand All @@ -32,33 +34,45 @@ func TestNewStateCmds_Structure(t *testing.T) {

func TestNewStateGenerateCmd_Metadata(t *testing.T) {
t.Parallel()

c := NewCommands(logger.Nop())
dom := domain.NewDomain("/tmp", "foo")
var cfg StateConfig
cmd := (&Commands{}).newStateGenerate(dom, cfg)

require.Equal(t, "generate", cmd.Use)
require.Contains(t, cmd.Short, "Generate latest state")

// local flags
p := cmd.Flags().Lookup("persist")
require.NotNil(t, p)
require.Equal(t, "p", p.Shorthand)
require.Equal(t, "false", p.Value.String())

o := cmd.Flags().Lookup("outputPath")
require.NotNil(t, o)
require.Equal(t, "o", o.Shorthand)
require.Empty(t, o.Value.String())

s := cmd.Flags().Lookup("previousState")
require.NotNil(t, s)
require.Equal(t, "s", s.Shorthand)
require.Empty(t, s.Value.String())
root := c.NewStateCmds(dom, cfg)

// Find generate subcommand
var found bool
for _, sub := range root.Commands() {
if sub.Use == "generate" {
found = true
require.Contains(t, sub.Short, "Generate latest state")

// local flags
p := sub.Flags().Lookup("persist")
require.NotNil(t, p)
require.Equal(t, "p", p.Shorthand)
require.Equal(t, "false", p.Value.String())

o := sub.Flags().Lookup("outputPath")
require.NotNil(t, o)
require.Equal(t, "o", o.Shorthand)
require.Empty(t, o.Value.String())

s := sub.Flags().Lookup("previousState")
require.NotNil(t, s)
require.Equal(t, "s", s.Shorthand)
require.Empty(t, s.Value.String())

break
}
}
require.True(t, found, "generate subcommand not found")
}

func TestStateGenerate_MissingEnvFails(t *testing.T) {
t.Parallel()
c := Commands{}

c := NewCommands(logger.Nop())
dom := domain.NewDomain("/tmp", "foo")
var cfg StateConfig
root := c.NewStateCmds(dom, cfg)
Expand All @@ -67,6 +81,5 @@ func TestStateGenerate_MissingEnvFails(t *testing.T) {
root.SetArgs([]string{"generate"})
err := root.Execute()

require.Error(t, err)
require.Contains(t, err.Error(), `required flag(s) "environment" not set`)
require.ErrorContains(t, err, `required flag(s) "environment" not set`)
}
67 changes: 67 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Package commands provides modular CLI command packages for domain CLIs.
//
// There are two ways to use commands from this package:
//
// 1. Via the Commands factory (recommended for most use cases):
//
// commands := commands.New(lggr)
// app.AddCommand(
// commands.State(domain, stateConfig),
// commands.EVM(domain),
// commands.JD(domain),
// )
//
// 2. Via direct package imports (for advanced DI/testing):
//
// import "github.com/smartcontractkit/chainlink-deployments-framework/pkg/commands/state"
//
// app.AddCommand(state.NewCommand(state.Config{
// Logger: lggr,
// Domain: domain,
// ViewState: myViewState,
// Deps: &state.Deps{...}, // inject mocks for testing
// }))
package commands

import (
"github.com/spf13/cobra"

"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/commands/state"
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
)

// Commands provides a factory for creating CLI commands with shared configuration.
// This allows setting the logger once and reusing it across all commands.
type Commands struct {
lggr logger.Logger
}

// New creates a new Commands factory with the given logger.
// The logger will be shared across all commands created by this factory.
func New(lggr logger.Logger) *Commands {
return &Commands{lggr: lggr}
}

// StateConfig holds configuration for state commands.
type StateConfig struct {
// ViewState is the function that generates state from an environment.
// This is domain-specific and must be provided by the user.
ViewState state.ViewStateFunc
}

// State creates the state command group for managing environment state.
//
// Usage:
//
// cmds := commands.New(lggr)
// rootCmd.AddCommand(cmds.State(domain, commands.StateConfig{
// ViewState: myViewStateFunc,
// }))
func (c *Commands) State(dom domain.Domain, cfg StateConfig) *cobra.Command {
return state.NewCommand(state.Config{
Logger: c.lggr,
Domain: dom,
ViewState: cfg.ViewState,
})
}
51 changes: 51 additions & 0 deletions pkg/commands/commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package commands

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

fdeployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
)

func TestNew(t *testing.T) {
t.Parallel()

lggr := logger.Nop()
cmds := New(lggr)

require.NotNil(t, cmds)
assert.Equal(t, lggr, cmds.lggr)
}

func TestCommands_State(t *testing.T) {
t.Parallel()

lggr := logger.Nop()
cmds := New(lggr)
dom := domain.NewDomain("/tmp", "testdomain")

cmd := cmds.State(dom, StateConfig{
ViewState: func(_ fdeployment.Environment, _ json.Marshaler) (json.Marshaler, error) {
return json.RawMessage(`{}`), nil
},
})

require.NotNil(t, cmd)
assert.Equal(t, "state", cmd.Use)
assert.Equal(t, "State commands", cmd.Short)

// Verify environment flag is present
envFlag := cmd.PersistentFlags().Lookup("environment")
require.NotNil(t, envFlag)
assert.Equal(t, "e", envFlag.Shorthand)

// Verify generate subcommand exists
subs := cmd.Commands()
require.Len(t, subs, 1)
assert.Equal(t, "generate", subs[0].Use)
}
64 changes: 64 additions & 0 deletions pkg/commands/state/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package state

import (
"github.com/spf13/cobra"

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

// Config holds the configuration for state commands.
type Config struct {
// Logger is the logger to use for command output. Required.
Logger logger.Logger

// Domain is the domain context for the commands. Required.
Domain domain.Domain

// ViewState is the function that generates state from an environment.
// This is domain-specific and must be provided by the user.
ViewState ViewStateFunc

// Deps holds optional dependencies that can be overridden.
// If fields are nil, production defaults are used.
Deps Deps
}

// deps returns the Deps with defaults applied.
func (c *Config) deps() *Deps {
c.Deps.applyDefaults()

return &c.Deps
}

// NewCommand creates a new state command with all subcommands.
// The command requires an environment flag (-e) which is used by all subcommands.
//
// Usage:
//
// rootCmd.AddCommand(state.NewCommand(state.Config{
// Logger: lggr,
// Domain: myDomain,
// ViewState: myViewStateFunc,
// }))
func NewCommand(cfg Config) *cobra.Command {
// Apply defaults for optional dependencies
cfg.deps()

cmd := &cobra.Command{
Use: "state",
Short: "State commands",
}

// Add subcommands
cmd.AddCommand(newGenerateCmd(cfg))

// The environment flag is persistent because all subcommands require it.
// Currently there's only "generate", but this pattern supports future subcommands
// that also need the environment context.
cmd.PersistentFlags().
StringP("environment", "e", "", "Deployment environment (required)")
_ = cmd.MarkPersistentFlagRequired("environment")

return cmd
}
Loading
Loading