Skip to content

Commit

Permalink
Plugin Framework (#1435)
Browse files Browse the repository at this point in the history
Provide a mechanism for Pantheon to integrate plugin code at runtime.
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
  • Loading branch information
Danno Ferrin authored May 17, 2019
1 parent 0b53195 commit 8b7c5f7
Show file tree
Hide file tree
Showing 24 changed files with 860 additions and 19 deletions.
17 changes: 12 additions & 5 deletions acceptance-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,20 @@ dependencies {
testImplementation project(':consensus:ibft')
testImplementation project(':crypto')
testImplementation project(':enclave')
testImplementation project(':ethereum:eth')
testImplementation project(':ethereum:core')
testImplementation project(':ethereum:blockcreation')
testImplementation project(':ethereum:core')
testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
testImplementation project(':ethereum:eth')
testImplementation project(':ethereum:graphqlrpc')
testImplementation project(':ethereum:jsonrpc')
testImplementation project(':ethereum:permissioning')
testImplementation project(':ethereum:graphqlrpc')
testImplementation project(':ethereum:rlp')
testImplementation project(':metrics:core')
testImplementation project(path: ':plugins', configuration: 'testArtifacts')
testImplementation project(':pantheon')
testImplementation project(':services:kvstore')
testImplementation project(':testutil')
testImplementation project(':util')
testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')

testImplementation 'com.google.guava:guava'
testImplementation 'com.squareup.okhttp3:okhttp'
Expand All @@ -52,10 +53,16 @@ dependencies {

test.enabled = false

sourceSets {
test { resources { srcDirs "${rootDir}/plugins/build/libs" } }
}

processTestResources.dependsOn(':plugins:testJar')

task acceptanceTest(type: Test) {
dependsOn(rootProject.installDist)
systemProperty 'acctests.runPantheonAsProcess', 'true'
mustRunAfter rootProject.subprojects*.test
description = 'Runs Pantheon acceptance tests.'
group = 'verification'
}
acceptanceTest.dependsOn(rootProject.installDist)
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ public interface NodeConfiguration {
boolean isDiscoveryEnabled();

boolean isBootnodeEligible();

List<String> getExtraCLIOptions();
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ public class PantheonNode implements NodeConfiguration, RunnableNode, AutoClosea
private HttpRequestFactory httpRequestFactory;
private boolean useWsForJsonRpc = false;
private String token = null;
private final List<String> plugins = new ArrayList<>();
private final List<String> extraCLIOptions;

public PantheonNode(
final String name,
Expand All @@ -110,15 +112,17 @@ public PantheonNode(
final GenesisConfigProvider genesisConfigProvider,
final boolean p2pEnabled,
final boolean discoveryEnabled,
final boolean bootnodeEligible)
final boolean bootnodeEligible,
final List<String> plugins,
final List<String> extraCLIOptions)
throws IOException {
this.bootnodeEligible = bootnodeEligible;
this.homeDirectory = Files.createTempDirectory("acctest");
keyfilePath.ifPresent(
path -> {
try {
copyResource(path, homeDirectory.resolve("key"));
} catch (IOException e) {
} catch (final IOException e) {
LOG.error("Could not find key file \"{}\" in resources", path);
}
});
Expand All @@ -135,6 +139,18 @@ public PantheonNode(
this.devMode = devMode;
this.p2pEnabled = p2pEnabled;
this.discoveryEnabled = discoveryEnabled;
plugins.forEach(
pluginName -> {
try {
homeDirectory.resolve("plugins").toFile().mkdirs();
copyResource(
pluginName + ".jar", homeDirectory.resolve("plugins/" + pluginName + ".jar"));
PantheonNode.this.plugins.add(pluginName);
} catch (final IOException e) {
LOG.error("Could not find plugin \"{}\" in resources", pluginName);
}
});
this.extraCLIOptions = extraCLIOptions;
LOG.info("Created PantheonNode {}", this.toString());
}

Expand Down Expand Up @@ -391,7 +407,7 @@ public Address getAddress() {
return Util.publicKeyToAddress(keyPair.getPublicKey());
}

Path homeDirectory() {
public Path homeDirectory() {
return homeDirectory;
}

Expand Down Expand Up @@ -481,6 +497,15 @@ Optional<PermissioningConfiguration> getPermissioningConfiguration() {
return permissioningConfiguration;
}

public List<String> getPlugins() {
return plugins;
}

@Override
public List<String> getExtraCLIOptions() {
return extraCLIOptions;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,21 @@ public void startNode(final PantheonNode node) {
params.add(permissioningConfiguration.getNodeSmartContractAddress().toString());
}
});
params.addAll(node.getExtraCLIOptions());

LOG.info("Creating pantheon process with params {}", params);
final ProcessBuilder processBuilder =
new ProcessBuilder(params)
.directory(new File(System.getProperty("user.dir")).getParentFile())
.redirectErrorStream(true)
.redirectInput(Redirect.INHERIT);
if (!node.getPlugins().isEmpty()) {
processBuilder
.environment()
.put(
"PANTHEON_OPTS",
"-Dpantheon.plugins.dir=" + dataDir.resolve("plugins").toAbsolutePath().toString());
}

try {
final Process process = processBuilder.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider;

import java.util.List;
import java.util.Optional;

class PantheonFactoryConfiguration {
Expand All @@ -37,6 +38,8 @@ class PantheonFactoryConfiguration {
private final boolean p2pEnabled;
private final boolean discoveryEnabled;
private final boolean bootnodeEligible;
private final List<String> plugins;
private final List<String> extraCLIOptions;

PantheonFactoryConfiguration(
final String name,
Expand All @@ -51,7 +54,9 @@ class PantheonFactoryConfiguration {
final GenesisConfigProvider genesisConfigProvider,
final boolean p2pEnabled,
final boolean discoveryEnabled,
final boolean bootnodeEligible) {
final boolean bootnodeEligible,
final List<String> plugins,
final List<String> extraCLIOptions) {
this.name = name;
this.miningParameters = miningParameters;
this.privacyParameters = privacyParameters;
Expand All @@ -65,6 +70,8 @@ class PantheonFactoryConfiguration {
this.p2pEnabled = p2pEnabled;
this.discoveryEnabled = discoveryEnabled;
this.bootnodeEligible = bootnodeEligible;
this.plugins = plugins;
this.extraCLIOptions = extraCLIOptions;
}

public String getName() {
Expand Down Expand Up @@ -118,4 +125,12 @@ public boolean isP2pEnabled() {
public boolean isBootnodeEligible() {
return bootnodeEligible;
}

public List<String> getPlugins() {
return plugins;
}

public List<String> getExtraCLIOptions() {
return extraCLIOptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

public class PantheonFactoryConfigurationBuilder {
Expand All @@ -45,6 +47,8 @@ public class PantheonFactoryConfigurationBuilder {
private Boolean p2pEnabled = true;
private boolean discoveryEnabled = true;
private boolean bootnodeEligible = true;
private List<String> plugins = new ArrayList<>();
private List<String> extraCLIOptions = new ArrayList<>();

public PantheonFactoryConfigurationBuilder name(final String name) {
this.name = name;
Expand Down Expand Up @@ -169,6 +173,18 @@ public PantheonFactoryConfigurationBuilder discoveryEnabled(final boolean discov
return this;
}

public PantheonFactoryConfigurationBuilder plugins(final List<String> plugins) {
this.plugins.clear();
this.plugins.addAll(plugins);
return this;
}

public PantheonFactoryConfigurationBuilder extraCLIOptions(final List<String> extraCLIOptions) {
this.extraCLIOptions.clear();
this.extraCLIOptions.addAll(extraCLIOptions);
return this;
}

public PantheonFactoryConfiguration build() {
return new PantheonFactoryConfiguration(
name,
Expand All @@ -183,6 +199,8 @@ public PantheonFactoryConfiguration build() {
genesisConfigProvider,
p2pEnabled,
discoveryEnabled,
bootnodeEligible);
bootnodeEligible,
plugins,
extraCLIOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ PantheonNode create(final PantheonFactoryConfiguration config) throws IOExceptio
config.getGenesisConfigProvider(),
config.isP2pEnabled(),
config.isDiscoveryEnabled(),
config.isBootnodeEligible());
config.isBootnodeEligible(),
config.getPlugins(),
config.getExtraCLIOptions());
}

public PantheonNode createMinerNode(final String name) throws IOException {
Expand Down Expand Up @@ -173,6 +175,17 @@ public PantheonNode createArchiveNodeWithRpcDisabled(final String name) throws I
return create(new PantheonFactoryConfigurationBuilder().name(name).build());
}

public PantheonNode createPluginsNode(
final String name, final List<String> plugins, final List<String> extraCLIOptions)
throws IOException {
return create(
new PantheonFactoryConfigurationBuilder()
.name(name)
.plugins(plugins)
.extraCLIOptions(extraCLIOptions)
.build());
}

public PantheonNode createArchiveNodeWithRpcApis(
final String name, final RpcApi... enabledRpcApis) throws IOException {
final JsonRpcConfiguration jsonRpcConfig = createJsonRpcEnabledConfig();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.tests.acceptance.plugins;

import static org.assertj.core.api.Assertions.assertThat;

import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase;
import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.awaitility.Awaitility;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

public class PluginsAcceptanceTest extends AcceptanceTestBase {
private PantheonNode node;

// context: https://en.wikipedia.org/wiki/The_Magic_Words_are_Squeamish_Ossifrage
private static final String MAGIC_WORDS = "Squemish Ossifrage";

@Before
public void setUp() throws Exception {
node =
pantheon.createPluginsNode(
"node1",
Collections.singletonList("testPlugin"),
Collections.singletonList("--Xtest-option=" + MAGIC_WORDS));
cluster.start(node);
}

@Test
public void shouldRegister() throws IOException {
final Path registrationFile = node.homeDirectory().resolve("plugins/testPlugin.registered");
waitForFile(registrationFile);

// this assert is false as CLI will not be parsed at this point
assertThat(Files.readAllLines(registrationFile).stream().anyMatch(s -> s.contains(MAGIC_WORDS)))
.isFalse();
}

@Test
public void shouldStart() throws IOException {
final Path registrationFile = node.homeDirectory().resolve("plugins/testPlugin.started");
waitForFile(registrationFile);

// this assert is true as CLI will be parsed at this point
assertThat(Files.readAllLines(registrationFile).stream().anyMatch(s -> s.contains(MAGIC_WORDS)))
.isTrue();
}

@Test
@Ignore("No way to do a graceful shutdown of Pantheon at the moment.")
public void shouldStop() {
cluster.stopNode(node);
waitForFile(node.homeDirectory().resolve("plugins/testPlugin.stopped"));
}

private void waitForFile(final Path path) {
final File file = path.toFile();
Awaitility.waitAtMost(30, TimeUnit.SECONDS)
.until(
() -> {
if (file.exists()) {
try (final Stream<String> s = Files.lines(path)) {
return s.count() > 0;
}
} else {
return false;
}
});
}
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ allprojects {
target '*.gradle'
greclipse().configFile(rootProject.file('gradle/formatter.properties'))
endWithNewline()
paddedCell()
}

// Below this line are currently only license header tasks
Expand Down
3 changes: 2 additions & 1 deletion pantheon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
implementation project(':consensus:clique')
implementation project(':consensus:ibft')
implementation project(':consensus:ibftlegacy')
implementation project(':enclave')
implementation project(':ethereum:blockcreation')
implementation project(':ethereum:core')
implementation project(':ethereum:eth')
Expand All @@ -40,8 +41,8 @@ dependencies {
implementation project(':ethereum:permissioning')
implementation project(':ethereum:p2p')
implementation project(':ethereum:rlp')
implementation project(':plugins')
implementation project(':metrics:core')
implementation project(':enclave')
implementation project(':services:kvstore')

implementation 'com.graphql-java:graphql-java'
Expand Down
Loading

0 comments on commit 8b7c5f7

Please sign in to comment.