diff --git a/bin/minimesos b/bin/minimesos old mode 100644 new mode 100755 diff --git a/cli/src/integration-test/java/com/containersol/minimesos/main/CommandPsTest.java b/cli/src/integration-test/java/com/containersol/minimesos/main/CommandPsTest.java index 7b8e9731..6b4cb99c 100644 --- a/cli/src/integration-test/java/com/containersol/minimesos/main/CommandPsTest.java +++ b/cli/src/integration-test/java/com/containersol/minimesos/main/CommandPsTest.java @@ -4,7 +4,10 @@ import com.containersol.minimesos.cluster.MesosCluster; import com.containersol.minimesos.cluster.MesosClusterFactory; import com.containersol.minimesos.mesos.MesosMasterContainer; +import com.containersol.minimesos.state.Discovery; import com.containersol.minimesos.state.Framework; +import com.containersol.minimesos.state.Port; +import com.containersol.minimesos.state.Ports; import com.containersol.minimesos.state.State; import com.containersol.minimesos.state.Task; import org.apache.commons.io.output.ByteArrayOutputStream; @@ -15,6 +18,7 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; @@ -22,9 +26,9 @@ public class CommandPsTest { - private static final String FORMAT = "%-20s %-20s %s\n"; - private static final Object[] COLUMNS = { "FRAMEWORK", "TASK", "STATE" }; - private static final Object[] VALUES = {"marathon", "weave-scope", "TASK_RUNNING" }; + private static final String FORMAT = "%-20s %-20s %-20s %-20s\n"; + private static final Object[] COLUMNS = { "FRAMEWORK", "TASK", "STATE", "PORT"}; + private static final Object[] VALUES = {"marathon", "weave-scope", "TASK_RUNNING", "4040" }; private ByteArrayOutputStream outputStream; @@ -46,6 +50,17 @@ public void execute() throws UnsupportedEncodingException { task.setName("weave-scope"); task.setState("TASK_RUNNING"); + Port port = new Port(); + port.setNumber(4040); + + Ports ports = new Ports(); + ports.setPorts(singletonList(port)); + + Discovery discovery = new Discovery(); + discovery.setPorts(ports); + + task.setDiscovery(discovery); + ArrayList tasks = new ArrayList<>(); tasks.add(task); diff --git a/cli/src/integration-test/java/com/containersol/minimesos/main/CommandTest.java b/cli/src/integration-test/java/com/containersol/minimesos/main/CommandTest.java index 5b097592..a304993b 100644 --- a/cli/src/integration-test/java/com/containersol/minimesos/main/CommandTest.java +++ b/cli/src/integration-test/java/com/containersol/minimesos/main/CommandTest.java @@ -150,7 +150,7 @@ public void testInstall() { commandUp.execute(); CommandInstall install = new CommandInstall(); - install.setMarathonFile("src/integration-test/resources/app.json"); + install.app = "src/integration-test/resources/app.json"; install.execute(); @@ -163,7 +163,7 @@ public void testInstall_alreadyRunning() { commandUp.execute(); CommandInstall install = new CommandInstall(); - install.setMarathonFile("src/integration-test/resources/app.json"); + install.app = "src/integration-test/resources/app.json"; install.execute(); install.execute(); diff --git a/cli/src/integration-test/java/com/containersol/minimesos/main/CommandUninstallTest.java b/cli/src/integration-test/java/com/containersol/minimesos/main/CommandUninstallTest.java index 7bf637ef..3cc50b9a 100644 --- a/cli/src/integration-test/java/com/containersol/minimesos/main/CommandUninstallTest.java +++ b/cli/src/integration-test/java/com/containersol/minimesos/main/CommandUninstallTest.java @@ -5,8 +5,8 @@ import com.containersol.minimesos.cluster.Marathon; import com.containersol.minimesos.cluster.MesosCluster; import com.containersol.minimesos.cluster.MesosClusterFactory; +import mesosphere.marathon.client.model.v2.Result; import org.apache.commons.io.output.ByteArrayOutputStream; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Matchers; @@ -15,6 +15,9 @@ import java.io.PrintStream; import java.io.UnsupportedEncodingException; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + public class CommandUninstallTest { private ByteArrayOutputStream outputStream; @@ -33,24 +36,55 @@ public void initTest() { marathon = Mockito.mock(Marathon.class); mesosCluster = Mockito.mock(MesosCluster.class); - Mockito.when(mesosCluster.getMarathon()).thenReturn(marathon); + when(mesosCluster.getMarathon()).thenReturn(marathon); repository = Mockito.mock(ClusterRepository.class); - Mockito.when(repository.loadCluster(Matchers.any(MesosClusterFactory.class))).thenReturn(mesosCluster); + when(repository.loadCluster(Matchers.any(MesosClusterFactory.class))).thenReturn(mesosCluster); commandUninstall = new CommandUninstall(ps); commandUninstall.setRepository(repository); - commandUninstall.setApp("app"); } @Test - public void execute() throws UnsupportedEncodingException { - Mockito.doNothing().when(marathon).deleteApp("app"); + public void execute_app() throws UnsupportedEncodingException { + // Given + commandUninstall.setApp("/app"); + when(marathon.deleteApp("/app")).thenReturn(new Result()); + // When commandUninstall.execute(); - String result = outputStream.toString("UTF-8"); - Assert.assertEquals("Deleted app 'app'\n", result); + // Then + String string = outputStream.toString("UTF-8"); + assertEquals("Deleted app '/app'\n", string); + } + + @Test + public void execute_group() throws UnsupportedEncodingException { + // Given + commandUninstall.setGroup("/group"); + when(marathon.deleteGroup("/group")).thenReturn(new Result()); + + // When + commandUninstall.execute(); + + // Then + String string = outputStream.toString("UTF-8"); + assertEquals("Deleted group '/group'\n", string); + } + + @Test + public void execute_appAndGroup() throws UnsupportedEncodingException { + // Given + commandUninstall.setGroup("/group1"); + commandUninstall.setApp("/app2"); + + // When + commandUninstall.execute(); + + // Then + String string = outputStream.toString("UTF-8"); + assertEquals("Please specify --app or --group to uninstall an app or group\n", string); } @Test @@ -60,6 +94,6 @@ public void execute_appDoesNotExist() throws UnsupportedEncodingException { commandUninstall.execute(); String result = outputStream.toString("UTF-8"); - Assert.assertEquals("", result); + assertEquals("Please specify --app or --group to uninstall an app or group\n", result); } } diff --git a/cli/src/main/java/com/containersol/minimesos/main/CommandInfo.java b/cli/src/main/java/com/containersol/minimesos/main/CommandInfo.java index a2968561..b0dda2c0 100644 --- a/cli/src/main/java/com/containersol/minimesos/main/CommandInfo.java +++ b/cli/src/main/java/com/containersol/minimesos/main/CommandInfo.java @@ -45,7 +45,7 @@ public void execute() { MesosDns mesosDns = cluster.getMesosDns(); if (mesosDns != null) { - output.println("Running dnsmasq? Add 'server=/mm/" + mesosDns.getIpAddress() + "#5353' to /etc/dnsmasq.d/10-minimesos to resolve master.mm, zookeeper.mm and Marathon apps on app.marathon.mm."); + output.println("Running dnsmasq? Add 'server=/mm/" + mesosDns.getIpAddress() + "#53' to /etc/dnsmasq.d/10-minimesos to resolve master.mm, zookeeper.mm and Marathon apps on app.marathon.mm."); } } else { output.println(String.format("Minimesos cluster %s is not running. %s is removed", clusterId, repository.getMinimesosFile().getAbsolutePath())); diff --git a/cli/src/main/java/com/containersol/minimesos/main/CommandInstall.java b/cli/src/main/java/com/containersol/minimesos/main/CommandInstall.java index e13a81c7..63e4ced2 100644 --- a/cli/src/main/java/com/containersol/minimesos/main/CommandInstall.java +++ b/cli/src/main/java/com/containersol/minimesos/main/CommandInstall.java @@ -7,65 +7,37 @@ import com.containersol.minimesos.cluster.Marathon; import com.containersol.minimesos.cluster.MesosCluster; import com.containersol.minimesos.mesos.MesosClusterContainersFactory; +import org.apache.commons.io.IOUtils; import java.io.IOException; -import java.io.InputStream; -import java.util.Scanner; + +import static org.apache.commons.lang.StringUtils.*; /** - * Installs a framework with Marathon + * Installs an Marathon application or application group. */ -@Parameters(commandDescription = "Install a framework with Marathon") +@Parameters(commandDescription = "Install a Marathon application or application group") public class CommandInstall implements Command { - public static final String CLINAME = "install"; + private static final String CLINAME = "install"; + + @Deprecated + @Parameter(names = "--marathonFile", description = "[Deprecated - Please use --marathonApp] Relative path or URL to a JSON file with a Marathon app definition.") + String marathonFile = null; + + @Parameter(names = "--app", description = "Relative path or URL to a JSON file with a Marathon app definition. See https://mesosphere.github.io/marathon/docs/application-basics.html.") + String app = null; - @Parameter(names = "--marathonFile", description = "Marathon JSON app install file path or URL. Either this or --stdin parameter must be used") - private String marathonFile = null; + @Parameter(names = "--group", description = "Relative path or URL to a JSON file with a group of Marathon apps. See https://mesosphere.github.io/marathon/docs/application-groups.html.") + String group = null; - @Parameter(names = "--stdin", description = "Use JSON from standard import. Allow piping JSON from other processes. Either this or --marathonFile parameter must be used") + @Parameter(names = "--stdin", description = "Read JSON file with Marathon app or group definition from stdin.") private boolean stdin = false; @Parameter(names = "--update", description = "Update a running application instead of attempting to deploy a new application") private boolean update = false; - private ClusterRepository repository = new ClusterRepository(); - - /** - * Getting content of marathonFile, if provided, or standard input - * - * @return content of the file or standard input - */ - public String getMarathonJson() throws IOException { - - String fileContents = ""; - Scanner scanner; - - if (marathonFile != null && !marathonFile.isEmpty()) { - - InputStream json = MesosCluster.getInputStream(marathonFile); - if (json == null) { - throw new MinimesosException("Failed to find content of " + marathonFile); - } - - scanner = new Scanner(json); - - } else if (stdin) { - scanner = new Scanner(System.in); - } else { - throw new MinimesosException("Neither --marathonFile nor --stdin parameters are provided"); - } - - try { - while (scanner.hasNextLine()) { - fileContents = fileContents.concat(scanner.nextLine()); - } - } finally { - scanner.close(); - } - - return fileContents; - } + ClusterRepository repository = new ClusterRepository(); @Override public void execute() { @@ -80,22 +52,46 @@ public void execute() { try { marathonJson = getMarathonJson(); } catch (IOException e) { - throw new MinimesosException("Failed to read JSON", e); + throw new MinimesosException("Failed to read JSON file from path, URL or stdin", e); } if (update) { marathon.updateApp(marathonJson); - } else { + } else if (isNotBlank(app) || isNotBlank(marathonFile)) { marathon.deployApp(marathonJson); + } else if (isNotBlank(group)) { + marathon.deployGroup(marathonJson); + } else { + throw new MinimesosException("Neither app, group, --stdinApp or --stdinGroup is provided"); } } else { throw new MinimesosException("Running cluster is not found"); } } + /** + * Getting content of the Marathon JSON file if specified or via standard input + * + * @return content of the file or standard input + */ + private String getMarathonJson() throws IOException { + if (stdin) { + return IOUtils.toString(System.in, "UTF-8"); + } else { + if (isNotBlank(marathonFile)) { + return IOUtils.toString(MesosCluster.getInputStream(marathonFile), "UTF-8"); + } else if (isNotBlank(app)) { + return IOUtils.toString(MesosCluster.getInputStream(app), "UTF-8"); + } else if (isNotBlank(group)) { + return IOUtils.toString(MesosCluster.getInputStream(group), "UTF-8"); + } + } + throw new IOException("Please specify a URL or path to Marathon JSON file or use --stdin"); + } + @Override public boolean validateParameters() { - return stdin || (marathonFile != null && !marathonFile.isEmpty()); + return isNotBlank(app) || isNotBlank(group) || isNotBlank(marathonFile); } @Override @@ -103,7 +99,4 @@ public String getName() { return CLINAME; } - public void setMarathonFile(String marathonFile) { - this.marathonFile = marathonFile; - } } diff --git a/cli/src/main/java/com/containersol/minimesos/main/CommandPs.java b/cli/src/main/java/com/containersol/minimesos/main/CommandPs.java index 3555bf97..5132bc4c 100644 --- a/cli/src/main/java/com/containersol/minimesos/main/CommandPs.java +++ b/cli/src/main/java/com/containersol/minimesos/main/CommandPs.java @@ -16,8 +16,9 @@ @Parameters(separators = "=", commandDescription = "List running tasks") public class CommandPs implements Command { - private static final String FORMAT = "%-20s %-20s %s\n"; - private static final Object[] COLUMNS = { "FRAMEWORK", "TASK", "STATE" }; + private static final String FORMAT = "%-20s %-20s %-20s %-20s\n"; + + private static final Object[] COLUMNS = { "FRAMEWORK", "TASK", "STATE", "PORT" }; private ClusterRepository repository = new ClusterRepository(); @@ -54,7 +55,7 @@ public void execute() { State state = cluster.getMaster().getState(); for (Framework framework : state.getFrameworks()) { for (Task task : framework.getTasks()) { - output.printf(FORMAT, framework.getName(), task.getName(), task.getState()); + output.printf(FORMAT, framework.getName(), task.getName(), task.getState(), task.getDiscovery().getPorts().getPorts().get(0).getNumber()); } } } diff --git a/cli/src/main/java/com/containersol/minimesos/main/CommandUninstall.java b/cli/src/main/java/com/containersol/minimesos/main/CommandUninstall.java index 72d08e4d..f5f991fb 100644 --- a/cli/src/main/java/com/containersol/minimesos/main/CommandUninstall.java +++ b/cli/src/main/java/com/containersol/minimesos/main/CommandUninstall.java @@ -7,26 +7,32 @@ import com.containersol.minimesos.cluster.Marathon; import com.containersol.minimesos.cluster.MesosCluster; import com.containersol.minimesos.mesos.MesosClusterContainersFactory; + import java.io.PrintStream; +import static org.apache.commons.lang.StringUtils.isNotBlank; + /** * Uninstalls a Marathon app or framework */ @Parameters(separators = "=", commandDescription = "Uninstall a Marathon app") public class CommandUninstall implements Command { - @Parameter(names = "--app", description = "Marathon app to uninstall", required = true) + @Parameter(names = "--app", description = "Marathon app to uninstall") private String app = null; + @Parameter(names = "--group", description = "Marathon group to uninstall") + private String group = null; + private ClusterRepository repository = new ClusterRepository(); private PrintStream output = System.out; // NOSONAR - public CommandUninstall(PrintStream output) { + CommandUninstall(PrintStream output) { this.output = output; } - public CommandUninstall() { + CommandUninstall() { // NOSONAR } @@ -54,20 +60,39 @@ public void execute() { throw new MinimesosException("Marathon container is not found in cluster " + cluster.getClusterId()); } - try { - marathon.deleteApp(app); - } catch (MinimesosException e) { // NOSONAR - // Only print message when uninstall succeeds + if (isNotBlank(app) && isNotBlank(group)) { + output.println("Please specify --app or --group to uninstall an app or group"); return; } - output.println("Deleted app '" + app + "'"); + + if (isNotBlank(app)) { + try { + marathon.deleteApp(app); + output.println("Deleted app '" + app + "'"); + } catch (MinimesosException e) { // NOSONAR + output.println(e.getMessage()); + } + } else if (isNotBlank(group)) { + try { + marathon.deleteGroup(group); + output.println("Deleted group '" + group + "'"); + } catch (MinimesosException e) { // NOSONAR + output.println(e.getMessage()); + } + } else { + output.println("Please specify --app or --group to uninstall an app or group"); + } } public void setRepository(ClusterRepository repository) { this.repository = repository; } - public void setApp(String app) { + void setApp(String app) { this.app = app; } + + void setGroup(String group) { + this.group = group; + } } diff --git a/cli/src/main/java/com/containersol/minimesos/main/Main.java b/cli/src/main/java/com/containersol/minimesos/main/Main.java index ecab1883..f611884a 100644 --- a/cli/src/main/java/com/containersol/minimesos/main/Main.java +++ b/cli/src/main/java/com/containersol/minimesos/main/Main.java @@ -1,6 +1,7 @@ package com.containersol.minimesos.main; import java.io.PrintStream; +import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; @@ -113,12 +114,14 @@ public int run(String[] args) { return EXIT_CODE_OK; } catch (Exception ex) { + PrintWriter writer = new PrintWriter(output); if (ex.getMessage() != null) { - output.println("Failed to run command '" + jc.getParsedCommand() + "'. " + ex.getMessage()); + output.println("Failed to run command '" + jc.getParsedCommand() + "'. " + ex.getCause()); } else { output.println("Failed to run command '" + jc.getParsedCommand() + "'."); - ex.printStackTrace(); + ex.printStackTrace(writer); } + writer.close(); LOGGER.debug("Exception while processing", ex); return EXIT_CODE_ERR; } diff --git a/cli/src/test/java/com/containersol/minimesos/main/CommandInstallTest.java b/cli/src/test/java/com/containersol/minimesos/main/CommandInstallTest.java new file mode 100644 index 00000000..a0e8c6aa --- /dev/null +++ b/cli/src/test/java/com/containersol/minimesos/main/CommandInstallTest.java @@ -0,0 +1,78 @@ +package com.containersol.minimesos.main; + +import com.containersol.minimesos.cluster.ClusterRepository; +import com.containersol.minimesos.cluster.Marathon; +import com.containersol.minimesos.cluster.MesosCluster; +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.FileReader; +import java.io.IOException; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CommandInstallTest { + + private Marathon marathon; + + private MesosCluster mesosCluster; + + private ClusterRepository repository; + + private CommandInstall command; + + @Before + public void before() { + marathon = mock(Marathon.class); + + mesosCluster = mock(MesosCluster.class); + when(mesosCluster.getMarathon()).thenReturn(marathon); + + repository = mock(ClusterRepository.class); + when(repository.loadCluster(any())).thenReturn(mesosCluster); + + command = new CommandInstall(); + command.repository = repository; + } + + @Test + public void testInstallMarathonFile() throws IOException { + // Given + command.marathonFile = "src/test/resources/app.json"; + + // When + command.execute(); + + // Then + verify(marathon).deployApp(IOUtils.toString(new FileReader(command.marathonFile))); + } + + @Test + public void testInstallMarathonApp() throws IOException { + // Given + command.app = "src/test/resources/app.json"; + + // When + command.execute(); + + // Then + verify(marathon).deployApp(IOUtils.toString(new FileReader(command.app))); + } + + @Test + public void testInstallMarathonGroup() throws IOException { + // Given + command.group = "src/test/resources/group.json"; + + // When + command.execute(); + + // Then + verify(marathon).deployGroup(IOUtils.toString(new FileReader(command.group))); + } + +} diff --git a/cli/src/test/resources/app.json b/cli/src/test/resources/app.json new file mode 100644 index 00000000..c1b20de3 --- /dev/null +++ b/cli/src/test/resources/app.json @@ -0,0 +1,13 @@ +{ + "id": "hello", + "container": { + "type": "MESOS", + "docker": { + "image": "weaveworks/", + "network": "HOST" + } + }, + "cpus": 0.1, + "mem": 16.0, + "instances": 1 +} diff --git a/cli/src/test/resources/group.json b/cli/src/test/resources/group.json new file mode 100644 index 00000000..f3ba7775 --- /dev/null +++ b/cli/src/test/resources/group.json @@ -0,0 +1,19 @@ +{ + "id": "pingPongGroup", + "groups": [ + { + "id": "ping", + "cmd": "echo 'ping'", + "cpus": 0.1, + "mem": 16.0, + "instances": 1 + } + ,{ + "id": "pong", + "cmd": "echo 'pong'", + "cpus": 0.1, + "mem": 16.0, + "instances": 1 + } + ] +} diff --git a/minimesos/src/main/groovy/com/containersol/minimesos/config/AppConfig.groovy b/minimesos/src/main/groovy/com/containersol/minimesos/config/AppConfig.groovy index 86f443ac..43640684 100644 --- a/minimesos/src/main/groovy/com/containersol/minimesos/config/AppConfig.groovy +++ b/minimesos/src/main/groovy/com/containersol/minimesos/config/AppConfig.groovy @@ -7,11 +7,11 @@ class AppConfig { private String marathonJson - public void setMarathonJson(String marathonJson) { + void setMarathonJson(String marathonJson) { this.marathonJson = marathonJson } - public String getMarathonJson() { + String getMarathonJson() { return marathonJson } diff --git a/minimesos/src/main/groovy/com/containersol/minimesos/config/ClusterConfig.groovy b/minimesos/src/main/groovy/com/containersol/minimesos/config/ClusterConfig.groovy index 33ca77d8..25852249 100644 --- a/minimesos/src/main/groovy/com/containersol/minimesos/config/ClusterConfig.groovy +++ b/minimesos/src/main/groovy/com/containersol/minimesos/config/ClusterConfig.groovy @@ -14,9 +14,9 @@ class ClusterConfig extends GroovyBlock { public static final String DEFAULT_LOGGING_LEVEL = "INFO" def call(Closure cl) { - cl.setDelegate(this); + cl.setDelegate(this) cl.setResolveStrategy(Closure.DELEGATE_ONLY) - cl.call(); + cl.call() } boolean mapPortsToHost = false @@ -53,7 +53,7 @@ class ClusterConfig extends GroovyBlock { if (zookeeper != null) { throw new RuntimeException("Multiple Zookeepers are not supported in this version yet") } - zookeeper = new ZooKeeperConfig(); + zookeeper = new ZooKeeperConfig() delegateTo(zookeeper, cl) } @@ -61,7 +61,7 @@ class ClusterConfig extends GroovyBlock { if (marathon != null) { throw new RuntimeException("Cannot have more than 1 marathon") } - marathon = new MarathonConfig(); + marathon = new MarathonConfig() delegateTo(marathon, cl) } diff --git a/minimesos/src/main/groovy/com/containersol/minimesos/config/GroupConfig.groovy b/minimesos/src/main/groovy/com/containersol/minimesos/config/GroupConfig.groovy new file mode 100644 index 00000000..6b62bfeb --- /dev/null +++ b/minimesos/src/main/groovy/com/containersol/minimesos/config/GroupConfig.groovy @@ -0,0 +1,18 @@ +package com.containersol.minimesos.config; + +/** + * Configuration for a Marathon group. Path is relative to the minimesosFile. + */ +class GroupConfig { + + private String marathonJson + + void setMarathonJson(String marathonJson) { + this.marathonJson = marathonJson + } + + String getMarathonJson() { + return marathonJson + } + +} diff --git a/minimesos/src/main/groovy/com/containersol/minimesos/config/MarathonConfig.groovy b/minimesos/src/main/groovy/com/containersol/minimesos/config/MarathonConfig.groovy index 273903aa..3b14f046 100644 --- a/minimesos/src/main/groovy/com/containersol/minimesos/config/MarathonConfig.groovy +++ b/minimesos/src/main/groovy/com/containersol/minimesos/config/MarathonConfig.groovy @@ -1,19 +1,19 @@ package com.containersol.minimesos.config -import com.containersol.minimesos.MinimesosException; +import com.containersol.minimesos.MinimesosException -public class MarathonConfig extends ContainerConfigBlock implements ContainerConfig { +class MarathonConfig extends ContainerConfigBlock implements ContainerConfig { public static final String MARATHON_IMAGE = "mesosphere/marathon" public static final String MARATHON_IMAGE_TAG = "v1.3.5" - public static final int MARATHON_PORT = 8080; + public static final int MARATHON_PORT = 8080 public static final String MARATHON_CMD = "--master zk://minimesos-zookeeper:2181/mesos --zk zk://minimesos-zookeeper:2181/marathon" - - List apps = new ArrayList<>(); + List apps = new ArrayList<>() + List groups = new ArrayList<>() String cmd - public MarathonConfig() { + MarathonConfig() { imageName = MARATHON_IMAGE imageTag = MARATHON_IMAGE_TAG cmd = MARATHON_CMD @@ -26,8 +26,17 @@ public class MarathonConfig extends ContainerConfigBlock implements ContainerCon if (app.getMarathonJson() == null) { throw new MinimesosException("App config must have a 'marathonJson' property") } - apps.add(app) } + def group(@DelegatesTo(GroupConfig) Closure cl) { + def group = new GroupConfig() + delegateTo(group, cl) + + if (group.getMarathonJson() == null) { + throw new MinimesosException("Group config must have a 'marathonJson' property") + } + groups.add(group) + } + } diff --git a/minimesos/src/main/java/com/containersol/minimesos/cluster/Marathon.java b/minimesos/src/main/java/com/containersol/minimesos/cluster/Marathon.java index 7d1798f3..0dda28d0 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/cluster/Marathon.java +++ b/minimesos/src/main/java/com/containersol/minimesos/cluster/Marathon.java @@ -1,5 +1,9 @@ package com.containersol.minimesos.cluster; +import com.mashape.unirest.http.HttpResponse; +import com.mashape.unirest.http.JsonNode; +import mesosphere.marathon.client.model.v2.Result; + /** * Functionality, which is expected from Marathon */ @@ -36,5 +40,19 @@ public interface Marathon extends ClusterProcess { * * @param app to be deleted */ - void deleteApp(String app); + Result deleteApp(String app); + + /** + * Deploy a Marathon application group. + * + * @param groupJson JSON string with Marathon application group definition + */ + void deployGroup(String groupJson); + + /** + * Deploy a Marathon application group. + * + * @param group group name + */ + Result deleteGroup(String group); } diff --git a/minimesos/src/main/java/com/containersol/minimesos/cluster/MesosCluster.java b/minimesos/src/main/java/com/containersol/minimesos/cluster/MesosCluster.java index 5a7da5fd..aef9cf86 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/cluster/MesosCluster.java +++ b/minimesos/src/main/java/com/containersol/minimesos/cluster/MesosCluster.java @@ -106,12 +106,7 @@ private MesosCluster(String clusterId, MesosClusterFactory factory) { for (MesosAgent mesosAgent : getAgents()) { mesosAgent.setZooKeeper(zookeeper); } - - master.setZooKeeper(zookeeper); - - if (getMarathon() != null) { - getMarathon().setZooKeeper(zookeeper); - } + getMarathon().setZooKeeper(zookeeper); } running = true; @@ -169,10 +164,8 @@ public void state(PrintStream out) { * Destroys the Mesos cluster and its containers */ public void destroy(MesosClusterFactory factory) { - LOGGER.debug("Cluster " + getClusterId() + " - destroy"); - // stop applications, which are installed through marathon Marathon marathon = getMarathon(); if (marathon != null) { marathon.killAllApps(); diff --git a/minimesos/src/main/java/com/containersol/minimesos/marathon/MarathonContainer.java b/minimesos/src/main/java/com/containersol/minimesos/marathon/MarathonContainer.java index e15369a0..22450199 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/marathon/MarathonContainer.java +++ b/minimesos/src/main/java/com/containersol/minimesos/marathon/MarathonContainer.java @@ -7,6 +7,7 @@ import com.containersol.minimesos.cluster.MesosCluster; import com.containersol.minimesos.cluster.ZooKeeper; import com.containersol.minimesos.config.AppConfig; +import com.containersol.minimesos.config.GroupConfig; import com.containersol.minimesos.config.MarathonConfig; import com.containersol.minimesos.integrationtest.container.AbstractContainer; import com.containersol.minimesos.docker.DockerClientFactory; @@ -16,13 +17,12 @@ import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.Ports; -import com.mashape.unirest.http.HttpResponse; -import com.mashape.unirest.http.JsonNode; import com.mashape.unirest.http.Unirest; import com.mashape.unirest.http.exceptions.UnirestException; +import mesosphere.marathon.client.model.v2.Group; +import mesosphere.marathon.client.model.v2.Result; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpStatus; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; @@ -42,10 +42,11 @@ import static com.containersol.minimesos.config.MarathonConfig.*; import static com.jayway.awaitility.Awaitility.await; +import static java.lang.String.format; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; /** - * Marathon container. Marathon is a cluster-wide init and control system for services in cgroups or Docker containers. + * Marathon is a cluster-wide init and control system for services. See https://mesosphere.github.io/marathon/docs/ */ public class MarathonContainer extends AbstractContainer implements Marathon { @@ -53,7 +54,8 @@ public class MarathonContainer extends AbstractContainer implements Marathon { private static final String TOKEN_HOST_DIR = "MINIMESOS_HOST_DIR"; - private static final String END_POINT_EXT = "/v2/apps"; + private static final String APPS_ENDPOINT = "/v2/apps"; + private static final String HEADER_ACCEPT = "accept"; private final MarathonConfig config; @@ -69,7 +71,7 @@ public MarathonContainer(MesosCluster cluster, String uuid, String containerId) this(cluster, uuid, containerId, new MarathonConfig()); } - public MarathonContainer(MesosCluster cluster, String uuid, String containerId, MarathonConfig config) { + private MarathonContainer(MesosCluster cluster, String uuid, String containerId, MarathonConfig config) { super(cluster, uuid, containerId, config); this.config = config; } @@ -111,20 +113,6 @@ public URI getServiceUrl() { return serviceUri; } - - @Override - public void deleteApp(String app) { - try { - HttpResponse deleteResponse = Unirest.delete(getServiceUrl().toString() + END_POINT_EXT + "/" + app).header(HEADER_ACCEPT, APPLICATION_JSON).asJson(); - deleteResponse.getBody().getObject(); - if (!(deleteResponse.getStatus() == HttpStatus.SC_OK)) { - throw new MinimesosException("Could not delete app: " + app); - } - } catch (UnirestException e) { - throw new MinimesosException("Could not delete app: " + app, e); - } - } - @Override protected CreateContainerCmd dockerCommand() { ExposedPort exposedPort = ExposedPort.tcp(MARATHON_PORT); @@ -157,14 +145,48 @@ private String getMarathonEndpoint() { @Override public void deployApp(String marathonJson) { mesosphere.marathon.client.Marathon marathon = MarathonClient.getInstance(getMarathonEndpoint()); - try { marathon.createApp(constructApp(marathonJson)); } catch (MarathonException e) { throw new MinimesosException("Marathon did not accept the app, error: " + e.toString()); } + LOGGER.debug(format("Installed app at '%s'", getMarathonEndpoint())); + } + + @Override + public Result deleteApp(String appId) { + mesosphere.marathon.client.Marathon marathon = MarathonClient.getInstance(getMarathonEndpoint()); + try { + Result result = marathon.deleteApp(appId); + LOGGER.debug(format("Deleted app '%s' at '%s'", appId, getMarathonEndpoint())); + return result; + } catch (MarathonException e) { + throw new MinimesosException("Could not delete app '" + appId + "'. " + e.getMessage()); + } + } - LOGGER.debug(String.format("Installing an app on marathon %s", getMarathonEndpoint())); + @Override + public void deployGroup(String groupJson) { + mesosphere.marathon.client.Marathon marathon = MarathonClient.getInstance(getMarathonEndpoint()); + try { + Group group = constructGroup(groupJson); + marathon.createGroup(group); + } catch (Exception e) { + throw new MinimesosException("Marathon did not accept the app, error: " + e.toString(), e); + } + LOGGER.debug(format("Installing group at %s", getMarathonEndpoint())); + } + + @Override + public Result deleteGroup(String groupId) { + mesosphere.marathon.client.Marathon marathon = MarathonClient.getInstance(getMarathonEndpoint()); + try { + Result result = marathon.deleteGroup(groupId); + LOGGER.debug(format("Deleted app '%s' at '%s'", groupId, getMarathonEndpoint())); + return result; + } catch (MarathonException e) { + throw new MinimesosException("Could not delete group '" + groupId + "'. " + e.getMessage()); + } } /** @@ -175,29 +197,23 @@ public void deployApp(String marathonJson) { @Override public void updateApp(String marathonJson) { mesosphere.marathon.client.Marathon marathon = MarathonClient.getInstance(getMarathonEndpoint()); - try { App app = constructApp(marathonJson); - boolean force = true; - marathon.updateApp(app.getId(), app, force); + marathon.updateApp(app.getId(), app, true); } catch (MarathonException e) { throw new MinimesosException("Marathon could not update the app, error: " + e.toString()); } + LOGGER.debug(format("Installing an app on marathon %s", getMarathonEndpoint())); + } - LOGGER.debug(String.format("Installing an app on marathon %s", getMarathonEndpoint())); + private Group constructGroup(String groupJson) { + Gson gson = new Gson(); + return gson.fromJson(replaceTokens(groupJson), Group.class); } - /** - * Return App given a JSON string - * - * @param appJson JSON string - * @return App object - */ private App constructApp(String appJson) { Gson gson = new Gson(); - App appObject = gson.fromJson(replaceTokens(appJson), App.class); - - return appObject; + return gson.fromJson(replaceTokens(appJson), App.class); } /** @@ -230,7 +246,7 @@ public String replaceTokens(String source) { } private static String replaceToken(String input, String token, String value) { - String tokenRegex = String.format("\\$\\{%s\\}", token); + String tokenRegex = format("\\$\\{%s\\}", token); return input.replaceAll(tokenRegex, value); } @@ -242,7 +258,7 @@ public void killAllApps() { String marathonEndpoint = getServiceUrl().toString(); JSONObject appsResponse; try { - appsResponse = Unirest.get(marathonEndpoint + END_POINT_EXT).header(HEADER_ACCEPT, APPLICATION_JSON).asJson().getBody().getObject(); + appsResponse = Unirest.get(marathonEndpoint + APPS_ENDPOINT).header(HEADER_ACCEPT, APPLICATION_JSON).asJson().getBody().getObject(); if (appsResponse.length() == 0) { return; } @@ -255,7 +271,7 @@ public void killAllApps() { JSONObject app = apps.getJSONObject(i); String appId = app.getString("id"); try { - Unirest.delete(marathonEndpoint + END_POINT_EXT + appId).asJson(); + Unirest.delete(marathonEndpoint + APPS_ENDPOINT + appId).asJson(); } catch (UnirestException e) { //NOSONAR // failed to delete one app; continue with others LOGGER.error("Could not delete app " + appId + " at " + marathonEndpoint, e); @@ -268,6 +284,7 @@ protected int getServicePort() { return MARATHON_PORT; } + @SuppressWarnings("WeakerAccess") public void waitFor() { LOGGER.debug("Waiting for Marathon to be ready at " + getServiceUrl().toString()); await("Marathon did not start responding").atMost(getCluster().getClusterConfig().getTimeout(), TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(new MarathonApiIsReady()); @@ -281,7 +298,7 @@ private class MarathonApiIsReady implements Callable { @Override public Boolean call() throws Exception { try { - Unirest.get(getServiceUrl().toString() + END_POINT_EXT).header(HEADER_ACCEPT, APPLICATION_JSON).asJson().getBody().getObject(); + Unirest.get(getServiceUrl().toString() + APPS_ENDPOINT).header(HEADER_ACCEPT, APPLICATION_JSON).asJson().getBody().getObject(); } catch (UnirestException e) { //NOSONAR // meaning API is not ready return false; @@ -295,24 +312,35 @@ public Boolean call() throws Exception { */ @Override public void installMarathonApps() { - waitFor(); List apps = getConfig().getApps(); for (AppConfig app : apps) { try { - InputStream json = MesosCluster.getInputStream(app.getMarathonJson()); if (json != null) { deployApp(IOUtils.toString(json, "UTF-8")); } else { - throw new MinimesosException("Failed to find content of " + app.getMarathonJson()); + throw new MinimesosException("Could not deploy app. Failed to find content of " + app.getMarathonJson()); } - } catch (IOException ioe) { - throw new MinimesosException("Failed to load JSON from " + app.getMarathonJson(), ioe); + throw new MinimesosException("Could not deploy app. Failed to load JSON from " + app.getMarathonJson(), ioe); } } + List groups = getConfig().getGroups(); + for (GroupConfig group : groups) { + try { + + InputStream json = MesosCluster.getInputStream(group.getMarathonJson()); + if (json != null) { + deployGroup(IOUtils.toString(json, "UTF-8")); + } else { + throw new MinimesosException("Could not deploy group. Failed to find content of " + group.getMarathonJson()); + } + } catch (IOException ioe) { + throw new MinimesosException("Could not deploy group. Failed to load JSON from " + group.getMarathonJson(), ioe); + } + } } } diff --git a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosAgentContainer.java b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosAgentContainer.java index 14dc30ac..cae57f47 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosAgentContainer.java +++ b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosAgentContainer.java @@ -2,6 +2,7 @@ import com.containersol.minimesos.cluster.MesosAgent; import com.containersol.minimesos.cluster.MesosCluster; +import com.containersol.minimesos.cluster.MesosDns; import com.containersol.minimesos.config.MesosAgentConfig; import com.containersol.minimesos.docker.DockerClientFactory; import com.containersol.minimesos.util.ResourceUtil; @@ -58,7 +59,7 @@ public int getServicePort() { return config.getPortNumber(); } - public CreateContainerCmd getBaseCommand() { + private CreateContainerCmd getBaseCommand() { String hostDir = MesosCluster.getClusterHostDir().getAbsolutePath(); List binds = new ArrayList<>(); binds.add(Bind.parse("/var/run/docker.sock:/var/run/docker.sock:rw")); @@ -67,7 +68,7 @@ public CreateContainerCmd getBaseCommand() { if (getCluster().getMapAgentSandboxVolume()) { binds.add(Bind.parse(String.format("%s:%s:rw", hostDir + "/.minimesos/sandbox-" + getClusterId() + "/" + hostName, MESOS_AGENT_WORK_DIR + hostName + "/slaves"))); } - return DockerClientFactory.build().createContainerCmd(getImageName() + ":" + getImageTag()) + CreateContainerCmd cmd = DockerClientFactory.build().createContainerCmd(getImageName() + ":" + getImageTag()) .withName(getName()) .withHostName(hostName) .withPrivileged(true) @@ -79,6 +80,13 @@ public CreateContainerCmd getBaseCommand() { .withPidMode("host") .withLinks(new Link(getZooKeeper().getContainerId(), "minimesos-zookeeper")) .withBinds(binds.stream().toArray(Bind[]::new)); + + MesosDns mesosDns = getCluster().getMesosDns(); + if (mesosDns != null) { + cmd.withDns(mesosDns.getIpAddress()); + } + + return cmd; } @Override diff --git a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosClusterContainersFactory.java b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosClusterContainersFactory.java index 1a925301..19778369 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosClusterContainersFactory.java +++ b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosClusterContainersFactory.java @@ -173,15 +173,19 @@ private static ClusterContainers createProcesses(ClusterConfig clusterConfig) { ZooKeeperContainer zooKeeper = new ZooKeeperContainer(clusterConfig.getZookeeper()); clusterContainers.add(zooKeeper); + if (clusterConfig.getMesosdns() != null) { + clusterContainers.add(new MesosDnsContainer(clusterConfig.getMesosdns())); + } + MesosMasterContainer mesosMaster = new MesosMasterContainer(clusterConfig.getMaster()); clusterContainers.add(mesosMaster); - clusterConfig.getAgents().forEach(config -> clusterContainers.add(new MesosAgentContainer(config))); - if (clusterConfig.getMarathon() != null) { clusterContainers.add(new MarathonContainer(clusterConfig.getMarathon())); } + clusterConfig.getAgents().forEach(config -> clusterContainers.add(new MesosAgentContainer(config))); + if (clusterConfig.getConsul() != null) { clusterContainers.add(new ConsulContainer(clusterConfig.getConsul())); } @@ -190,10 +194,6 @@ private static ClusterContainers createProcesses(ClusterConfig clusterConfig) { clusterContainers.add(new RegistratorContainer(clusterConfig.getRegistrator())); } - if (clusterConfig.getMesosdns() != null) { - clusterContainers.add(new MesosDnsContainer(clusterConfig.getMesosdns())); - } - return clusterContainers; } diff --git a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosDnsContainer.java b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosDnsContainer.java index 9c7345d3..498b9376 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosDnsContainer.java +++ b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosDnsContainer.java @@ -19,7 +19,7 @@ */ public class MesosDnsContainer extends AbstractContainer implements MesosDns { - private static final String DNS_PORT = "5353"; + private static final String DNS_PORT = "53"; private static final String DOMAIN = "mm"; @@ -54,7 +54,8 @@ protected CreateContainerCmd dockerCommand() { .withValues(getMesosDNSEnvVars()) .createEnvironment()) .withCmd("-v=2", "-config=/etc/mesos-dns/config.json") - .withExposedPorts(new ExposedPort(Integer.valueOf(DNS_PORT), InternetProtocol.UDP)) + .withExposedPorts(new ExposedPort(Integer.valueOf(DNS_PORT), InternetProtocol.UDP), + new ExposedPort(Integer.valueOf(DNS_PORT), InternetProtocol.TCP)) .withName(getName()); } diff --git a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosMasterContainer.java b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosMasterContainer.java index ee44c5bf..b2e03146 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosMasterContainer.java +++ b/minimesos/src/main/java/com/containersol/minimesos/mesos/MesosMasterContainer.java @@ -2,6 +2,7 @@ import com.containersol.minimesos.MinimesosException; import com.containersol.minimesos.cluster.MesosCluster; +import com.containersol.minimesos.cluster.MesosDns; import com.containersol.minimesos.cluster.MesosMaster; import com.containersol.minimesos.config.ClusterConfig; import com.containersol.minimesos.config.MesosMasterConfig; @@ -106,14 +107,21 @@ protected CreateContainerCmd dockerCommand() { portBindings.bind(exposedPort, Ports.Binding.bindPort(port)); } - return DockerClientFactory.build().createContainerCmd(getImageName() + ":" + getImageTag()) - .withName(getName()) - .withExposedPorts(new ExposedPort(getServicePort())) - .withEnv(newEnvironment() - .withValues(getMesosMasterEnvVars()) - .withValues(getSharedEnvVars()) - .createEnvironment()) - .withPortBindings(portBindings); + CreateContainerCmd cmd = DockerClientFactory.build().createContainerCmd(getImageName() + ":" + getImageTag()) + .withName(getName()) + .withExposedPorts(new ExposedPort(getServicePort())) + .withEnv(newEnvironment() + .withValues(getMesosMasterEnvVars()) + .withValues(getSharedEnvVars()) + .createEnvironment()) + .withPortBindings(portBindings); + + MesosDns mesosDns = getCluster().getMesosDns(); + if (mesosDns != null) { + cmd.withDns(mesosDns.getIpAddress()); + } + + return cmd; } @Override diff --git a/minimesos/src/main/java/com/containersol/minimesos/mesos/ZooKeeperContainer.java b/minimesos/src/main/java/com/containersol/minimesos/mesos/ZooKeeperContainer.java index b5b34094..896891ac 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/mesos/ZooKeeperContainer.java +++ b/minimesos/src/main/java/com/containersol/minimesos/mesos/ZooKeeperContainer.java @@ -2,6 +2,7 @@ import com.containersol.minimesos.MinimesosException; import com.containersol.minimesos.cluster.MesosCluster; +import com.containersol.minimesos.cluster.MesosDns; import com.containersol.minimesos.cluster.ZooKeeper; import com.containersol.minimesos.config.ZooKeeperConfig; import com.containersol.minimesos.integrationtest.container.AbstractContainer; @@ -47,8 +48,8 @@ public String getRole() { @Override protected CreateContainerCmd dockerCommand() { return DockerClientFactory.build().createContainerCmd(config.getImageName() + ":" + config.getImageTag()) - .withName(getName()) - .withExposedPorts(new ExposedPort(ZooKeeperConfig.DEFAULT_ZOOKEEPER_PORT), new ExposedPort(2888), new ExposedPort(3888)); + .withName(getName()) + .withExposedPorts(new ExposedPort(ZooKeeperConfig.DEFAULT_ZOOKEEPER_PORT), new ExposedPort(2888), new ExposedPort(3888)); } @Override diff --git a/minimesos/src/main/java/com/containersol/minimesos/state/Discovery.java b/minimesos/src/main/java/com/containersol/minimesos/state/Discovery.java new file mode 100644 index 00000000..ee9644c3 --- /dev/null +++ b/minimesos/src/main/java/com/containersol/minimesos/state/Discovery.java @@ -0,0 +1,20 @@ +package com.containersol.minimesos.state; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Maps Mesos task discovery information from JSON string to a Java object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Discovery { + + private Ports ports; + + public Ports getPorts() { + return ports; + } + + public void setPorts(Ports ports) { + this.ports = ports; + } +} diff --git a/minimesos/src/main/java/com/containersol/minimesos/state/Port.java b/minimesos/src/main/java/com/containersol/minimesos/state/Port.java new file mode 100644 index 00000000..a82f1510 --- /dev/null +++ b/minimesos/src/main/java/com/containersol/minimesos/state/Port.java @@ -0,0 +1,30 @@ +package com.containersol.minimesos.state; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Maps Mesos port information from JSON string to a Java object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Port { + + private int number; + + private String protocol; + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } +} diff --git a/minimesos/src/main/java/com/containersol/minimesos/state/Ports.java b/minimesos/src/main/java/com/containersol/minimesos/state/Ports.java new file mode 100644 index 00000000..99aaf497 --- /dev/null +++ b/minimesos/src/main/java/com/containersol/minimesos/state/Ports.java @@ -0,0 +1,22 @@ +package com.containersol.minimesos.state; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +/** + * Maps Mesos task ports information from JSON string to a Java object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Ports { + + private List ports; + + public List getPorts() { + return ports; + } + + public void setPorts(List ports) { + this.ports = ports; + } +} diff --git a/minimesos/src/main/java/com/containersol/minimesos/state/Task.java b/minimesos/src/main/java/com/containersol/minimesos/state/Task.java index a1c9f4de..8c739e95 100644 --- a/minimesos/src/main/java/com/containersol/minimesos/state/Task.java +++ b/minimesos/src/main/java/com/containersol/minimesos/state/Task.java @@ -15,6 +15,8 @@ public class Task { @JsonProperty("slave_id") private String slaveId; + private Discovery discovery; + public String getState() { return state; } @@ -38,4 +40,12 @@ public String getSlaveId() { public void setSlaveId(String slaveId) { this.slaveId = slaveId; } + + public Discovery getDiscovery() { + return discovery; + } + + public void setDiscovery(Discovery discovery) { + this.discovery = discovery; + } }