From 652dc1eff2ad605f5fab4a633c454454d00d8fb5 Mon Sep 17 00:00:00 2001 From: Yong Zhang Date: Tue, 8 Dec 2020 20:45:09 +0800 Subject: [PATCH] Package management client command (#8817) --- Master Issue: #8676 *Motivation* Add commands for package management service in pulsar-admin tool. *Modifications* - Add commands for package management service --- .../pulsar/client/admin/PulsarAdmin.java | 10 + .../client/admin/internal/PackagesImpl.java | 2 +- .../apache/pulsar/admin/cli/CmdPackages.java | 165 +++++++++++++++ .../pulsar/admin/cli/TestCmdPackages.java | 193 ++++++++++++++++++ 4 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdPackages.java create mode 100644 pulsar-client-tools/src/test/java/org/apache/pulsar/admin/cli/TestCmdPackages.java diff --git a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/PulsarAdmin.java b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/PulsarAdmin.java index c0758d63ede62..f2023975986c7 100644 --- a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/PulsarAdmin.java +++ b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/PulsarAdmin.java @@ -37,6 +37,7 @@ import org.apache.pulsar.client.admin.internal.LookupImpl; import org.apache.pulsar.client.admin.internal.NamespacesImpl; import org.apache.pulsar.client.admin.internal.NonPersistentTopicsImpl; +import org.apache.pulsar.client.admin.internal.PackagesImpl; import org.apache.pulsar.client.admin.internal.ProxyStatsImpl; import org.apache.pulsar.client.admin.internal.PulsarAdminBuilderImpl; import org.apache.pulsar.client.admin.internal.ResourceQuotasImpl; @@ -94,6 +95,7 @@ public class PulsarAdmin implements Closeable { private final Sinks sinks; private final Worker worker; private final Schemas schemas; + private final Packages packages; protected final WebTarget root; protected final Authentication auth; private final int connectTimeout; @@ -218,6 +220,7 @@ public PulsarAdmin(String serviceUrl, this.worker = new WorkerImpl(root, auth, readTimeoutMs); this.schemas = new SchemasImpl(root, auth, readTimeoutMs); this.bookies = new BookiesImpl(root, auth, readTimeoutMs); + this.packages = new PackagesImpl(root, auth, asyncHttpConnector.getHttpClient(), readTimeoutMs); if (originalCtxLoader != null) { Thread.currentThread().setContextClassLoader(originalCtxLoader); @@ -434,6 +437,13 @@ public Schemas schemas() { return schemas; } + /** + * @return the packages management object + */ + public Packages packages() { + return packages; + } + /** * Close the Pulsar admin client to release all the resources. */ diff --git a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PackagesImpl.java b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PackagesImpl.java index 951e434bed36d..19c9178de00a8 100644 --- a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PackagesImpl.java +++ b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PackagesImpl.java @@ -55,7 +55,7 @@ public class PackagesImpl extends ComponentResource implements Packages { private final WebTarget packages; private final AsyncHttpClient httpClient; - protected PackagesImpl(WebTarget webTarget, Authentication auth, AsyncHttpClient client, long readTimeoutMs) { + public PackagesImpl(WebTarget webTarget, Authentication auth, AsyncHttpClient client, long readTimeoutMs) { super(auth, readTimeoutMs); this.httpClient = client; this.packages = webTarget.path("/admin/v3/packages"); diff --git a/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdPackages.java b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdPackages.java new file mode 100644 index 0000000000000..bb9e6669f184d --- /dev/null +++ b/pulsar-client-tools/src/main/java/org/apache/pulsar/admin/cli/CmdPackages.java @@ -0,0 +1,165 @@ +/** + * 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.DynamicParameter; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.apache.pulsar.client.admin.Packages; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.packages.management.core.common.PackageMetadata; + +import java.util.HashMap; +import java.util.Map; + +/** + * Commands for administering packages. + */ +@Parameters(commandDescription = "Operations about packages") +class CmdPackages extends CmdBase { + + private final Packages packages; + + CmdPackages(PulsarAdmin admin) { + super("packages", admin); + this.packages = admin.packages(); + + jcommander.addCommand("get-metadata", new GetMetadataCmd()); + jcommander.addCommand("update-metadata", new UpdateMetadataCmd()); + jcommander.addCommand("upload", new UploadCmd()); + jcommander.addCommand("download", new DownloadCmd()); + jcommander.addCommand("list", new ListPackagesCmd()); + jcommander.addCommand("list-versions", new ListPackageVersionsCmd()); + jcommander.addCommand("delete", new DeletePackageCmd()); + } + + @Parameters(commandDescription = "Get a package metadata information.") + private class GetMetadataCmd extends CliCommand { + @Parameter(description = "type://tenant/namespace/packageName@version", required = true) + private String packageName; + + @Override + void run() throws Exception { + print(packages.getMetadata(packageName)); + } + } + + @Parameters(commandDescription = "Update a package metadata information.") + private class UpdateMetadataCmd extends CliCommand { + @Parameter(description = "type://tenant/namespace/packageName@version", required = true) + private String packageName; + + @Parameter(names = "--description", description= "descriptions of a package", required = true) + private String description; + + @Parameter(names = "--contact", description = "contact info of a package") + private String contact; + + @DynamicParameter(names = {"--properties", "-P"}, description ="external information of a package") + private Map properties = new HashMap<>(); + + @Override + void run() throws Exception { + packages.updateMetadata(packageName, PackageMetadata.builder() + .description(description).contact(contact).properties(properties).build()); + print(String.format("The metadata of the package '%s' updated successfully", packageName)); + } + } + + @Parameters(commandDescription = "Upload a package") + private class UploadCmd extends CliCommand { + @Parameter(description = "type://tenant/namespace/packageName@version", required = true) + private String packageName; + + @Parameter(names = "--description", description= "descriptions of a package", required = true) + private String description; + + @Parameter(names = "--contact", description = "contact information of a package") + private String contact; + + @DynamicParameter(names = {"--properties", "-P"}, description ="external infromations of a package") + private Map properties = new HashMap<>(); + + @Parameter(names = "--path", description = "descriptions of a package", required = true) + private String path; + + @Override + void run() throws Exception { + PackageMetadata metadata = PackageMetadata.builder() + .description(description) + .contact(contact) + .properties(properties).build(); + packages.upload(metadata, packageName, path); + print(String.format("The package '%s' uploaded from path '%s' successfully", packageName, path)); + } + } + + @Parameters(commandDescription = "Download a package") + private class DownloadCmd extends CliCommand { + @Parameter(description = "type://tenant/namespace/packageName@version", required = true) + private String packageName; + + @Parameter(names = "--path", description = "download destiny path of the package", required = true) + private String path; + + @Override + void run() throws Exception { + packages.download(packageName, path); + print(String.format("The package '%s' downloaded to path '%s' successfully", packageName, path)); + } + } + + @Parameters(commandDescription = "List all versions of the given package") + private class ListPackageVersionsCmd extends CliCommand { + @Parameter(description = "the package name you want to query, don't need to specify the package version.\n" + + "type://tenant/namespace/packageName", required = true) + private String packageName; + + @Override + void run() throws Exception { + print(packages.listPackageVersions(packageName).toString()); + } + } + + @Parameters(commandDescription = "List all packages with given type in the specified namespace") + private class ListPackagesCmd extends CliCommand { + @Parameter(names = "--type", description = "type of the package", required = true) + private String type; + + @Parameter(description = "namespace of the package", required = true) + private String namespace; + + @Override + void run() throws Exception { + print(packages.listPackages(type, namespace)); + } + } + + @Parameters(commandDescription = "Delete a package") + private class DeletePackageCmd extends CliCommand{ + @Parameter(description = "type://tenant/namespace/packageName@version", required = true) + private String packageName; + + @Override + void run() throws Exception { + packages.delete(packageName); + print(String.format("The package '%s' deleted successfully", packageName)); + } + } +} diff --git a/pulsar-client-tools/src/test/java/org/apache/pulsar/admin/cli/TestCmdPackages.java b/pulsar-client-tools/src/test/java/org/apache/pulsar/admin/cli/TestCmdPackages.java new file mode 100644 index 0000000000000..a5aa55c014a68 --- /dev/null +++ b/pulsar-client-tools/src/test/java/org/apache/pulsar/admin/cli/TestCmdPackages.java @@ -0,0 +1,193 @@ +/** + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import lombok.Data; +import org.apache.pulsar.client.admin.Packages; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminException; +import org.apache.pulsar.packages.management.core.common.PackageMetadata; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Unit test for packages commands. + */ +@PrepareForTest(CmdPackages.class) +public class TestCmdPackages { + + private PulsarAdmin pulsarAdmin; + private Packages packages; + private CmdPackages cmdPackages; + + @BeforeMethod + public void setup() throws Exception { + pulsarAdmin = mock(PulsarAdmin.class); + packages = mock(Packages.class); + when(pulsarAdmin.packages()).thenReturn(packages); + + cmdPackages = spy(new CmdPackages(pulsarAdmin)); + } + + @DataProvider(name = "commandsWithoutArgs") + public static Object[][] commandsWithoutArgs() { + return new Object[][]{ + {"get-metadata"}, + {"update-metadata"}, + {"upload"}, + {"download"}, + {"list"}, + {"list-versions"}, + {"delete"}, + }; + } + + @Test(timeOut = 60000, dataProvider = "commandsWithoutArgs") + public void testCommandsWithoutArgs(String command) { + String packageName = "test-package-name"; + boolean result = cmdPackages.run(new String[]{command}); + assertFalse(result); + } + + // test command `bin/pulsar-admin packages get-metadata package-name` + @Test(timeOut = 1000) + public void testGetMetadataCmd() throws PulsarAdminException { + String packageName = randomName(8); + boolean result = cmdPackages.run(new String[]{"get-metadata", packageName}); + assertTrue(result); + verify(packages, times(1)).getMetadata(eq(packageName)); + } + + // test command `bin/pulsar-admin packages update-metadata package-name --description tests` + @Test(timeOut = 1000) + public void testUpdateMetadataCmdWithRequiredArgs() throws PulsarAdminException { + String packageName = randomName(8); + boolean result = cmdPackages.run(new String[]{"update-metadata", packageName, "--description", "tests"}); + assertTrue(result); + verify(packages, times(1)) + .updateMetadata(eq(packageName), + eq(PackageMetadata.builder().description("tests").properties(Collections.emptyMap()).build())); + } + + // test command `bin/pulsar-admin packages update-metadata package-name --description tests + // --contact test@apache.org -PpropertyA=A` + @Test(timeOut = 1000) + public void testUpdateMetadataCmdWithAllArgs() throws PulsarAdminException { + String packageName = randomName(8); + Map properties = new HashMap<>(); + properties.put("propertyA", "A"); + boolean result = cmdPackages.run(new String[]{ + "update-metadata", packageName, "--description", "tests", "--contact", "test@apache.org", "-PpropertyA=A"}); + assertTrue(result); + verify(packages, times(1)) + .updateMetadata(eq(packageName), eq(PackageMetadata.builder().description("tests") + .contact("test@apache.org").properties(properties).build())); + } + + // test command `bin/pulsar-admin packages upload package-name --description tests --path /path/to/package` + @Test(timeOut = 1000) + public void testUploadCmdWithRequiredArgs() throws PulsarAdminException { + String packageName = randomName(8); + boolean result = cmdPackages.run(new String[]{ + "upload", packageName, "--description", "tests", "--path", "/path/to/package"}); + assertTrue(result); + verify(packages, times(1)).upload( + eq(PackageMetadata.builder().description("tests").properties(Collections.emptyMap()).build()), + eq(packageName), + eq("/path/to/package")); + } + + // test command `bin/pulsar-admin packages upload package-name --description tests --contact test@apache.org + // -PpropertyA=A --path /path/to/package` + @Test(timeOut = 1000) + public void testUploadCmdWithAllArgs() throws PulsarAdminException { + String packageName = randomName(8); + Map properties = new HashMap<>(); + properties.put("propertyA", "A"); + boolean result = cmdPackages.run(new String[]{ + "upload", packageName, "--description", "tests", "--contact", "test@apache.org", "-PpropertyA=A", + "--path", "/path/to/package"}); + assertTrue(result); + verify(packages, times(1)).upload( + eq(PackageMetadata.builder().description("tests").contact("test@apache.org").properties(properties).build()), + eq(packageName), + eq("/path/to/package")); + } + + // test command `bin/pulsar-admin download package-name --path /path/to/package` + @Test(timeOut = 1000) + public void testDownloadCmd() throws PulsarAdminException { + String packageName = randomName(8); + boolean result = cmdPackages.run(new String[]{"download", packageName, "--path", "/path/to/package"}); + assertTrue(result); + verify(packages, times(1)).download(eq(packageName), eq("/path/to/package")); + } + + // test command `bin/pulsar-admin list public/default --type function` + @Test(timeOut = 1000) + public void testListCmd() throws PulsarAdminException { + String namespace = String.format("%s/%s", randomName(4), randomName(4)); + boolean result = cmdPackages.run(new String[]{"list", namespace, "--type", "function"}); + assertTrue(result); + verify(packages, times(1)).listPackages(eq("function"), eq(namespace)); + } + + // test command `bin/pulsar-admin list-versions package-name` + @Test(timeOut = 1000) + public void testListVersionsCmd() throws PulsarAdminException { + String packageName = randomName(8); + boolean result = cmdPackages.run(new String[]{"list-versions", packageName}); + assertTrue(result); + verify(packages, times(1)).listPackageVersions(eq(packageName)); + } + + // test command `bin/pulsar-admin delete package-name` + @Test(timeOut = 1000) + public void testDeleteCmd() throws PulsarAdminException { + String packageName = randomName(8); + boolean result = cmdPackages.run(new String[]{"delete", packageName}); + assertTrue(result); + verify(packages, times(1)).delete(eq(packageName)); + } + + private static String randomName(int numChars) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < numChars; i++) { + sb.append((char) (ThreadLocalRandom.current().nextInt(26) + 'a')); + } + return sb.toString(); + } +}