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 <adrian.sutton@consensys.net>
pull/2/head
Adrian Sutton 6 years ago committed by GitHub
parent e42db83cc2
commit ca438df9d6
  1. 33
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/MiningParameters.java
  2. 3
      pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java
  3. 40
      pantheon/src/main/java/tech/pegasys/pantheon/cli/CascadingDefaultProvider.java
  4. 13
      pantheon/src/main/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandler.java
  5. 51
      pantheon/src/main/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProvider.java
  6. 8
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  7. 16
      pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java
  8. 3
      pantheon/src/test/java/tech/pegasys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java
  9. 53
      pantheon/src/test/java/tech/pegasys/pantheon/cli/EnvironmentVariableDefaultProviderTest.java
  10. 36
      pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java

@ -14,8 +14,11 @@ package tech.pegasys.pantheon.ethereum.core;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import com.google.common.base.MoreObjects;
public class MiningParameters { public class MiningParameters {
private final Optional<Address> coinbase; private final Optional<Address> coinbase;
@ -49,4 +52,34 @@ public class MiningParameters {
public Boolean isMiningEnabled() { public Boolean isMiningEnabled() {
return enabled; 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();
}
} }

@ -43,7 +43,8 @@ public final class Pantheon {
new SynchronizerConfiguration.Builder(), new SynchronizerConfiguration.Builder(),
EthereumWireProtocolConfiguration.builder(), EthereumWireProtocolConfiguration.builder(),
new RocksDbConfiguration.Builder(), new RocksDbConfiguration.Builder(),
new PantheonPluginContextImpl()); new PantheonPluginContextImpl(),
System.getenv());
pantheonCommand.parse( pantheonCommand.parse(
new RunLast().andExit(SUCCESS_EXIT_CODE), new RunLast().andExit(SUCCESS_EXIT_CODE),

@ -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<IDefaultValueProvider> 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;
}
}

@ -15,10 +15,12 @@ package tech.pegasys.pantheon.cli;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import picocli.CommandLine; import picocli.CommandLine;
import picocli.CommandLine.AbstractParseResultHandler; import picocli.CommandLine.AbstractParseResultHandler;
import picocli.CommandLine.ExecutionException; import picocli.CommandLine.ExecutionException;
import picocli.CommandLine.IDefaultValueProvider;
import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Model.OptionSpec;
import picocli.CommandLine.ParseResult; import picocli.CommandLine.ParseResult;
@ -28,16 +30,19 @@ class ConfigOptionSearchAndRunHandler extends AbstractParseResultHandler<List<Ob
private final AbstractParseResultHandler<List<Object>> resultHandler; private final AbstractParseResultHandler<List<Object>> resultHandler;
private final CommandLine.IExceptionHandler2<List<Object>> exceptionHandler; private final CommandLine.IExceptionHandler2<List<Object>> exceptionHandler;
private final String configFileOptionName; private final String configFileOptionName;
private final Map<String, String> environment;
private final boolean isDocker; private final boolean isDocker;
ConfigOptionSearchAndRunHandler( ConfigOptionSearchAndRunHandler(
final AbstractParseResultHandler<List<Object>> resultHandler, final AbstractParseResultHandler<List<Object>> resultHandler,
final CommandLine.IExceptionHandler2<List<Object>> exceptionHandler, final CommandLine.IExceptionHandler2<List<Object>> exceptionHandler,
final String configFileOptionName, final String configFileOptionName,
final Map<String, String> environment,
final boolean isDocker) { final boolean isDocker) {
this.resultHandler = resultHandler; this.resultHandler = resultHandler;
this.exceptionHandler = exceptionHandler; this.exceptionHandler = exceptionHandler;
this.configFileOptionName = configFileOptionName; this.configFileOptionName = configFileOptionName;
this.environment = environment;
this.isDocker = isDocker; this.isDocker = isDocker;
// use the same output as the regular options handler to ensure that outputs are all going // 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. // 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<List<Ob
} catch (final Exception e) { } catch (final Exception e) {
throw new ExecutionException(commandLine, e.getMessage(), e); throw new ExecutionException(commandLine, e.getMessage(), e);
} }
final TomlConfigFileDefaultProvider tomlConfigFileDefaultProvider = final IDefaultValueProvider defaultValueProvider =
new TomlConfigFileDefaultProvider(commandLine, configFile); new CascadingDefaultProvider(
commandLine.setDefaultValueProvider(tomlConfigFileDefaultProvider); new EnvironmentVariableDefaultProvider(environment),
new TomlConfigFileDefaultProvider(commandLine, configFile));
commandLine.setDefaultValueProvider(defaultValueProvider);
} else if (isDocker) { } else if (isDocker) {
final File configFile = new File(DOCKER_CONFIG_LOCATION); final File configFile = new File(DOCKER_CONFIG_LOCATION);
if (configFile.exists()) { if (configFile.exists()) {

@ -0,0 +1,51 @@
/*
* 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 java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import picocli.CommandLine.IDefaultValueProvider;
import picocli.CommandLine.Model.ArgSpec;
import picocli.CommandLine.Model.OptionSpec;
public class EnvironmentVariableDefaultProvider implements IDefaultValueProvider {
private static final String ENV_VAR_PREFIX = "PANTHEON_";
private final Map<String, String> environment;
public EnvironmentVariableDefaultProvider(final Map<String, String> 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<String> 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));
}
}

@ -95,6 +95,7 @@ import java.time.Clock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -145,6 +146,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
private final RunnerBuilder runnerBuilder; private final RunnerBuilder runnerBuilder;
private final PantheonController.Builder controllerBuilderFactory; private final PantheonController.Builder controllerBuilderFactory;
private final PantheonPluginContextImpl pantheonPluginContext; private final PantheonPluginContextImpl pantheonPluginContext;
private final Map<String, String> environment;
protected KeyLoader getKeyLoader() { protected KeyLoader getKeyLoader() {
return KeyPairUtil::loadKeyPair; return KeyPairUtil::loadKeyPair;
@ -607,7 +609,8 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
final SynchronizerConfiguration.Builder synchronizerConfigurationBuilder, final SynchronizerConfiguration.Builder synchronizerConfigurationBuilder,
final EthereumWireProtocolConfiguration.Builder ethereumWireConfigurationBuilder, final EthereumWireProtocolConfiguration.Builder ethereumWireConfigurationBuilder,
final RocksDbConfiguration.Builder rocksDbConfigurationBuilder, final RocksDbConfiguration.Builder rocksDbConfigurationBuilder,
final PantheonPluginContextImpl pantheonPluginContext) { final PantheonPluginContextImpl pantheonPluginContext,
final Map<String, String> environment) {
this.logger = logger; this.logger = logger;
this.blockImporter = blockImporter; this.blockImporter = blockImporter;
this.runnerBuilder = runnerBuilder; this.runnerBuilder = runnerBuilder;
@ -616,6 +619,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
this.ethereumWireConfigurationBuilder = ethereumWireConfigurationBuilder; this.ethereumWireConfigurationBuilder = ethereumWireConfigurationBuilder;
this.rocksDbConfigurationBuilder = rocksDbConfigurationBuilder; this.rocksDbConfigurationBuilder = rocksDbConfigurationBuilder;
this.pantheonPluginContext = pantheonPluginContext; this.pantheonPluginContext = pantheonPluginContext;
this.environment = environment;
} }
private StandaloneCommand standaloneCommands; private StandaloneCommand standaloneCommands;
@ -680,7 +684,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
// and eventually it will run regular parsing of the remaining options. // and eventually it will run regular parsing of the remaining options.
final ConfigOptionSearchAndRunHandler configParsingHandler = final ConfigOptionSearchAndRunHandler configParsingHandler =
new ConfigOptionSearchAndRunHandler( new ConfigOptionSearchAndRunHandler(
resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, isDocker); resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, environment, isDocker);
commandLine.parseWithHandlers(configParsingHandler, exceptionHandler, args); commandLine.parseWithHandlers(configParsingHandler, exceptionHandler, args);
} }

@ -49,6 +49,8 @@ import java.io.InputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -75,6 +77,7 @@ public abstract class CommandTestAbstract {
protected final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream(); protected final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream();
private final PrintStream errPrintStream = new PrintStream(commandErrorOutput); private final PrintStream errPrintStream = new PrintStream(commandErrorOutput);
private final HashMap<String, String> environment = new HashMap<>();
@Mock RunnerBuilder mockRunnerBuilder; @Mock RunnerBuilder mockRunnerBuilder;
@Mock Runner mockRunner; @Mock Runner mockRunner;
@ -187,6 +190,10 @@ public abstract class CommandTestAbstract {
commandErrorOutput.close(); commandErrorOutput.close();
} }
protected void setEnvironemntVariable(final String name, final String value) {
environment.put(name, value);
}
protected CommandLine.Model.CommandSpec parseCommand(final String... args) { protected CommandLine.Model.CommandSpec parseCommand(final String... args) {
return parseCommand(System.in, args); return parseCommand(System.in, args);
} }
@ -215,7 +222,8 @@ public abstract class CommandTestAbstract {
mockEthereumWireProtocolConfigurationBuilder, mockEthereumWireProtocolConfigurationBuilder,
mockRocksDbConfBuilder, mockRocksDbConfBuilder,
keyLoader, keyLoader,
mockPantheonPluginContext); mockPantheonPluginContext,
environment);
// parse using Ansi.OFF to be able to assert on non formatted output results // parse using Ansi.OFF to be able to assert on non formatted output results
pantheonCommand.parse( pantheonCommand.parse(
@ -245,7 +253,8 @@ public abstract class CommandTestAbstract {
final EthereumWireProtocolConfiguration.Builder mockEthereumConfigurationMockBuilder, final EthereumWireProtocolConfiguration.Builder mockEthereumConfigurationMockBuilder,
final RocksDbConfiguration.Builder mockRocksDbConfBuilder, final RocksDbConfiguration.Builder mockRocksDbConfBuilder,
final KeyLoader keyLoader, final KeyLoader keyLoader,
final PantheonPluginContextImpl pantheonPluginContext) { final PantheonPluginContextImpl pantheonPluginContext,
final Map<String, String> environment) {
super( super(
mockLogger, mockLogger,
mockBlockImporter, mockBlockImporter,
@ -254,7 +263,8 @@ public abstract class CommandTestAbstract {
mockSyncConfBuilder, mockSyncConfBuilder,
mockEthereumConfigurationMockBuilder, mockEthereumConfigurationMockBuilder,
mockRocksDbConfBuilder, mockRocksDbConfBuilder,
pantheonPluginContext); pantheonPluginContext,
environment);
this.keyLoader = keyLoader; this.keyLoader = keyLoader;
} }
} }

@ -12,6 +12,7 @@
*/ */
package tech.pegasys.pantheon.cli; package tech.pegasys.pantheon.cli;
import static java.util.Collections.emptyMap;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
@ -63,7 +64,7 @@ public class ConfigOptionSearchAndRunHandlerTest {
new DefaultExceptionHandler<List<Object>>().useErr(errPrintStream).useAnsi(Ansi.OFF); new DefaultExceptionHandler<List<Object>>().useErr(errPrintStream).useAnsi(Ansi.OFF);
private final ConfigOptionSearchAndRunHandler configParsingHandler = private final ConfigOptionSearchAndRunHandler configParsingHandler =
new ConfigOptionSearchAndRunHandler( new ConfigOptionSearchAndRunHandler(
resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, false); resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME, emptyMap(), false);
@Mock ParseResult mockParseResult; @Mock ParseResult mockParseResult;
@Mock CommandLine mockCommandLine; @Mock CommandLine mockCommandLine;

@ -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<String, String> 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();
}
}

@ -732,6 +732,42 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString()).isEmpty(); 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 @Test
public void configOptionDisabledUnderDocker() { public void configOptionDisabledUnderDocker() {
System.setProperty("pantheon.docker", "true"); System.setProperty("pantheon.docker", "true");

Loading…
Cancel
Save