Skip to content

Commit

Permalink
feat: Enhance --profile to load external profiles (#7292)
Browse files Browse the repository at this point in the history
* feat: --profile can load external profiles
* fix external profile name method
* fix ProfilesCompletionCandidate
* test: Add unit tests
* changelog: Update changelog
* test: Fix TomlConfigurationDefaultProviderTest
* test: Fix BesuCommandTest

---------

Signed-off-by: Usman Saleem <usman@usmans.info>
  • Loading branch information
usmansaleem authored Jul 10, 2024
1 parent 5660ebc commit ae7ddd1
Show file tree
Hide file tree
Showing 12 changed files with 355 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Breaking Changes

### Additions and Improvements
- Add support to load external profiles using `--profile` [#7265](https://github.com/hyperledger/besu/issues/7265)

### Bug fixes

Expand Down
7 changes: 4 additions & 3 deletions besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import org.hyperledger.besu.chainimport.RlpBlockImporter;
import org.hyperledger.besu.cli.config.EthNetworkConfig;
import org.hyperledger.besu.cli.config.NetworkName;
import org.hyperledger.besu.cli.config.ProfileName;
import org.hyperledger.besu.cli.config.ProfilesCompletionCandidates;
import org.hyperledger.besu.cli.converter.MetricCategoryConverter;
import org.hyperledger.besu.cli.converter.PercentageConverter;
import org.hyperledger.besu.cli.converter.SubnetInfoConverter;
Expand Down Expand Up @@ -565,9 +565,10 @@ private InetAddress autoDiscoverDefaultIP() {
@Option(
names = {PROFILE_OPTION_NAME},
paramLabel = PROFILE_FORMAT_HELP,
completionCandidates = ProfilesCompletionCandidates.class,
description =
"Overwrite default settings. Possible values are ${COMPLETION-CANDIDATES}. (default: none)")
private final ProfileName profile = null;
private String profile = null; // don't set it as final due to picocli completion candidates

@Option(
names = {"--nat-method"},
Expand Down Expand Up @@ -2773,7 +2774,7 @@ private String generateConfigurationOverview() {
}

if (profile != null) {
builder.setProfile(profile.toString());
builder.setProfile(profile);
}

builder.setHasCustomGenesis(genesisFile != null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@
*/
package org.hyperledger.besu.cli.config;

import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;

/** Enum for profile names. Each profile corresponds to a configuration file. */
public enum ProfileName {
/**
* Enum for profile names which are bundled. Each profile corresponds to a bundled configuration
* file.
*/
public enum InternalProfileName {
/** The 'STAKER' profile */
STAKER("profiles/staker.toml"),
/** The 'MINIMALIST_STAKER' profile */
Expand All @@ -31,12 +39,36 @@ public enum ProfileName {

private final String configFile;

/**
* Returns the InternalProfileName that matches the given name, ignoring case.
*
* @param name The profile name
* @return Optional InternalProfileName if found, otherwise empty
*/
public static Optional<InternalProfileName> valueOfIgnoreCase(final String name) {
return Arrays.stream(values())
.filter(profile -> profile.name().equalsIgnoreCase(name))
.findFirst();
}

/**
* Returns the set of internal profile names as lowercase.
*
* @return Set of internal profile names
*/
public static Set<String> getInternalProfileNames() {
return Arrays.stream(InternalProfileName.values())
.map(InternalProfileName::name)
.map(String::toLowerCase)
.collect(Collectors.toSet());
}

/**
* Constructs a new ProfileName.
*
* @param configFile the configuration file corresponding to the profile
*/
ProfileName(final String configFile) {
InternalProfileName(final String configFile) {
this.configFile = configFile;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.cli.config;

import org.hyperledger.besu.cli.util.ProfileFinder;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

/** Provides a list of profile names that can be used for command line completion. */
public class ProfilesCompletionCandidates implements Iterable<String> {
/**
* Create a new instance of ProfilesCompletionCandidates. This constructor is required for
* Picocli.
*/
public ProfilesCompletionCandidates() {}

@Override
public Iterator<String> iterator() {
final Set<String> profileNames = new TreeSet<>(InternalProfileName.getInternalProfileNames());
profileNames.addAll(ProfileFinder.getExternalProfileNames());
return profileNames.iterator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void run() {
checkNotNull(parentCommand);
try {
TomlConfigurationDefaultProvider.fromFile(commandLine, dataPath.toFile())
.loadConfigurationFromFile();
.loadConfigurationIfNotLoaded();
} catch (Exception e) {
this.out.println(e);
return;
Expand Down
94 changes: 83 additions & 11 deletions besu/src/main/java/org/hyperledger/besu/cli/util/ProfileFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@

import static org.hyperledger.besu.cli.DefaultCommandValues.PROFILE_OPTION_NAME;

import org.hyperledger.besu.cli.config.ProfileName;
import org.hyperledger.besu.cli.config.InternalProfileName;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import picocli.CommandLine;

Expand Down Expand Up @@ -50,30 +58,94 @@ protected String getConfigEnvName() {
@Override
public Optional<InputStream> getFromOption(
final CommandLine.ParseResult parseResult, final CommandLine commandLine) {
final String profileName;
try {
return getProfile(parseResult.matchedOption(PROFILE_OPTION_NAME).getter().get(), commandLine);
} catch (Exception e) {
throw new RuntimeException(e);
profileName = parseResult.matchedOption(PROFILE_OPTION_NAME).getter().get();
} catch (final Exception e) {
throw new CommandLine.ParameterException(
commandLine, "Unexpected error in obtaining value of --profile", e);
}
return getProfile(profileName, commandLine);
}

@Override
public Optional<InputStream> getFromEnvironment(
final Map<String, String> environment, final CommandLine commandLine) {
return getProfile(ProfileName.valueOf(environment.get(PROFILE_ENV_NAME)), commandLine);
return getProfile(environment.get(PROFILE_ENV_NAME), commandLine);
}

private static Optional<InputStream> getProfile(
final ProfileName profileName, final CommandLine commandLine) {
return Optional.of(getTomlFile(commandLine, profileName.getConfigFile()));
final String profileName, final CommandLine commandLine) {
final Optional<String> internalProfileConfigPath =
InternalProfileName.valueOfIgnoreCase(profileName).map(InternalProfileName::getConfigFile);
if (internalProfileConfigPath.isPresent()) {
return Optional.of(getTomlFileFromClasspath(internalProfileConfigPath.get()));
} else {
final Path externalProfileFile = defaultProfilesDir().resolve(profileName + ".toml");
if (Files.exists(externalProfileFile)) {
try {
return Optional.of(Files.newInputStream(externalProfileFile));
} catch (IOException e) {
throw new CommandLine.ParameterException(
commandLine, "Error reading external profile: " + profileName);
}
} else {
throw new CommandLine.ParameterException(
commandLine, "Unable to load external profile: " + profileName);
}
}
}

private static InputStream getTomlFile(final CommandLine commandLine, final String file) {
InputStream resourceUrl = ProfileFinder.class.getClassLoader().getResourceAsStream(file);
private static InputStream getTomlFileFromClasspath(final String profileConfigFile) {
InputStream resourceUrl =
ProfileFinder.class.getClassLoader().getResourceAsStream(profileConfigFile);
// this is not meant to happen, because for each InternalProfileName there is a corresponding
// TOML file in resources
if (resourceUrl == null) {
throw new CommandLine.ParameterException(
commandLine, String.format("TOML file %s not found", file));
throw new IllegalStateException(
String.format("Internal Profile TOML %s not found", profileConfigFile));
}
return resourceUrl;
}

/**
* Returns the external profile names which are file names without extension in the default
* profiles directory.
*
* @return Set of external profile names
*/
public static Set<String> getExternalProfileNames() {
final Path profilesDir = defaultProfilesDir();
if (!Files.exists(profilesDir)) {
return Set.of();
}

try (Stream<Path> pathStream = Files.list(profilesDir)) {
return pathStream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".toml"))
.map(
path ->
path.getFileName()
.toString()
.substring(0, path.getFileName().toString().length() - 5))
.collect(Collectors.toSet());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

/**
* Return default profiles directory location
*
* @return Path to default profiles directory
*/
private static Path defaultProfilesDir() {
final String profilesDir = System.getProperty("besu.profiles.dir");
if (profilesDir == null) {
return Paths.get(System.getProperty("besu.home", "."), "profiles");
} else {
return Paths.get(profilesDir);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static TomlConfigurationDefaultProvider fromInputStream(

@Override
public String defaultValue(final ArgSpec argSpec) {
loadConfigurationFromFile();
loadConfigurationIfNotLoaded();

// only options can be used in config because a name is needed for the key
// so we skip default for positional params
Expand Down Expand Up @@ -227,10 +227,10 @@ private String getNumericEntryAsString(final OptionSpec spec) {
}

private void checkConfigurationValidity() {
if (result == null || result.isEmpty())
if (result == null || result.isEmpty()) {
throw new ParameterException(
commandLine,
String.format("Unable to read TOML configuration file %s", configurationInputStream));
commandLine, "Unable to read from empty TOML configuration file.");
}

if (!isUnknownOptionsChecked && !commandLine.isUnmatchedArgumentsAllowed()) {
checkUnknownOptions(result);
Expand All @@ -239,8 +239,7 @@ private void checkConfigurationValidity() {
}

/** Load configuration from file. */
public void loadConfigurationFromFile() {

public void loadConfigurationIfNotLoaded() {
if (result == null) {
try {
final TomlParseResult result = Toml.parse(configurationInputStream);
Expand Down Expand Up @@ -289,12 +288,12 @@ private void checkUnknownOptions(final TomlParseResult result) {
.collect(Collectors.toSet());

if (!unknownOptionsList.isEmpty()) {
final String options = unknownOptionsList.size() > 1 ? "options" : "option";
final String csvUnknownOptions =
unknownOptionsList.stream().collect(Collectors.joining(", "));
final String csvUnknownOptions = String.join(", ", unknownOptionsList);
throw new ParameterException(
commandLine,
String.format("Unknown %s in TOML configuration file: %s", options, csvUnknownOptions));
String.format(
"Unknown option%s in TOML configuration file: %s",
unknownOptionsList.size() > 1 ? "s" : "", csvUnknownOptions));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ public void callingWithConfigOptionButNonExistingFileShouldDisplayHelp() throws
final Path tempConfigFilePath = createTempFile("an-invalid-file-name-without-extension", "");
parseCommand("--config-file", tempConfigFilePath.toString());

final String expectedOutputStart = "Unable to read TOML configuration file";
final String expectedOutputStart = "Unable to read from empty TOML configuration file.";
assertThat(commandErrorOutput.toString(UTF_8)).startsWith(expectedOutputStart);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.Implementation.SEQUENCED;
import static org.mockito.Mockito.mock;

import org.hyperledger.besu.cli.config.ProfileName;
import org.hyperledger.besu.cli.config.InternalProfileName;
import org.hyperledger.besu.evm.internal.EvmConfiguration;

import java.math.BigInteger;
Expand Down Expand Up @@ -213,7 +213,7 @@ void setWorldStateUpdateModeJournaled() {

@Test
void setProfile() {
builder.setProfile(ProfileName.DEV.name());
builder.setProfile(InternalProfileName.DEV.name());
final String profileSelected = builder.build();
assertThat(profileSelected).contains("Profile: DEV");
}
Expand Down
Loading

0 comments on commit ae7ddd1

Please sign in to comment.