diff --git a/docs/quickstart/install.md b/docs/quickstart/install.md index 6160a7c85..d3add98d1 100644 --- a/docs/quickstart/install.md +++ b/docs/quickstart/install.md @@ -412,11 +412,12 @@ resulting images without authentication. #### Downloading plugin bundle To download plugins you will use the `tanzu plugin download-bundle` command and -specify the different plugin groups relevant to your environment. +specify the different plugin groups or plugins relevant to your environment. -This command will download the plugin bundle containing the plugin versions specified -in the plugin group definition. The bundle will also include the plugin group -definition itself, so that it can be used for plugin installation by users. +This command will download the plugin bundle containing specified plugins and +the plugin versions specified in the plugin group definition. The bundle will +also include the plugin group definition itself if specified, so that it can be +used for plugin installation by users. Note that the latest version of the `vmware-tanzucli/essentials` plugin group and the plugin versions it contains will automatically be included in any plugin @@ -446,6 +447,32 @@ If you do not specify a group's version, the latest version available for the gr tanzu plugin download-bundle --group vmware-tkg/default --to-tar /tmp/plugin_bundle_tkg_latest.tar.gz ``` +If you want to download a specific plugin, the `--plugin` flag can be used. Below are the support format for `--plugin` flag: + +```sh +--plugin name : Downloads all available versions of the plugin for all matching targets. +--plugin name:version : Downloads specified version of the plugin for all matching targets. Use 'latest' as version for latest available version +--plugin name@target:version : Downloads specified version of the plugin for the specified target. Use 'latest' as version for latest available version +--plugin name@target : Downloads all available versions of the plugin for the specified target. +``` + +```sh +# To download plugin bundle with all available versions 'cluster' plugin across all targets +tanzu plugin download-bundle --plugin cluster --to-tar /tmp/plugin_bundle_cluster.tar.gz + +# To download plugin bundle with all available versions 'cluster' plugin for `kubernetes` target +tanzu plugin download-bundle --plugin cluster@kubernetes --to-tar /tmp/plugin_bundle_cluster.tar.gz + +# To download plugin bundle with v1.0.0 version of 'cluster' plugin for `kubernetes` target +tanzu plugin download-bundle --plugin cluster@kubernetes:v1.0.0 --to-tar /tmp/plugin_bundle_cluster.tar.gz + +# To download plugin bundle with latest available version of 'cluster' plugin for `kubernetes` target +tanzu plugin download-bundle --plugin cluster@kubernetes:latest --to-tar /tmp/plugin_bundle_cluster.tar.gz +``` + +Using `--group` and `--plugin` flag together are also supported and if specified the union of all the +plugins and plugin-groups will be downloaded. + To migrate plugins from a specific plugin repository and not use the default plugin repository you can provide a `--image` flag with the above command, for example: diff --git a/pkg/airgapped/plugin_bundle_download.go b/pkg/airgapped/plugin_bundle_download.go index 114cbd2eb..0d856d948 100644 --- a/pkg/airgapped/plugin_bundle_download.go +++ b/pkg/airgapped/plugin_bundle_download.go @@ -18,7 +18,9 @@ import ( "github.com/vmware-tanzu/tanzu-cli/pkg/cosignhelper/sigverifier" "github.com/vmware-tanzu/tanzu-cli/pkg/essentials" "github.com/vmware-tanzu/tanzu-cli/pkg/plugininventory" + "github.com/vmware-tanzu/tanzu-cli/pkg/utils" + configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" "github.com/vmware-tanzu/tanzu-plugin-runtime/log" ) @@ -29,6 +31,7 @@ type DownloadPluginBundleOptions struct { PluginInventoryImage string ToTar string Groups []string + Plugins []string DryRun bool ImageProcessor carvelhelpers.ImageOperationsImpl @@ -134,7 +137,7 @@ func (o *DownloadPluginBundleOptions) getSelectedPluginInfo() ([]*plugininventor selectedPluginEntries := []*plugininventory.PluginInventoryEntry{} // If groups were not provided as argument select all available plugin groups and all available plugins - if len(o.Groups) == 0 { + if len(o.Groups) == 0 && len(o.Plugins) == 0 { selectedPluginGroups, err = pi.GetPluginGroups(plugininventory.PluginGroupFilter{IncludeHidden: true}) // Include the hidden plugin groups during plugin migration if err != nil { return nil, nil, errors.Wrap(err, "unable to read all plugin groups from database") @@ -173,6 +176,21 @@ func (o *DownloadPluginBundleOptions) getSelectedPluginInfo() ([]*plugininventor } } + for _, pluginID := range o.Plugins { + pluginName, pluginTarget, pluginVersion := utils.ParsePluginID(pluginID) + pluginEntries, err := pi.GetPlugins(&plugininventory.PluginInventoryFilter{ + Name: pluginName, + Target: configtypes.Target(pluginTarget), + Version: pluginVersion, + IncludeHidden: true, + }) // Include the hidden plugins during plugin migration + + if err != nil { + return nil, nil, errors.Wrap(err, "unable to read plugins from database") + } + selectedPluginEntries = append(selectedPluginEntries, pluginEntries...) + } + // Remove duplicate PluginInventoryEntries and PluginGroups from the selected list selectedPluginEntries = plugininventory.RemoveDuplicatePluginInventoryEntries(selectedPluginEntries) selectedPluginGroups = plugininventory.RemoveDuplicatePluginGroups(selectedPluginGroups) diff --git a/pkg/airgapped/plugin_bundle_test.go b/pkg/airgapped/plugin_bundle_test.go index a6ea14fd8..ff1b6a766 100644 --- a/pkg/airgapped/plugin_bundle_test.go +++ b/pkg/airgapped/plugin_bundle_test.go @@ -203,6 +203,60 @@ imagesToCopy: - sourceTarFilePath: telemetry-global-darwin_amd64-v0.0.1.tar.gz relativeImagePath: /path/darwin/amd64/global/telemetry ` + // Plugin bundle manifest file generated based on the above mentioned + // plugin entry in the inventory database with only foo plugin specified + pluginBundleManifestFooPluginOnlyString := `relativeInventoryImagePathWithTag: /plugin-inventory:latest +inventoryMetadataImage: + sourceFilePath: plugin_inventory_metadata.db + relativeImagePathWithTag: /plugin-inventory-metadata:latest +imagesToCopy: + - sourceTarFilePath: plugin-inventory-image.tar.gz + relativeImagePath: /plugin-inventory + - sourceTarFilePath: telemetry-global-darwin_amd64-v0.0.1.tar.gz + relativeImagePath: /path/darwin/amd64/global/telemetry + - sourceTarFilePath: foo-global-darwin_amd64-v0.0.2.tar.gz + relativeImagePath: /path/darwin/amd64/global/foo + - sourceTarFilePath: foo-global-linux_amd64-v0.0.2.tar.gz + relativeImagePath: /path/linux/amd64/global/foo +` + + // Plugin bundle manifest file generated based on the above mentioned + // plugin entry in the inventory database with only single plugin group and specific plugin specified + pluginBundleManifestDefaultGroupAndFooPluginOnlyString := `relativeInventoryImagePathWithTag: /plugin-inventory:latest +inventoryMetadataImage: + sourceFilePath: plugin_inventory_metadata.db + relativeImagePathWithTag: /plugin-inventory-metadata:latest +imagesToCopy: + - sourceTarFilePath: plugin-inventory-image.tar.gz + relativeImagePath: /plugin-inventory + - sourceTarFilePath: bar-kubernetes-darwin_amd64-v0.0.1.tar.gz + relativeImagePath: /path/darwin/amd64/kubernetes/bar + - sourceTarFilePath: telemetry-global-darwin_amd64-v0.0.1.tar.gz + relativeImagePath: /path/darwin/amd64/global/telemetry + - sourceTarFilePath: foo-global-darwin_amd64-v0.0.2.tar.gz + relativeImagePath: /path/darwin/amd64/global/foo + - sourceTarFilePath: foo-global-linux_amd64-v0.0.2.tar.gz + relativeImagePath: /path/linux/amd64/global/foo +` + + // Plugin bundle manifest file generated based on the above mentioned + // plugin entry in the inventory database with only foo and bar plugin specified + pluginBundleManifestFooAndBarPluginOnlyString := `relativeInventoryImagePathWithTag: /plugin-inventory:latest +inventoryMetadataImage: + sourceFilePath: plugin_inventory_metadata.db + relativeImagePathWithTag: /plugin-inventory-metadata:latest +imagesToCopy: + - sourceTarFilePath: plugin-inventory-image.tar.gz + relativeImagePath: /plugin-inventory + - sourceTarFilePath: telemetry-global-darwin_amd64-v0.0.1.tar.gz + relativeImagePath: /path/darwin/amd64/global/telemetry + - sourceTarFilePath: foo-global-darwin_amd64-v0.0.2.tar.gz + relativeImagePath: /path/darwin/amd64/global/foo + - sourceTarFilePath: foo-global-linux_amd64-v0.0.2.tar.gz + relativeImagePath: /path/linux/amd64/global/foo + - sourceTarFilePath: bar-kubernetes-darwin_amd64-v0.0.1.tar.gz + relativeImagePath: /path/darwin/amd64/kubernetes/bar +` // Configure the configuration before running the tests BeforeEach(func() { @@ -420,6 +474,109 @@ imagesToCopy: } }) + var _ = It("when group and plugin is specified and everything works as expected, it should download plugin bundle as tar file", func() { + fakeImageOperations.DownloadImageAndSaveFilesToDirCalls(downloadInventoryImageAndSaveFilesToDirStub) + fakeImageOperations.CopyImageToTarCalls(copyImageToTarStub) + + // Provide a plugin group and a plugin + dpbo.Groups = []string{"fakevendor-fakepublisher/default:v1.0.0"} + dpbo.Plugins = []string{"foo"} + err := dpbo.DownloadPluginBundle() + Expect(err).NotTo(HaveOccurred()) + + // Verify that tar file was generated correctly with untar + tempDir, err := os.MkdirTemp("", "") + Expect(tempDir).ToNot(BeEmpty()) + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) + + err = tarinator.UnTarinate(tempDir, dpbo.ToTar) + Expect(err).NotTo(HaveOccurred()) + + // Verify the plugin bundle manifest file is accurate + bytes, err := os.ReadFile(filepath.Join(tempDir, PluginBundleDirName, PluginMigrationManifestFile)) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bytes)).To(Equal(pluginBundleManifestDefaultGroupAndFooPluginOnlyString)) + manifest := &PluginMigrationManifest{} + err = yaml.Unmarshal(bytes, &manifest) + Expect(err).NotTo(HaveOccurred()) + + // Iterate through all the images in the manifest and verify that all image archive + // files mentioned in the manifest exists in the bundle + for _, pi := range manifest.ImagesToCopy { + exists := utils.PathExists(filepath.Join(tempDir, PluginBundleDirName, pi.SourceTarFilePath)) + Expect(exists).To(BeTrue()) + } + }) + + var _ = It("when only one plugin is specified and everything works as expected, it should download plugin bundle as tar file", func() { + fakeImageOperations.DownloadImageAndSaveFilesToDirCalls(downloadInventoryImageAndSaveFilesToDirStub) + fakeImageOperations.CopyImageToTarCalls(copyImageToTarStub) + + // Provide a plugin + dpbo.Plugins = []string{"foo"} + err := dpbo.DownloadPluginBundle() + Expect(err).NotTo(HaveOccurred()) + + // Verify that tar file was generated correctly with untar + tempDir, err := os.MkdirTemp("", "") + Expect(tempDir).ToNot(BeEmpty()) + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) + + err = tarinator.UnTarinate(tempDir, dpbo.ToTar) + Expect(err).NotTo(HaveOccurred()) + + // Verify the plugin bundle manifest file is accurate + bytes, err := os.ReadFile(filepath.Join(tempDir, PluginBundleDirName, PluginMigrationManifestFile)) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bytes)).To(Equal(pluginBundleManifestFooPluginOnlyString)) + manifest := &PluginMigrationManifest{} + err = yaml.Unmarshal(bytes, &manifest) + Expect(err).NotTo(HaveOccurred()) + + // Iterate through all the images in the manifest and verify that all image archive + // files mentioned in the manifest exists in the bundle + for _, pi := range manifest.ImagesToCopy { + exists := utils.PathExists(filepath.Join(tempDir, PluginBundleDirName, pi.SourceTarFilePath)) + Expect(exists).To(BeTrue()) + } + }) + + var _ = It("when multiple plugins are specified and everything works as expected, it should download plugin bundle as tar file", func() { + fakeImageOperations.DownloadImageAndSaveFilesToDirCalls(downloadInventoryImageAndSaveFilesToDirStub) + fakeImageOperations.CopyImageToTarCalls(copyImageToTarStub) + + // Provide multiple plugins + dpbo.Plugins = []string{"foo@global:v0.0.2", "bar@kubernetes"} + err := dpbo.DownloadPluginBundle() + Expect(err).NotTo(HaveOccurred()) + + // Verify that tar file was generated correctly with untar + tempDir, err := os.MkdirTemp("", "") + Expect(tempDir).ToNot(BeEmpty()) + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) + + err = tarinator.UnTarinate(tempDir, dpbo.ToTar) + Expect(err).NotTo(HaveOccurred()) + + // Verify the plugin bundle manifest file is accurate + bytes, err := os.ReadFile(filepath.Join(tempDir, PluginBundleDirName, PluginMigrationManifestFile)) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bytes)).To(Equal(pluginBundleManifestFooAndBarPluginOnlyString)) + manifest := &PluginMigrationManifest{} + err = yaml.Unmarshal(bytes, &manifest) + Expect(err).NotTo(HaveOccurred()) + + // Iterate through all the images in the manifest and verify that all image archive + // files mentioned in the manifest exists in the bundle + for _, pi := range manifest.ImagesToCopy { + exists := utils.PathExists(filepath.Join(tempDir, PluginBundleDirName, pi.SourceTarFilePath)) + Expect(exists).To(BeTrue()) + } + }) + var _ = It("when using --dry-run option, it should work and write the images yaml to the standard output", func() { fakeImageOperations.DownloadImageAndSaveFilesToDirCalls(downloadInventoryImageAndSaveFilesToDirStub) fakeImageOperations.CopyImageToTarCalls(copyImageToTarStub) diff --git a/pkg/command/plugin_bundle.go b/pkg/command/plugin_bundle.go index cdfbb2e46..420f35ce5 100644 --- a/pkg/command/plugin_bundle.go +++ b/pkg/command/plugin_bundle.go @@ -25,6 +25,7 @@ type downloadPluginBundleOptions struct { pluginDiscoveryOCIImage string tarFile string groups []string + plugins []string dryRun bool } @@ -42,7 +43,14 @@ func newDownloadBundlePluginCmd() *cobra.Command { to an internet-restricted environment. Please also see the "upload-bundle" command.`, Example: ` # Download a plugin bundle for a specific group version from the default discovery source - tanzu plugin download-bundle --to-tar /tmp/plugin_bundle_vmware_tkg_default_v1.0.0.tar.gz --group vmware-tkg/default:v1.0.0 + tanzu plugin download-bundle --group vmware-tkg/default:v1.0.0 --to-tar /tmp/plugin_bundle_vmware_tkg_default_v1.0.0.tar.gz + + # To download plugin bundle with specific plugin from the default discovery source + # --plugin name : Downloads all available versions of the plugin for all matching targets. + # --plugin name:version : Downloads specified version of the plugin for all matching targets. Use 'latest' as version for latest available version + # --plugin name@target:version : Downloads specified version of the plugin for the specified target. Use 'latest' as version for latest available version + # --plugin name@target : Downloads all available versions of the plugin for the specified target. + tanzu plugin download-bundle --plugin cluster:v1.0.0 --to-tar /tmp/plugin_bundle_cluster.tar.gz # Download a plugin bundle with the entire plugin repository from a custom discovery source tanzu plugin download-bundle --image custom.registry.vmware.com/tkg/tanzu-plugins/plugin-inventory:latest --to-tar /tmp/plugin_bundle_complete.tar.gz`, @@ -55,6 +63,7 @@ to an internet-restricted environment. Please also see the "upload-bundle" comma PluginInventoryImage: dpbo.pluginDiscoveryOCIImage, ToTar: dpbo.tarFile, Groups: dpbo.groups, + Plugins: dpbo.plugins, DryRun: dpbo.dryRun, ImageProcessor: carvelhelpers.NewImageOperationsImpl(), } @@ -73,6 +82,9 @@ to an internet-restricted environment. Please also see the "upload-bundle" comma f.StringSliceVarP(&dpbo.groups, "group", "", []string{}, "only download the plugins specified in the plugin-group version (can specify multiple)") utils.PanicOnErr(downloadBundleCmd.RegisterFlagCompletionFunc("group", completeGroupsAndVersionForBundleDownload)) + f.StringSliceVarP(&dpbo.plugins, "plugin", "", []string{}, "only download plugins matching specified pluginID. Format: name/name:version/name@target:version (can specify multiple)") + utils.PanicOnErr(downloadBundleCmd.RegisterFlagCompletionFunc("plugin", cobra.NoFileCompletions)) // TODO: Implement Shell completion + f.BoolVarP(&dpbo.dryRun, "dry-run", "", false, "perform a dry run by listing the images to download without actually downloading them") _ = downloadBundleCmd.Flags().MarkHidden("dry-run") diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 0f37d0a6f..7fbed6ebf 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -57,3 +57,18 @@ func PanicOnErr(err error) { panic(err) } + +// ParsePluginID parses the plugin id and returns (name, target, version) strings +func ParsePluginID(pluginID string) (string, string, string) { + var name, target, version string + parts := strings.Split(pluginID, ":") + if len(parts) > 1 { + version = parts[1] + } + parts = strings.Split(parts[0], "@") + name = parts[0] + if len(parts) > 1 { + target = parts[1] + } + return name, target, version +} diff --git a/pkg/utils/common_test.go b/pkg/utils/common_test.go index 947b157a1..549e8d6a2 100644 --- a/pkg/utils/common_test.go +++ b/pkg/utils/common_test.go @@ -175,3 +175,33 @@ func TestEnsureMutualExclusiveCurrentContexts(t *testing.T) { assert.NoError(t, err) assert.Equal(t, len(ccmap), 0) } + +func TestParsePluginID(t *testing.T) { + tests := []struct { + input string + expectedName string + expectedTarget string + expectedVersion string + }{ + {"app@kubernetes:v1.2.3", "app", "kubernetes", "v1.2.3"}, + {"app:v1.2.3", "app", "", "v1.2.3"}, + {"app", "app", "", ""}, + {"", "", "", ""}, + } + + for _, test := range tests { + name, target, version := ParsePluginID(test.input) + + if name != test.expectedName { + t.Errorf("For input '%s', expected name '%s', but got '%s'", test.input, test.expectedName, name) + } + + if target != test.expectedTarget { + t.Errorf("For input '%s', expected target '%s', but got '%s'", test.input, test.expectedTarget, target) + } + + if version != test.expectedVersion { + t.Errorf("For input '%s', expected version '%s', but got '%s'", test.input, test.expectedVersion, version) + } + } +} diff --git a/test/e2e/airgapped/airgapped_test.go b/test/e2e/airgapped/airgapped_test.go index f09f89fe7..137d6fb9d 100644 --- a/test/e2e/airgapped/airgapped_test.go +++ b/test/e2e/airgapped/airgapped_test.go @@ -23,11 +23,10 @@ const fileExists = "the file '%s' already exists" const showThrowErr = "should throw error for incorrect input path" var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-DownloadBundle-UploadBundle-Lifecycle]", func() { - Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests with plugin group 'vmware-tkg/default:v0.0.1'", func() { // Test case: download plugin bundle for plugin-group vmware-tkg/default:v0.0.1 It("download plugin bundle with specific plugin-group vmware-tkg/default:v0.0.1", func() { - err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tkg/default:v0.0.1"}, filepath.Join(tempDir, "plugin_bundle_vmware-tkg-default-v0.0.1.tar.gz")) + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tkg/default:v0.0.1"}, []string{}, filepath.Join(tempDir, "plugin_bundle_vmware-tkg-default-v0.0.1.tar.gz")) Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle with specific group") }) @@ -112,7 +111,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests with plugin group 'vmware-tmc/tmc-user:v9.9.9'", func() { // Test case: download plugin bundle for plugin-group vmware-tmc/tmc-user:v9.9.9 It("download plugin bundle for plugin-group vmware-tmc/tmc-user:v9.9.9", func() { - err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tmc/tmc-user:v9.9.9"}, filepath.Join(tempDir, "plugin_bundle_vmware-tmc-default-v9.9.9.tar.gz")) + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tmc/tmc-user:v9.9.9"}, []string{}, filepath.Join(tempDir, "plugin_bundle_vmware-tmc-default-v9.9.9.tar.gz")) Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle with specific group") }) @@ -191,16 +190,16 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download }) - Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests with plugin group 'vmware-tmc/tmc-user:v0.0.1' provided 2 times", func() { - // Test case: download plugin bundle for plugin-groups vmware-tmc/tmc-user:v0.0.1 and vmware-tmc/tmc-user:v0.0.1 + Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests with plugin group 'vmware-tmc/tmc-user:v0.0.1' provided 2 times and plugin 'isolated-cluster:v0.0.1'", func() { + // Test case: download plugin bundle for plugin-groups vmware-tmc/tmc-user:v0.0.1 and vmware-tmc/tmc-user:v0.0.1 and plugin 'isolated-cluster:v0.0.1' // Note: we are passing same plugin group multiple times to make sure we test the conflicts in the plugin groups // as well as plugins itself are handled properly while downloading and uploading bundle - It("download plugin bundle for plugin-group vmware-tmc/tmc-user:v0.0.1", func() { - err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tmc/tmc-user:v0.0.1", "vmware-tmc/tmc-user:v0.0.1"}, filepath.Join(tempDir, "plugin_bundle_vmware-tmc-v0.0.1.tar.gz")) + It("download plugin bundle for plugin-group vmware-tmc/tmc-user:v0.0.1 and plugin isolated-cluster:v0.0.1", func() { + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{"vmware-tmc/tmc-user:v0.0.1", "vmware-tmc/tmc-user:v0.0.1"}, []string{"isolated-cluster:v0.0.1"}, filepath.Join(tempDir, "plugin_bundle_vmware-tmc-v0.0.1.tar.gz")) Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle with specific group") }) - // Test case: upload plugin bundle downloaded using vmware-tmc/tmc-user:v0.0.1 plugin-group to the airgapped repository + // Test case: upload plugin bundle downloaded to the airgapped repository It("upload plugin bundle downloaded using vmware-tmc/tmc-user:v0.0.1 plugin-group to the airgapped repository", func() { // We are modifying the plugin source and the CLI will need to download the new DB. // However, the CLI will only refresh the DB after the cache TTL has expired. @@ -212,7 +211,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download Expect(err).To(BeNil(), "should not get any error for plugin source update") }) - It("validate the plugins from group 'vmware-tmc/tmc-user:v0.0.1' exists", func() { + It("validate the plugins from group 'vmware-tmc/tmc-user:v0.0.1' exists along with isolated-cluster:v0.0.1 plugin", func() { // search plugin groups pluginGroups, err = pluginlifecyclee2e.SearchAllPluginGroups(tf) Expect(err).To(BeNil(), framework.NoErrorForPluginGroupSearch) @@ -226,30 +225,75 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download // search plugins and make sure correct number of plugins available // check expected plugins are available in the `plugin search` output from the airgapped repository expectedPlugins := append(pluginsForPGTKG001, pluginsForPGTMC999...) - expectedPlugins = append(expectedPlugins, essentialPlugins...) // Essential plugin will be always installed + expectedPlugins = append(expectedPlugins, essentialPlugins...) // Essential plugin will be always installed + expectedPlugins = append(expectedPlugins, &framework.PluginInfo{Name: "isolated-cluster", Target: "global", Version: "v0.0.1", Description: "isolated-cluster " + functionality}) // Include isolated-cluster plugin with version v0.0.1 + pluginsSearchList, err = pluginlifecyclee2e.SearchAllPlugins(tf) Expect(err).To(BeNil(), framework.NoErrorForPluginGroupSearch) Expect(len(pluginsSearchList)).To(Equal(len(expectedPlugins))) Expect(framework.CheckAllPluginsExists(pluginsSearchList, expectedPlugins)).To(BeTrue()) }) - It("validate that plugins can be installed from group 'vmware-tmc/tmc-user:v0.0.1'", func() { + It("validate that plugins can be installed from group 'vmware-tmc/tmc-user:v0.0.1' and 'isolated-cluster:v0.0.1' can also be downloaded", func() { // All plugins should get installed from the group _, _, err := tf.PluginCmd.InstallPluginsFromGroup("", "vmware-tmc/tmc-user:v0.0.1") Expect(err).To(BeNil()) + // Install 'isolated-cluster` plugin individually + _, _, err = tf.PluginCmd.InstallPlugin("isolated-cluster", "global", "v0.0.1") + Expect(err).To(BeNil()) // Verify all plugins got installed with `tanzu plugin list` installedPlugins, err := tf.PluginCmd.ListInstalledPlugins() Expect(err).To(BeNil()) Expect(framework.CheckAllPluginsExists(installedPlugins, pluginsForPGTMC001)).To(BeTrue()) + Expect(framework.CheckAllPluginsExists(installedPlugins, []*framework.PluginInfo{{Name: "isolated-cluster", Target: "global", Version: "v0.0.1", Description: "isolated-cluster " + functionality}})).To(BeTrue()) }) }) + Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests with plugins 'pinniped-auth', 'isolated-cluster@global:v9.9.9', 'clustergroup@operations' specified", func() { + // Test case: download plugin bundle for plugins: 'pinniped-auth', 'isolated-cluster@global:v9.9.9', 'clustergroup@operations' + It("download plugin bundle for plugins: 'pinniped-auth', 'isolated-cluster@global:v9.9.9', 'clustergroup@operations'", func() { + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, []string{"pinniped-auth", "isolated-cluster@global:v9.9.9", "clustergroup@operations"}, filepath.Join(tempDir, "plugin_bundle_plugins_plugins.tar.gz")) + Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle with specific group") + }) + + // Test case: upload plugin bundle downloaded to the airgapped repository + It("upload plugin bundle downloaded using 'pinniped-auth', 'isolated-cluster@global:v9.9.9', 'clustergroup@operations' plugins to the airgapped repository", func() { + // We are modifying the plugin source and the CLI will need to download the new DB. + // However, the CLI will only refresh the DB after the cache TTL has expired. + err := tf.PluginCmd.UploadPluginBundle(e2eAirgappedCentralRepo, filepath.Join(tempDir, "plugin_bundle_plugins_plugins.tar.gz")) + Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle with specific group") + + // Force a DB refresh by updating the plugin source + err = framework.UpdatePluginDiscoverySource(tf, e2eAirgappedCentralRepoImage) + Expect(err).To(BeNil(), "should not get any error for plugin source update") + }) + + It("validate the plugins 'pinniped-auth', 'isolated-cluster@global:v9.9.9', 'clustergroup@operations' exists for all matching versions and targets", func() { + // search plugins and make sure correct number of plugins available + // check expected plugins are available in the `plugin search` output from the airgapped repository + expectedPlugins := allExpectedPluginForPluginMigration + expectedPlugins = append(expectedPlugins, essentialPlugins...) // Essential plugin will be always installed + + pluginsSearchList, err = pluginlifecyclee2e.SearchAllPluginsAndAllVersions(tf) + Expect(err).To(BeNil(), framework.NoErrorForPluginGroupSearch) + Expect(framework.CheckAllPluginsExists(pluginsSearchList, expectedPlugins)).To(BeTrue()) + }) + + It("validate that all migrated plugins can be installed individually", func() { + // All plugins should get installed from the group + for _, pi := range allExpectedPluginForPluginMigration { + _, _, err := tf.PluginCmd.InstallPlugin(pi.Name, pi.Target, pi.Version) + Expect(err).To(BeNil()) + } + }) + }) + Context("Download plugin bundle, Upload plugin bundle and plugin lifecycle tests without specifying any plugin group", func() { // Test case: download the entire plugin bundle without specifying plugin group It("download the entire plugin bundle without specifying plugin group", func() { - err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, filepath.Join(tempDir, "plugin_bundle_complete.tar.gz")) + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, []string{}, filepath.Join(tempDir, "plugin_bundle_complete.tar.gz")) Expect(err).To(BeNil(), "should not get any error while downloading plugin bundle without specifying group") }) @@ -331,14 +375,14 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download // Test case: (negative use case) empty path for --to-tar It("plugin download-bundle when to-tar path is empty", func() { - err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, "") + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, []string{}, "") Expect(err).NotTo(BeNil(), showThrowErr) Expect(strings.Contains(err.Error(), "flag '--to-tar' is required")).To(BeTrue()) }) // Test case: (negative use case) directory name only for --to-tar It("plugin download-bundle when to-tar path is a directory", func() { // Attempt download bundle specifying directory as output - err = tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, tempDir) + err = tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, []string{}, tempDir) // Expect error and validate text Expect(err).NotTo(BeNil()) @@ -346,7 +390,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Airgapped-Plugin-Download }) // Test case: (negative use case) current directory only for --to-tar It("plugin download-bundle when to-tar path is current directory", func() { - err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, ".") + err := tf.PluginCmd.DownloadPluginBundle(e2eTestLocalCentralRepoImage, []string{}, []string{}, ".") Expect(err).NotTo(BeNil(), showThrowErr) Expect(strings.Contains(err.Error(), fmt.Sprintf(fileExists, "."))).To(BeTrue()) }) diff --git a/test/e2e/airgapped/plugins.go b/test/e2e/airgapped/plugins.go index 4b4ff9520..3679f916e 100644 --- a/test/e2e/airgapped/plugins.go +++ b/test/e2e/airgapped/plugins.go @@ -88,3 +88,13 @@ var essentialPlugins = []*framework.PluginInfo{ var pluginsNotInAnyPGAndUsingSha = []*framework.PluginInfo{ {Name: "plugin-with-sha", Target: "global", Version: "v9.9.9", Description: "plugin-with-sha " + functionality}, } + +// allExpectedPluginForPluginMigration when download-bundle is invoked with following pluginIDs +// 'pinniped-auth', 'isolated-cluster@global:v9.9.9', 'clustergroup@operations' +var allExpectedPluginForPluginMigration = []*framework.PluginInfo{ + {Name: "isolated-cluster", Target: "global", Version: "v9.9.9", Description: "isolated-cluster " + functionality}, + {Name: "pinniped-auth", Target: "global", Version: "v0.0.1", Description: "pinniped-auth " + functionality}, + {Name: "pinniped-auth", Target: "global", Version: "v9.9.9", Description: "pinniped-auth " + functionality}, + {Name: "clustergroup", Target: "operations", Version: "v0.0.1", Description: "clustergroup " + functionality}, + {Name: "clustergroup", Target: "operations", Version: "v9.9.9", Description: "clustergroup " + functionality}, +} diff --git a/test/e2e/framework/framework_helper.go b/test/e2e/framework/framework_helper.go index 15eaf63cd..520ddaefd 100644 --- a/test/e2e/framework/framework_helper.go +++ b/test/e2e/framework/framework_helper.go @@ -286,6 +286,13 @@ func CheckAllPluginsExists(superList, subList []*PluginInfo) bool { // } _, ok := superSet[key] if !ok { + for i := range superList { + log.Infof("SuperList: %v, %v, %v", superList[i].Name, superList[i].Target, superList[i].Version) + } + log.Infof("") + for i := range subList { + log.Infof("SubList: %v, %v, %v", subList[i].Name, subList[i].Target, subList[i].Version) + } return false } } diff --git a/test/e2e/framework/output_handling.go b/test/e2e/framework/output_handling.go index 71bc8d3d7..3624cb087 100644 --- a/test/e2e/framework/output_handling.go +++ b/test/e2e/framework/output_handling.go @@ -15,10 +15,11 @@ type PluginInfo struct { } type PluginSearch struct { - Name string `json:"name"` - Description string `json:"description"` - Target string `json:"target"` - Latest string `json:"latest"` + Name string `json:"name"` + Description string `json:"description"` + Target string `json:"target"` + Latest string `json:"latest"` + Versions []string `json:"versions,omitempty"` } type PluginGroup struct { diff --git a/test/e2e/framework/plugin_lifecycle_operations.go b/test/e2e/framework/plugin_lifecycle_operations.go index 8951cc380..4ea6cabca 100644 --- a/test/e2e/framework/plugin_lifecycle_operations.go +++ b/test/e2e/framework/plugin_lifecycle_operations.go @@ -73,7 +73,7 @@ type PluginGroupOps interface { type PluginDownloadAndUploadOps interface { // DownloadPluginBundle downloads the plugin inventory and plugin bundles to local tar file - DownloadPluginBundle(image string, groups []string, toTar string, opts ...E2EOption) error + DownloadPluginBundle(image string, groups []string, plugins []string, toTar string, opts ...E2EOption) error // UploadPluginBundle performs the uploading plugin bundle to the remote repository // Based on the remote repository status, it setups a new discovery source endpoint @@ -186,12 +186,23 @@ func (po *pluginCmdOps) SearchPlugins(filter string, opts ...E2EOption) (plugins } // Convert from PluginSearch to PluginInfo for _, p := range result { - plugins = append(plugins, &PluginInfo{ - Name: p.Name, - Description: p.Description, - Target: p.Target, - Version: p.Latest, - }) + if len(p.Versions) != 0 { + for _, v := range p.Versions { + plugins = append(plugins, &PluginInfo{ + Name: p.Name, + Description: p.Description, + Target: p.Target, + Version: v, + }) + } + } else { + plugins = append(plugins, &PluginInfo{ + Name: p.Name, + Description: p.Description, + Target: p.Target, + Version: p.Latest, + }) + } } return plugins, stdOutStr, stdErrStr, err } @@ -312,7 +323,7 @@ func (po *pluginCmdOps) RunPluginCmd(options string, opts ...E2EOption) (string, return stdOut.String(), stdErr.String(), nil } -func (po *pluginCmdOps) DownloadPluginBundle(image string, groups []string, toTar string, opts ...E2EOption) error { +func (po *pluginCmdOps) DownloadPluginBundle(image string, groups []string, plugins []string, toTar string, opts ...E2EOption) error { downloadPluginBundle := PluginDownloadBundleCmd if len(strings.TrimSpace(image)) > 0 { downloadPluginBundle += " --image " + image @@ -320,6 +331,9 @@ func (po *pluginCmdOps) DownloadPluginBundle(image string, groups []string, toTa if len(groups) > 0 { downloadPluginBundle += " --group " + strings.Join(groups, ",") } + if len(plugins) > 0 { + downloadPluginBundle += " --plugin " + strings.Join(plugins, ",") + } downloadPluginBundle += " --to-tar " + strings.TrimSpace(toTar) out, stdErr, err := po.cmdExe.TanzuCmdExec(downloadPluginBundle, opts...) diff --git a/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go b/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go index 1314f4214..60263bbd8 100644 --- a/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go +++ b/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go @@ -30,6 +30,12 @@ func SearchAllPlugins(tf *framework.Framework, opts ...framework.E2EOption) ([]* return pluginsSearchList, err } +// SearchAllPluginsAndAllVersions runs the plugin search command and returns all the plugins along with all versions from the search output +func SearchAllPluginsAndAllVersions(tf *framework.Framework, opts ...framework.E2EOption) ([]*framework.PluginInfo, error) { + pluginsSearchList, _, _, err := tf.PluginCmd.SearchPlugins("--show-details", opts...) + return pluginsSearchList, err +} + // SearchAllPluginGroups runs the plugin group search command and returns all the plugin groups func SearchAllPluginGroups(tf *framework.Framework, opts ...framework.E2EOption) ([]*framework.PluginGroup, error) { pluginGroups, err := tf.PluginCmd.SearchPluginGroups("--show-details", opts...)