diff --git a/.changeset/flat-baboons-lose.md b/.changeset/flat-baboons-lose.md new file mode 100644 index 000000000..113866515 --- /dev/null +++ b/.changeset/flat-baboons-lose.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +Add CRE CLI runner object. diff --git a/.mockery.yml b/.mockery.yml index 7b70107d9..cd5ba3f96 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -149,3 +149,12 @@ packages: filename: "{{.InterfaceName | snakecase}}.go" interfaces: APIClientWrapped: + github.com/smartcontractkit/chainlink-deployments-framework/cre: + config: + all: false + pkgname: "cremocks" + dir: "cre/mocks" + filename: "mock_{{.InterfaceName | snakecase}}.go" + structname: "{{.Mock}}{{.InterfaceName}}" + interfaces: + Runner: diff --git a/cre/cli_runner.go b/cre/cli_runner.go new file mode 100644 index 000000000..67a47c031 --- /dev/null +++ b/cre/cli_runner.go @@ -0,0 +1,76 @@ +package cre + +import ( + "bytes" + "context" + "errors" + "io" + "os/exec" +) + +const defaultBinary = "cre" + +// CLIRunner runs the CRE CLI via os/exec. Run executes the binary and captures stdout/stderr. +type CLIRunner struct { + binaryPath string + // Stdout, if set, receives a real-time copy of the process stdout while it runs. + Stdout io.Writer + // Stderr, if set, receives a real-time copy of the process stderr while it runs. + Stderr io.Writer +} + +// NewCLIRunner returns a CLIRunner for the given binary path. An empty path defaults to "cre" +// (resolved via PATH). +func NewCLIRunner(binaryPath string) *CLIRunner { + if binaryPath == "" { + binaryPath = defaultBinary + } + + return &CLIRunner{binaryPath: binaryPath} +} + +// Run executes the binary and captures stdout and stderr. Exit code 0 returns (res, nil); +// exit code != 0 returns (res, *ExitError) so callers get both result and error. +// Runner-related failures (binary not found, context canceled) return (nil, err). +func (r *CLIRunner) Run(ctx context.Context, args ...string) (*CallResult, error) { + //nolint:gosec // G204: This is intentional - we're running a CLI tool with user-provided arguments. + // The binary path is controlled via configuration, and args are expected to be user-provided CLI arguments. + cmd := exec.CommandContext(ctx, r.binaryPath, args...) + + var stdout, stderr bytes.Buffer + cmd.Stdout = wrapWriter(&stdout, r.Stdout) + cmd.Stderr = wrapWriter(&stderr, r.Stderr) + + err := cmd.Run() + res := &CallResult{ + Stdout: stdout.Bytes(), + Stderr: stderr.Bytes(), + ExitCode: 0, + } + if err != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + res.ExitCode = exitErr.ExitCode() + return res, &ExitError{ + ExitCode: res.ExitCode, + Stdout: res.Stdout, + Stderr: res.Stderr, + } + } + + return nil, err + } + + return res, nil +} + +func wrapWriter(buf *bytes.Buffer, stream io.Writer) io.Writer { + if stream == nil { + return buf + } + + return io.MultiWriter(buf, stream) +} diff --git a/cre/cli_runner_test.go b/cre/cli_runner_test.go new file mode 100644 index 000000000..c66df573a --- /dev/null +++ b/cre/cli_runner_test.go @@ -0,0 +1,183 @@ +package cre + +import ( + "bytes" + "context" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewCLIRunner(t *testing.T) { + t.Parallel() + tests := []struct { + name string + binaryPath string + want string + }{ + {"empty_defaults_to_cre", "", defaultBinary}, + {"custom_path", "/opt/cre", "/opt/cre"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := NewCLIRunner(tt.binaryPath) + require.Equal(t, tt.want, r.binaryPath) + }) + } +} + +func TestCLIRunner_Run(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setupCtx func(*testing.T) context.Context + runner *CLIRunner + args []string + wantErr bool + wantResNil bool + wantExitCode int + wantStdout string + wantStderr string + wantErrIs error + wantExitErr bool + }{ + { + name: "binary_not_found", + runner: NewCLIRunner(filepath.Join(t.TempDir(), "nonexistent-cre-xyz")), + args: []string{"build"}, + wantErr: true, + wantResNil: true, + }, + { + name: "context_already_canceled", + runner: NewCLIRunner("/bin/sh"), + setupCtx: func(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + return ctx + }, + args: []string{"-c", "echo unreachable"}, + wantErr: true, + wantResNil: true, + wantErrIs: context.Canceled, + }, + { + name: "nonzero_exit_captures_output", + runner: NewCLIRunner("/bin/sh"), + args: []string{"-c", `echo "fail out"; echo "fail err" >&2; exit 41`}, + wantErr: true, + wantExitCode: 41, + wantStdout: "fail out\n", + wantStderr: "fail err\n", + wantExitErr: true, + }, + { + name: "success_with_output", + runner: NewCLIRunner("/bin/sh"), + args: []string{"-c", `echo "hello stdout"; echo "hello stderr" >&2`}, + wantStdout: "hello stdout\n", + wantStderr: "hello stderr\n", + wantExitCode: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := t.Context() + if tt.setupCtx != nil { + ctx = tt.setupCtx(t) + } + + res, err := tt.runner.Run(ctx, tt.args...) + + if tt.wantResNil { + require.Nil(t, res) + } + if tt.wantErr { + require.Error(t, err) + if tt.wantErrIs != nil { + require.ErrorIs(t, err, tt.wantErrIs) + } + if tt.wantExitErr { + var exitErr *ExitError + require.ErrorAs(t, err, &exitErr) + require.Equal(t, tt.wantExitCode, exitErr.ExitCode) + } + } else { + require.NoError(t, err) + } + + if res != nil { + require.Equal(t, tt.wantExitCode, res.ExitCode) + require.Equal(t, tt.wantStdout, string(res.Stdout)) + require.Equal(t, tt.wantStderr, string(res.Stderr)) + } + }) + } +} + +func TestCLIRunner_StreamingWriters(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + wantStdout string + wantStderr string + }{ + { + name: "stdout_streamed", + args: []string{"-c", `echo "hello from stdout"`}, + wantStdout: "hello from stdout\n", + wantStderr: "", + }, + { + name: "stderr_streamed", + args: []string{"-c", `echo "hello from stderr" >&2`}, + wantStdout: "", + wantStderr: "hello from stderr\n", + }, + { + name: "both_streamed", + args: []string{"-c", `echo "out"; echo "err" >&2`}, + wantStdout: "out\n", + wantStderr: "err\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var streamOut, streamErr bytes.Buffer + r := NewCLIRunner("/bin/sh") + r.Stdout = &streamOut + r.Stderr = &streamErr + + res, err := r.Run(t.Context(), tt.args...) + require.NoError(t, err) + + require.Equal(t, tt.wantStdout, streamOut.String(), "streamed stdout") + require.Equal(t, tt.wantStderr, streamErr.String(), "streamed stderr") + + require.Equal(t, tt.wantStdout, string(res.Stdout), "captured stdout") + require.Equal(t, tt.wantStderr, string(res.Stderr), "captured stderr") + }) + } +} + +func TestCLIRunner_NilWriters_DefaultBehavior(t *testing.T) { + t.Parallel() + + r := NewCLIRunner("/bin/sh") + res, err := r.Run(t.Context(), "-c", `echo "works"`) + require.NoError(t, err) + require.Equal(t, "works\n", string(res.Stdout)) +} diff --git a/cre/exit.go b/cre/exit.go new file mode 100644 index 000000000..0edf5b62f --- /dev/null +++ b/cre/exit.go @@ -0,0 +1,16 @@ +package cre + +import "fmt" + +// ExitError is returned when the CRE process ran and exited with a non-zero code. +// Use errors.As to inspect ExitCode, Stdout, and Stderr. Result is still returned +// from Run (for example Runner.Run or CLIRunner.Run) so callers can log or inspect output. +type ExitError struct { + ExitCode int + Stdout []byte + Stderr []byte +} + +func (e *ExitError) Error() string { + return fmt.Sprintf("cre: exited with code %d", e.ExitCode) +} diff --git a/cre/exit_test.go b/cre/exit_test.go new file mode 100644 index 000000000..c599032d7 --- /dev/null +++ b/cre/exit_test.go @@ -0,0 +1,17 @@ +package cre + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExitError(t *testing.T) { + t.Parallel() + e := &ExitError{ExitCode: 3, Stderr: []byte("failed")} + require.ErrorContains(t, e, "code 3") + var out *ExitError + require.ErrorAs(t, e, &out) + require.Equal(t, 3, out.ExitCode) + require.Equal(t, "failed", string(out.Stderr)) +} diff --git a/cre/mocks/mock_runner.go b/cre/mocks/mock_runner.go new file mode 100644 index 000000000..f5d034b9d --- /dev/null +++ b/cre/mocks/mock_runner.go @@ -0,0 +1,116 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package cremocks + +import ( + "context" + + "github.com/smartcontractkit/chainlink-deployments-framework/cre" + mock "github.com/stretchr/testify/mock" +) + +// NewMockRunner creates a new instance of MockRunner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRunner(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRunner { + mock := &MockRunner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockRunner is an autogenerated mock type for the Runner type +type MockRunner struct { + mock.Mock +} + +type MockRunner_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRunner) EXPECT() *MockRunner_Expecter { + return &MockRunner_Expecter{mock: &_m.Mock} +} + +// Call provides a mock function for the type MockRunner +func (_mock *MockRunner) Run(ctx context.Context, args ...string) (*cre.CallResult, error) { + var tmpRet mock.Arguments + if len(args) > 0 { + tmpRet = _mock.Called(ctx, args) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Run") + } + + var r0 *cre.CallResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) (*cre.CallResult, error)); ok { + return returnFunc(ctx, args...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *cre.CallResult); ok { + r0 = returnFunc(ctx, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*cre.CallResult) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, ...string) error); ok { + r1 = returnFunc(ctx, args...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockRunner_Call_Call is a *mock.Run that shadows Run/Return methods with type explicit version for method 'Run' +type MockRunner_Call_Call struct { + *mock.Call +} + +// Call is a helper method to define mock.On call +// - ctx context.Context +// - args ...string +func (_e *MockRunner_Expecter) Call(ctx interface{}, args ...interface{}) *MockRunner_Call_Call { + return &MockRunner_Call_Call{Call: _e.mock.On("Run", + append([]interface{}{ctx}, args...)...)} +} + +func (_c *MockRunner_Call_Call) Run(run func(ctx context.Context, args ...string)) *MockRunner_Call_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + var variadicArgs []string + if len(args) > 1 { + variadicArgs = args[1].([]string) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockRunner_Call_Call) Return(callResult *cre.CallResult, err error) *MockRunner_Call_Call { + _c.Call.Return(callResult, err) + return _c +} + +func (_c *MockRunner_Call_Call) RunAndReturn(run func(ctx context.Context, args ...string) (*cre.CallResult, error)) *MockRunner_Call_Call { + _c.Call.Return(run) + return _c +} diff --git a/cre/runner.go b/cre/runner.go new file mode 100644 index 000000000..5bc293987 --- /dev/null +++ b/cre/runner.go @@ -0,0 +1,15 @@ +package cre + +import "context" + +// CallResult holds stdout, stderr, and exit code from a completed CRE call. +type CallResult struct { + Stdout []byte + Stderr []byte + ExitCode int +} + +// Runner is used to invoke the CRE CLI. +type Runner interface { + Run(ctx context.Context, args ...string) (*CallResult, error) +} diff --git a/deployment/environment.go b/deployment/environment.go index 8b05e82c1..ab7b30d99 100644 --- a/deployment/environment.go +++ b/deployment/environment.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/cre" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/offchain" "github.com/smartcontractkit/chainlink-deployments-framework/offchain/ocr" @@ -60,6 +61,10 @@ type Environment struct { OperationsBundle operations.Bundle // BlockChains is the container of all chains in the environment. BlockChains chain.BlockChains + // CRERunner invokes the CRE CLI from changesets. Optional in environment loading, so it won't + // fail to load environment if cre binary not available; failures (e.g. binary not found) only occur + // when CRERunner.Run() is used inside changesets. May be nil in test environments that did not set it. + CRERunner cre.Runner } // EnvironmentOption is a functional option for configuring an Environment @@ -100,6 +105,13 @@ func NewEnvironment( return env } +// WithCRERunner sets the CRE CLI runner +func WithCRERunner(r cre.Runner) EnvironmentOption { + return func(e *Environment) { + e.CRERunner = r + } +} + // Clone creates a copy of the environment with a new reference to the address book. func (e Environment) Clone() Environment { ab := NewMemoryAddressBook() @@ -125,6 +137,7 @@ func (e Environment) Clone() Environment { OCRSecrets: e.OCRSecrets, OperationsBundle: e.OperationsBundle, BlockChains: e.BlockChains, + CRERunner: e.CRERunner, } } diff --git a/engine/cld/config/env/config.go b/engine/cld/config/env/config.go index fb5d66b00..411cf1960 100644 --- a/engine/cld/config/env/config.go +++ b/engine/cld/config/env/config.go @@ -140,6 +140,25 @@ type CatalogConfig struct { Auth *CatalogAuthConfig `mapstructure:"auth" yaml:"auth,omitempty"` // The authentication configuration for the Catalog. } +// CREAuthConfig holds authentication settings for CRE (Chainlink Runtime Environment) deploy operations. +// WARNING: This data type contains sensitive fields and should not be logged or set in file +// configuration. +type CREAuthConfig struct { + HMACKeyID string `mapstructure:"hmac_key_id" yaml:"hmac_key_id"` // Secret: HMAC key ID + HMACKeySecret string `mapstructure:"hmac_key_secret" yaml:"hmac_key_secret"` // Secret: HMAC key secret + TenantID string `mapstructure:"tenant_id" yaml:"tenant_id"` + OrgID string `mapstructure:"org_id" yaml:"org_id"` +} + +// CREConfig is the configuration for CRE deploy and related CLI usage (credentials, endpoints, timeouts). +type CREConfig struct { + Auth CREAuthConfig `mapstructure:"auth" yaml:"auth"` + TLS string `mapstructure:"tls" yaml:"tls"` + Timeout string `mapstructure:"timeout" yaml:"timeout"` + StorageAddress string `mapstructure:"storage_address" yaml:"storage_address"` + DonFamily string `mapstructure:"don_family" yaml:"don_family"` +} + // OnchainConfig wraps the configuration for the onchain components. type OnchainConfig struct { KMS KMSConfig `mapstructure:"kms" yaml:"kms"` @@ -164,6 +183,7 @@ type Config struct { Onchain OnchainConfig `mapstructure:"onchain" yaml:"onchain"` Offchain OffchainConfig `mapstructure:"offchain" yaml:"offchain"` Catalog CatalogConfig `mapstructure:"catalog" yaml:"catalog"` + CRE CREConfig `mapstructure:"cre" yaml:"cre,omitempty"` } // Load loads the config from the file path, falling back to env vars if the file does not exist. @@ -259,6 +279,14 @@ var ( "catalog.grpc": {"CATALOG_GRPC"}, "catalog.auth.kms_key_id": {"CATALOG_AUTH_KMS_KEY_ID"}, "catalog.auth.kms_key_region": {"CATALOG_AUTH_KMS_KEY_REGION"}, + "cre.auth.hmac_key_id": {"CRE_DEPLOY_HMAC_KEY_ID"}, + "cre.auth.hmac_key_secret": {"CRE_DEPLOY_HMAC_SECRET"}, + "cre.auth.tenant_id": {"CRE_TENANT_ID"}, + "cre.auth.org_id": {"CRE_ORG_ID"}, + "cre.tls": {"CRE_TLS"}, + "cre.timeout": {"CRE_TIMEOUT"}, + "cre.storage_address": {"CRE_STORAGE_ADDR"}, + "cre.don_family": {"CRE_DON_FAMILY"}, } ) diff --git a/engine/cld/config/env/config_test.go b/engine/cld/config/env/config_test.go index 0d382cdc3..0850f0016 100644 --- a/engine/cld/config/env/config_test.go +++ b/engine/cld/config/env/config_test.go @@ -73,6 +73,7 @@ var ( KMSKeyRegion: "us-east-1", }, }, + CRE: CREConfig{}, } // envVars is the environment variables that used to set the config. @@ -194,6 +195,7 @@ var ( KMSKeyRegion: "us-east-1", }, }, + CRE: CREConfig{}, } ) @@ -235,6 +237,7 @@ func Test_Load(t *testing.T) { //nolint:paralleltest // see comment in setupTest OCR: OCRConfig{}, }, Catalog: CatalogConfig{}, + CRE: CREConfig{}, }, }, { @@ -333,6 +336,29 @@ func Test_LoadEnv_Legacy(t *testing.T) { //nolint:paralleltest // see comment in assert.Equal(t, envCfg, got) } +func Test_LoadEnv_BindsCREFromEnv(t *testing.T) { //nolint:paralleltest // see comment in setupEnvVars + t.Setenv("CRE_DEPLOY_HMAC_KEY_ID", "kid-1") + t.Setenv("CRE_DEPLOY_HMAC_SECRET", "secret-1") + t.Setenv("CRE_TENANT_ID", "tenant-1") + t.Setenv("CRE_ORG_ID", "org-1") + t.Setenv("CRE_TLS", "true") + t.Setenv("CRE_TIMEOUT", "30s") + t.Setenv("CRE_STORAGE_ADDR", "addr-1") + t.Setenv("CRE_DON_FAMILY", "family-1") + + got, err := LoadEnv() + require.NoError(t, err) + + require.Equal(t, "kid-1", got.CRE.Auth.HMACKeyID) + require.Equal(t, "secret-1", got.CRE.Auth.HMACKeySecret) + require.Equal(t, "tenant-1", got.CRE.Auth.TenantID) + require.Equal(t, "org-1", got.CRE.Auth.OrgID) + require.Equal(t, "true", got.CRE.TLS) + require.Equal(t, "30s", got.CRE.Timeout) + require.Equal(t, "addr-1", got.CRE.StorageAddress) + require.Equal(t, "family-1", got.CRE.DonFamily) +} + func Test_YAML_Marshal_Unmarshal(t *testing.T) { t.Parallel() diff --git a/engine/cld/environment/environment.go b/engine/cld/environment/environment.go index a0f8ae2da..c778de446 100644 --- a/engine/cld/environment/environment.go +++ b/engine/cld/environment/environment.go @@ -144,6 +144,7 @@ func Load( OCRSecrets: sharedSecrets, OperationsBundle: operations.NewBundle(getCtx, lggr, loadcfg.reporter, operations.WithOperationRegistry(loadcfg.operationRegistry)), BlockChains: blockChains, + CRERunner: loadcfg.creRunner, }, nil } diff --git a/engine/cld/environment/fork.go b/engine/cld/environment/fork.go index fb86f4e10..62eebd178 100644 --- a/engine/cld/environment/fork.go +++ b/engine/cld/environment/fork.go @@ -149,6 +149,7 @@ func LoadFork( func() context.Context { return ctx }, focr.XXXGenerateTestOCRSecrets(), fchain.NewBlockChains(blockChains), + fdeployment.WithCRERunner(loadcfg.creRunner), ) return ForkedEnvironment{ diff --git a/engine/cld/environment/options.go b/engine/cld/environment/options.go index 55b26d3b0..7f1fdfd95 100644 --- a/engine/cld/environment/options.go +++ b/engine/cld/environment/options.go @@ -1,10 +1,10 @@ package environment import ( - "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" - + "github.com/smartcontractkit/chainlink-deployments-framework/cre" cfgdomain "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/config/domain" "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" ) // LoadConfig contains configuration parameters for loading an environment. @@ -44,6 +44,8 @@ type LoadConfig struct { // datastoreType when set, overrides the datastore type from domain config (e.g. from --datastore flag). datastoreType *cfgdomain.DatastoreType + + creRunner cre.Runner } // Configure applies a slice of LoadEnvironmentOption functions to the LoadConfig. @@ -63,11 +65,11 @@ func newLoadConfig() (*LoadConfig, error) { return nil, err } - // Default options return &LoadConfig{ reporter: operations.NewMemoryReporter(), operationRegistry: operations.NewOperationRegistry(), lggr: lggr, + creRunner: cre.NewCLIRunner(""), }, nil } @@ -189,3 +191,12 @@ func WithDatastoreType(t cfgdomain.DatastoreType) LoadEnvironmentOption { o.datastoreType = &t } } + +// WithCRERunner overrides the default CRE CLI runner. The default is a [cre.CLIRunner] that +// resolves "cre" from PATH. Use this to supply a custom binary path +// (e.g. WithCRERunner(cre.NewCLIRunner("/opt/cre"))) or a mock in tests. +func WithCRERunner(r cre.Runner) LoadEnvironmentOption { + return func(o *LoadConfig) { + o.creRunner = r + } +} diff --git a/engine/cld/environment/options_test.go b/engine/cld/environment/options_test.go index 9582d8c59..b7d9582ea 100644 --- a/engine/cld/environment/options_test.go +++ b/engine/cld/environment/options_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-deployments-framework/cre" foperations "github.com/smartcontractkit/chainlink-deployments-framework/operations" "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" ) @@ -110,3 +111,27 @@ func Test_WithDryRunJobDistributor(t *testing.T) { assert.True(t, opts.useDryRunJobDistributor) } + +func Test_WithCRERunner(t *testing.T) { + t.Parallel() + + opts := &LoadConfig{} + assert.Nil(t, opts.creRunner) + + runner := cre.NewCLIRunner("/path/to/cre") + option := WithCRERunner(runner) + option(opts) + + assert.Equal(t, runner, opts.creRunner) +} + +func Test_newLoadConfig_defaultCRERunner(t *testing.T) { + t.Parallel() + + cfg, err := newLoadConfig() + require.NoError(t, err) + + require.NotNil(t, cfg.creRunner) + _, ok := cfg.creRunner.(*cre.CLIRunner) + require.True(t, ok, "expected *cre.CLIRunner, got %T", cfg.creRunner) +} diff --git a/engine/test/environment/components.go b/engine/test/environment/components.go index a63d52e5b..f94e345f5 100644 --- a/engine/test/environment/components.go +++ b/engine/test/environment/components.go @@ -6,6 +6,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" fchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/cre" fdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" fdeployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" foffchain "github.com/smartcontractkit/chainlink-deployments-framework/offchain" @@ -21,6 +22,7 @@ type components struct { OffchainClient foffchain.Client NodeIDs []string Logger logger.Logger + CRERunner cre.Runner } // newComponents creates a new components instance. diff --git a/engine/test/environment/environment.go b/engine/test/environment/environment.go index 4eea237af..d7dddc27f 100644 --- a/engine/test/environment/environment.go +++ b/engine/test/environment/environment.go @@ -67,6 +67,7 @@ func (l *Loader) Load(ctx context.Context, opts ...LoadOpt) (*fdeployment.Enviro oc = foffchain.NewMemoryJobDistributor() } + // CRERunner may be nil; tests that need CRE use WithCRERunner(cremocks.NewMockRunner(t)). return &fdeployment.Environment{ Name: environmentName, Logger: cmps.Logger, @@ -78,6 +79,7 @@ func (l *Loader) Load(ctx context.Context, opts ...LoadOpt) (*fdeployment.Enviro GetContext: getCtx, OCRSecrets: focr.XXXGenerateTestOCRSecrets(), OperationsBundle: foperations.NewBundle(getCtx, cmps.Logger, foperations.NewMemoryReporter()), + CRERunner: cmps.CRERunner, }, nil } diff --git a/engine/test/environment/options.go b/engine/test/environment/options.go index 0957cf430..573df7fac 100644 --- a/engine/test/environment/options.go +++ b/engine/test/environment/options.go @@ -6,6 +6,7 @@ import ( fchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + "github.com/smartcontractkit/chainlink-deployments-framework/cre" fdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" fdeployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/onchain" @@ -264,3 +265,11 @@ func withChainLoaderN(t *testing.T, loader *onchain.ChainLoader, n int) LoadOpt return nil } } + +// WithCRERunner sets the CRE CLI runner for the test environment. +func WithCRERunner(r cre.Runner) LoadOpt { + return func(cmps *components) error { + cmps.CRERunner = r + return nil + } +} diff --git a/engine/test/environment/options_test.go b/engine/test/environment/options_test.go index 6db5a3b07..3cafe126f 100644 --- a/engine/test/environment/options_test.go +++ b/engine/test/environment/options_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" fchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/cre" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/internal/testutils" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/onchain" ) @@ -125,3 +126,17 @@ func Test_withChainLoaderN(t *testing.T) { }) } } + +func Test_WithCRERunner(t *testing.T) { + t.Parallel() + + cmps := newComponents() + require.Nil(t, cmps.CRERunner) + + runner := cre.NewCLIRunner("/path/to/cre") + option := WithCRERunner(runner) + err := option(cmps) + + require.NoError(t, err) + require.Equal(t, runner, cmps.CRERunner) +} diff --git a/engine/test/runtime/environment.go b/engine/test/runtime/environment.go index 7c04bee71..abf81fa87 100644 --- a/engine/test/runtime/environment.go +++ b/engine/test/runtime/environment.go @@ -17,6 +17,7 @@ func newEnvFromState(fromEnv fdeployment.Environment, state *State) fdeployment. Offchain: fromEnv.Offchain, BlockChains: fromEnv.BlockChains, NodeIDs: fromEnv.NodeIDs, + CRERunner: fromEnv.CRERunner, // These fields are updated by changesets and are pulled from state ExistingAddresses: state.AddressBook,