Skip to content
Permalink
Browse files
IGNITE-14871 Add cluster initialization commands to the Ignite CLI (#647
)
  • Loading branch information
rpuch committed Feb 10, 2022
1 parent bd0333d commit f35d063a5da300b9215a705dfbb387ef6005640a
Showing 16 changed files with 691 additions and 185 deletions.
@@ -17,6 +17,7 @@

<modernizer>
<!-- Additional rules that augment the standard ones -->
<!-- Method/constructor descriptors (used in 'name') can be obtained using 'javap -s'. -->

<!-- Disallow String#getBytes() -->
<violation>
@@ -38,4 +39,11 @@
<version>1.6</version>
<comment>Prefer java.lang.String.&lt;init&gt;(byte[], java.nio.charset.Charset)</comment>
</violation>

<!-- Disallow ByteArrayOutputStream#toString() -->
<violation>
<name>java/io/ByteArrayOutputStream.toString:()Ljava/lang/String;</name>
<version>1.10</version>
<comment>Prefer java.io.ByteArrayOutputStream.toString(java.nio.charset.Charset)</comment>
</violation>
</modernizer>
@@ -17,6 +17,7 @@

package org.apache.ignite.example;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@@ -58,7 +59,7 @@ public static String captureConsole(ExampleConsumer consumer, String[] args) thr
System.setOut(old);
}

return outStream.toString();
return outStream.toString(UTF_8);
}

/**
@@ -114,6 +114,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.github.npathai</groupId>
<artifactId>hamcrest-optional</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
@@ -17,6 +17,7 @@

package org.apache.ignite.cli;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.both;
@@ -119,7 +120,7 @@ public void setAndGetWithManualHost() {
assertEquals(
"Configuration was updated successfully." + nl + nl
+ "Use the ignite config get command to view the updated configuration." + nl,
out.toString()
out.toString(UTF_8)
);

resetStreams();
@@ -134,7 +135,7 @@ public void setAndGetWithManualHost() {

assertEquals(0, exitCode);

DocumentContext document = JsonPath.parse(removeTrailingQuotes(unescapeQuotes(out.toString())));
DocumentContext document = JsonPath.parse(removeTrailingQuotes(unescapeQuotes(out.toString(UTF_8))));

assertEquals("localhost1", document.read("$.node.metastorageNodes[0]"));
}
@@ -152,7 +153,7 @@ public void setWithWrongData() {

assertEquals(1, exitCode);
assertThat(
err.toString(),
err.toString(UTF_8),
both(startsWith("org.apache.ignite.cli.IgniteCliException: Failed to set configuration"))
.and(containsString("'node' configuration doesn't have the 'metastorgeNodes' sub-configuration"))
);
@@ -170,7 +171,7 @@ public void setWithWrongData() {

assertEquals(1, exitCode);
assertThat(
err.toString(),
err.toString(UTF_8),
both(startsWith("org.apache.ignite.cli.IgniteCliException: Failed to set configuration"))
.and(containsString("'String[]' is expected as a type for the 'node.metastorageNodes' configuration value"))
);
@@ -190,7 +191,7 @@ public void partialGet() {

assertEquals(0, exitCode);

JSONObject outResult = (JSONObject) JSONValue.parse(removeTrailingQuotes(unescapeQuotes(out.toString())));
JSONObject outResult = (JSONObject) JSONValue.parse(removeTrailingQuotes(unescapeQuotes(out.toString(UTF_8))));

assertTrue(outResult.containsKey("inbound"));

@@ -0,0 +1,111 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.ignite.cli.builtins.cluster;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.ignite.cli.IgniteCliException;

/**
* Client for Ignite Cluster management API.
*/
@Singleton
public class ClusterApiClient {
private static final String CLUSTER_INIT_URL = "/management/v1/cluster/init/";

private final HttpClient httpClient;

private final ObjectMapper objectMapper = new ObjectMapper();

@Inject
public ClusterApiClient(HttpClient httpClient) {
this.httpClient = httpClient;
}

/**
* Sends 'cluster init' command to the specified node.
*
* @param nodeEndpoint host:port on which REST API is listening
* @param metastorageNodeIds consistent IDs of the nodes that will host the Meta Storage Raft group
* @param cmgNodeIds consistent IDs of the nodes that will host the Cluster Management Raft Group; if empty,
* {@code metastorageNodeIds} will be used to host the CMG as well
* @param out {@link PrintWriter} to which to report about the command outcome
*/
public void init(String nodeEndpoint, List<String> metastorageNodeIds, List<String> cmgNodeIds, PrintWriter out) {
InitClusterRequest requestPayload = new InitClusterRequest(metastorageNodeIds, cmgNodeIds);
String requestJson = toJson(requestPayload);

var httpRequest = HttpRequest
.newBuilder()
.uri(URI.create("http://" + nodeEndpoint + CLUSTER_INIT_URL))
.method("POST", BodyPublishers.ofString(requestJson))
.header("Content-Type", "application/json")
.build();

HttpResponse<String> httpResponse;
try {
httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new IgniteCliException("Connection issues while trying to send http request", e);
}

if (httpResponse.statusCode() == HttpURLConnection.HTTP_OK) {
out.println("Cluster was initialized successfully.");
} else {
throw error("Failed to initialize cluster", httpResponse);
}
}

private String toJson(InitClusterRequest requestPayload) {
try {
return objectMapper.writeValueAsString(requestPayload);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize JSON", e);
}
}

private IgniteCliException error(String message, HttpResponse<String> httpResponse) {
String errorPayload;
try {
errorPayload = prettifyJson(httpResponse);
} catch (JsonProcessingException e) {
// not a valid JSON probably
errorPayload = httpResponse.body();
}

return new IgniteCliException(message + System.lineSeparator().repeat(2) + errorPayload);
}

private String prettifyJson(HttpResponse<String> httpResponse) throws JsonProcessingException {
return objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(objectMapper.readValue(httpResponse.body(), JsonNode.class));
}
}
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.ignite.cli.builtins.cluster;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

/**
* A request to the Cluster Init REST API.
*/
public class InitClusterRequest {
@SuppressWarnings("unused")
@JsonProperty
private final List<String> metastorageNodes;

@SuppressWarnings("unused")
@JsonProperty
private final List<String> cmgNodes;

public InitClusterRequest(List<String> metastorageNodes, List<String> cmgNodes) {
this.metastorageNodes = List.copyOf(metastorageNodes);
this.cmgNodes = List.copyOf(cmgNodes);
}
}
@@ -152,7 +152,7 @@ private IgniteCliException error(String msg, HttpResponse<String> res) throws Js
var errorMsg = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(mapper.readValue(res.body(), JsonNode.class));

return new IgniteCliException(msg + "\n\n" + errorMsg);
return new IgniteCliException(msg + System.lineSeparator().repeat(2) + errorMsg);
}

/**
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.ignite.cli.spec;

import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.apache.ignite.cli.builtins.cluster.ClusterApiClient;
import picocli.CommandLine;
import picocli.CommandLine.Option;

/**
* Commands for managing Ignite cluster as a whole.
*/
@CommandLine.Command(
name = "cluster",
description = "Manages an Ignite cluster as a whole.",
subcommands = {
ClusterCommandSpec.InitClusterCommandSpec.class,
}
)
public class ClusterCommandSpec extends CategorySpec {
/**
* Initializes an Ignite cluster.
*/
@CommandLine.Command(name = "init", description = "Initializes an Ignite cluster.")
public static class InitClusterCommandSpec extends CommandSpec {

@Inject
private ClusterApiClient clusterApiClient;

/**
* Address of the REST endpoint of the receiving node in host:port format.
*/
@Option(names = "--node-endpoint", required = true,
description = "Address of the REST endpoint of the receiving node in host:port format")
private String nodeEndpoint;

/**
* List of names of the nodes (each represented by a separate command line argument) that will host the Metastorage Raft group.
* If the "--cmg-nodes" parameter is omitted, the same nodes will also host the Cluster Management Raft group.
*/
@Option(names = "--meta-storage-node", required = true, description = {
"Name of the node (repeat like '--meta-storage-node node1 --meta-storage-node node2' to specify more than one node)",
"that will host the Meta Storage Raft group.",
"If the --cmg-node parameter is omitted, the same nodes will also host the Cluster Management Raft group."
})
private List<String> metastorageNodes;

/**
* List of names of the nodes (each represented by a separate command line argument) that will host
* the Cluster Management Raft group.
*/
@Option(names = "--cmg-node", description = {
"Name of the node (repeat like '--cmg-node node1 --cmg-node node2' to specify more than one node)",
"that will host the Cluster Management Raft group.",
"If omitted, then --meta-storage-node values will also supply the nodes for the Cluster Management Raft group."
})
private List<String> cmgNodes = new ArrayList<>();

/** {@inheritDoc} */
@Override
public void run() {
clusterApiClient.init(
nodeEndpoint,
metastorageNodes,
cmgNodes,
spec.commandLine().getOut()
);
}
}
}

0 comments on commit f35d063

Please sign in to comment.