From 2380ecdeb5c26b98651748482f061d108d8fe94d Mon Sep 17 00:00:00 2001 From: mark-terry <36909937+mark-terry@users.noreply.github.com> Date: Mon, 3 Dec 2018 22:31:40 +1000 Subject: [PATCH] [NC-1856] Extracted non-Docker CLI parameters to picoCLI mixin. (#323) --- .../pantheon/cli/DefaultCommandValues.java | 82 +++++++++++ .../pegasys/pantheon/cli/PantheonCommand.java | 130 +++++------------- .../pantheon/cli/StandaloneCommand.java | 48 +++++++ .../pantheon/cli/PantheonCommandTest.java | 50 +++++++ 4 files changed, 216 insertions(+), 94 deletions(-) create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/cli/StandaloneCommand.java diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java new file mode 100644 index 0000000000..d3c39585d4 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 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.io.File; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import picocli.CommandLine; + +interface DefaultCommandValues { + String CONFIG_FILE_OPTION_NAME = "--config"; + + String MANDATORY_PATH_FORMAT_HELP = ""; + String PANTHEON_HOME_PROPERTY_NAME = "pantheon.home"; + String DEFAULT_DATA_DIR_PATH = "./build/data"; + + static Path getDefaultPantheonDataDir(final Object command) { + // this property is retrieved from Gradle tasks or Pantheon running shell script. + final String pantheonHomeProperty = System.getProperty(PANTHEON_HOME_PROPERTY_NAME); + final Path pantheonHome; + + // If prop is found, then use it + if (pantheonHomeProperty != null) { + try { + pantheonHome = Paths.get(pantheonHomeProperty); + } catch (final InvalidPathException e) { + throw new CommandLine.ParameterException( + new CommandLine(command), + String.format( + "Unable to define default data directory from %s property.", + PANTHEON_HOME_PROPERTY_NAME), + e); + } + } else { + // otherwise use a default path. + // That may only be used when NOT run from distribution script and Gradle as they all define + // the property. + try { + final String path = new File(DEFAULT_DATA_DIR_PATH).getCanonicalPath(); + pantheonHome = Paths.get(path); + } catch (final IOException e) { + throw new CommandLine.ParameterException( + new CommandLine(command), "Unable to create default data directory."); + } + } + + // Try to create it, then verify if the provided path is not already existing and is not a + // directory .Otherwise, if it doesn't exist or exists but is already a directory, + // Runner will use it to store data. + try { + Files.createDirectories(pantheonHome); + } catch (final FileAlreadyExistsException e) { + // Only thrown if it exist but is not a directory + throw new CommandLine.ParameterException( + new CommandLine(command), + String.format( + "%s: already exists and is not a directory.", pantheonHome.toAbsolutePath()), + e); + } catch (final Exception e) { + throw new CommandLine.ParameterException( + new CommandLine(command), + String.format("Error creating directory %s.", pantheonHome.toAbsolutePath()), + e); + } + return pantheonHome; + } +} 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 ae240ec217..cda7fdbc64 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -14,6 +14,7 @@ package tech.pegasys.pantheon.cli; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; +import static tech.pegasys.pantheon.cli.DefaultCommandValues.getDefaultPantheonDataDir; import tech.pegasys.pantheon.Runner; import tech.pegasys.pantheon.RunnerBuilder; @@ -41,11 +42,7 @@ import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.File; import java.io.IOException; import java.net.InetAddress; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -81,7 +78,7 @@ import picocli.CommandLine.ParameterException; footerHeading = "%n", footer = "Pantheon is licensed under the Apache License 2.0" ) -public class PantheonCommand implements Runnable { +public class PantheonCommand implements DefaultCommandValues, Runnable { private static final int DEFAULT_MAX_PEERS = 25; @@ -89,19 +86,13 @@ public class PantheonCommand implements Runnable { // but we use FULL for the moment as Fast is still in progress private static final SyncMode DEFAULT_SYNC_MODE = SyncMode.FULL; - private static final String PANTHEON_HOME_PROPERTY_NAME = "pantheon.home"; - private static final String DEFAULT_DATA_DIR_PATH = "./build/data"; - private static final String MANDATORY_HOST_AND_PORT_FORMAT_HELP = ""; - private static final String MANDATORY_PATH_FORMAT_HELP = ""; private static final String MANDATORY_INTEGER_FORMAT_HELP = ""; private static final String MANDATORY_MODE_FORMAT_HELP = ""; private static final Wei DEFAULT_MIN_TRANSACTION_GAS_PRICE = Wei.of(1000); private static final BytesValue DEFAULT_EXTRA_DATA = BytesValue.EMPTY; - private static final String CONFIG_FILE_OPTION_NAME = "--config"; - public static class RpcApisConverter implements ITypeConverter { @Override public RpcApi convert(final String name) throws RpcApisConversionException { @@ -132,23 +123,12 @@ public class PantheonCommand implements Runnable { // Public IP stored to prevent having to research it each time we need it. private InetAddress autoDiscoveredDefaultIP = null; + // Property to indicate whether Pantheon has been launched via docker + private final boolean isDocker = Boolean.getBoolean("pantheon.docker"); + // CLI options defined by user at runtime. // Options parsing is done with CLI library Picocli https://picocli.info/ - @Option( - names = {CONFIG_FILE_OPTION_NAME}, - paramLabel = MANDATORY_PATH_FORMAT_HELP, - description = "TOML config file (default: none)" - ) - private final File configFile = null; - - @Option( - names = {"--datadir"}, - paramLabel = MANDATORY_PATH_FORMAT_HELP, - description = "the path to Pantheon data directory (default: ${DEFAULT-VALUE})" - ) - private final Path dataDir = getDefaultPantheonDataDir(); - @Option( names = {"--node-private-key"}, paramLabel = MANDATORY_PATH_FORMAT_HELP, @@ -157,17 +137,6 @@ public class PantheonCommand implements Runnable { ) private final File nodePrivateKeyFile = null; - // Genesis file path with null default option if the option - // is not defined on command line as this default is handled by Runner - // to use mainnet json file from resources - // NOTE: we have no control over default value here. - @Option( - names = {"--genesis"}, - paramLabel = MANDATORY_PATH_FORMAT_HELP, - description = "The path to genesis file (default: Pantheon embedded mainnet genesis file)" - ) - private final File genesisFile = null; - // Boolean option to indicate if peers should NOT be discovered, default to false indicates that // the peers should be discovered by default. // @@ -400,6 +369,8 @@ public class PantheonCommand implements Runnable { this.synchronizerConfigurationBuilder = synchronizerConfigurationBuilder; } + private StandaloneCommand standaloneCommands; + public void parse( final AbstractParseResultHandler> resultHandler, final DefaultExceptionHandler> exceptionHandler, @@ -407,6 +378,12 @@ public class PantheonCommand implements Runnable { final CommandLine commandLine = new CommandLine(this); + standaloneCommands = new StandaloneCommand(); + + if (isFullInstantiation()) { + commandLine.addMixin("standaloneCommands", standaloneCommands); + } + final ImportSubCommand importSubCommand = new ImportSubCommand(blockImporter); commandLine.addSubcommand("import", importSubCommand); commandLine.addSubcommand("export-pub-key", new ExportPublicKeySubCommand()); @@ -462,7 +439,7 @@ public class PantheonCommand implements Runnable { try { return controllerBuilder .synchronizerConfiguration(buildSyncConfig(syncMode)) - .homePath(dataDir) + .homePath(dataDir()) .ethNetworkConfig(ethNetworkConfig()) .syncWithOttoman(syncWithOttoman) .miningParameters( @@ -478,7 +455,9 @@ public class PantheonCommand implements Runnable { } private File getNodePrivateKeyFile() { - return nodePrivateKeyFile != null ? nodePrivateKeyFile : KeyPairUtil.getDefaultKeyFile(dataDir); + return nodePrivateKeyFile != null + ? nodePrivateKeyFile + : KeyPairUtil.getDefaultKeyFile(dataDir()); } private JsonRpcConfiguration jsonRpcConfiguration() { @@ -539,7 +518,7 @@ public class PantheonCommand implements Runnable { .maxPeers(maxPeers) .jsonRpcConfiguration(jsonRpcConfiguration) .webSocketConfiguration(webSocketConfiguration) - .dataDir(dataDir) + .dataDir(dataDir()) .bannedNodeIds(bannedNodeIds) .permissioningConfiguration(permissioningConfiguration) .build(); @@ -579,57 +558,6 @@ public class PantheonCommand implements Runnable { return HostAndPort.fromParts(autoDiscoverDefaultIP().getHostAddress(), port); } - private Path getDefaultPantheonDataDir() { - // this property is retrieved from Gradle tasks or Pantheon running shell script. - final String pantheonHomeProperty = System.getProperty(PANTHEON_HOME_PROPERTY_NAME); - final Path pantheonHome; - - // If prop is found, then use it - if (pantheonHomeProperty != null) { - try { - pantheonHome = Paths.get(pantheonHomeProperty); - } catch (final InvalidPathException e) { - throw new ParameterException( - new CommandLine(this), - String.format( - "Unable to define default data directory from %s property.", - PANTHEON_HOME_PROPERTY_NAME), - e); - } - } else { - // otherwise use a default path. - // That may only be used when NOT run from distribution script and Gradle as they all define - // the property. - try { - final String path = new File(DEFAULT_DATA_DIR_PATH).getCanonicalPath(); - pantheonHome = Paths.get(path); - } catch (final IOException e) { - throw new ParameterException( - new CommandLine(this), "Unable to create default data directory."); - } - } - - // Try to create it, then verify if the provided path is not already existing and is not a - // directory .Otherwise, if it doesn't exist or exists but is already a directory, - // Runner will use it to store data. - try { - Files.createDirectories(pantheonHome); - } catch (final FileAlreadyExistsException e) { - // Only thrown if it exist but is not a directory - throw new ParameterException( - new CommandLine(this), - String.format( - "%s: already exists and is not a directory.", pantheonHome.toAbsolutePath()), - e); - } catch (final Exception e) { - throw new ParameterException( - new CommandLine(this), - String.format("Error creating directory %s.", pantheonHome.toAbsolutePath()), - e); - } - return pantheonHome; - } - private EthNetworkConfig ethNetworkConfig() { final EthNetworkConfig predefinedNetworkConfig; if (rinkeby) { @@ -644,7 +572,7 @@ public class PantheonCommand implements Runnable { private EthNetworkConfig updateNetworkConfig(final EthNetworkConfig ethNetworkConfig) { final EthNetworkConfig.Builder builder = new EthNetworkConfig.Builder(ethNetworkConfig); - if (genesisFile != null) { + if (genesisFile() != null) { builder.setGenesisConfig(genesisConfig()); } if (networkId != null) { @@ -658,10 +586,24 @@ public class PantheonCommand implements Runnable { private String genesisConfig() { try { - return Resources.toString(genesisFile.toURI().toURL(), UTF_8); - } catch (final IOException e) { + return Resources.toString(genesisFile().toURI().toURL(), UTF_8); + } catch (IOException e) { throw new ParameterException( - new CommandLine(this), String.format("Unable to load genesis file %s.", genesisFile), e); + new CommandLine(this), + String.format("Unable to load genesis file %s.", genesisFile()), + e); } } + + private File genesisFile() { + return isFullInstantiation() ? standaloneCommands.genesisFile : null; + } + + private Path dataDir() { + return isFullInstantiation() ? standaloneCommands.dataDir : getDefaultPantheonDataDir(this); + } + + private boolean isFullInstantiation() { + return !isDocker; + } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/StandaloneCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/StandaloneCommand.java new file mode 100644 index 0000000000..7e5cd7c4dc --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/StandaloneCommand.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 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 tech.pegasys.pantheon.cli.DefaultCommandValues.getDefaultPantheonDataDir; + +import java.io.File; +import java.nio.file.Path; + +import picocli.CommandLine; + +class StandaloneCommand implements DefaultCommandValues { + + @CommandLine.Option( + names = {CONFIG_FILE_OPTION_NAME}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "TOML config file (default: none)" + ) + private final File configFile = null; + + @CommandLine.Option( + names = {"--datadir"}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "The path to Pantheon data directory (default: ${DEFAULT-VALUE})" + ) + final Path dataDir = getDefaultPantheonDataDir(this); + + // Genesis file path with null default option if the option + // is not defined on command line as this default is handled by Runner + // to use mainnet json file from resources + // NOTE: we have no control over default value here. + @CommandLine.Option( + names = {"--genesis"}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "The path to genesis file (default: Pantheon embedded mainnet genesis file)" + ) + final File genesisFile = null; +} 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 650b4de919..4993ec775f 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -15,6 +15,7 @@ package tech.pegasys.pantheon.cli; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -79,6 +80,11 @@ public class PantheonCommandTest extends CommandTestAbstract { defaultWebSocketConfiguration = websocketConf; } + @Before + public void resetSystemProps() { + System.setProperty("pantheon.docker", "false"); + } + @Override @Before public void initMocks() throws Exception { @@ -161,6 +167,7 @@ public class PantheonCommandTest extends CommandTestAbstract { // Testing each option @Test public void callingWithConfigOptionButNoConfigFileShouldDisplayHelp() { + assumeTrue(isFullInstantiation()); parseCommand("--config"); @@ -171,6 +178,8 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void callingWithConfigOptionButNonExistingFileShouldDisplayHelp() throws IOException { + assumeTrue(isFullInstantiation()); + final File tempConfigFile = temp.newFile("an-invalid-file-name-without-extension"); parseCommand("--config", tempConfigFile.getPath()); @@ -181,6 +190,7 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void callingWithConfigOptionButTomlFileNotFoundShouldDisplayHelp() { + assumeTrue(isFullInstantiation()); parseCommand("--config", "./an-invalid-file-name-sdsd87sjhqoi34io23.toml"); @@ -191,6 +201,7 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void callingWithConfigOptionButInvalidContentTomlFileShouldDisplayHelp() throws Exception { + assumeTrue(isFullInstantiation()); // We write a config file to prevent an invalid file in resource folder to raise errors in // code checks (CI + IDE) @@ -212,6 +223,7 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void callingWithConfigOptionButInvalidValueTomlFileShouldDisplayHelp() throws Exception { + assumeTrue(isFullInstantiation()); // We write a config file to prevent an invalid file in resource folder to raise errors in // code checks (CI + IDE) @@ -233,6 +245,8 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void overrideDefaultValuesIfKeyIsPresentInConfigFile() throws IOException { + assumeTrue(isFullInstantiation()); + final URL configFile = Resources.getResource("complete_config.toml"); final Path genesisFile = createFakeGenesisFile(); final String updatedConfig = @@ -292,6 +306,8 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() throws IOException { + assumeTrue(isFullInstantiation()); + final String configFile = Resources.getResource("partial_config.toml").getFile(); parseCommand("--config", configFile); @@ -344,6 +360,8 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void dataDirOptionMustBeUsed() throws Exception { + assumeTrue(isFullInstantiation()); + final Path path = Paths.get("."); parseCommand("--datadir", path.toString()); @@ -361,6 +379,8 @@ public class PantheonCommandTest extends CommandTestAbstract { @Test public void genesisPathOptionMustBeUsed() throws Exception { + assumeTrue(isFullInstantiation()); + final Path genesisFile = createFakeGenesisFile(); final ArgumentCaptor networkArg = ArgumentCaptor.forClass(EthNetworkConfig.class); @@ -843,9 +863,39 @@ public class PantheonCommandTest extends CommandTestAbstract { assertThat(networkArg.getValue().getNetworkId()).isEqualTo(1); } + @Test + public void fullCLIOptionsNotShownWhenInDockerContainer() { + System.setProperty("pantheon.docker", "true"); + + parseCommand("--help"); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString()).doesNotContain("--config"); + assertThat(commandOutput.toString()).doesNotContain("--datadir"); + assertThat(commandOutput.toString()).doesNotContain("--genesis"); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void fullCLIOptionsShownWhenNotInDockerContainer() { + parseCommand("--help"); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString()).contains("--config"); + assertThat(commandOutput.toString()).contains("--datadir"); + assertThat(commandOutput.toString()).contains("--genesis"); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + private Path createFakeGenesisFile() throws IOException { final Path genesisFile = Files.createTempFile("genesisFile", ""); Files.write(genesisFile, "genesis_config".getBytes(UTF_8)); return genesisFile; } + + private boolean isFullInstantiation() { + return !Boolean.getBoolean("pantheon.docker"); + } }