Skip to content

Commit

Permalink
Add support for individual plugin download with `tanzu plugin downloa…
Browse files Browse the repository at this point in the history
…d-bundle` command
  • Loading branch information
anujc25 committed Apr 5, 2024
1 parent 2fbe3a9 commit cc1c913
Show file tree
Hide file tree
Showing 12 changed files with 374 additions and 33 deletions.
35 changes: 31 additions & 4 deletions docs/quickstart/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
20 changes: 19 additions & 1 deletion pkg/airgapped/plugin_bundle_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -29,6 +31,7 @@ type DownloadPluginBundleOptions struct {
PluginInventoryImage string
ToTar string
Groups []string
Plugins []string

DryRun bool
ImageProcessor carvelhelpers.ImageOperationsImpl
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
157 changes: 157 additions & 0 deletions pkg/airgapped/plugin_bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion pkg/command/plugin_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type downloadPluginBundleOptions struct {
pluginDiscoveryOCIImage string
tarFile string
groups []string
plugins []string
dryRun bool
}

Expand All @@ -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`,
Expand All @@ -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(),
}
Expand All @@ -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")

Expand Down
15 changes: 15 additions & 0 deletions pkg/utils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
30 changes: 30 additions & 0 deletions pkg/utils/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Loading

0 comments on commit cc1c913

Please sign in to comment.