diff --git a/config-cli/pom.xml b/config-cli/pom.xml
index e58df25dae..176edec2d4 100644
--- a/config-cli/pom.xml
+++ b/config-cli/pom.xml
@@ -24,6 +24,11 @@
config
+
+ com.quorum.tessera
+ jaxrs-client
+
+
javax.validation
validation-api
@@ -44,11 +49,29 @@
test-util
+
+ javax.ws.rs
+ javax.ws.rs-api
+
+
com.quorum.tessera
key-generation
+
+ com.quorum.tessera
+ jersey-server
+ test
+
+
+
+ org.glassfish.jersey.test-framework
+ jersey-test-framework-core
+ 2.27
+ test
+ jar
+
diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/AdminCliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/AdminCliAdapter.java
new file mode 100644
index 0000000000..aab51d26cd
--- /dev/null
+++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/AdminCliAdapter.java
@@ -0,0 +1,124 @@
+package com.quorum.tessera.config.cli;
+
+import com.quorum.tessera.config.Config;
+import com.quorum.tessera.config.Peer;
+import com.quorum.tessera.config.ServerConfig;
+import com.quorum.tessera.config.cli.parsers.ConfigurationParser;
+import com.quorum.tessera.jaxrs.client.ClientFactory;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+
+/**
+ * Cli Adapter to be used for runtime updates
+ */
+public class AdminCliAdapter implements CliAdapter {
+
+ private final ClientFactory clientFactory;
+
+ public AdminCliAdapter(ClientFactory clientFactory) {
+ this.clientFactory = Objects.requireNonNull(clientFactory);
+ }
+
+ /**
+ *
+ * @param args
+ * @return CliResult with config object always null.
+ * @throws Exception
+ */
+ @Override
+ public CliResult execute(String... args) throws Exception {
+
+ final Options options = new Options();
+
+ options.addOption(
+ Option.builder("configfile")
+ .desc("Path to node configuration file")
+ .hasArg(true)
+ .required()
+ .optionalArg(false)
+ .numberOfArgs(1)
+ .argName("PATH")
+ .build());
+
+ options.addOption(Option.builder("addpeer")
+ .desc("Add peer to running node")
+ .hasArg(true)
+ .optionalArg(false)
+ .numberOfArgs(1)
+ .argName("URL")
+ .build());
+
+ final List argsList = Arrays.asList(args);
+
+ if (argsList.contains("help") || argsList.isEmpty()) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.setWidth(200);
+ formatter.printHelp("tessera admin", options);
+ return new CliResult(0, true, null);
+ }
+
+ final CommandLine line = new DefaultParser().parse(options, args);
+ if(!line.hasOption("addpeer")) {
+ System.out.println("No peer defined");
+ return new CliResult(1, true, null);
+ }
+
+ Config config = new ConfigurationParser().parse(line);
+
+ Client restClient = clientFactory.buildFrom(config.getServerConfig());
+
+ String peerUrl = line.getOptionValue("addpeer");
+
+ final Peer peer = new Peer(peerUrl);
+
+ String scheme = Optional.of(config)
+ .map(Config::getServerConfig)
+ .map(ServerConfig::getServerUri)
+ .map(URI::getScheme)
+ .orElse("http");
+
+ Integer port = Optional.of(config)
+ .map(Config::getServerConfig)
+ .map(ServerConfig::getPort)
+ .orElse(80);
+
+ URI uri = UriBuilder.fromPath("/")
+ .port(port)
+ .host("localhost")
+ .scheme(scheme).build();
+
+
+ Response response = restClient.target(uri)
+ .path("config")
+ .path("peers")
+ .request(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .put(Entity.entity(peer, MediaType.APPLICATION_JSON));
+
+ if(response.getStatus() == Response.Status.CREATED.getStatusCode()) {
+
+ System.out.printf("Peer %s added.",response.getLocation());
+ System.out.println();
+
+ return new CliResult(0, true, null);
+ }
+
+ System.err.println("Unable to create peer");
+
+ return new CliResult(1, true, null);
+ }
+
+}
diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java
index 6af3b68d3b..6b2a908c13 100644
--- a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java
+++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliAdapter.java
@@ -4,8 +4,4 @@ public interface CliAdapter {
CliResult execute(String... args) throws Exception;
- static CliAdapter create() {
- return new DefaultCliAdapter();
- }
-
}
diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java
index 768a52f72c..07cfd70f77 100644
--- a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java
+++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliDelegate.java
@@ -1,6 +1,9 @@
package com.quorum.tessera.config.cli;
import com.quorum.tessera.config.Config;
+import com.quorum.tessera.jaxrs.client.ClientFactory;
+import java.util.Arrays;
+import java.util.List;
public enum CliDelegate {
@@ -18,7 +21,15 @@ public Config getConfig() {
public CliResult execute(String... args) throws Exception {
- final CliAdapter cliAdapter = CliAdapter.create();
+ final List argsList = Arrays.asList(args);
+
+ final CliAdapter cliAdapter;
+
+ if(argsList.contains("admin")) {
+ cliAdapter = new AdminCliAdapter(new ClientFactory());
+ } else {
+ cliAdapter = new DefaultCliAdapter();
+ }
final CliResult result = cliAdapter.execute(args);
diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java
index 4100d480cb..4bb7563c38 100644
--- a/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java
+++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/CliResult.java
@@ -8,14 +8,14 @@
public class CliResult {
private final Integer status;
- private final boolean isHelpOn;
- private final boolean isKeyGenOn;
+
+ private final boolean suppressStartup;
+
private final Config config;
- public CliResult(Integer status, boolean isHelpOn, boolean isKeyGenOn, Config config) {
+ public CliResult(Integer status, boolean suppressStartup,Config config) {
this.status = Objects.requireNonNull(status);
- this.isHelpOn = isHelpOn;
- this.isKeyGenOn = isKeyGenOn;
+ this.suppressStartup = suppressStartup;
this.config = config;
}
@@ -23,11 +23,10 @@ public Integer getStatus() {
return status;
}
- public boolean isHelpOn() {
- return isHelpOn;
+ public boolean isSuppressStartup() {
+ return suppressStartup;
}
- public boolean isKeyGenOn() {return isKeyGenOn;}
public Optional getConfig() {
return Optional.ofNullable(config);
diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java
index 3524669347..3595be46a1 100644
--- a/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java
+++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/DefaultCliAdapter.java
@@ -59,7 +59,7 @@ public CliResult execute(String... args) throws Exception {
HelpFormatter formatter = new HelpFormatter();
formatter.setWidth(200);
formatter.printHelp("tessera -configfile [-keygen ] [-pidfile ]", options);
- return new CliResult(0, true, false, null);
+ return new CliResult(0, true, null);
}
try {
@@ -87,7 +87,7 @@ public CliResult execute(String... args) throws Exception {
new PidFileParser().parse(line);
- return new CliResult(0, false, line.hasOption("keygen"), config);
+ return new CliResult(0, line.hasOption("keygen"), config);
} catch (ParseException exp) {
throw new CliException(exp.getMessage());
diff --git a/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java b/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java
index 043643308c..f873b35d15 100644
--- a/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java
+++ b/config-cli/src/main/java/com/quorum/tessera/config/cli/parsers/ConfigurationParser.java
@@ -2,6 +2,7 @@
import com.quorum.tessera.config.Config;
import com.quorum.tessera.config.ConfigFactory;
+import com.quorum.tessera.config.util.ConfigFileStore;
import com.quorum.tessera.config.keypairs.ConfigKeyPair;
import com.quorum.tessera.config.util.JaxbUtil;
import org.apache.commons.cli.CommandLine;
@@ -50,6 +51,8 @@ public Config parse(final CommandLine commandLine) throws IOException {
//we have generated new keys, so we need to output the new configuration
output(commandLine, config);
}
+
+ ConfigFileStore.create(path);
}
diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/AdminCliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/AdminCliAdapterTest.java
new file mode 100644
index 0000000000..47a8ff5875
--- /dev/null
+++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/AdminCliAdapterTest.java
@@ -0,0 +1,116 @@
+package com.quorum.tessera.config.cli;
+
+import com.quorum.tessera.config.Peer;
+import com.quorum.tessera.config.ServerConfig;
+import com.quorum.tessera.jaxrs.client.ClientFactory;
+import com.quorum.tessera.test.util.ElUtil;
+import java.net.URI;
+import java.nio.file.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Before;
+import org.junit.Test;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class AdminCliAdapterTest {
+
+ private AdminCliAdapter adminCliAdapter;
+
+ private ClientFactory clientFactory;
+
+ private Invocation.Builder invocationBuilder;
+
+ @Before
+ public void onSetUp() {
+
+ invocationBuilder = mock(Invocation.Builder.class);
+
+ clientFactory = mock(ClientFactory.class);
+
+ Client client = mock(Client.class);
+ WebTarget webTarget = mock(WebTarget.class);
+
+ when(client.target(any(URI.class))).thenReturn(webTarget);
+ when(webTarget.path(anyString())).thenReturn(webTarget,webTarget);
+
+ when(webTarget.request(MediaType.APPLICATION_JSON)).thenReturn(invocationBuilder);
+
+ when(invocationBuilder.accept(MediaType.APPLICATION_JSON)).thenReturn(invocationBuilder);
+
+ when(clientFactory.buildFrom(any(ServerConfig.class))).thenReturn(client);
+
+ adminCliAdapter = new AdminCliAdapter(clientFactory);
+ }
+
+ public void onTearDown() {
+ verifyNoMoreInteractions(invocationBuilder);
+ }
+
+
+ @Test
+ public void help() throws Exception {
+ //new CliResult(0, true, false, null);
+ CliResult result = adminCliAdapter.execute("help");
+ assertThat(result).isNotNull();
+ assertThat(result.getConfig()).isNotPresent();
+ assertThat(result.isSuppressStartup()).isTrue();
+
+ }
+
+ @Test
+ public void addPeer() throws Exception {
+
+ Peer peer = new Peer("http://junit.com:8989");
+ Entity entity = Entity.entity(peer, MediaType.APPLICATION_JSON);
+
+ URI uri = UriBuilder.fromPath("/result").build();
+ when(invocationBuilder.put(entity)).thenReturn(Response.created(uri).build());
+
+ Path configFile = ElUtil.createAndPopulatePaths(getClass().getResource("/sample-config.json"));
+
+ CliResult result = adminCliAdapter.execute("-addpeer",peer.getUrl(),"-configfile",configFile.toString());
+ assertThat(result).isNotNull();
+ assertThat(result.getConfig()).isNotPresent();
+ assertThat(result.isSuppressStartup()).isTrue();
+
+ assertThat(result.getStatus()).isEqualTo(0);
+
+ verify(invocationBuilder).put(entity);
+
+
+ }
+
+ @Test
+ public void addPeerSomethingBadWentDown() throws Exception {
+
+ Peer peer = new Peer("http://junit.com:8989");
+ Entity entity = Entity.entity(peer, MediaType.APPLICATION_JSON);
+
+ URI uri = UriBuilder.fromPath("/result").build();
+ when(invocationBuilder.put(entity)).thenReturn(Response.serverError().build());
+
+ Path configFile = ElUtil.createAndPopulatePaths(getClass().getResource("/sample-config.json"));
+
+ CliResult result = adminCliAdapter.execute("-addpeer",peer.getUrl(),"-configfile",configFile.toString());
+ assertThat(result).isNotNull();
+ assertThat(result.getConfig()).isNotPresent();
+ assertThat(result.isSuppressStartup()).isTrue();
+
+ assertThat(result.getStatus()).isEqualTo(1);
+
+ verify(invocationBuilder).put(entity);
+
+
+ }
+}
diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/CliAdapterTest.java
deleted file mode 100644
index bf96ed95c3..0000000000
--- a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliAdapterTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.quorum.tessera.config.cli;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class CliAdapterTest {
-
- @Test
- public void createDefault() {
-
- CliAdapter result = CliAdapter.create();
-
- assertThat(result).isExactlyInstanceOf(DefaultCliAdapter.class);
-
- }
-}
diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java
index dd248b47be..64711d6f18 100644
--- a/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java
+++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/CliDelegateTest.java
@@ -18,6 +18,15 @@ public void createInstance() {
}
+ @Test
+ public void createAdminInstance() throws Exception {
+
+ Path configFile = ElUtil.createAndPopulatePaths(getClass().getResource("/sample-config.json"));
+ CliResult result = instance.execute("admin", "-configfile", configFile.toString());
+
+ assertThat(result).isNotNull();
+ }
+
@Test
public void withValidConfig() throws Exception {
@@ -31,8 +40,8 @@ public void withValidConfig() throws Exception {
assertThat(result.getConfig()).isPresent();
assertThat(result.getConfig().get()).isSameAs(instance.getConfig());
assertThat(result.getStatus()).isEqualTo(0);
- assertThat(result.isHelpOn()).isFalse();
- assertThat(result.isKeyGenOn()).isFalse();
+ assertThat(result.isSuppressStartup()).isFalse();
+
}
@Test
diff --git a/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java b/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java
index 7db7b34a98..c77ead533a 100644
--- a/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java
+++ b/config-cli/src/test/java/com/quorum/tessera/config/cli/DefaultCliAdapterTest.java
@@ -36,7 +36,7 @@ public class DefaultCliAdapterTest {
@Before
public void setUp() {
MockKeyGeneratorFactory.reset();
- this.cliDelegate = CliAdapter.create();
+ this.cliDelegate = new DefaultCliAdapter();
}
@After
@@ -53,8 +53,8 @@ public void help() throws Exception {
assertThat(result).isNotNull();
assertThat(result.getConfig()).isNotPresent();
assertThat(result.getStatus()).isEqualTo(0);
- assertThat(result.isHelpOn()).isTrue();
- assertThat(result.isKeyGenOn()).isFalse();
+ assertThat(result.isSuppressStartup()).isTrue();
+
}
@@ -66,8 +66,8 @@ public void noArgsPrintsHelp() throws Exception {
assertThat(result).isNotNull();
assertThat(result.getConfig()).isNotPresent();
assertThat(result.getStatus()).isEqualTo(0);
- assertThat(result.isHelpOn()).isTrue();
- assertThat(result.isKeyGenOn()).isFalse();
+ assertThat(result.isSuppressStartup()).isTrue();
+
}
@@ -80,7 +80,7 @@ public void withValidConfig() throws Exception {
assertThat(result).isNotNull();
assertThat(result.getConfig()).isPresent();
assertThat(result.getStatus()).isEqualTo(0);
- assertThat(result.isHelpOn()).isFalse();
+ assertThat(result.isSuppressStartup()).isFalse();
}
@Test(expected = FileNotFoundException.class)
@@ -133,7 +133,7 @@ public void keygen() throws Exception {
assertThat(result).isNotNull();
assertThat(result.getStatus()).isEqualTo(0);
assertThat(result.getConfig()).isNotNull();
- assertThat(result.isHelpOn()).isFalse();
+ assertThat(result.isSuppressStartup()).isTrue();
verify(keyGenerator).generate(anyString(), eq(null));
verifyNoMoreInteractions(keyGenerator);
@@ -146,7 +146,7 @@ public void keygenThenExit() throws Exception {
final CliResult result = cliDelegate.execute("-keygen");
assertThat(result).isNotNull();
- assertThat(result.isKeyGenOn()).isTrue();
+ assertThat(result.isSuppressStartup()).isTrue();
}
diff --git a/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java b/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java
index 514b4385a1..e2356de125 100644
--- a/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java
+++ b/config-migration/src/main/java/com/quorum/tessera/config/migration/LegacyCliAdapter.java
@@ -45,7 +45,7 @@ public CliResult execute(String... args) throws Exception {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("tessera-config-migration", header, options, null);
final int exitCode = argsList.isEmpty() ? 1 : 0;
- return new CliResult(exitCode, true, false, null);
+ return new CliResult(exitCode, true, null);
}
CommandLineParser parser = new DefaultParser();
@@ -86,7 +86,7 @@ static CliResult writeToOutputFile(Config config, Path outputPath) throws IOExce
JaxbUtil.marshal(config, outputStream);
System.out.printf("Saved config to %s", outputPath);
System.out.println();
- return new CliResult(0, false, false, config);
+ return new CliResult(0, false, config);
} catch (ConstraintViolationException validationException) {
validationException.getConstraintViolations()
.stream()
@@ -96,7 +96,7 @@ static CliResult writeToOutputFile(Config config, Path outputPath) throws IOExce
Files.write(outputPath, JaxbUtil.marshalToStringNoValidation(config).getBytes());
System.out.printf("Saved config to %s", outputPath);
System.out.println();
- return new CliResult(2, false, false, config);
+ return new CliResult(2, false, config);
}
}
diff --git a/config/src/main/java/com/quorum/tessera/config/Config.java b/config/src/main/java/com/quorum/tessera/config/Config.java
index 57df24cce5..8bd68d448b 100644
--- a/config/src/main/java/com/quorum/tessera/config/Config.java
+++ b/config/src/main/java/com/quorum/tessera/config/Config.java
@@ -12,6 +12,7 @@
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.List;
@XmlRootElement
@@ -98,7 +99,7 @@ public Path getUnixSocketFile() {
}
public List getPeers() {
- return this.peers;
+ return Collections.unmodifiableList(peers);
}
public KeyConfiguration getKeys() {
@@ -116,5 +117,10 @@ public boolean isUseWhiteList() {
public boolean isDisablePeerDiscovery() {
return disablePeerDiscovery;
}
+
+ @XmlTransient
+ public void addPeer(Peer peer) {
+ this.peers.add(peer);
+ }
}
diff --git a/config/src/main/java/com/quorum/tessera/config/util/ConfigFileStore.java b/config/src/main/java/com/quorum/tessera/config/util/ConfigFileStore.java
new file mode 100644
index 0000000000..711cb06340
--- /dev/null
+++ b/config/src/main/java/com/quorum/tessera/config/util/ConfigFileStore.java
@@ -0,0 +1,47 @@
+package com.quorum.tessera.config.util;
+
+import com.quorum.tessera.config.Config;
+import com.quorum.tessera.io.IOCallback;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.UUID;
+
+public interface ConfigFileStore {
+
+ enum Store implements ConfigFileStore {
+
+ INSTANCE;
+
+ private Path path;
+
+ @Override
+ public void save(Config config) {
+
+ IOCallback.execute(() -> {
+ Path temp = Files.createTempFile(UUID.randomUUID().toString(), ".tmp");
+ try(OutputStream fout = Files.newOutputStream(temp)){
+ JaxbUtil.marshalWithNoValidation(config, fout);
+ }
+ Files.copy(temp, path,StandardCopyOption.REPLACE_EXISTING);
+ return null;
+ });
+ }
+ }
+
+ static ConfigFileStore create(Path path) {
+ Store.INSTANCE.path = path;
+ return Store.INSTANCE;
+ }
+
+
+ static ConfigFileStore get() {
+ return Store.INSTANCE;
+ }
+
+ void save(Config config);
+
+
+
+}
diff --git a/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java b/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java
index 77f10cb4ae..f37758abc4 100644
--- a/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java
+++ b/config/src/main/java/com/quorum/tessera/config/util/ConsolePasswordReader.java
@@ -10,6 +10,7 @@ public ConsolePasswordReader(final Console console) {
this.console = console;
}
+ @Override
public String readPasswordFromConsole() {
final char[] consolePassword = this.console.readPassword();
return new String(consolePassword);
diff --git a/config/src/test/java/com/quorum/tessera/config/ConfigTest.java b/config/src/test/java/com/quorum/tessera/config/ConfigTest.java
new file mode 100644
index 0000000000..de3cae6f25
--- /dev/null
+++ b/config/src/test/java/com/quorum/tessera/config/ConfigTest.java
@@ -0,0 +1,25 @@
+package com.quorum.tessera.config;
+
+import java.util.ArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Test;
+
+public class ConfigTest {
+
+ @Test
+ public void createWithNullArgs() {
+ Config config = new Config(null, null, null, null, null, null, false, false);
+ assertThat(config).isNotNull();
+ }
+
+ @Test
+ public void addPeer() {
+ Config config = new Config(null, null, new ArrayList<>(), null, null, null, false, false);
+ assertThat(config.getPeers()).isEmpty();
+ Peer peer = new Peer("Junit");
+ config.addPeer(peer);
+ assertThat(config.getPeers()).containsOnly(peer);
+
+ }
+
+}
diff --git a/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java b/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java
index 81413c76e4..266bb3b3b9 100644
--- a/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java
+++ b/config/src/test/java/com/quorum/tessera/config/OpenPojoTest.java
@@ -24,6 +24,7 @@ public void executeOpenPojoValidations() {
pc -> !pc.getClazz().isAssignableFrom(ObjectFactory.class),
pc -> !pc.getClazz().isAssignableFrom(JaxbConfigFactory.class),
pc -> !pc.getClazz().isAssignableFrom(ConfigException.class),
+ pc -> !pc.getClazz().isAssignableFrom(Config.class),
pc -> !pc.getClazz().getName().contains(ConfigItem.class.getName()),
pc -> !pc.getClazz().getSimpleName().contains("Test")
};
diff --git a/config/src/test/java/com/quorum/tessera/config/util/ConfigFileStoreTest.java b/config/src/test/java/com/quorum/tessera/config/util/ConfigFileStoreTest.java
new file mode 100644
index 0000000000..0958862455
--- /dev/null
+++ b/config/src/test/java/com/quorum/tessera/config/util/ConfigFileStoreTest.java
@@ -0,0 +1,64 @@
+package com.quorum.tessera.config.util;
+
+import com.quorum.tessera.config.Config;
+import com.quorum.tessera.io.FilesDelegate;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.UUID;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConfigFileStoreTest {
+
+ private ConfigFileStore configFileStore;
+
+ private Path path;
+
+ @Before
+ public void onSetUp() throws Exception {
+ path = Files.createTempFile(UUID.randomUUID().toString(), ".junit");
+ path.toFile().deleteOnExit();
+
+ final URL sampleConfig = getClass().getResource("/sample.json");
+ try (InputStream in = sampleConfig.openStream()) {
+ Config initialConfig = JaxbUtil.unmarshal(in, Config.class);
+ JaxbUtil.marshal(initialConfig, Files.newOutputStream(path));
+ }
+
+ configFileStore = ConfigFileStore.create(path);
+ }
+
+ @Test
+ public void getReturnsSameInstance() {
+ assertThat(ConfigFileStore.get()).isSameAs(configFileStore);
+
+ }
+
+ @Test
+ public void save() throws Exception {
+
+ final URL updatedConfig = getClass().getResource("/sample_full.json");
+ try (InputStream in = updatedConfig.openStream()) {
+ Config config = JaxbUtil.unmarshal(in, Config.class);
+ configFileStore.save(config);
+ }
+
+ final JsonObject result = Optional.of(path)
+ .map(FilesDelegate.create()::newInputStream)
+ .map(Json::createReader)
+ .map(JsonReader::readObject)
+ .get();
+
+ assertThat(result.getJsonObject("server").getString("hostName"))
+ .isEqualTo("http://localhost");
+
+ }
+
+}
diff --git a/config/src/test/resources/sample.json b/config/src/test/resources/sample.json
index ffb517ae43..731152846a 100644
--- a/config/src/test/resources/sample.json
+++ b/config/src/test/resources/sample.json
@@ -45,7 +45,7 @@
},
"type": "argon2sbox"
},
- "publicKey": "PUBLIC_KEY"
+ "publicKey": "UFVCTElDX0tFWQ=="
}
]
},
diff --git a/jaxrs-client/pom.xml b/jaxrs-client/pom.xml
new file mode 100644
index 0000000000..2dd9c9a623
--- /dev/null
+++ b/jaxrs-client/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+ com.quorum.tessera
+ tessera
+ 0.7-SNAPSHOT
+
+ jaxrs-client
+ jar
+
+
+ com.quorum.tessera
+ config
+
+
+ com.quorum.tessera
+ security
+
+
+ javax.ws.rs
+ javax.ws.rs-api
+
+
+
+ com.quorum.tessera
+ jersey-server
+ test
+
+
+
\ No newline at end of file
diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/client/ClientFactory.java b/jaxrs-client/src/main/java/com/quorum/tessera/jaxrs/client/ClientFactory.java
similarity index 96%
rename from jaxrs-service/src/main/java/com/quorum/tessera/client/ClientFactory.java
rename to jaxrs-client/src/main/java/com/quorum/tessera/jaxrs/client/ClientFactory.java
index 3d6f22d45f..0e57238ac5 100644
--- a/jaxrs-service/src/main/java/com/quorum/tessera/client/ClientFactory.java
+++ b/jaxrs-client/src/main/java/com/quorum/tessera/jaxrs/client/ClientFactory.java
@@ -1,4 +1,4 @@
-package com.quorum.tessera.client;
+package com.quorum.tessera.jaxrs.client;
import com.quorum.tessera.config.ServerConfig;
import com.quorum.tessera.ssl.context.SSLContextFactory;
diff --git a/jaxrs-service/src/test/java/com/quorum/tessera/node/ClientFactoryTest.java b/jaxrs-client/src/test/java/com/quorum/tessera/jaxrs/client/ClientFactoryTest.java
similarity index 96%
rename from jaxrs-service/src/test/java/com/quorum/tessera/node/ClientFactoryTest.java
rename to jaxrs-client/src/test/java/com/quorum/tessera/jaxrs/client/ClientFactoryTest.java
index 13f09f8a8b..e853834e4a 100644
--- a/jaxrs-service/src/test/java/com/quorum/tessera/node/ClientFactoryTest.java
+++ b/jaxrs-client/src/test/java/com/quorum/tessera/jaxrs/client/ClientFactoryTest.java
@@ -1,6 +1,6 @@
-package com.quorum.tessera.node;
+package com.quorum.tessera.jaxrs.client;
+
-import com.quorum.tessera.client.ClientFactory;
import com.quorum.tessera.config.ServerConfig;
import com.quorum.tessera.config.SslConfig;
import com.quorum.tessera.ssl.context.SSLContextFactory;
diff --git a/jaxrs-service/pom.xml b/jaxrs-service/pom.xml
index f8fed45324..ac0125066c 100644
--- a/jaxrs-service/pom.xml
+++ b/jaxrs-service/pom.xml
@@ -30,6 +30,11 @@
com.quorum.tessera
config
+
+ com.quorum.tessera
+ jaxrs-client
+
+
javax.servlet
javax.servlet-api
@@ -82,7 +87,7 @@
-
+
com.github.kongchen
swagger-maven-plugin
3.1.7
diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/api/ConfigResource.java b/jaxrs-service/src/main/java/com/quorum/tessera/api/ConfigResource.java
new file mode 100644
index 0000000000..86692f3202
--- /dev/null
+++ b/jaxrs-service/src/main/java/com/quorum/tessera/api/ConfigResource.java
@@ -0,0 +1,59 @@
+package com.quorum.tessera.api;
+
+import com.quorum.tessera.api.filter.PrivateApi;
+import com.quorum.tessera.config.Peer;
+import com.quorum.tessera.core.config.ConfigService;
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+import javax.validation.Valid;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+@PrivateApi
+@Path("/config")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class ConfigResource {
+
+ private final ConfigService configService;
+
+ public ConfigResource(ConfigService configService) {
+ this.configService = Objects.requireNonNull(configService);
+ }
+
+ @PUT
+ @Path("/peers")
+ public Response addPeer(@Valid Peer peer) {
+
+ configService.addPeer(peer.getUrl());
+
+ int index = configService.getPeers().size() - 1;
+
+ URI uri = UriBuilder.fromPath("config")
+ .path("peers")
+ .path(String.valueOf(index))
+ .build();
+ return Response.created(uri).build();
+ }
+
+ @GET
+ @Path("/peers/{index}")
+ public Response getPeer(@PathParam("index") Integer index) {
+
+ List peers = configService.getPeers();
+ if (peers.size() <= index) {
+ throw new NotFoundException("No peer found at index "+ index);
+ }
+ return Response.ok(peers.get(index)).build();
+ }
+
+}
diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java b/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java
index 2c7cafb3f0..7728e038ca 100644
--- a/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java
+++ b/jaxrs-service/src/main/java/com/quorum/tessera/api/exception/AutoDiscoveryDisabledExceptionMapper.java
@@ -1,6 +1,7 @@
package com.quorum.tessera.api.exception;
import com.quorum.tessera.node.AutoDiscoveryDisabledException;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@@ -12,6 +13,7 @@ public class AutoDiscoveryDisabledExceptionMapper implements ExceptionMapper whitelisted;
+ private static final Logger LOGGER = LoggerFactory.getLogger(IPWhitelistFilter.class);
+
+ private final ConfigService configService;
+ //private final Set whitelisted;
private boolean disabled;
private HttpServletRequest httpServletRequest;
- public IPWhitelistFilter(final Config configuration) {
-
- this.whitelisted = configuration
- .getPeers()
- .stream()
- .map(Peer::getUrl)
- .map(s -> {
- try {
- return new URL(s);
- } catch (final MalformedURLException ex) {
- throw new RuntimeException(ex);
- }
- }).map(URL::getHost)
- .collect(Collectors.toSet());
-
- //add ourself to the whitelist to let the unix socket in
- //don't use the advertised address, as we only want to talk to ourselves
- this.whitelisted.add("127.0.0.1");
-
- this.disabled = !configuration.isUseWhiteList();
+ public IPWhitelistFilter(ConfigService configService) {
+ this.configService = Objects.requireNonNull(configService);
+ this.disabled = !configService.isUseWhiteList();
}
/**
- * If the filter is disabled, return immediately
- * Otherwise, extract the callers hostname and address, and check it against the whitelist
+ * If the filter is disabled, return immediately Otherwise, extract the
+ * callers hostname and address, and check it against the whitelist
*
* If a problem occurs, then disable the filter
*
- * If the host is not whitelisted, finish the filter chain here and return an Unauthorized response
+ * If the host is not whitelisted, finish the filter chain here and return
+ * an Unauthorized response
*
* @param requestContext the context of the current request
*/
@Override
public void filter(final ContainerRequestContext requestContext) {
- if(disabled) {
+ if (disabled) {
return;
}
try {
+ final Set whitelisted = configService.getPeers().stream()
+ .map(Peer::getUrl)
+ .map(s -> IOCallback.execute(() -> new URL(s)))
+ .map(URL::getHost)
+ .collect(Collectors.toSet());
+
+ //add ourself to the whitelist to let the unix socket in
+ //don't use the advertised address, as we only want to talk to ourselves
+ whitelisted.add("127.0.0.1");
+
final String remoteAddress = httpServletRequest.getRemoteAddr();
final String remoteHost = httpServletRequest.getRemoteHost();
@@ -81,6 +81,7 @@ public void filter(final ContainerRequestContext requestContext) {
}
} catch (final Exception ex) {
+ LOGGER.error("Unexpected error while processing request.", ex);
this.disabled = true;
}
diff --git a/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java b/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java
index 30971ee99e..38cbe8513f 100644
--- a/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java
+++ b/jaxrs-service/src/main/java/com/quorum/tessera/client/RestP2pClientFactory.java
@@ -2,6 +2,7 @@
import com.quorum.tessera.config.CommunicationType;
import com.quorum.tessera.config.Config;
+import com.quorum.tessera.jaxrs.client.ClientFactory;
import com.quorum.tessera.ssl.context.ClientSSLContextFactory;
import com.quorum.tessera.ssl.context.SSLContextFactory;
import javax.ws.rs.client.Client;
diff --git a/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml b/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml
index 4b7e3759b4..0b9cc3deb2 100644
--- a/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml
+++ b/jaxrs-service/src/main/resources/tessera-jaxrs-spring.xml
@@ -24,6 +24,10 @@
+
+
+
+
@@ -37,7 +41,7 @@
-
+
@@ -46,7 +50,7 @@
-
+
diff --git a/jaxrs-service/src/test/java/com/quorum/tessera/api/ConfigResourceTest.java b/jaxrs-service/src/test/java/com/quorum/tessera/api/ConfigResourceTest.java
new file mode 100644
index 0000000000..9ed0ab7c2e
--- /dev/null
+++ b/jaxrs-service/src/test/java/com/quorum/tessera/api/ConfigResourceTest.java
@@ -0,0 +1,84 @@
+package com.quorum.tessera.api;
+
+import com.quorum.tessera.config.Peer;
+import com.quorum.tessera.core.config.ConfigService;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+import static org.assertj.core.api.Assertions.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import static org.mockito.ArgumentMatchers.anyString;
+import org.mockito.Mockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class ConfigResourceTest {
+
+ private ConfigResource configResource;
+
+ private ConfigService configService;
+
+ @Before
+ public void onSetUp() {
+ configService = mock(ConfigService.class);
+ configResource = new ConfigResource(configService);
+ }
+
+ @After
+ public void onTearDown() {
+ verifyNoMoreInteractions(configService);
+ }
+
+ @Test
+ public void addPeerIsSucessful() {
+
+ List peers = new ArrayList<>();
+ Mockito.doAnswer((inv) -> {
+ peers.add(new Peer(inv.getArgument(0)));
+ return null;
+ }).when(configService).addPeer(anyString());
+ when(configService.getPeers()).thenReturn(peers);
+
+ Peer peer = new Peer("junit");
+
+ Response response = configResource.addPeer(peer);
+ assertThat(response.getStatus()).isEqualTo(201);
+ assertThat(response.getLocation().toString()).isEqualTo("config/peers/0");
+ assertThat(peers).containsExactly(peer);
+ verify(configService).addPeer(peer.getUrl());
+ verify(configService).getPeers();
+ }
+
+ @Test
+ public void getPeerIsSucessful() {
+ Peer peer = new Peer("getPeerIsSucessfulUrl");
+ when(configService.getPeers()).thenReturn(Arrays.asList(peer));
+
+ Response response = configResource.getPeer(0);
+
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getEntity()).isEqualTo(peer);
+
+ verify(configService).getPeers();
+
+ }
+
+ @Test
+ public void getPeerNotFound() {
+ Peer peer = new Peer("getPeerNoptFound");
+ when(configService.getPeers()).thenReturn(Arrays.asList(peer));
+
+ try {
+ configResource.getPeer(2);
+ failBecauseExceptionWasNotThrown(NotFoundException.class);
+ } catch (NotFoundException ex) {
+ verify(configService).getPeers();
+ }
+ }
+}
diff --git a/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java b/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java
index 61c0c8caf0..9a2dcf2070 100644
--- a/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java
+++ b/jaxrs-service/src/test/java/com/quorum/tessera/api/filter/IPWhitelistFilterTest.java
@@ -1,8 +1,7 @@
package com.quorum.tessera.api.filter;
-import com.quorum.tessera.config.Config;
import com.quorum.tessera.config.Peer;
-import com.quorum.tessera.config.ServerConfig;
+import com.quorum.tessera.core.config.ConfigService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -10,14 +9,11 @@
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Response;
-import java.net.MalformedURLException;
-import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.Mockito.*;
public class IPWhitelistFilterTest {
@@ -26,36 +22,30 @@ public class IPWhitelistFilterTest {
private IPWhitelistFilter filter;
+
@Before
public void init() throws URISyntaxException, UnknownHostException {
this.ctx = mock(ContainerRequestContext.class);
- final Config configuration = mock(Config.class);
+ final ConfigService configService = mock(ConfigService.class);
Peer peer = new Peer("http://whitelistedHost:8080");
- when(configuration.getPeers()).thenReturn(Collections.singletonList(peer));
- when(configuration.isUseWhiteList()).thenReturn(true);
+ when(configService.getPeers()).thenReturn(Collections.singletonList(peer));
+ when(configService.isUseWhiteList()).thenReturn(true);
- final ServerConfig serverConfig = mock(ServerConfig.class);
- when(serverConfig.getBindingAddress()).thenReturn("http://localhost:8080");
- when(configuration.getServerConfig()).thenReturn(serverConfig);
-
- this.filter = new IPWhitelistFilter(configuration);
+ this.filter = new IPWhitelistFilter(configService);
}
@Test
public void disabledFilterAllowsAllRequests() throws URISyntaxException, UnknownHostException {
- final Config configuration = mock(Config.class);
- when(configuration.getPeers()).thenReturn(Collections.emptyList());
- when(configuration.isUseWhiteList()).thenReturn(false);
+ final ConfigService configService = mock(ConfigService.class);
+ when(configService.getPeers()).thenReturn(Collections.emptyList());
+ when(configService.isUseWhiteList()).thenReturn(false);
- final ServerConfig serverConfig = mock(ServerConfig.class);
- when(serverConfig.getBindingAddress()).thenReturn("http://localhost:8080");
- when(configuration.getServerConfig()).thenReturn(serverConfig);
- final IPWhitelistFilter filter = new IPWhitelistFilter(configuration);
+ final IPWhitelistFilter filter = new IPWhitelistFilter(configService);
final HttpServletRequest request = mock(HttpServletRequest.class);
doReturn("someotherhost").when(request).getRemoteAddr();
@@ -148,25 +138,6 @@ public void selfIsWhitelisted() {
verifyZeroInteractions(ctx);
}
- @Test
- public void invalidPeerCantBeWhitelisted() throws URISyntaxException {
- final Config configuration = mock(Config.class);
-
- Peer peer = new Peer("ht:whitelistedHost:8080");
- when(configuration.getPeers()).thenReturn(Collections.singletonList(peer));
- when(configuration.isUseWhiteList()).thenReturn(true);
-
- final ServerConfig serverConfig = mock(ServerConfig.class);
- when(serverConfig.getServerUri()).thenReturn(new URI("http://localhost:8080"));
- when(configuration.getServerConfig()).thenReturn(serverConfig);
- final Throwable throwable = catchThrowable(() -> new IPWhitelistFilter(configuration));
-
- assertThat(throwable)
- .isInstanceOf(RuntimeException.class)
- .hasCauseExactlyInstanceOf(MalformedURLException.class);
-
- assertThat(throwable.getCause()).hasMessage("unknown protocol: ht");
- }
}
diff --git a/pom.xml b/pom.xml
index ad8217b5bb..3fe14b39f8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -220,18 +220,18 @@
- org.ec4j.maven
- editorconfig-maven-plugin
- 0.0.5
-
-
- check
- verify
-
- check
-
-
-
+ org.ec4j.maven
+ editorconfig-maven-plugin
+ 0.0.5
+
+
+ check
+ verify
+
+ check
+
+
+
@@ -257,6 +257,7 @@
grpc-service
jaxrs-service
tessera-core
+ jaxrs-client
key-generation
@@ -398,6 +399,12 @@
0.7-SNAPSHOT
+
+ com.quorum.tessera
+ jaxrs-client
+ 0.7-SNAPSHOT
+
+
org.glassfish.jersey
jersey-bom
@@ -719,7 +726,7 @@
-