You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by eo...@apache.org on 2022/08/31 09:20:46 UTC
[pulsar] branch master updated: PIP-201 : Extensions mechanism for Pulsar Admin CLI tools (#17158)
This is an automated email from the ASF dual-hosted git repository.
eolivelli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new 57bcc977ce9 PIP-201 : Extensions mechanism for Pulsar Admin CLI tools (#17158)
57bcc977ce9 is described below
commit 57bcc977ce956d2bee120d1a88a9f1cb509fe5ec
Author: Enrico Olivelli <eo...@apache.org>
AuthorDate: Wed Aug 31 11:20:36 2022 +0200
PIP-201 : Extensions mechanism for Pulsar Admin CLI tools (#17158)
---
bin/pulsar-admin-common.sh | 2 +-
conf/client.conf | 6 +
pom.xml | 4 +
.../pom.xml | 58 ++----
.../cli/extensions/CommandExecutionContext.java | 33 +++
.../pulsar/admin/cli/extensions/CustomCommand.java | 56 ++++++
.../admin/cli/extensions/CustomCommandFactory.java | 35 ++++
.../admin/cli/extensions/CustomCommandGroup.java | 47 +++++
.../admin/cli/extensions/ParameterDescriptor.java | 38 ++++
.../pulsar/admin/cli/extensions/ParameterType.java | 26 +++
.../pulsar/admin/cli/extensions/package-info.java | 19 ++
pulsar-client-tools-customcommand-example/pom.xml | 77 +++++++
.../admin/cli/examples/MyCommandFactory.java | 151 ++++++++++++++
.../META-INF/services/command_factory.yml | 19 ++
pulsar-client-tools-test/pom.xml | 26 +++
.../pulsar/admin/cli/PulsarAdminToolTest.java | 127 ++++++++++++
pulsar-client-tools/pom.xml | 10 +
.../org/apache/pulsar/admin/cli/CliCommand.java | 2 +-
.../pulsar/admin/cli/CmdGenerateDocument.java | 2 +-
.../pulsar/admin/cli/CustomCommandsUtils.java | 223 +++++++++++++++++++++
.../apache/pulsar/admin/cli/PulsarAdminTool.java | 63 ++++--
.../cli/utils/CustomCommandFactoryDefinition.java | 37 ++++
.../cli/utils/CustomCommandFactoryDefinitions.java | 28 +++
.../cli/utils/CustomCommandFactoryMetaData.java | 37 ++++
.../cli/utils/CustomCommandFactoryProvider.java | 173 ++++++++++++++++
25 files changed, 1238 insertions(+), 61 deletions(-)
diff --git a/bin/pulsar-admin-common.sh b/bin/pulsar-admin-common.sh
index fdfed60beda..7d4b0d861bf 100755
--- a/bin/pulsar-admin-common.sh
+++ b/bin/pulsar-admin-common.sh
@@ -95,7 +95,7 @@ IS_JAVA_8=`$JAVA -version 2>&1 |grep version|grep '"1\.8'`
# Start --add-opens options
# '--add-opens' option is not supported in jdk8
if [[ -z "$IS_JAVA_8" ]]; then
- OPTS="$OPTS --add-opens java.base/sun.net=ALL-UNNAMED"
+ OPTS="$OPTS --add-opens java.base/sun.net=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED"
fi
OPTS="-cp $PULSAR_CLASSPATH $OPTS"
diff --git a/conf/client.conf b/conf/client.conf
index 50d9bf374c1..ea1d339a09c 100644
--- a/conf/client.conf
+++ b/conf/client.conf
@@ -87,3 +87,9 @@ tlsKeyStorePassword=
# When TLS authentication with CACert is used, the valid value is either OPENSSL or JDK.
# When TLS authentication with KeyStore is used, available options can be SunJSSE, Conscrypt and so on.
webserviceTlsProvider=
+
+
+
+# Pulsar Admin Custom Commands
+#customCommandFactoriesDirectory=commandFactories
+#customCommandFactories=
diff --git a/pom.xml b/pom.xml
index 26273cbf030..e861f9ae0a9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2089,7 +2089,9 @@ flexible messaging model and an intuitive client API.</description>
<module>pulsar-client-admin-api</module>
<module>pulsar-client-admin</module>
<module>pulsar-client-admin-shaded</module>
+ <module>pulsar-client-tools-api</module>
<module>pulsar-client-tools</module>
+ <module>pulsar-client-tools-customcommand-example</module>
<module>pulsar-client-tools-test</module>
<module>pulsar-client-all</module>
<module>pulsar-websocket</module>
@@ -2152,7 +2154,9 @@ flexible messaging model and an intuitive client API.</description>
<module>pulsar-client</module>
<module>pulsar-client-admin-api</module>
<module>pulsar-client-admin</module>
+ <module>pulsar-client-tools-api</module>
<module>pulsar-client-tools</module>
+ <module>pulsar-client-tools-customcommand-example</module>
<module>pulsar-client-tools-test</module>
<module>pulsar-websocket</module>
<module>pulsar-proxy</module>
diff --git a/pulsar-client-tools-test/pom.xml b/pulsar-client-tools-api/pom.xml
similarity index 60%
copy from pulsar-client-tools-test/pom.xml
copy to pulsar-client-tools-api/pom.xml
index 7d06e42b05f..302f184e9c2 100644
--- a/pulsar-client-tools-test/pom.xml
+++ b/pulsar-client-tools-api/pom.xml
@@ -19,7 +19,7 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.pulsar</groupId>
@@ -28,43 +28,27 @@
<relativePath>..</relativePath>
</parent>
- <artifactId>pulsar-client-tools-test</artifactId>
- <name>Pulsar Client Tools Test</name>
- <description>Pulsar Client Tools Test</description>
+ <artifactId>pulsar-client-tools-api</artifactId>
+ <name>Pulsar Client Tools API</name>
+ <description>Pulsar Client Tools API</description>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
- <artifactId>pulsar-client-tools</artifactId>
+ <artifactId>pulsar-client-admin-api</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>testmocks</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.awaitility</groupId>
- <artifactId>awaitility</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>pulsar-broker</artifactId>
- <version>${project.version}</version>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>pulsar-broker</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
</dependencies>
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <!-- same version of Pulsar Admin API -->
+ <release>${pulsar.client.compiler.release}</release>
+ </configuration>
+ </plugin>
<plugin>
<groupId>org.gaul</groupId>
<artifactId>modernizer-maven-plugin</artifactId>
@@ -85,21 +69,10 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-deploy-plugin</artifactId>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <plugin>
- <groupId>com.github.spotbugs</groupId>
- <artifactId>spotbugs-maven-plugin</artifactId>
- <version>${spotbugs-maven-plugin.version}</version>
- <configuration>
- <excludeFilterFile>${basedir}/src/test/resources/findbugsExclude.xml</excludeFilterFile>
- </configuration>
+ <artifactId>maven-checkstyle-plugin</artifactId>
<executions>
<execution>
- <id>spotbugs</id>
+ <id>checkstyle</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
@@ -109,4 +82,5 @@
</plugin>
</plugins>
</build>
+
</project>
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CommandExecutionContext.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CommandExecutionContext.java
new file mode 100644
index 00000000000..47f28479ab1
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CommandExecutionContext.java
@@ -0,0 +1,33 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
+
+import java.util.Properties;
+import org.apache.pulsar.client.admin.PulsarAdmin;
+
+/**
+ * Access to the Environment.
+ */
+public interface CommandExecutionContext {
+
+ PulsarAdmin getPulsarAdmin();
+
+ Properties getConfiguration();
+
+}
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommand.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommand.java
new file mode 100644
index 00000000000..bae1698eed6
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommand.java
@@ -0,0 +1,56 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Custom command.
+ */
+public interface CustomCommand {
+
+ /**
+ * Name of the command.
+ * @return the name
+ */
+ String name();
+
+ /**
+ * Descritption of the command.
+ * @return the description
+ */
+ String description();
+
+ /**
+ * The parameters for the command.
+ * @return the parameters
+ */
+ List<ParameterDescriptor> parameters();
+
+ /**
+ * Execute the command.
+ * @param parameters the parameters, one entry per each parameter name
+ * @param context access the environment
+ * @return false in case of failure
+ * @throws Exception
+ */
+ boolean execute(Map<String, Object> parameters, CommandExecutionContext context) throws Exception;
+
+}
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommandFactory.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommandFactory.java
new file mode 100644
index 00000000000..d4412703d0f
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommandFactory.java
@@ -0,0 +1,35 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
+
+import java.util.List;
+
+/**
+ * Entry point to build custom commands for the Pulsar Admin CLI tools.
+ */
+public interface CustomCommandFactory {
+
+ /**
+ * Generate the available command groups.
+ *
+ * @param context
+ * @return the list of new commands groups.
+ */
+ List<CustomCommandGroup> commandGroups(CommandExecutionContext context);
+}
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommandGroup.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommandGroup.java
new file mode 100644
index 00000000000..b20f5c6895c
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/CustomCommandGroup.java
@@ -0,0 +1,47 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
+
+import java.util.List;
+
+/**
+ * A group of commands.
+ */
+public interface CustomCommandGroup {
+
+ /**
+ * The name of the group.
+ * @return the name
+ */
+ String name();
+
+ /**
+ * The description of the group.
+ * @return the description
+ */
+ String description();
+
+ /**
+ * Generate the available commands.
+ *
+ * @param context
+ * @return the list of new commands.
+ */
+ List<CustomCommand> commands(CommandExecutionContext context);
+}
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/ParameterDescriptor.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/ParameterDescriptor.java
new file mode 100644
index 00000000000..21f1074f864
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/ParameterDescriptor.java
@@ -0,0 +1,38 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+public final class ParameterDescriptor {
+ @Builder.Default
+ private List<String> names = new ArrayList<>();
+ private boolean mainParameter;
+ @Builder.Default
+ private String description = "";
+ @Builder.Default
+ private ParameterType type = ParameterType.STRING;
+ @Builder.Default
+ private boolean required = false;
+}
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/ParameterType.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/ParameterType.java
new file mode 100644
index 00000000000..a4f491cf492
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/ParameterType.java
@@ -0,0 +1,26 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
+
+public enum ParameterType {
+ STRING,
+ INTEGER,
+ BOOLEAN,
+ BOOLEAN_FLAG
+}
diff --git a/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/package-info.java b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/package-info.java
new file mode 100644
index 00000000000..dbefcce7516
--- /dev/null
+++ b/pulsar-client-tools-api/src/main/java/org/apache/pulsar/admin/cli/extensions/package-info.java
@@ -0,0 +1,19 @@
+/**
+ * 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.pulsar.admin.cli.extensions;
diff --git a/pulsar-client-tools-customcommand-example/pom.xml b/pulsar-client-tools-customcommand-example/pom.xml
new file mode 100644
index 00000000000..433eaf75ecd
--- /dev/null
+++ b/pulsar-client-tools-customcommand-example/pom.xml
@@ -0,0 +1,77 @@
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>pulsar</artifactId>
+ <version>2.11.0-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>pulsar-client-tools-customcommand-example</artifactId>
+ <packaging>jar</packaging>
+ <name>Pulsar CLI Custom command example</name>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>pulsar-client-tools-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-nar-maven-plugin</artifactId>
+ <version>1.3.2</version>
+ <extensions>true</extensions>
+ <configuration>
+ <finalName>customCommands</finalName>
+ <classifier>nar</classifier>
+ </configuration>
+ <executions>
+ <execution>
+ <id>default-nar</id>
+ <phase>package</phase>
+ <goals>
+ <goal>nar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.sonatype.plugins</groupId>
+ <artifactId>nexus-staging-maven-plugin</artifactId>
+ <configuration>
+ <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/pulsar-client-tools-customcommand-example/src/main/java/org/apache/pulsar/admin/cli/examples/MyCommandFactory.java b/pulsar-client-tools-customcommand-example/src/main/java/org/apache/pulsar/admin/cli/examples/MyCommandFactory.java
new file mode 100644
index 00000000000..e431d002465
--- /dev/null
+++ b/pulsar-client-tools-customcommand-example/src/main/java/org/apache/pulsar/admin/cli/examples/MyCommandFactory.java
@@ -0,0 +1,151 @@
+/**
+ * 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.pulsar.admin.cli.examples;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pulsar.admin.cli.extensions.CommandExecutionContext;
+import org.apache.pulsar.admin.cli.extensions.CustomCommand;
+import org.apache.pulsar.admin.cli.extensions.CustomCommandFactory;
+import org.apache.pulsar.admin.cli.extensions.CustomCommandGroup;
+import org.apache.pulsar.admin.cli.extensions.ParameterDescriptor;
+import org.apache.pulsar.admin.cli.extensions.ParameterType;
+import org.apache.pulsar.common.policies.data.TopicStats;
+
+@Slf4j
+public class MyCommandFactory implements CustomCommandFactory {
+ @Override
+ public List<CustomCommandGroup> commandGroups(CommandExecutionContext context) {
+ return Arrays.asList(
+ new MyCustomCommandGroup());
+ }
+
+ private static class MyCustomCommandGroup implements CustomCommandGroup {
+ @Override
+ public String name() {
+ return "customgroup";
+ }
+
+ @Override
+ public String description() {
+ return "Custom group 1 description";
+ }
+
+ @Override
+ public List<CustomCommand> commands(CommandExecutionContext context) {
+ return Arrays.asList(new Command1(), new Command2());
+ }
+
+ private static class Command1 implements CustomCommand {
+ @Override
+ public String name() {
+ return "command1";
+ }
+
+ @Override
+ public String description() {
+ return "Command 1 description";
+ }
+
+ @Override
+ public List<ParameterDescriptor> parameters() {
+ return Arrays.asList(
+ ParameterDescriptor.builder()
+ .description("Operation type")
+ .type(ParameterType.STRING)
+ .names(Arrays.asList("--type", "-t"))
+ .required(true)
+ .build(),
+ ParameterDescriptor.builder()
+ .description("Topic")
+ .type(ParameterType.STRING)
+ .mainParameter(true)
+ .names(Arrays.asList("topic"))
+ .required(true)
+ .build());
+ }
+
+ @Override
+ public boolean execute(
+ Map<String, Object> parameters, CommandExecutionContext context)
+ throws Exception {
+ System.out.println(
+ "Execute: " + parameters + " properties " + context.getConfiguration());
+ String destination = parameters.getOrDefault("topic", "").toString();
+ TopicStats stats = context.getPulsarAdmin().topics().getStats(destination);
+ System.out.println("Topic stats: " + stats);
+ return false;
+ }
+ }
+
+ private static class Command2 implements CustomCommand {
+ @Override
+ public String name() {
+ return "command2";
+ }
+
+ @Override
+ public String description() {
+ return "Command 2 description";
+ }
+
+ @Override
+ public List<ParameterDescriptor> parameters() {
+ return Arrays.asList(
+ ParameterDescriptor.builder()
+ .description("mystring")
+ .type(ParameterType.STRING)
+ .names(Arrays.asList("-s"))
+ .build(),
+ ParameterDescriptor.builder()
+ .description("myint")
+ .type(ParameterType.INTEGER)
+ .names(Arrays.asList("-i"))
+ .build(),
+ ParameterDescriptor.builder()
+ .description("myboolean")
+ .type(ParameterType.BOOLEAN)
+ .names(Arrays.asList("-b"))
+ .build(),
+ ParameterDescriptor.builder()
+ .description("mybooleanflag")
+ .type(ParameterType.BOOLEAN_FLAG)
+ .names(Arrays.asList("-bf"))
+ .build(),
+ ParameterDescriptor.builder()
+ .description("main")
+ .type(ParameterType.STRING)
+ .mainParameter(true)
+ .names(Arrays.asList("main"))
+ .build());
+ }
+
+ @Override
+ public boolean execute(
+ Map<String, Object> parameters, CommandExecutionContext context)
+ throws Exception {
+ System.out.println(
+ "Execute: " + parameters + " properties " + context.getConfiguration());
+ return false;
+ }
+ }
+ }
+}
diff --git a/pulsar-client-tools-customcommand-example/src/main/resources/META-INF/services/command_factory.yml b/pulsar-client-tools-customcommand-example/src/main/resources/META-INF/services/command_factory.yml
new file mode 100644
index 00000000000..e6007cb4b09
--- /dev/null
+++ b/pulsar-client-tools-customcommand-example/src/main/resources/META-INF/services/command_factory.yml
@@ -0,0 +1,19 @@
+# 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.
+factoryClass: org.apache.pulsar.admin.cli.examples.MyCommandFactory
+name: dummy
+description: Example of a Custom command factory
diff --git a/pulsar-client-tools-test/pom.xml b/pulsar-client-tools-test/pom.xml
index 7d06e42b05f..d70184af9b6 100644
--- a/pulsar-client-tools-test/pom.xml
+++ b/pulsar-client-tools-test/pom.xml
@@ -62,6 +62,13 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <!-- add the dependency in order to let maven build the module before this module -->
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>pulsar-client-tools-customcommand-example</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
@@ -107,6 +114,25 @@
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>process-test-resources</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <echo>copy filters</echo>
+ <mkdir dir="${project.build.outputDirectory}/cliExtensions" />
+ <copy verbose="true" file="${basedir}/../pulsar-client-tools-customcommand-example/target/customCommands-nar.nar" tofile="${project.build.outputDirectory}/cliExtensions/customCommands-nar.nar" />
+ </target>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/pulsar-client-tools-test/src/test/java/org/apache/pulsar/admin/cli/PulsarAdminToolTest.java b/pulsar-client-tools-test/src/test/java/org/apache/pulsar/admin/cli/PulsarAdminToolTest.java
index 7ca09a2b14d..deda7c41d1b 100644
--- a/pulsar-client-tools-test/src/test/java/org/apache/pulsar/admin/cli/PulsarAdminToolTest.java
+++ b/pulsar-client-tools-test/src/test/java/org/apache/pulsar/admin/cli/PulsarAdminToolTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.pulsar.admin.cli;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.longThat;
import static org.mockito.Mockito.doReturn;
@@ -27,14 +28,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+
+import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.PrintStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@@ -44,6 +50,8 @@ import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
+
+import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.admin.cli.utils.SchemaExtractor;
import org.apache.pulsar.client.admin.Bookies;
import org.apache.pulsar.client.admin.BrokerStats;
@@ -57,6 +65,7 @@ import org.apache.pulsar.client.admin.Namespaces;
import org.apache.pulsar.client.admin.NonPersistentTopics;
import org.apache.pulsar.client.admin.ProxyStats;
import org.apache.pulsar.client.admin.PulsarAdmin;
+import org.apache.pulsar.client.admin.PulsarAdminBuilder;
import org.apache.pulsar.client.admin.ResourceQuotas;
import org.apache.pulsar.client.admin.Schemas;
import org.apache.pulsar.client.admin.Tenants;
@@ -100,6 +109,7 @@ import org.apache.pulsar.common.policies.data.ResourceQuota;
import org.apache.pulsar.common.policies.data.RetentionPolicies;
import org.apache.pulsar.common.policies.data.SubscribeRate;
import org.apache.pulsar.common.policies.data.TenantInfoImpl;
+import org.apache.pulsar.common.policies.data.TopicStats;
import org.apache.pulsar.common.policies.data.TopicType;
import org.apache.pulsar.common.protocol.schema.PostSchemaPayload;
import org.apache.pulsar.common.util.ObjectMapperFactory;
@@ -107,6 +117,7 @@ import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.testng.annotations.Test;
+@Slf4j
public class PulsarAdminToolTest {
@Test
@@ -2255,6 +2266,122 @@ public class PulsarAdminToolTest {
verify(schemas).createSchema("persistent://tn1/ns1/tp1", postSchemaPayload);
}
+ @Test
+ public void customCommands() throws Exception {
+
+ // see the custom command help in the main help
+ String logs = runCustomCommand(new String[]{"-h"});
+ assertTrue(logs.contains("customgroup"));
+ assertTrue(logs.contains("Custom group 1 description"));
+
+ logs = runCustomCommand(new String[]{"customgroup"});
+ assertTrue(logs.contains("command1"));
+ assertTrue(logs.contains("Command 1 description"));
+ assertTrue(logs.contains("command2"));
+ assertTrue(logs.contains("Command 2 description"));
+
+ logs = runCustomCommand(new String[]{"customgroup", "command1"});
+ assertTrue(logs.contains("Command 1 description"));
+ assertTrue(logs.contains("Usage: command1 [options] Topic"));
+
+ // missing required parameter
+ logs = runCustomCommand(new String[]{"customgroup", "command1", "mytopic"});
+ assertTrue(logs.contains("Command 1 description"));
+ assertTrue(logs.contains("Usage: command1 [options] Topic"));
+ assertTrue(logs.contains("The following option is required"));
+
+ // run a comand that uses PulsarAdmin API
+ logs = runCustomCommand(new String[]{"customgroup", "command1", "--type", "stats", "mytopic"});
+ assertTrue(logs.contains("Execute:"));
+ // parameters
+ assertTrue(logs.contains("--type=stats"));
+ // configuration
+ assertTrue(logs.contains("webServiceUrl=http://localhost:2181"));
+ // execution of the PulsarAdmin command
+ assertTrue(logs.contains("Topic stats: MOCK-TOPIC-STATS"));
+
+
+ // run a command that uses all parameter types
+ logs = runCustomCommand(new String[]{"customgroup", "command2",
+ "-s", "mystring",
+ "-i", "123",
+ "-b", "true", // boolean variable, true|false
+ "-bf", // boolean flag, no arguments
+ "mainParameterValue"});
+ assertTrue(logs.contains("Execute:"));
+ // parameters
+ assertTrue(logs.contains("-s=mystring"));
+ assertTrue(logs.contains("-i=123"));
+ assertTrue(logs.contains("-b=true"));
+ assertTrue(logs.contains("-bf=true")); // boolean flag, passed = true
+ assertTrue(logs.contains("main=mainParameterValue"));
+
+
+ // run a command that uses all parameter types, see the default value
+ logs = runCustomCommand(new String[]{"customgroup", "command2"});
+ assertTrue(logs.contains("Execute:"));
+ // parameters
+ assertTrue(logs.contains("-s=null"));
+ assertTrue(logs.contains("-i=0"));
+ assertTrue(logs.contains("-b=null"));
+ assertTrue(logs.contains("-bf=false")); // boolean flag, not passed = false
+ assertTrue(logs.contains("main=null"));
+
+ }
+
+ private static String runCustomCommand(String[] args) throws Exception {
+ File narFile = new File(PulsarAdminTool.class.getClassLoader()
+ .getResource("cliExtensions/customCommands-nar.nar").getFile());
+ log.info("NAR FILE is {}", narFile);
+
+ PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class);
+ PulsarAdmin admin = mock(PulsarAdmin.class);
+ when(builder.build()).thenReturn(admin);
+ Topics topics = mock(Topics.class);
+ when(admin.topics()).thenReturn(topics);
+ TopicStats topicStats = mock(TopicStats.class);
+ when(topics.getStats(anyString())).thenReturn(topicStats);
+ when(topicStats.toString()).thenReturn("MOCK-TOPIC-STATS");
+
+ Properties properties = new Properties();
+ properties.put("webServiceUrl", "http://localhost:2181");
+ properties.put("cliExtensionsDirectory", narFile.getParentFile().getAbsolutePath());
+ properties.put("customCommandFactories", "dummy");
+ PulsarAdminTool tool = new PulsarAdminTool(properties) {
+ @Override
+ protected PulsarAdminBuilder createAdminBuilder(Properties properties) {
+ return builder;
+ }
+ };
+
+ // see the custom command help in the main help
+ StringBuilder logs = new StringBuilder();
+ try (CaptureStdOut capture = new CaptureStdOut(logs)){
+ tool.run(args);
+ }
+ log.info("Captured out: {}", logs);
+ return logs.toString();
+ }
+
+ private static class CaptureStdOut implements AutoCloseable {
+ final PrintStream currentOut = System.out;
+ final PrintStream currentErr = System.err;
+ final ByteArrayOutputStream logs = new ByteArrayOutputStream();
+ final PrintStream capturedOut = new PrintStream(logs, true);
+ final StringBuilder receiver;
+ public CaptureStdOut(StringBuilder receiver) {
+ this.receiver = receiver;
+ System.setOut(capturedOut);
+ System.setErr(capturedOut);
+ }
+ public void close() {
+ capturedOut.flush();
+ System.setOut(currentOut);
+ System.setErr(currentErr);
+ receiver.append(logs.toString(StandardCharsets.UTF_8));
+ }
+ }
+
public static class SchemaDemo {
public SchemaDemo() {
}
diff --git a/pulsar-client-tools/pom.xml b/pulsar-client-tools/pom.xml
index 4f16d8ebf84..948e420ee18 100644
--- a/pulsar-client-tools/pom.xml
+++ b/pulsar-client-tools/pom.xml
@@ -43,6 +43,11 @@
<artifactId>pulsar-client-admin-api</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>pulsar-client-tools-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>pulsar-client-admin-original</artifactId>
@@ -74,6 +79,11 @@
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
</dependency>
+ <dependency>
+ <!-- custom commands -->
+ <groupId>org.javassist</groupId>
+ <artifactId>javassist</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CliCommand.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CliCommand.java
index a609025faa5..1a2136ee967 100644
--- a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CliCommand.java
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CliCommand.java
@@ -36,7 +36,7 @@ import org.apache.pulsar.common.naming.TopicName;
import org.apache.pulsar.common.policies.data.AuthAction;
import org.apache.pulsar.common.util.ObjectMapperFactory;
-abstract class CliCommand {
+public abstract class CliCommand {
static String[] validatePropertyCluster(List<String> params) {
return splitParameter(params, 2);
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdGenerateDocument.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdGenerateDocument.java
index 70878f8ef53..c8cc58a83ee 100644
--- a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdGenerateDocument.java
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdGenerateDocument.java
@@ -58,7 +58,7 @@ public class CmdGenerateDocument extends CmdBase {
}
for (Map.Entry<String, Class<?>> c : tool.commandMap.entrySet()) {
try {
- if (!c.getKey().equals("documents")) {
+ if (!c.getKey().equals("documents") && c.getValue() != null) {
baseJcommander.addCommand(
c.getKey(), c.getValue().getConstructor(Supplier.class).newInstance(admin));
}
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CustomCommandsUtils.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CustomCommandsUtils.java
new file mode 100644
index 00000000000..9063d0d1d0c
--- /dev/null
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CustomCommandsUtils.java
@@ -0,0 +1,223 @@
+/**
+ * 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.pulsar.admin.cli;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtConstructor;
+import javassist.CtField;
+import javassist.CtNewConstructor;
+import javassist.Modifier;
+import javassist.bytecode.AnnotationsAttribute;
+import javassist.bytecode.ClassFile;
+import javassist.bytecode.ConstPool;
+import javassist.bytecode.annotation.Annotation;
+import javassist.bytecode.annotation.ArrayMemberValue;
+import javassist.bytecode.annotation.BooleanMemberValue;
+import javassist.bytecode.annotation.IntegerMemberValue;
+import javassist.bytecode.annotation.MemberValue;
+import javassist.bytecode.annotation.StringMemberValue;
+import lombok.Setter;
+import org.apache.pulsar.admin.cli.extensions.CommandExecutionContext;
+import org.apache.pulsar.admin.cli.extensions.CustomCommand;
+import org.apache.pulsar.admin.cli.extensions.CustomCommandGroup;
+import org.apache.pulsar.admin.cli.extensions.ParameterDescriptor;
+import org.apache.pulsar.admin.cli.extensions.ParameterType;
+import org.apache.pulsar.client.admin.PulsarAdmin;
+
+public final class CustomCommandsUtils {
+ private CustomCommandsUtils() {
+ }
+
+ public static Object generateCliCommand(CustomCommandGroup group, CommandExecutionContext context,
+ Supplier<PulsarAdmin> pulsarAdmin){
+ List<CustomCommand> commands = group.commands(context);
+ String description = group.description();
+
+ try {
+ ClassPool pool = ClassPool.getDefault();
+ CtClass ctClass = pool.makeClass("CustomCommandGroup" + group
+ + "_" + System.nanoTime());
+ ctClass.setSuperclass(pool.get(CmdBaseAdapter.class.getName()));
+
+ // add class annotation
+ ClassFile classFile = ctClass.getClassFile();
+ ConstPool constpool = classFile.getConstPool();
+ AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constpool,
+ AnnotationsAttribute.visibleTag);
+ Annotation annotation = new Annotation(Parameters.class.getName(), constpool);
+ annotation.addMemberValue("commandDescription", new StringMemberValue(description,
+ classFile.getConstPool()));
+ annotationsAttribute.setAnnotation(annotation);
+ ctClass.getClassFile().addAttribute(annotationsAttribute);
+
+ // Add a constructor which calls super( ... );
+ CtClass[] params = new CtClass[]{
+ pool.get(String.class.getName()),
+ pool.get(Supplier.class.getName()),
+ pool.get(List.class.getName()),
+ pool.get(CommandExecutionContext.class.getName())
+ };
+ final CtConstructor ctor = CtNewConstructor.make(params, null, CtNewConstructor.PASS_PARAMS,
+ null, null, ctClass);
+ ctClass.addConstructor(ctor);
+
+ return ctClass.toClass().getConstructor(String.class, Supplier.class, List.class,
+ CommandExecutionContext.class)
+ .newInstance(group.name(), pulsarAdmin, commands, context);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ public static class CmdBaseAdapter extends CmdBase {
+ public CmdBaseAdapter(String cmdName, Supplier<PulsarAdmin> adminSupplier,
+ List<CustomCommand> customCommands, CommandExecutionContext context) {
+ super(cmdName, adminSupplier);
+ for (CustomCommand command : customCommands) {
+ String name = command.name();
+ DecoratedCommand commandImpl = generateCustomCommand(cmdName, name, command);
+ commandImpl.setCommand(command);
+ commandImpl.setContext(context);
+ jcommander.addCommand(name, commandImpl);
+ }
+ }
+ }
+
+
+ @Setter
+ public static class DecoratedCommand extends CliCommand {
+
+ private CustomCommand command;
+ private CommandExecutionContext context;
+
+ public DecoratedCommand() {
+ }
+
+ @Override
+ public void run() throws Exception {
+ Map<String, Object> parameters = new HashMap<>();
+ for (Field f : this.getClass().getFields()) {
+ parameters.put(f.getName(), f.get(this));
+ }
+ command.execute(parameters, context);
+ }
+ }
+
+ private static DecoratedCommand generateCustomCommand(String group, String name, CustomCommand command) {
+ try {
+ String description = command.description();
+ ClassPool pool = ClassPool.getDefault();
+ CtClass ctClass = pool.makeClass("CustomCommand" + group
+ + "_" + name + "_" + System.nanoTime());
+ ctClass.setSuperclass(pool.get(DecoratedCommand.class.getName()));
+
+ // add class annotation
+
+ ClassFile classFile = ctClass.getClassFile();
+ ConstPool constpool = classFile.getConstPool();
+
+ AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constpool,
+ AnnotationsAttribute.visibleTag);
+ Annotation annotation = new Annotation(Parameters.class.getName(), constpool);
+ annotation.addMemberValue("commandDescription",
+ new StringMemberValue(description, classFile.getConstPool()));
+ annotationsAttribute.setAnnotation(annotation);
+ ctClass.getClassFile().addAttribute(annotationsAttribute);
+
+
+ // add fields
+ List<ParameterDescriptor> parameters = command.parameters();
+ for (ParameterDescriptor parameterDescriptor : parameters) {
+ CtClass fieldType;
+ switch (parameterDescriptor.getType()) {
+ case BOOLEAN_FLAG:
+ // command -parameter
+ fieldType = CtClass.booleanType;
+ break;
+ case BOOLEAN:
+ // command -parameter true|false
+ fieldType = pool.get(Boolean.class.getName());
+ break;
+ case INTEGER:
+ // command -parameter 123
+ fieldType = CtClass.intType;
+ break;
+ case STRING:
+ // command -parameter foo
+ fieldType = pool.get(String.class.getName());
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ List<String> parameterNames = parameterDescriptor.getNames();
+ if (parameterNames == null || parameterNames.isEmpty()) {
+ // ignore
+ continue;
+ }
+ String fieldName = parameterNames.get(0);
+ CtField field = new CtField(fieldType, fieldName, ctClass);
+
+ AnnotationsAttribute fieldAnnotationsAttribute = new AnnotationsAttribute(constpool,
+ AnnotationsAttribute.visibleTag);
+ Annotation fieldAnnotation = new Annotation(Parameter.class.getName(), constpool);
+
+ // in JCommander if you don't set the "names" property then you want to get all the other
+ // parameters
+ if (!parameterDescriptor.isMainParameter()) {
+ MemberValue[] memberValues = new MemberValue[parameterNames.size()];
+ int i = 0;
+ for (String parameterName : parameterNames) {
+ memberValues[i++] = new StringMemberValue(parameterName, classFile.getConstPool());
+ }
+ ArrayMemberValue arrayMemberValue = new ArrayMemberValue(classFile.getConstPool());
+ arrayMemberValue.setValue(memberValues);
+ fieldAnnotation.addMemberValue("names", arrayMemberValue);
+ }
+
+ fieldAnnotation.addMemberValue("description",
+ new StringMemberValue(parameterDescriptor.getDescription(), classFile.getConstPool()));
+ fieldAnnotation.addMemberValue("required",
+ new BooleanMemberValue(parameterDescriptor.isRequired(), classFile.getConstPool()));
+ if (parameterDescriptor.getType() == ParameterType.BOOLEAN) {
+ fieldAnnotation.addMemberValue("arity",
+ new IntegerMemberValue(classFile.getConstPool(), 1));
+ }
+ fieldAnnotationsAttribute.setAnnotation(fieldAnnotation);
+ field.getFieldInfo().addAttribute(fieldAnnotationsAttribute);
+ field.setModifiers(Modifier.PUBLIC);
+
+ ctClass.addField(field);
+ }
+
+
+ return (DecoratedCommand) ctClass.toClass().getConstructor().newInstance();
+ } catch (Throwable t) {
+ t.printStackTrace(System.out);
+ throw new RuntimeException(t);
+ }
+ }
+}
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/PulsarAdminTool.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/PulsarAdminTool.java
index b4a0e04439f..16c9d58efe1 100644
--- a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/PulsarAdminTool.java
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/PulsarAdminTool.java
@@ -25,8 +25,10 @@ import com.beust.jcommander.Parameter;
import com.google.common.annotations.VisibleForTesting;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
@@ -34,6 +36,10 @@ import java.util.function.Function;
import java.util.function.Supplier;
import lombok.Getter;
import org.apache.pulsar.PulsarVersion;
+import org.apache.pulsar.admin.cli.extensions.CommandExecutionContext;
+import org.apache.pulsar.admin.cli.extensions.CustomCommandFactory;
+import org.apache.pulsar.admin.cli.extensions.CustomCommandGroup;
+import org.apache.pulsar.admin.cli.utils.CustomCommandFactoryProvider;
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.admin.PulsarAdminBuilder;
import org.apache.pulsar.client.admin.internal.PulsarAdminImpl;
@@ -44,6 +50,7 @@ public class PulsarAdminTool {
private static int lastExitCode = Integer.MIN_VALUE;
+ protected List<CustomCommandFactory> customCommandFactories = new ArrayList();
protected Map<String, Class<?>> commandMap;
protected JCommander jcommander;
protected final PulsarAdminBuilder adminBuilder;
@@ -167,21 +174,34 @@ public class PulsarAdminTool {
}
}
- protected void setupCommands(Function<PulsarAdminBuilder, ? extends PulsarAdmin> adminFactory) {
+ public void setupCommands(Function<PulsarAdminBuilder, ? extends PulsarAdmin> adminFactory) {
try {
- adminBuilder.serviceHttpUrl(rootParams.serviceUrl);
- adminBuilder.authentication(rootParams.authPluginClassName, rootParams.authParams);
- adminBuilder.requestTimeout(rootParams.requestTimeout, TimeUnit.SECONDS);
- if (isBlank(rootParams.tlsProvider)) {
- rootParams.tlsProvider = properties.getProperty("webserviceTlsProvider");
- }
- if (isNotBlank(rootParams.tlsProvider)) {
- adminBuilder.sslProvider(rootParams.tlsProvider);
- }
Supplier<PulsarAdmin> admin = new PulsarAdminSupplier(adminBuilder, adminFactory);
for (Map.Entry<String, Class<?>> c : commandMap.entrySet()) {
addCommand(c, admin);
}
+
+ CommandExecutionContext context = new CommandExecutionContext() {
+ @Override
+ public PulsarAdmin getPulsarAdmin() {
+ return admin.get();
+ }
+
+ @Override
+ public Properties getConfiguration() {
+ return properties;
+ }
+ };
+ loadCustomCommandFactories();
+
+ for (CustomCommandFactory factory : customCommandFactories) {
+ List<CustomCommandGroup> customCommandGroups = factory.commandGroups(context);
+ for (CustomCommandGroup group : customCommandGroups) {
+ Object generated = CustomCommandsUtils.generateCliCommand(group, context, admin);
+ jcommander.addCommand(group.name(), generated);
+ commandMap.put(group.name(), null);
+ }
+ }
} catch (Exception e) {
Throwable cause;
if (e instanceof InvocationTargetException && null != e.getCause()) {
@@ -194,6 +214,11 @@ public class PulsarAdminTool {
}
}
+ private void loadCustomCommandFactories() throws Exception {
+ customCommandFactories.addAll(CustomCommandFactoryProvider.createCustomCommandFactories(properties));
+ }
+
+
private void addCommand(Map.Entry<String, Class<?>> c, Supplier<PulsarAdmin> admin) throws Exception {
// To remain backwards compatibility for "source" and "sink" commands
// TODO eventually remove this
@@ -215,8 +240,8 @@ public class PulsarAdminTool {
}
boolean run(String[] args, Function<PulsarAdminBuilder, ? extends PulsarAdmin> adminFactory) {
+ setupCommands(adminFactory);
if (args.length == 0) {
- setupCommands(adminFactory);
jcommander.usage();
return false;
}
@@ -230,10 +255,20 @@ public class PulsarAdminTool {
try {
jcommander.parse(Arrays.copyOfRange(args, 0, Math.min(cmdPos, args.length)));
+
+ //rootParams are populated by jcommander.parse
+ adminBuilder.serviceHttpUrl(rootParams.serviceUrl);
+ adminBuilder.authentication(rootParams.authPluginClassName, rootParams.authParams);
+ adminBuilder.requestTimeout(rootParams.requestTimeout, TimeUnit.SECONDS);
+ if (isBlank(rootParams.tlsProvider)) {
+ rootParams.tlsProvider = properties.getProperty("webserviceTlsProvider");
+ }
+ if (isNotBlank(rootParams.tlsProvider)) {
+ adminBuilder.sslProvider(rootParams.tlsProvider);
+ }
} catch (Exception e) {
System.err.println(e.getMessage());
System.err.println();
- setupCommands(adminFactory);
jcommander.usage();
return false;
}
@@ -250,17 +285,14 @@ public class PulsarAdminTool {
}
if (rootParams.help) {
- setupCommands(adminFactory);
jcommander.usage();
return true;
}
if (cmdPos == args.length) {
- setupCommands(adminFactory);
jcommander.usage();
return false;
} else {
- setupCommands(adminFactory);
String cmd = args[cmdPos];
// To remain backwards compatibility for "source" and "sink" commands
@@ -391,7 +423,6 @@ public class PulsarAdminTool {
// Automatically generate documents for pulsar-admin
commandMap.put("documents", CmdGenerateDocument.class);
-
// To remain backwards compatibility for "source" and "sink" commands
// TODO eventually remove this
commandMap.put("source", CmdSources.class);
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryDefinition.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryDefinition.java
new file mode 100644
index 00000000000..f91f4bb6dd4
--- /dev/null
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryDefinition.java
@@ -0,0 +1,37 @@
+/**
+ * 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.pulsar.admin.cli.utils;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class CustomCommandFactoryDefinition {
+
+ /**
+ * The name of the command factory.
+ */
+ private String name;
+
+ /**
+ * The class name for factory.
+ */
+ private String factoryClass;
+}
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryDefinitions.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryDefinitions.java
new file mode 100644
index 00000000000..fdd73c2fc37
--- /dev/null
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryDefinitions.java
@@ -0,0 +1,28 @@
+/**
+ * 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.pulsar.admin.cli.utils;
+
+import java.util.Map;
+import java.util.TreeMap;
+import lombok.Data;
+
+@Data
+public class CustomCommandFactoryDefinitions {
+ private final Map<String, CustomCommandFactoryMetaData> factories = new TreeMap<>();
+}
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryMetaData.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryMetaData.java
new file mode 100644
index 00000000000..fce33157435
--- /dev/null
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryMetaData.java
@@ -0,0 +1,37 @@
+/**
+ * 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.pulsar.admin.cli.utils;
+
+import java.nio.file.Path;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class CustomCommandFactoryMetaData {
+ /**
+ * The definition of the entry filter.
+ */
+ private CustomCommandFactoryDefinition definition;
+
+ /**
+ * The path to the handler package.
+ */
+ private Path archivePath;
+}
diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryProvider.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryProvider.java
new file mode 100644
index 00000000000..34155efd4a5
--- /dev/null
+++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/utils/CustomCommandFactoryProvider.java
@@ -0,0 +1,173 @@
+/**
+ * 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.pulsar.admin.cli.utils;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.pulsar.admin.cli.extensions.CustomCommandFactory;
+import org.apache.pulsar.common.nar.NarClassLoader;
+import org.apache.pulsar.common.nar.NarClassLoaderBuilder;
+import org.apache.pulsar.common.util.ObjectMapperFactory;
+
+@Slf4j
+public class CustomCommandFactoryProvider {
+
+ @VisibleForTesting
+ static final String COMMAND_FACTORY_ENTRY = "command_factory";
+
+ /**
+ * create a Command Factory.
+ */
+ public static List<CustomCommandFactory> createCustomCommandFactories(
+ Properties conf) throws IOException {
+ String names = conf.getProperty("customCommandFactories", "");
+ List<CustomCommandFactory> result = new ArrayList<>();
+ if (names.isEmpty()) {
+ // early exit
+ return result;
+ }
+
+ String directory = conf.getProperty("cliExtensionsDirectory", "cliExtensions");
+ String narExtractionDirectory = NarClassLoader.DEFAULT_NAR_EXTRACTION_DIR;
+ CustomCommandFactoryDefinitions definitions = searchForCustomCommandFactories(directory,
+ narExtractionDirectory);
+ for (String name : names.split(",")) {
+ CustomCommandFactoryMetaData metaData = definitions.getFactories().get(name);
+ if (null == metaData) {
+ throw new RuntimeException("No factory is found for name `" + name
+ + "`. Available names are : " + definitions.getFactories());
+ }
+ CustomCommandFactory factory = load(metaData, narExtractionDirectory);
+ if (factory != null) {
+ result.add(factory);
+ }
+ log.debug("Successfully loaded command factory for name `{}`", name);
+ }
+ return result;
+ }
+
+ private static CustomCommandFactoryDefinitions searchForCustomCommandFactories(String directory,
+ String narExtractionDirectory)
+ throws IOException {
+ Path path = Paths.get(directory).toAbsolutePath();
+ log.debug("Searching for command factories in {}", path);
+
+ CustomCommandFactoryDefinitions customCommandFactoryDefinitions = new CustomCommandFactoryDefinitions();
+ if (!path.toFile().exists()) {
+ log.error("Pulsar command factories directory not found");
+ return customCommandFactoryDefinitions;
+ }
+
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, "*.nar")) {
+ for (Path archive : stream) {
+ try {
+ CustomCommandFactoryDefinition def =
+ getCustomCommandFactoryDefinition(archive.toString(), narExtractionDirectory);
+ log.debug("Found command factory from {} : {}", archive, def);
+
+ checkArgument(StringUtils.isNotBlank(def.getName()));
+ checkArgument(StringUtils.isNotBlank(def.getFactoryClass()));
+
+ CustomCommandFactoryMetaData metadata = new CustomCommandFactoryMetaData();
+ metadata.setDefinition(def);
+ metadata.setArchivePath(archive);
+
+ customCommandFactoryDefinitions.getFactories().put(def.getName(), metadata);
+ } catch (Throwable t) {
+ log.warn("Failed to load command factories from {}."
+ + " It is OK however if you want to use this command factory,"
+ + " please make sure you put the correct NAR"
+ + " package in the directory.", archive, t);
+ }
+ }
+ }
+
+ return customCommandFactoryDefinitions;
+ }
+
+ private static CustomCommandFactoryDefinition getCustomCommandFactoryDefinition(String narPath,
+ String narExtractionDirectory)
+ throws IOException {
+ try (NarClassLoader ncl = NarClassLoaderBuilder.builder()
+ .narFile(new File(narPath))
+ .extractionDirectory(narExtractionDirectory)
+ .build()) {
+ return getCustomCommandFactoryDefinition(ncl);
+ }
+ }
+
+ @VisibleForTesting
+ static CustomCommandFactoryDefinition getCustomCommandFactoryDefinition(NarClassLoader ncl) throws IOException {
+ String configStr;
+
+ try {
+ configStr = ncl.getServiceDefinition(COMMAND_FACTORY_ENTRY + ".yaml");
+ } catch (NoSuchFileException e) {
+ configStr = ncl.getServiceDefinition(COMMAND_FACTORY_ENTRY + ".yml");
+ }
+
+ return ObjectMapperFactory.getThreadLocalYaml().readValue(
+ configStr, CustomCommandFactoryDefinition.class
+ );
+ }
+
+ private static CustomCommandFactory load(CustomCommandFactoryMetaData metadata,
+ String narExtractionDirectory)
+ throws IOException {
+ final File narFile = metadata.getArchivePath().toAbsolutePath().toFile();
+ NarClassLoader ncl = NarClassLoaderBuilder.builder()
+ .narFile(narFile)
+ .parentClassLoader(CustomCommandFactory.class.getClassLoader())
+ .extractionDirectory(narExtractionDirectory)
+ .build();
+ CustomCommandFactoryDefinition def = getCustomCommandFactoryDefinition(ncl);
+ if (StringUtils.isBlank(def.getFactoryClass())) {
+ throw new IOException("Command Factory `" + def.getName() + "` does NOT provide a Command Factory"
+ + " implementation");
+ }
+
+ try {
+ Class commandFactoryClass = ncl.loadClass(def.getFactoryClass());
+ Object factory = commandFactoryClass.getDeclaredConstructor().newInstance();
+ if (!(factory instanceof CustomCommandFactory)) {
+ throw new IOException("Class " + def.getFactoryClass()
+ + " does not implement CustomCommandFactory interface");
+ }
+ return (CustomCommandFactory) factory;
+ } catch (Exception e) {
+ if (e instanceof IOException) {
+ throw (IOException) e;
+ }
+ log.error("Failed to load class {}", def.getFactoryClass(), e);
+ throw new IOException(e);
+ }
+ }
+}