From ca438df9d670e9ae137e479f8a6fb341147de41a Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Mon, 24 Jun 2019 12:18:38 +1000 Subject: [PATCH] Support specifying config options via environment variables. (#1597) In descending order, the priority is CLI arg, env var, config file, default value. Signed-off-by: Adrian Sutton --- .../ethereum/core/MiningParameters.java | 33 ++++++++++++ .../java/tech/pegasys/pantheon/Pantheon.java | 3 +- .../cli/CascadingDefaultProvider.java | 40 ++++++++++++++ .../cli/ConfigOptionSearchAndRunHandler.java | 13 +++-- .../EnvironmentVariableDefaultProvider.java | 51 ++++++++++++++++++ .../pegasys/pantheon/cli/PantheonCommand.java | 8 ++- .../pantheon/cli/CommandTestAbstract.java | 16 ++++-- .../ConfigOptionSearchAndRunHandlerTest.java | 3 +- ...nvironmentVariableDefaultProviderTest.java | 53 +++++++++++++++++++ .../pantheon/cli/PantheonCommandTest.java | 36 +++++++++++++ 10 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/cli/CascadingDefaultProvider.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProvider.java create mode 100644 pantheon/src/test/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProviderTest.java diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/MiningParameters.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/MiningParameters.java index 3b7799512b..9aa9fafc46 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/MiningParameters.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/MiningParameters.java @@ -14,8 +14,11 @@ package tech.pegasys.pantheon.ethereum.core; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.Objects; import java.util.Optional; +import com.google.common.base.MoreObjects; + public class MiningParameters { private final Optional
coinbase; @@ -49,4 +52,34 @@ public class MiningParameters { public Boolean isMiningEnabled() { return enabled; } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final MiningParameters that = (MiningParameters) o; + return Objects.equals(coinbase, that.coinbase) + && Objects.equals(minTransactionGasPrice, that.minTransactionGasPrice) + && Objects.equals(extraData, that.extraData) + && Objects.equals(enabled, that.enabled); + } + + @Override + public int hashCode() { + return Objects.hash(coinbase, minTransactionGasPrice, extraData, enabled); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("coinbase", coinbase) + .add("minTransactionGasPrice", minTransactionGasPrice) + .add("extraData", extraData) + .add("enabled", enabled) + .toString(); + } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java b/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java index 2ab80980f7..edca09d11b 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java @@ -43,7 +43,8 @@ public final class Pantheon { new SynchronizerConfiguration.Builder(), EthereumWireProtocolConfiguration.builder(), new RocksDbConfiguration.Builder(), - new PantheonPluginContextImpl()); + new PantheonPluginContextImpl(), + System.getenv()); pantheonCommand.parse( new RunLast().andExit(SUCCESS_EXIT_CODE), diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/CascadingDefaultProvider.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/CascadingDefaultProvider.java new file mode 100644 index 0000000000..9cadbe46ad --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/CascadingDefaultProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli; + +import static java.util.Arrays.asList; + +import java.util.List; + +import picocli.CommandLine.IDefaultValueProvider; +import picocli.CommandLine.Model.ArgSpec; + +public class CascadingDefaultProvider implements IDefaultValueProvider { + + private final List defaultValueProviders; + + public CascadingDefaultProvider(final IDefaultValueProvider... defaultValueProviders) { + this.defaultValueProviders = asList(defaultValueProviders); + } + + @Override + public String defaultValue(final ArgSpec argSpec) throws Exception { + for (final IDefaultValueProvider provider : defaultValueProviders) { + final String defaultValue = provider.defaultValue(argSpec); + if (defaultValue != null) { + return defaultValue; + } + } + return null; + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java index 03409c4980..e51997abd1 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java @@ -15,10 +15,12 @@ package tech.pegasys.pantheon.cli; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Map; import picocli.CommandLine; import picocli.CommandLine.AbstractParseResultHandler; import picocli.CommandLine.ExecutionException; +import picocli.CommandLine.IDefaultValueProvider; import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.ParseResult; @@ -28,16 +30,19 @@ class ConfigOptionSearchAndRunHandler extends AbstractParseResultHandler> resultHandler; private final CommandLine.IExceptionHandler2> exceptionHandler; private final String configFileOptionName; + private final Map environment; private final boolean isDocker; ConfigOptionSearchAndRunHandler( final AbstractParseResultHandler> resultHandler, final CommandLine.IExceptionHandler2> exceptionHandler, final String configFileOptionName, + final Map environment, final boolean isDocker) { this.resultHandler = resultHandler; this.exceptionHandler = exceptionHandler; this.configFileOptionName = configFileOptionName; + this.environment = environment; this.isDocker = isDocker; // use the same output as the regular options handler to ensure that outputs are all going // in the same place. No need to do this for the exception handler as we reuse it directly. @@ -55,9 +60,11 @@ class ConfigOptionSearchAndRunHandler extends AbstractParseResultHandler environment; + + public EnvironmentVariableDefaultProvider(final Map environment) { + this.environment = environment; + } + + @Override + public String defaultValue(final ArgSpec argSpec) { + return envVarNames((OptionSpec) argSpec) + .map(environment::get) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private Stream envVarNames(final OptionSpec spec) { + return Arrays.stream(spec.names()) + .filter(name -> name.startsWith("--")) // Only long options are allowed + .map( + name -> + ENV_VAR_PREFIX + + name.substring("--".length()).replace('-', '_').toUpperCase(Locale.US)); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index adf68d6d65..5ed6432c44 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -95,6 +95,7 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Supplier; @@ -145,6 +146,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { private final RunnerBuilder runnerBuilder; private final PantheonController.Builder controllerBuilderFactory; private final PantheonPluginContextImpl pantheonPluginContext; + private final Map environment; protected KeyLoader getKeyLoader() { return KeyPairUtil::loadKeyPair; @@ -607,7 +609,8 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { final SynchronizerConfiguration.Builder synchronizerConfigurationBuilder, final EthereumWireProtocolConfiguration.Builder ethereumWireConfigurationBuilder, final RocksDbConfiguration.Builder rocksDbConfigurationBuilder, - final PantheonPluginContextImpl pantheonPluginContext) { + final PantheonPluginContextImpl pantheonPluginContext, + final Map environment) { this.logger = logger; this.blockImporter = blockImporter; this.runnerBuilder = runnerBuilder; @@ -616,6 +619,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { this.ethereumWireConfigurationBuilder = ethereumWireConfigurationBuilder; this.rocksDbConfigurationBuilder = rocksDbConfigurationBuilder; this.pantheonPluginContext = pantheonPluginContext; + this.environment = environment; } private StandaloneCommand standaloneCommands; @@ -680,7 +684,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { // and eventually it will run regular parsing of the remaining options. final ConfigOptionSearchAndRunHandler configParsingHandler = new ConfigOptionSearchAndRunHandler( - resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, isDocker); + resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, environment, isDocker); commandLine.parseWithHandlers(configParsingHandler, exceptionHandler, args); } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index f3c162d378..896021b72e 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -49,6 +49,8 @@ import java.io.InputStream; import java.io.PrintStream; import java.nio.file.Path; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -75,6 +77,7 @@ public abstract class CommandTestAbstract { protected final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream(); private final PrintStream errPrintStream = new PrintStream(commandErrorOutput); + private final HashMap environment = new HashMap<>(); @Mock RunnerBuilder mockRunnerBuilder; @Mock Runner mockRunner; @@ -187,6 +190,10 @@ public abstract class CommandTestAbstract { commandErrorOutput.close(); } + protected void setEnvironemntVariable(final String name, final String value) { + environment.put(name, value); + } + protected CommandLine.Model.CommandSpec parseCommand(final String... args) { return parseCommand(System.in, args); } @@ -215,7 +222,8 @@ public abstract class CommandTestAbstract { mockEthereumWireProtocolConfigurationBuilder, mockRocksDbConfBuilder, keyLoader, - mockPantheonPluginContext); + mockPantheonPluginContext, + environment); // parse using Ansi.OFF to be able to assert on non formatted output results pantheonCommand.parse( @@ -245,7 +253,8 @@ public abstract class CommandTestAbstract { final EthereumWireProtocolConfiguration.Builder mockEthereumConfigurationMockBuilder, final RocksDbConfiguration.Builder mockRocksDbConfBuilder, final KeyLoader keyLoader, - final PantheonPluginContextImpl pantheonPluginContext) { + final PantheonPluginContextImpl pantheonPluginContext, + final Map environment) { super( mockLogger, mockBlockImporter, @@ -254,7 +263,8 @@ public abstract class CommandTestAbstract { mockSyncConfBuilder, mockEthereumConfigurationMockBuilder, mockRocksDbConfBuilder, - pantheonPluginContext); + pantheonPluginContext, + environment); this.keyLoader = keyLoader; } } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java index 68b66ed61d..7167f43941 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon.cli; +import static java.util.Collections.emptyMap; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; @@ -63,7 +64,7 @@ public class ConfigOptionSearchAndRunHandlerTest { new DefaultExceptionHandler>().useErr(errPrintStream).useAnsi(Ansi.OFF); private final ConfigOptionSearchAndRunHandler configParsingHandler = new ConfigOptionSearchAndRunHandler( - resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, false); + resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, emptyMap(), false); @Mock ParseResult mockParseResult; @Mock CommandLine mockCommandLine; diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProviderTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProviderTest.java new file mode 100644 index 0000000000..75fa4f57d0 --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProviderTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import picocli.CommandLine.Model.OptionSpec; + +public class EnvironmentVariableDefaultProviderTest { + + private final Map environment = new HashMap<>(); + + private final EnvironmentVariableDefaultProvider provider = + new EnvironmentVariableDefaultProvider(environment); + + @Test + public void shouldReturnNullWhenEnvironmentVariableIsNotSet() { + assertThat(provider.defaultValue(OptionSpec.builder("--no-env-var-set").build())).isNull(); + } + + @Test + public void shouldReturnValueWhenEnvironmentVariableIsSet() { + environment.put("PANTHEON_ENV_VAR_SET", "abc"); + assertThat(provider.defaultValue(OptionSpec.builder("--env-var-set").build())).isEqualTo("abc"); + } + + @Test + public void shouldReturnValueWhenEnvironmentVariableIsSetForAlternateName() { + environment.put("PANTHEON_ENV_VAR_SET", "abc"); + assertThat(provider.defaultValue(OptionSpec.builder("--env-var", "--env-var-set").build())) + .isEqualTo("abc"); + } + + @Test + public void shouldNotReturnValueForShortOptions() { + environment.put("PANTHEON_H", "abc"); + assertThat(provider.defaultValue(OptionSpec.builder("-h").build())).isNull(); + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index 7873d67da6..f09a98f8cb 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -732,6 +732,42 @@ public class PantheonCommandTest extends CommandTestAbstract { assertThat(commandErrorOutput.toString()).isEmpty(); } + @Test + public void envVariableOverridesValueFromConfigFile() { + assumeTrue(isFullInstantiation()); + + final String configFile = this.getClass().getResource("/partial_config.toml").getFile(); + final String expectedCoinbase = "0x0000000000000000000000000000000000000004"; + setEnvironemntVariable("PANTHEON_MINER_COINBASE", expectedCoinbase); + parseCommand("--config-file", configFile); + + verify(mockControllerBuilder) + .miningParameters( + new MiningParameters( + Address.fromHexString(expectedCoinbase), + DefaultCommandValues.DEFAULT_MIN_TRANSACTION_GAS_PRICE, + DefaultCommandValues.DEFAULT_EXTRA_DATA, + false)); + } + + @Test + public void cliOptionOverridesEnvVariableAndConfig() { + assumeTrue(isFullInstantiation()); + + final String configFile = this.getClass().getResource("/partial_config.toml").getFile(); + final String expectedCoinbase = "0x0000000000000000000000000000000000000006"; + setEnvironemntVariable("PANTHEON_MINER_COINBASE", "0x0000000000000000000000000000000000000004"); + parseCommand("--config-file", configFile, "--miner-coinbase", expectedCoinbase); + + verify(mockControllerBuilder) + .miningParameters( + new MiningParameters( + Address.fromHexString(expectedCoinbase), + DefaultCommandValues.DEFAULT_MIN_TRANSACTION_GAS_PRICE, + DefaultCommandValues.DEFAULT_EXTRA_DATA, + false)); + } + @Test public void configOptionDisabledUnderDocker() { System.setProperty("pantheon.docker", "true");