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
5 changes: 5 additions & 0 deletions .changeset/forty-islands-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

feat(state): add --datastore flag (file|catalog) for state generate; defaults to domain.yaml when unset
111 changes: 102 additions & 9 deletions engine/cld/commands/state/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ func TestNewCommand_GenerateFlags(t *testing.T) {
require.NotNil(t, pr)
assert.Equal(t, "true", pr.Value.String())

// Datastore flag (default empty = use domain.yaml)
ds := sub.Flags().Lookup("datastore")
require.NotNil(t, ds)
assert.Empty(t, ds.Value.String())

break
}
}
Expand Down Expand Up @@ -363,9 +368,8 @@ func TestGenerate_EnvironmentLoadError(t *testing.T) {

execErr := cmd.Execute()

require.Error(t, execErr)
assert.Contains(t, execErr.Error(), "failed to load environment")
assert.Contains(t, execErr.Error(), expectedError.Error())
require.ErrorContains(t, execErr, "failed to load environment")
require.ErrorContains(t, execErr, expectedError.Error())
}

// TestGenerate_ViewStateError verifies error handling.
Expand Down Expand Up @@ -399,9 +403,99 @@ func TestGenerate_ViewStateError(t *testing.T) {

execErr := cmd.Execute()

require.Error(t, execErr)
assert.Contains(t, execErr.Error(), "unable to snapshot state")
assert.Contains(t, execErr.Error(), expectedError.Error())
require.ErrorContains(t, execErr, "unable to snapshot state")
require.ErrorContains(t, execErr, expectedError.Error())
}

// TestGenerate_WithDatastoreFlag verifies that --datastore file and --datastore catalog pass the corresponding option to the environment loader.
func TestGenerate_WithDatastoreFlag(t *testing.T) {
t.Parallel()

t.Run("file", func(t *testing.T) {
t.Parallel()

var receivedOpts []environment.LoadEnvironmentOption
cmd, err := NewCommand(Config{
Logger: logger.Nop(),
Domain: domain.NewDomain("/tmp", "testdomain"),
ViewState: func(_ fdeployment.Environment, _ json.Marshaler) (json.Marshaler, error) {
return &mockState{Data: map[string]any{}}, nil
},
Deps: Deps{
EnvironmentLoader: func(_ context.Context, _ domain.Domain, envKey string, opts ...environment.LoadEnvironmentOption) (fdeployment.Environment, error) {
receivedOpts = opts
return fdeployment.Environment{Name: envKey}, nil
},
StateLoader: func(_ domain.EnvDir) (domain.JSONSerializer, error) {
return nil, os.ErrNotExist
},
},
})
require.NoError(t, err)
out := new(bytes.Buffer)
cmd.SetOut(out)
cmd.SetErr(out)
cmd.SetArgs([]string{"generate", "-e", "staging", "--datastore", "file"})
require.NoError(t, cmd.Execute())
require.Len(t, receivedOpts, 2, "expected WithLogger + WithDatastoreType(file)")
})

t.Run("catalog", func(t *testing.T) {
t.Parallel()

var receivedOpts []environment.LoadEnvironmentOption
cmd, err := NewCommand(Config{
Logger: logger.Nop(),
Domain: domain.NewDomain("/tmp", "testdomain"),
ViewState: func(_ fdeployment.Environment, _ json.Marshaler) (json.Marshaler, error) {
return &mockState{Data: map[string]any{}}, nil
},
Deps: Deps{
EnvironmentLoader: func(_ context.Context, _ domain.Domain, envKey string, opts ...environment.LoadEnvironmentOption) (fdeployment.Environment, error) {
receivedOpts = opts
return fdeployment.Environment{Name: envKey}, nil
},
StateLoader: func(_ domain.EnvDir) (domain.JSONSerializer, error) {
return nil, os.ErrNotExist
},
},
})
require.NoError(t, err)
out := new(bytes.Buffer)
cmd.SetOut(out)
cmd.SetErr(out)
cmd.SetArgs([]string{"generate", "-e", "staging", "--datastore", "catalog"})
require.NoError(t, cmd.Execute())
require.Len(t, receivedOpts, 2, "expected WithLogger + WithDatastoreType(catalog)")
})
}

// TestGenerate_InvalidDatastoreFlag verifies that an invalid --datastore value returns an error.
func TestGenerate_InvalidDatastoreFlag(t *testing.T) {
t.Parallel()

cmd, err := NewCommand(Config{
Logger: logger.Nop(),
Domain: domain.NewDomain("/tmp", "testdomain"),
ViewState: func(_ fdeployment.Environment, _ json.Marshaler) (json.Marshaler, error) {
return &mockState{Data: map[string]any{}}, nil
},
Deps: Deps{
EnvironmentLoader: func(_ context.Context, _ domain.Domain, _ string, _ ...environment.LoadEnvironmentOption) (fdeployment.Environment, error) {
return fdeployment.Environment{}, nil
},
StateLoader: func(_ domain.EnvDir) (domain.JSONSerializer, error) {
return nil, os.ErrNotExist
},
},
})
require.NoError(t, err)
out := new(bytes.Buffer)
cmd.SetOut(out)
cmd.SetErr(out)
cmd.SetArgs([]string{"generate", "-e", "staging", "--datastore", "invalid"})
execErr := cmd.Execute()
require.ErrorContains(t, execErr, `--datastore must be "file" or "catalog"`)
}

// TestGenerate_StateSaveError verifies error handling.
Expand Down Expand Up @@ -438,9 +532,8 @@ func TestGenerate_StateSaveError(t *testing.T) {

execErr := cmd.Execute()

require.Error(t, execErr)
assert.Contains(t, execErr.Error(), "failed to save state")
assert.Contains(t, execErr.Error(), expectedError.Error())
require.ErrorContains(t, execErr, "failed to save state")
require.ErrorContains(t, execErr, expectedError.Error())
}

// TestConfig_Validate verifies validation catches missing required fields.
Expand Down
24 changes: 23 additions & 1 deletion engine/cld/commands/state/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/commands/flags"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/commands/text"
cfgdomain "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/domain"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/environment"
)
Expand Down Expand Up @@ -43,6 +44,9 @@ var (

By default, the generated state is not saved. Use --persist to save
to disk, or --print to output the full JSON to stdout.

Use --datastore file or --datastore catalog to override the datastore
source; when omitted, the setting from domain.yaml is used.
`)

generateExample = text.Examples(`
Expand All @@ -52,6 +56,9 @@ var (
# Generate and save state to default location (also prints)
myapp state generate -e staging --persist

# Use file datastore (overrides domain.yaml); omit --datastore to use domain default
myapp state generate -e testnet --persist --datastore=file

# Generate and save to custom path without printing
myapp state generate -e staging -p -o /path/to/state.json --print=false

Expand All @@ -66,6 +73,7 @@ type generateFlags struct {
output string
previousState string
print bool
datastore string // "file", "catalog", or empty for domain default
}

// newGenerateCmd creates the "generate" subcommand for generating state.
Expand All @@ -82,6 +90,7 @@ func newGenerateCmd(cfg Config) *cobra.Command {
output: flags.MustString(cmd.Flags().GetString("out")),
previousState: flags.MustString(cmd.Flags().GetString("prev")),
print: flags.MustBool(cmd.Flags().GetBool("print")),
datastore: flags.MustString(cmd.Flags().GetString("datastore")),
}

return runGenerate(cmd, cfg, f)
Expand All @@ -96,6 +105,7 @@ func newGenerateCmd(cfg Config) *cobra.Command {
// Local flags specific to this command
cmd.Flags().BoolP("persist", "p", false, "Persist state to disk")
cmd.Flags().StringP("prev", "s", "", "Previous state file path")
cmd.Flags().String("datastore", "", "Datastore to use: file or catalog. Defaults to domain.yaml setting when unset.")

// Deprecated alias: --previousState -> --prev
addPreviousStateAlias(cmd)
Expand All @@ -111,6 +121,11 @@ func runGenerate(cmd *cobra.Command, cfg Config, f generateFlags) error {
output := f.output
previousState := f.previousState
shouldPrint := f.print
datastoreFlag := f.datastore

if datastoreFlag != "" && datastoreFlag != "file" && datastoreFlag != "catalog" {
return fmt.Errorf("--datastore must be %q or %q, got %q", "file", "catalog", datastoreFlag)
}

deps := cfg.deps()
envdir := cfg.Domain.EnvDir(envKey)
Expand All @@ -124,7 +139,14 @@ func runGenerate(cmd *cobra.Command, cfg Config, f generateFlags) error {
ctx, cancel := context.WithTimeout(cmd.Context(), viewTimeout)
defer cancel()

env, err := deps.EnvironmentLoader(ctx, cfg.Domain, envKey, environment.WithLogger(cfg.Logger))
envOpts := []environment.LoadEnvironmentOption{environment.WithLogger(cfg.Logger)}
switch datastoreFlag {
case "file":
envOpts = append(envOpts, environment.WithDatastoreType(cfgdomain.DatastoreTypeFile))
case "catalog":
envOpts = append(envOpts, environment.WithDatastoreType(cfgdomain.DatastoreTypeCatalog))
}
env, err := deps.EnvironmentLoader(ctx, cfg.Domain, envKey, envOpts...)
if err != nil {
return fmt.Errorf("failed to load environment: %w", err)
}
Expand Down
11 changes: 9 additions & 2 deletions engine/cld/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ func Load(

var ds fdatastore.DataStore

if cfg.DatastoreType == cfgdomain.DatastoreTypeCatalog || cfg.DatastoreType == cfgdomain.DatastoreTypeAll {
effectiveDatastoreType := cfg.DatastoreType
if loadcfg.datastoreType != nil {
effectiveDatastoreType = *loadcfg.datastoreType
}
if useCatalog(effectiveDatastoreType) {
if cfg.Env.Catalog.GRPC != "" {
lggr.Infow("Fetching data from Catalog", "url", cfg.Env.Catalog.GRPC)
catalogStore, catalogErr := cldcatalog.LoadCatalog(ctx, envKey, cfg, domain)
Expand All @@ -67,7 +71,6 @@ func Load(
return fdeployment.Environment{}, fmt.Errorf("catalog GRPC endpoint is required when datastore location is set to '%s'", cfgdomain.DatastoreTypeCatalog)
}
} else {
// Load datastore from file system (default behavior)
ds, err = envdir.DataStore()
if err != nil {
return fdeployment.Environment{}, err
Expand Down Expand Up @@ -143,3 +146,7 @@ func Load(
BlockChains: blockChains,
}, nil
}

func useCatalog(datastoreType cfgdomain.DatastoreType) bool {
return datastoreType == cfgdomain.DatastoreTypeCatalog || datastoreType == cfgdomain.DatastoreTypeAll
}
12 changes: 12 additions & 0 deletions engine/cld/environment/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package environment
import (
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"

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

Expand Down Expand Up @@ -40,6 +41,9 @@ type LoadConfig struct {
// useDryRunJobDistributor configures the environment to use a dry-run Job Distributor
// that allows read operations but performs noop write operations.
useDryRunJobDistributor bool

// datastoreType when set, overrides the datastore type from domain config (e.g. from --datastore flag).
datastoreType *cfgdomain.DatastoreType
}

// Configure applies a slice of LoadEnvironmentOption functions to the LoadConfig.
Expand Down Expand Up @@ -177,3 +181,11 @@ func WithDryRunJobDistributor() LoadEnvironmentOption {
o.useDryRunJobDistributor = true
}
}

// WithDatastoreType overrides the datastore type from domain config. Use when the caller
// explicitly requests "file" or "catalog" (e.g. from a CLI flag). Omit to use domain default.
func WithDatastoreType(t cfgdomain.DatastoreType) LoadEnvironmentOption {
return func(o *LoadConfig) {
o.datastoreType = &t
}
}
3 changes: 1 addition & 2 deletions engine/cld/environment/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"

foperations "github.com/smartcontractkit/chainlink-deployments-framework/operations"
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
)

func Test_WithAnvilKeyAsDeployer(t *testing.T) {
Expand Down
Loading