diff --git a/core/config/env/env.go b/core/config/env/env.go index 37ae131ddf1..28218b232e8 100644 --- a/core/config/env/env.go +++ b/core/config/env/env.go @@ -8,12 +8,27 @@ import ( ) var ( - Config = Var("CL_CONFIG") + Config = Var("CL_CONFIG") + DatabaseAllowSimplePasswords = Var("CL_DATABASE_ALLOW_SIMPLE_PASSWORDS") + DatabaseURL = Secret("CL_DATABASE_URL") + DatabaseBackupURL = Secret("CL_DATABASE_BACKUP_URL") + PasswordKeystore = Secret("CL_PASSWORD_KEYSTORE") + PasswordVRF = Secret("CL_PASSWORD_VRF") + PyroscopeAuthToken = Secret("CL_PYROSCOPE_AUTH_TOKEN") + PrometheusAuthToken = Secret("CL_PROMETHEUS_AUTH_TOKEN") + ThresholdKeyShare = Secret("CL_THRESHOLD_KEY_SHARE") + // Migrations env vars + EVMChainIDNotNullMigration0195 = "CL_EVM_CHAINID_NOT_NULL_MIGRATION_0195" +) - // LOOPP commands and vars +// LOOPP commands and vars +var ( MedianPluginCmd = Var("CL_MEDIAN_CMD") + MedianPluginEnv = Var("CL_MEDIAN_ENV") SolanaPluginCmd = Var("CL_SOLANA_CMD") + SolanaPluginEnv = Var("CL_SOLANA_ENV") StarknetPluginCmd = Var("CL_STARKNET_CMD") + StarknetPluginEnv = Var("CL_STARKNET_ENV") // PrometheusDiscoveryHostName is the externally accessible hostname // published by the node in the `/discovery` endpoint. Generally, it is expected to match // the public hostname of node. @@ -22,24 +37,13 @@ var ( // In house we observed that the resolved value of os.Hostname was not accessible to // outside of the given pod PrometheusDiscoveryHostName = Var("CL_PROMETHEUS_DISCOVERY_HOSTNAME") - // EnvLooopHostName is the hostname used for HTTP communication between the + // LOOPPHostName is the hostname used for HTTP communication between the // node and LOOPps. In most cases this does not need to be set explicitly. LOOPPHostName = Var("CL_LOOPP_HOSTNAME") // Work around for Solana LOOPPs configured with zero values. MinOCR2MaxDurationQuery = Var("CL_MIN_OCR2_MAX_DURATION_QUERY") // PipelineOvertime is an undocumented escape hatch for overriding the default padding in pipeline executions. PipelineOvertime = Var("CL_PIPELINE_OVERTIME") - - DatabaseAllowSimplePasswords = Var("CL_DATABASE_ALLOW_SIMPLE_PASSWORDS") - DatabaseURL = Secret("CL_DATABASE_URL") - DatabaseBackupURL = Secret("CL_DATABASE_BACKUP_URL") - PasswordKeystore = Secret("CL_PASSWORD_KEYSTORE") - PasswordVRF = Secret("CL_PASSWORD_VRF") - PyroscopeAuthToken = Secret("CL_PYROSCOPE_AUTH_TOKEN") - PrometheusAuthToken = Secret("CL_PROMETHEUS_AUTH_TOKEN") - ThresholdKeyShare = Secret("CL_THRESHOLD_KEY_SHARE") - // Migrations env vars - EVMChainIDNotNullMigration0195 = "CL_EVM_CHAINID_NOT_NULL_MIGRATION_0195" ) type Var string diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 98ab5cbfaf9..24bd2815521 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -164,6 +164,7 @@ require ( github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect + github.com/hashicorp/go-envparse v0.1.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-plugin v1.5.2 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 170e36ad2cc..ec605e276f8 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -691,6 +691,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= +github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY= github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= diff --git a/core/services/chainlink/relayer_factory.go b/core/services/chainlink/relayer_factory.go index b4bd530d080..ae40e6d6544 100644 --- a/core/services/chainlink/relayer_factory.go +++ b/core/services/chainlink/relayer_factory.go @@ -127,10 +127,14 @@ func (r *RelayerFactory) NewSolana(ks keystore.Solana, chainCfgs solana.TOMLConf if err != nil { return nil, fmt.Errorf("failed to marshal Solana configs: %w", err) } - + envVars, err := plugins.ParseEnvFile(env.SolanaPluginEnv.Get()) + if err != nil { + return nil, fmt.Errorf("failed to parse Solana env file: %w", err) + } solCmdFn, err := plugins.NewCmdFactory(r.Register, plugins.CmdConfig{ ID: relayID.Name(), Cmd: cmdName, + Env: envVars, }) if err != nil { return nil, fmt.Errorf("failed to create Solana LOOP command: %w", err) @@ -197,9 +201,14 @@ func (r *RelayerFactory) NewStarkNet(ks keystore.StarkNet, chainCfgs config.TOML return nil, fmt.Errorf("failed to marshal StarkNet configs: %w", err) } + envVars, err := plugins.ParseEnvFile(env.StarknetPluginEnv.Get()) + if err != nil { + return nil, fmt.Errorf("failed to parse Starknet env file: %w", err) + } starknetCmdFn, err := plugins.NewCmdFactory(r.Register, plugins.CmdConfig{ ID: relayID.Name(), Cmd: cmdName, + Env: envVars, }) if err != nil { return nil, fmt.Errorf("failed to create StarkNet LOOP command: %w", err) diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 3136de44b8f..a4e3298eb28 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -25,6 +25,7 @@ import ( ocr2keepers20runner "github.com/smartcontractkit/chainlink-automation/pkg/v2/runner" ocr2keepers21config "github.com/smartcontractkit/chainlink-automation/pkg/v3/config" ocr2keepers21 "github.com/smartcontractkit/chainlink-automation/pkg/v3/plugin" + "github.com/smartcontractkit/chainlink/v2/core/config/env" "github.com/smartcontractkit/chainlink-vrf/altbn_128" dkgpkg "github.com/smartcontractkit/chainlink-vrf/dkg" @@ -588,8 +589,26 @@ func (d *Delegate) newServicesGenericPlugin( OffchainConfigDigester: provider.OffchainConfigDigester(), } + var envVars []string + switch p.PluginName { + case "median": + envVars, err = plugins.ParseEnvFile(env.MedianPluginEnv.Get()) + if err != nil { + return nil, fmt.Errorf("failed to parse median env file: %w", err) + } + } + if len(p.EnvVars) > 0 { + for k, v := range p.EnvVars { + envVars = append(envVars, k+"="+v) + } + } + pluginLggr := lggr.Named(p.PluginName).Named(spec.ContractID).Named(spec.GetID()) - cmdFn, grpcOpts, err := d.cfg.RegisterLOOP(fmt.Sprintf("%s-%s-%s", p.PluginName, spec.ContractID, spec.GetID()), command) + cmdFn, grpcOpts, err := d.cfg.RegisterLOOP(plugins.CmdConfig{ + ID: fmt.Sprintf("%s-%s-%s", p.PluginName, spec.ContractID, spec.GetID()), + Cmd: command, + Env: envVars, + }) if err != nil { return nil, fmt.Errorf("failed to register loop: %w", err) } diff --git a/core/services/ocr2/plugins/median/services.go b/core/services/ocr2/plugins/median/services.go index 4adfc306d64..2fe358d4346 100644 --- a/core/services/ocr2/plugins/median/services.go +++ b/core/services/ocr2/plugins/median/services.go @@ -162,7 +162,17 @@ func NewMedianServices(ctx context.Context, if medianLoopEnabled { // use unique logger names so we can use it to register a loop medianLggr := lggr.Named("Median").Named(spec.ContractID).Named(spec.GetID()) - cmdFn, telem, err2 := cfg.RegisterLOOP(medianLggr.Name(), medianPluginCmd) + envVars, err2 := plugins.ParseEnvFile(env.MedianPluginEnv.Get()) + if err2 != nil { + err = fmt.Errorf("failed to parse median env file: %w", err2) + abort() + return + } + cmdFn, telem, err2 := cfg.RegisterLOOP(plugins.CmdConfig{ + ID: medianLggr.Name(), + Cmd: medianPluginCmd, + Env: envVars, + }) if err2 != nil { err = fmt.Errorf("failed to register loop: %w", err2) abort() diff --git a/core/services/ocr2/validate/validate.go b/core/services/ocr2/validate/validate.go index bb9bb03a8ac..ad54ba4fea2 100644 --- a/core/services/ocr2/validate/validate.go +++ b/core/services/ocr2/validate/validate.go @@ -136,10 +136,11 @@ type Config struct { } type innerConfig struct { - Command string `json:"command"` - ProviderType string `json:"providerType"` - PluginName string `json:"pluginName"` - TelemetryType string `json:"telemetryType"` + Command string `json:"command"` + EnvVars map[string]string `json:"envVars"` + ProviderType string `json:"providerType"` + PluginName string `json:"pluginName"` + TelemetryType string `json:"telemetryType"` Config } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 36ee68412ca..fe7a38295a3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `chainlink health` CLI command and HTML `/health` endpoint, to provide human-readable views of the underlying JSON health data. +- Environment variables `CL_MEDIAN_ENV`, `CL_SOLANA_ENV`, and `CL_STARKNET_ENV` for setting environment variables in LOOP Plugins with an `.env` file. + ``` + echo "Foo=Bar" >> median.env + echo "Baz=Val" >> median.env + CL_MEDIAN_ENV="median.env" + ``` ### Fixed diff --git a/go.mod b/go.mod index 7fe535a425e..32cd9603b5e 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/graph-gophers/dataloader v5.0.0+incompatible github.com/graph-gophers/graphql-go v1.3.0 github.com/hashicorp/consul/sdk v0.14.1 + github.com/hashicorp/go-envparse v0.1.0 github.com/hashicorp/go-plugin v1.5.2 github.com/hdevalence/ed25519consensus v0.1.0 github.com/jackc/pgconn v1.14.1 diff --git a/go.sum b/go.sum index 38eaa89993a..47bc4601855 100644 --- a/go.sum +++ b/go.sum @@ -676,6 +676,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= +github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY= github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 673fbf4dbd5..8574aa8c5fa 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -246,6 +246,7 @@ require ( github.com/hashicorp/consul/api v1.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-envparse v0.1.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 9977ed84b8b..5a070e186c3 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -887,6 +887,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= +github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY= github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= diff --git a/plugins/utils.go b/plugins/cmd.go similarity index 73% rename from plugins/utils.go rename to plugins/cmd.go index 5e5e4142e86..9a312c53882 100644 --- a/plugins/utils.go +++ b/plugins/cmd.go @@ -7,8 +7,9 @@ import ( // CmdConfig is configuration used to register the LOOP and generate an exec type CmdConfig struct { - ID string // unique string used by the node to track the LOOP. typically supplied by the loop logger name - Cmd string // string value of executable to exec + ID string // unique string used by the node to track the LOOP. typically supplied by the loop logger name + Cmd string // string value of executable to exec + Env []string // environment variables as described in [exec.Cmd.Env] } // NewCmdFactory is helper to ensure synchronization between the loop registry and os cmd to exec the LOOP @@ -19,6 +20,7 @@ func NewCmdFactory(register func(id string) (*RegisteredLoop, error), lcfg CmdCo } return func() *exec.Cmd { cmd := exec.Command(lcfg.Cmd) //#nosec G204 -- we control the value of the cmd so the lint/sec error is a false positive + cmd.Env = append(cmd.Env, lcfg.Env...) cmd.Env = append(cmd.Env, registeredLoop.EnvCfg.AsCmdEnv()...) return cmd }, nil diff --git a/plugins/env.go b/plugins/env.go new file mode 100644 index 00000000000..016a4e862d8 --- /dev/null +++ b/plugins/env.go @@ -0,0 +1,31 @@ +package plugins + +import ( + "os" + + "github.com/hashicorp/go-envparse" +) + +// ParseEnvFile returns a slice of key/value pairs parsed from the file at filepath. +// As a special case, empty filepath returns nil without error. +func ParseEnvFile(filepath string) ([]string, error) { + if filepath == "" { + return nil, nil + } + f, err := os.Open(filepath) + if err != nil { + return nil, err + } + defer func() { + _ = f.Close() + }() + m, err := envparse.Parse(f) + if err != nil { + return nil, err + } + r := make([]string, 0, len(m)) + for k, v := range m { + r = append(r, k+"="+v) + } + return r, nil +} diff --git a/plugins/config.go b/plugins/registrar.go similarity index 77% rename from plugins/config.go rename to plugins/registrar.go index 01574d82099..90300b738b6 100644 --- a/plugins/config.go +++ b/plugins/registrar.go @@ -8,7 +8,7 @@ import ( // RegistrarConfig generates contains static configuration inher type RegistrarConfig interface { - RegisterLOOP(loopId string, cmdName string) (func() *exec.Cmd, loop.GRPCOpts, error) + RegisterLOOP(config CmdConfig) (func() *exec.Cmd, loop.GRPCOpts, error) } type registarConfig struct { @@ -27,11 +27,8 @@ func NewRegistrarConfig(grpcOpts loop.GRPCOpts, loopRegistrationFn func(loopId s } // RegisterLOOP calls the configured loopRegistrationFn. The loopRegistrationFn must act as a global registry for LOOPs and must be idempotent. -func (pc *registarConfig) RegisterLOOP(loopID string, cmdName string) (func() *exec.Cmd, loop.GRPCOpts, error) { - cmdFn, err := NewCmdFactory(pc.loopRegistrationFn, CmdConfig{ - ID: loopID, - Cmd: cmdName, - }) +func (pc *registarConfig) RegisterLOOP(cfg CmdConfig) (func() *exec.Cmd, loop.GRPCOpts, error) { + cmdFn, err := NewCmdFactory(pc.loopRegistrationFn, cfg) if err != nil { return nil, loop.GRPCOpts{}, err }