From 2aef4564701eed4f53d6ff83aeaa207c04348383 Mon Sep 17 00:00:00 2001 From: Anuj Chaudhari Date: Mon, 25 Oct 2021 09:41:45 -0700 Subject: [PATCH 1/4] Make tanzu cli detect plugin commands based on context awareness - This changes makes sure that only plugins available to current context are added to tanzu cli when "context-aware-discovery" feature is enabled --- apis/cli/v1alpha1/groupversion_info.go | 7 ++- pkg/v1/cli/catalog.go | 2 +- pkg/v1/cli/catalog/catalog.go | 2 +- pkg/v1/cli/cmd.go | 10 ++-- pkg/v1/cli/command/core/doc.go | 2 +- pkg/v1/cli/command/core/root.go | 70 +++++++++++++++++++++----- pkg/v1/cli/common/defaults.go | 4 +- pkg/v1/cli/runner.go | 19 ++++--- pkg/v1/tkg/utils/files.go | 20 ++++++++ 9 files changed, 104 insertions(+), 32 deletions(-) diff --git a/apis/cli/v1alpha1/groupversion_info.go b/apis/cli/v1alpha1/groupversion_info.go index f99983124b..a3ef5a3818 100644 --- a/apis/cli/v1alpha1/groupversion_info.go +++ b/apis/cli/v1alpha1/groupversion_info.go @@ -21,6 +21,9 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme - // GroupVersionKind has information about group, version and kind of this object. - GroupVersionKind = GroupVersion.WithKind("Catalog") + // GroupVersionKindCatalog has information about group, version and kind of Catalog object. + GroupVersionKindCatalog = GroupVersion.WithKind("Catalog") + + // GroupVersionKindCLIPlugin has information about group, version and kind of CLIPlugin object. + GroupVersionKindCLIPlugin = GroupVersion.WithKind("CLIPlugin") ) diff --git a/pkg/v1/cli/catalog.go b/pkg/v1/cli/catalog.go index 6ffc5d1454..01292c1b39 100644 --- a/pkg/v1/cli/catalog.go +++ b/pkg/v1/cli/catalog.go @@ -214,7 +214,7 @@ func saveCatalogCache(catalog *cliv1alpha1.Catalog) error { s := apimachineryjson.NewSerializerWithOptions(apimachineryjson.DefaultMetaFactory, scheme, scheme, apimachineryjson.SerializerOptions{Yaml: true, Pretty: false, Strict: false}) - catalog.GetObjectKind().SetGroupVersionKind(cliv1alpha1.GroupVersionKind) + catalog.GetObjectKind().SetGroupVersionKind(cliv1alpha1.GroupVersionKindCatalog) buf := new(bytes.Buffer) if err := s.Encode(catalog, buf); err != nil { return errors.Wrap(err, "failed to encode catalog cache file") diff --git a/pkg/v1/cli/catalog/catalog.go b/pkg/v1/cli/catalog/catalog.go index 58eda1a696..c1b49a957e 100644 --- a/pkg/v1/cli/catalog/catalog.go +++ b/pkg/v1/cli/catalog/catalog.go @@ -189,7 +189,7 @@ func saveCatalogCache(catalog *cliv1alpha1.Catalog) error { s := apimachineryjson.NewSerializerWithOptions(apimachineryjson.DefaultMetaFactory, scheme, scheme, apimachineryjson.SerializerOptions{Yaml: true, Pretty: false, Strict: false}) - catalog.GetObjectKind().SetGroupVersionKind(cliv1alpha1.GroupVersionKind) + catalog.GetObjectKind().SetGroupVersionKind(cliv1alpha1.GroupVersionKindCatalog) buf := new(bytes.Buffer) if err := s.Encode(catalog, buf); err != nil { return errors.Wrap(err, "failed to encode catalog cache file") diff --git a/pkg/v1/cli/cmd.go b/pkg/v1/cli/cmd.go index 3f7bdba276..3e8fbbdcc0 100644 --- a/pkg/v1/cli/cmd.go +++ b/pkg/v1/cli/cmd.go @@ -35,7 +35,7 @@ func GetCmd(p *cliv1alpha1.PluginDescriptor) *cobra.Command { Use: p.Name, Short: p.Description, RunE: func(cmd *cobra.Command, args []string) error { - runner := NewRunner(p.Name, args) + runner := NewRunner(p.Name, p.InstallationPath, args) ctx := context.Background() return runner.Run(ctx) }, @@ -58,7 +58,7 @@ func GetCmd(p *cliv1alpha1.PluginDescriptor) *cobra.Command { completion = append(completion, args...) completion = append(completion, toComplete) - runner := NewRunner(p.Name, completion) + runner := NewRunner(p.Name, p.InstallationPath, completion) ctx := context.Background() output, _, err := runner.RunOutput(ctx) if err != nil { @@ -91,7 +91,7 @@ func GetCmd(p *cliv1alpha1.PluginDescriptor) *cobra.Command { completion = append(completion, args...) completion = append(completion, toComplete) - runner := NewRunner(p.Name, completion) + runner := NewRunner(p.Name, p.InstallationPath, completion) ctx := context.Background() output, stderr, err := runner.RunOutput(ctx) if err != nil || stderr != "" { @@ -116,7 +116,7 @@ func GetCmd(p *cliv1alpha1.PluginDescriptor) *cobra.Command { helpArgs := getHelpArguments() // Pass this new command in to our plugin to have it handle help output - runner := NewRunner(p.Name, helpArgs) + runner := NewRunner(p.Name, p.InstallationPath, helpArgs) ctx := context.Background() err := runner.Run(ctx) if err != nil { @@ -154,7 +154,7 @@ func TestCmd(p *cliv1alpha1.PluginDescriptor) *cobra.Command { Use: p.Name, Short: p.Description, RunE: func(cmd *cobra.Command, args []string) error { - runner := NewRunner(p.Name, args) + runner := NewRunner(p.Name, p.InstallationPath, args) ctx := context.Background() return runner.RunTest(ctx) }, diff --git a/pkg/v1/cli/command/core/doc.go b/pkg/v1/cli/command/core/doc.go index 6cbaecce69..7748d90c23 100644 --- a/pkg/v1/cli/command/core/doc.go +++ b/pkg/v1/cli/command/core/doc.go @@ -89,7 +89,7 @@ func genREADME(plugins []*cliv1alpha1.PluginDescriptor) error { func genMarkdownTreePlugins(plugins []*cliv1alpha1.PluginDescriptor) error { args := []string{"generate-docs"} for _, p := range plugins { - runner := cli.NewRunner(p.Name, args) + runner := cli.NewRunner(p.Name, p.InstallationPath, args) ctx := context.Background() if err := runner.Run(ctx); err != nil { return err diff --git a/pkg/v1/cli/command/core/root.go b/pkg/v1/cli/command/core/root.go index 565d6c67d3..f7e018c429 100644 --- a/pkg/v1/cli/command/core/root.go +++ b/pkg/v1/cli/command/core/root.go @@ -5,16 +5,18 @@ package core import ( "fmt" - "log" "os" "strings" "time" + "github.com/aunum/log" "github.com/briandowns/spinner" "github.com/logrusorgru/aurora" "github.com/spf13/cobra" + "github.com/vmware-tanzu/tanzu-framework/apis/cli/v1alpha1" "github.com/vmware-tanzu/tanzu-framework/pkg/v1/cli" + "github.com/vmware-tanzu/tanzu-framework/pkg/v1/cli/pluginmanager" "github.com/vmware-tanzu/tanzu-framework/pkg/v1/config" ) @@ -58,15 +60,66 @@ func NewRootCmd() (*cobra.Command, error) { genAllDocsCmd, ) - plugins, err := cli.ListPlugins() + plugins, err := getAvailablePlugins() if err != nil { - return nil, fmt.Errorf("find available plugins: %w", err) + return nil, err } if err = config.CopyLegacyConfigDir(); err != nil { return nil, fmt.Errorf("failed to copy legacy configuration directory to new location: %w", err) } + // If context-aware-discovery is not enabled + // check that all plugins in the core distro are installed or do so. + if !config.IsFeatureActivated(config.FeatureContextAwareDiscovery) { + plugins, err = checkAndInstallMissingPlugins(plugins) + if err != nil { + return nil, err + } + } + + for _, plugin := range plugins { + RootCmd.AddCommand(cli.GetCmd(plugin)) + } + + duplicateAliasWarning() + + // Flag parsing must be deactivated because the root plugin won't know about all flags. + RootCmd.DisableFlagParsing = true + + return RootCmd, nil +} + +func getAvailablePlugins() ([]*v1alpha1.PluginDescriptor, error) { + plugins := make([]*v1alpha1.PluginDescriptor, 0) + var err error + + if config.IsFeatureActivated(config.FeatureContextAwareDiscovery) { + currentServerName := "" + + server, err := config.GetCurrentServer() + if err == nil && server != nil { + currentServerName = server.Name + } + + serverPlugin, standalonePlugins, err := pluginmanager.InstalledPlugins(currentServerName) + if err != nil { + return nil, fmt.Errorf("find installed plugins: %w", err) + } + p := append(serverPlugin, standalonePlugins...) + for i := range p { + plugins = append(plugins, &p[i]) + } + } else { + plugins, err = cli.ListPlugins() + if err != nil { + return nil, fmt.Errorf("find available plugins: %w", err) + } + } + return plugins, nil +} + +func checkAndInstallMissingPlugins(plugins []*v1alpha1.PluginDescriptor) ([]*v1alpha1.PluginDescriptor, error) { // check that all plugins in the core distro are installed or do so. if !noInit && !cli.IsDistributionSatisfied(plugins) { s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) @@ -90,16 +143,7 @@ func NewRootCmd() (*cobra.Command, error) { } s.Stop() } - for _, plugin := range plugins { - RootCmd.AddCommand(cli.GetCmd(plugin)) - } - - duplicateAliasWarning() - - // Flag parsing must be deactivated because the root plugin won't know about all flags. - RootCmd.DisableFlagParsing = true - - return RootCmd, nil + return plugins, nil } func duplicateAliasWarning() { diff --git a/pkg/v1/cli/common/defaults.go b/pkg/v1/cli/common/defaults.go index 3d2e25d56c..2b4addb7f9 100644 --- a/pkg/v1/cli/common/defaults.go +++ b/pkg/v1/cli/common/defaults.go @@ -12,14 +12,14 @@ import ( var ( // DefaultCacheDir is the default cache directory - DefaultCacheDir = filepath.Join(xdg.CacheHome, "tanzu") + DefaultCacheDir = filepath.Join(xdg.Home, ".cache", "tanzu") // DefaultPluginRoot is the default plugin root. DefaultPluginRoot = filepath.Join(xdg.DataHome, "tanzu-cli") // DefaultLocalPluginDistroDir is the default Local plugin distribution root directory // This directory will be used for local discovery and local distribute of plugins - DefaultLocalPluginDistroDir = filepath.Join(xdg.ConfigHome, "tanzu-plugin") + DefaultLocalPluginDistroDir = filepath.Join(xdg.Home, ".config", "tanzu-plugins") ) const ( diff --git a/pkg/v1/cli/runner.go b/pkg/v1/cli/runner.go index bb7fb859ea..f471955c25 100644 --- a/pkg/v1/cli/runner.go +++ b/pkg/v1/cli/runner.go @@ -17,19 +17,21 @@ import ( // Runner is a plugin runner. type Runner struct { - name string - args []string - pluginRoot string + name string + args []string + pluginRoot string + pluginAbsPath string } // NewRunner creates an instance of Runner. -func NewRunner(name string, args []string, options ...Option) *Runner { +func NewRunner(name, pluginAbsPath string, args []string, options ...Option) *Runner { opts := makeDefaultOptions(options...) r := &Runner{ - name: name, - args: args, - pluginRoot: opts.pluginRoot, + name: name, + args: args, + pluginRoot: opts.pluginRoot, + pluginAbsPath: pluginAbsPath, } return r } @@ -130,6 +132,9 @@ func (r *Runner) pluginName() string { } func (r *Runner) pluginPath() string { + if r.pluginAbsPath != "" { + return r.pluginAbsPath + } return filepath.Join(r.pluginRoot, r.pluginName()) } diff --git a/pkg/v1/tkg/utils/files.go b/pkg/v1/tkg/utils/files.go index 8471c3b97e..a6cc64ec63 100644 --- a/pkg/v1/tkg/utils/files.go +++ b/pkg/v1/tkg/utils/files.go @@ -4,6 +4,9 @@ package utils import ( + "crypto/sha256" + "encoding/hex" + "io" "os" "github.com/vmware-tanzu/tanzu-framework/pkg/v1/tkg/constants" @@ -51,3 +54,20 @@ func WriteToFile(sourceFile string, data []byte) error { func DeleteFile(filePath string) error { return os.Remove(filePath) } + +// SHA256FromFile returns SHA256 sum of a file +func SHA256FromFile(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + b := h.Sum(nil) + + return hex.EncodeToString(b), nil +} From 27dbaa4a69829ae257d0baba2f9f4b48ca053f07 Mon Sep 17 00:00:00 2001 From: Anuj Chaudhari Date: Mon, 25 Oct 2021 09:42:55 -0700 Subject: [PATCH 2/4] Implement missing plugin command for context awareness - Implement plugin install, upgrade, clean, delete commands for context awareness --- pkg/v1/cli/command/core/plugin_manager.go | 60 +++++++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/pkg/v1/cli/command/core/plugin_manager.go b/pkg/v1/cli/command/core/plugin_manager.go index 9917d92903..54b46f31f1 100644 --- a/pkg/v1/cli/command/core/plugin_manager.go +++ b/pkg/v1/cli/command/core/plugin_manager.go @@ -229,7 +229,27 @@ var installPluginCmd = &cobra.Command{ if err != nil { return err } - return pluginmanager.InstallPlugin(server.Name, name, version) + + pluginVersion := version + + if pluginVersion == cli.VersionLatest { + availablePlugins, err := pluginmanager.AvailablePlugins(server.Name) + if err != nil { + return err + } + for i := range availablePlugins { + if availablePlugins[i].Name == name { + pluginVersion = availablePlugins[i].RecommendedVersion + } + } + } + + err = pluginmanager.InstallPlugin(server.Name, name, pluginVersion) + if err != nil { + return err + } + log.Successf("successfully installed '%s' plugin", name) + return nil } repos := getRepositories() @@ -268,7 +288,27 @@ var upgradePluginCmd = &cobra.Command{ name := args[0] if config.IsFeatureActivated(config.FeatureContextAwareDiscovery) { - return errors.New("context-aware discovery is enabled but function is not yet implemented") + server, err := config.GetCurrentServer() + if err != nil { + return err + } + availablePlugins, err := pluginmanager.AvailablePlugins(server.Name) + if err != nil { + return err + } + pluginVersion := "" + for i := range availablePlugins { + if availablePlugins[i].Name == name { + pluginVersion = availablePlugins[i].RecommendedVersion + } + } + + err = pluginmanager.UpgradePlugin(server.Name, name, pluginVersion) + if err != nil { + return err + } + log.Successf("successfully upgraded plugin '%s' to version '%s'", name, pluginVersion) + return nil } repos := getRepositories() @@ -298,7 +338,18 @@ var deletePluginCmd = &cobra.Command{ name := args[0] if config.IsFeatureActivated(config.FeatureContextAwareDiscovery) { - return errors.New("context-aware discovery is enabled but function is not yet implemented") + server, err := config.GetCurrentServer() + if err != nil { + return err + } + + err = pluginmanager.DeletePlugin(server.Name, name) + if err != nil { + return err + } + + log.Successf("successfully deleted plugin '%s'", name) + return nil } err = cli.DeletePlugin(name) @@ -312,9 +363,8 @@ var cleanPluginCmd = &cobra.Command{ Short: "Clean the plugins", RunE: func(cmd *cobra.Command, args []string) (err error) { if config.IsFeatureActivated(config.FeatureContextAwareDiscovery) { - return errors.New("context-aware discovery is enabled but function is not yet implemented") + return pluginmanager.Clean() } - return cli.Clean() }, } From 5e01b76295e852be007b8d3fcc0982013a8ee78a Mon Sep 17 00:00:00 2001 From: Anuj Chaudhari Date: Mon, 25 Oct 2021 09:27:14 -0700 Subject: [PATCH 3/4] Implement plugin artifacts publish command with 'builder' plugin - Use artifact output directory generated with `tanzu builder compile cli` command as an input to the new `tanzu builder publish` command, and publish the generated artifacts to different discovery and distribution based on the `type` - This change implements `tanzu builder publish` command by supporting `local` discovery/distribution - Adds a new `Makefile` section for "Building and publishing CLIPlugin Discovery resources and binaries --- Makefile | 25 +++ cmd/cli/plugin-admin/builder/main.go | 1 + go.mod | 2 + go.sum | 1 + pkg/v1/builder/command/publish.go | 80 ++++++++ pkg/v1/builder/command/publish/helper.go | 193 ++++++++++++++++++ pkg/v1/builder/command/publish/helper_test.go | 86 ++++++++ pkg/v1/builder/command/publish/local.go | 42 ++++ pkg/v1/builder/command/publish/oci.go | 37 ++++ pkg/v1/builder/command/publish/publisher.go | 80 ++++++++ pkg/v1/cli/command/core/plugin_manager.go | 16 +- pkg/v1/cli/common/arch.go | 5 + pkg/v1/cli/common/constants.go | 29 +++ pkg/v1/cli/pluginmanager/manager.go | 38 ++-- pkg/v1/cli/pluginmanager/manager_test.go | 20 +- 15 files changed, 616 insertions(+), 39 deletions(-) create mode 100644 pkg/v1/builder/command/publish.go create mode 100644 pkg/v1/builder/command/publish/helper.go create mode 100644 pkg/v1/builder/command/publish/helper_test.go create mode 100644 pkg/v1/builder/command/publish/local.go create mode 100644 pkg/v1/builder/command/publish/oci.go create mode 100644 pkg/v1/builder/command/publish/publisher.go create mode 100644 pkg/v1/cli/common/constants.go diff --git a/Makefile b/Makefile index ace75d6c88..4e3d536b17 100644 --- a/Makefile +++ b/Makefile @@ -84,9 +84,11 @@ BUILD_TAGS ?= ARTIFACTS_DIR ?= ./artifacts XDG_CACHE_HOME := ${HOME}/.cache +XDG_CONFIG_HOME :=${HOME}/.config export XDG_DATA_HOME export XDG_CACHE_HOME +export XDG_CONFIG_HOME export OCI_REGISTRY ## -------------------------------------- @@ -249,6 +251,29 @@ build-cli-local: configure-buildtags-embedproviders build-cli-${GOHOSTOS}-${GOHO .PHONY: build-install-cli-local build-install-cli-local: clean-catalog-cache clean-cli-plugins build-cli-local install-cli-plugins install-cli ## Local build and install the CLI plugins +## -------------------------------------- +## Build and publish CLIPlugin Discovery resource files and binaries +## -------------------------------------- + +STANDALONE_PLUGINS := login management-cluster package pinniped-auth +CONTEXT_PLUGINS := cluster kubernetes-release secret + +.PHONY: publish-plugins +publish-plugins: + $(GO) run ./cmd/cli/plugin-admin/builder/main.go publish --type local --plugins "$(STANDALONE_PLUGINS)" --version $(BUILD_VERSION) --os-arch "${ENVS}" --local-output-discovery-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/discovery/standalone" --local-output-distribution-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/distribution" --input-artifact-dir $(ARTIFACTS_DIR) + $(GO) run ./cmd/cli/plugin-admin/builder/main.go publish --type local --plugins "$(CONTEXT_PLUGINS)" --version $(BUILD_VERSION) --os-arch "${ENVS}" --local-output-discovery-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/discovery/context" --local-output-distribution-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/distribution" --input-artifact-dir $(ARTIFACTS_DIR) + +.PHONY: publish-plugins-local +publish-plugins-local: + $(GO) run ./cmd/cli/plugin-admin/builder/main.go publish --type local --plugins "$(STANDALONE_PLUGINS)" --version $(BUILD_VERSION) --os-arch "${GOHOSTOS}-${GOHOSTARCH}" --local-output-discovery-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/discovery/standalone" --local-output-distribution-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/distribution" --input-artifact-dir $(ARTIFACTS_DIR) + $(GO) run ./cmd/cli/plugin-admin/builder/main.go publish --type local --plugins "$(CONTEXT_PLUGINS)" --version $(BUILD_VERSION) --os-arch "${GOHOSTOS}-${GOHOSTARCH}" --local-output-discovery-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/discovery/context" --local-output-distribution-dir "$(XDG_CONFIG_HOME)/tanzu-plugins/distribution" --input-artifact-dir $(ARTIFACTS_DIR) + +.PHONY: build-publish-plugins +build-publish-plugins: clean-catalog-cache clean-cli-plugins build-cli install-cli publish-plugins + +.PHONY: build-publish-plugins-local +build-publish-plugins-local: clean-catalog-cache clean-cli-plugins build-cli-local install-cli publish-plugins-local + ## -------------------------------------- ## manage cli mocks ## -------------------------------------- diff --git a/cmd/cli/plugin-admin/builder/main.go b/cmd/cli/plugin-admin/builder/main.go index e5f42efac1..9c9e61f120 100644 --- a/cmd/cli/plugin-admin/builder/main.go +++ b/cmd/cli/plugin-admin/builder/main.go @@ -28,6 +28,7 @@ func main() { p.AddCommands( command.CLICmd, command.NewInitCmd(), + command.PublishCmd, ) if err := p.Execute(); err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index a174d81a15..40a1ee9cdc 100644 --- a/go.mod +++ b/go.mod @@ -78,10 +78,12 @@ require ( github.com/rs/xid v1.2.1 github.com/satori/go.uuid v1.2.0 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 + github.com/spf13/afero v1.2.2 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 + github.com/tj/assert v0.0.0-20171129193455-018094318fb0 github.com/vmware-labs/yaml-jsonpath v0.3.2 github.com/vmware-tanzu/carvel-kapp-controller v0.25.0 github.com/vmware-tanzu/carvel-secretgen-controller v0.5.0 diff --git a/go.sum b/go.sum index d8593718cb..d2cdce993c 100644 --- a/go.sum +++ b/go.sum @@ -1161,6 +1161,7 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= diff --git a/pkg/v1/builder/command/publish.go b/pkg/v1/builder/command/publish.go new file mode 100644 index 0000000000..ed51287149 --- /dev/null +++ b/pkg/v1/builder/command/publish.go @@ -0,0 +1,80 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/vmware-tanzu/tanzu-framework/pkg/v1/builder/command/publish" + "github.com/vmware-tanzu/tanzu-framework/pkg/v1/cli/common" +) + +var ( + distroType, pluginsString, oa, inputArtifactDir string + localOutputDiscoveryDir, localOutputDistributionDir string + ociDiscoveryImage, ociDistributionImageRepository string + recommendedVersion string +) + +// PublishCmd publishes plugin resources +var PublishCmd = &cobra.Command{ + Use: "publish", + Short: "publish operations", + RunE: publishPlugins, +} + +func init() { + PublishCmd.Flags().StringVar(&distroType, "type", "", "type of discovery and distribution for publishing plugins. Supported: local") + PublishCmd.Flags().StringVar(&pluginsString, "plugins", "", "list of plugin names. Example: 'login management-cluster cluster'") + PublishCmd.Flags().StringVar(&inputArtifactDir, "input-artifact-dir", "", "artifact directory which is a output of 'tanzu builder cli compile' command") + + PublishCmd.Flags().StringVar(&oa, "os-arch", common.DefaultOSArch, "list of os-arch") + PublishCmd.Flags().StringVar(&recommendedVersion, "version", "", "recommended version of the plugins") + + PublishCmd.Flags().StringVar(&localOutputDiscoveryDir, "local-output-discovery-dir", "", "local output directory where CLIPlugin resource yamls for discovery will be placed. Applicable to 'local' type") + PublishCmd.Flags().StringVar(&localOutputDistributionDir, "local-output-distribution-dir", "", "local output directory where plugin binaries will be placed. Applicable to 'local' type") + + PublishCmd.Flags().StringVar(&ociDiscoveryImage, "oci-discovery-image", "", "image path to publish oci image with CLIPlugin resource yamls. Applicable to 'oci' type") + PublishCmd.Flags().StringVar(&ociDistributionImageRepository, "oci-distribution-image-repository", "", "image path prefix to publish oci image for plugin binaries. Applicable to 'oci' type") + + _ = PublishCmd.MarkFlagRequired("type") + _ = PublishCmd.MarkFlagRequired("version") + _ = PublishCmd.MarkFlagRequired("plugins") + _ = PublishCmd.MarkFlagRequired("input-artifact-dir") +} + +func publishPlugins(cmd *cobra.Command, args []string) error { + plugins := strings.Split(pluginsString, " ") + osArch := strings.Split(oa, " ") + + if localOutputDiscoveryDir == "" { + localOutputDiscoveryDir = filepath.Join(common.DefaultLocalPluginDistroDir, "discovery", "oci") + } + + var publisherInterface publish.Publisher + + switch strings.ToLower(distroType) { + case "local": + publisherInterface = publish.NewLocalPublisher(localOutputDistributionDir) + case "oci": + publisherInterface = publish.NewOCIPublisher(ociDiscoveryImage, ociDistributionImageRepository, localOutputDiscoveryDir) + default: + return errors.Errorf("publish plugins with type %s is not yet supported", distroType) + } + + publishMetadata := publish.Metadata{ + Plugins: plugins, + OSArch: osArch, + RecommendedVersion: recommendedVersion, + InputArtifactDir: inputArtifactDir, + LocalDiscoveryPath: localOutputDiscoveryDir, + PublisherInterface: publisherInterface, + } + + return publish.PublishPlugins(&publishMetadata) +} diff --git a/pkg/v1/builder/command/publish/helper.go b/pkg/v1/builder/command/publish/helper.go new file mode 100644 index 0000000000..6d07f17e99 --- /dev/null +++ b/pkg/v1/builder/command/publish/helper.go @@ -0,0 +1,193 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package publish + +import ( + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/afero" + "gopkg.in/yaml.v2" + + "github.com/vmware-tanzu/tanzu-framework/apis/cli/v1alpha1" + "github.com/vmware-tanzu/tanzu-framework/pkg/v1/cli/common" + "github.com/vmware-tanzu/tanzu-framework/pkg/v1/tkg/utils" + + apimachineryjson "k8s.io/apimachinery/pkg/runtime/serializer/json" +) + +const ( + osTypeWindows = "windows" + fileExtensionWindows = ".exe" +) + +type osArch struct { + os string + arch string +} + +type pluginInfo struct { + recommendedVersion string + description string + versions map[string][]osArch +} + +var fs = afero.NewOsFs() + +func detectAvailablePluginInfo(artifactDir string, plugins, arrOSArch []string, recommandedVersion string) (map[string]*pluginInfo, error) { + mapPluginInfo := make(map[string]*pluginInfo) + + // For all plugins + for _, plugin := range plugins { + // For all supported OS + for _, osArch := range arrOSArch { + o, a, err := splitOSArch(osArch) + if err != nil { + return nil, err + } + + // get all directory under plugin directory + pluginDir := filepath.Join(artifactDir, o, a, "cli", plugin) + files, err := afero.ReadDir(fs, pluginDir) + if err != nil { + return nil, errors.Errorf("unable to find plugin artifact directory for plugin:'%s' os:'%s', arch:'%s' [directory: '%s']", plugin, o, a, pluginDir) + } + + // Each directory under the plugin directory is considered version directory + for _, file := range files { + if file.IsDir() { + updatePluginInfoMapWithVersionOSArch(mapPluginInfo, plugin, file.Name(), o, a) + } + } + + description := getDescriptionFromPluginYaml(filepath.Join(artifactDir, o, a, "cli", plugin, "plugin.yaml")) + // Update recommanded version and Description + updatePluginInfoMapWithRecommandedVersionDescription(mapPluginInfo, plugin, recommandedVersion, description) + } + } + + return mapPluginInfo, nil +} + +func updatePluginInfoMapWithRecommandedVersionDescription(mapPluginInfo map[string]*pluginInfo, plugin, recommendedVersion, description string) { + if mapPluginInfo[plugin] == nil { + mapPluginInfo[plugin] = &pluginInfo{} + mapPluginInfo[plugin].versions = make(map[string][]osArch) + } + mapPluginInfo[plugin].recommendedVersion = recommendedVersion + mapPluginInfo[plugin].description = description +} + +func updatePluginInfoMapWithVersionOSArch(mapPluginInfo map[string]*pluginInfo, plugin, version, osType, arch string) { + if mapPluginInfo[plugin] == nil { + mapPluginInfo[plugin] = &pluginInfo{} + mapPluginInfo[plugin].versions = make(map[string][]osArch) + } + + if mapPluginInfo[plugin].versions[version] == nil { + mapPluginInfo[plugin].versions[version] = make([]osArch, 0) + } + + oa := mapPluginInfo[plugin].versions[version] + oa = append(oa, osArch{os: osType, arch: arch}) + + mapPluginInfo[plugin].versions[version] = oa +} + +func getDescriptionFromPluginYaml(pluginYaml string) string { + b, err := afero.ReadFile(fs, pluginYaml) + if err == nil { + pd := &v1alpha1.PluginDescriptor{} + err := yaml.Unmarshal(b, pd) + if err == nil { + return pd.Description + } + } + return "" +} + +func newCLIPluginResource(plugin, description, version string, artifacts map[string]v1alpha1.ArtifactList) v1alpha1.CLIPlugin { + cliPlugin := v1alpha1.CLIPlugin{} + cliPlugin.SetGroupVersionKind(v1alpha1.GroupVersionKindCLIPlugin) + cliPlugin.SetName(plugin) + cliPlugin.Spec.Description = description + cliPlugin.Spec.RecommendedVersion = version + cliPlugin.Spec.Artifacts = artifacts + return cliPlugin +} + +func newArtifactObject(osType, arch, artifactType, digest, uri string) v1alpha1.Artifact { + artifact := v1alpha1.Artifact{ + Type: artifactType, + OS: osType, + Arch: arch, + Digest: digest, + } + + if artifactType == common.DistributionTypeOCI { + artifact.Image = uri + } else { + artifact.URI = uri + } + return artifact +} + +func getPluginPathAndDigestFromMetadata(artifactDir, plugin, version, osType, arch string) (string, string, error) { + sourcePath := filepath.Join(artifactDir, osType, arch, "cli", plugin, version, "tanzu-"+plugin+"-"+osType+"_"+arch) + if osType == osTypeWindows { + sourcePath += fileExtensionWindows + } + digest, err := utils.SHA256FromFile(sourcePath) + if err != nil { + return "", "", errors.Wrap(err, "error while calculating sha256") + } + return sourcePath, digest, nil +} + +func saveCLIPluginResource(cliPlugin *v1alpha1.CLIPlugin, discoveryResourceFile string) error { + discoveryResourceDir := filepath.Dir(discoveryResourceFile) + + err := fs.MkdirAll(discoveryResourceDir, 0755) + if err != nil { + return errors.Wrap(err, "could not create dir") + } + + fo, err := fs.Create(discoveryResourceFile) + if err != nil { + return errors.Wrap(err, "could not create resource file") + } + defer fo.Close() + + scheme, err := v1alpha1.SchemeBuilder.Build() + if err != nil { + return errors.Wrap(err, "failed to create scheme") + } + e := apimachineryjson.NewSerializerWithOptions(apimachineryjson.DefaultMetaFactory, scheme, scheme, + apimachineryjson.SerializerOptions{Yaml: true, Pretty: false, Strict: false}) + + err = e.Encode(cliPlugin, fo) + if err != nil { + return errors.Wrap(err, "could not write to CLIPlugin resource file") + } + return nil +} + +func ensureResourceDir(resourceDir string, cleanDir bool) error { + if cleanDir { + _ = fs.RemoveAll(resourceDir) + } + if err := fs.MkdirAll(resourceDir, 0755); err != nil { + return errors.Wrapf(err, "unable to create resource directory '%v'", resourceDir) + } + return nil +} + +func splitOSArch(osArch string) (string, string, error) { + arr := strings.Split(osArch, "-") + if len(arr) < 2 { + return "", "", errors.Errorf("provided os-arch '%s' is invalid", osArch) + } + return arr[0], arr[1], nil +} diff --git a/pkg/v1/builder/command/publish/helper_test.go b/pkg/v1/builder/command/publish/helper_test.go new file mode 100644 index 0000000000..1f3836db36 --- /dev/null +++ b/pkg/v1/builder/command/publish/helper_test.go @@ -0,0 +1,86 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package publish + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/aunum/log" + "github.com/spf13/afero" + "github.com/tj/assert" +) + +func Test_DetectAvailablePluginInfo(t *testing.T) { + assert := assert.New(t) + artifactDir := "artifacts" + // Set `fs` as in memory file system for testing + fs = afero.NewMemMapFs() + + // Setup artifacts directory for testing + createDummyArtifactDir(filepath.Join(artifactDir, "windows", "amd64", "cli"), "fake-plugin-foo", "v1.1.0", "fake description", []string{"v1.0.0"}) + createDummyArtifactDir(filepath.Join(artifactDir, "windows", "arm32", "cli"), "fake-plugin-foo", "v1.1.0", "fake description", []string{"v1.0.0"}) + createDummyArtifactDir(filepath.Join(artifactDir, "darwin", "amd64", "cli"), "fake-plugin-foo", "v1.1.0", "fake description", []string{"v1.0.0", "v1.1.0"}) + createDummyArtifactDir(filepath.Join(artifactDir, "darwin", "arm32", "cli"), "fake-plugin-foo", "v1.1.0", "fake description", []string{"v1.0.0", "v1.1.0"}) + + plugins := []string{"fake-plugin-foo"} + osArch := []string{"darwin-amd64", "darwin-arm32", "windows-amd64", "windows-arm32"} + pluginInfo, err := detectAvailablePluginInfo(artifactDir, plugins, osArch, "v1.1.0") + assert.Nil(err) + assert.NotNil(pluginInfo["fake-plugin-foo"]) + + // should include 2 versions. "v1.0.0" and "v1.1.0" and no other versions + assert.Equal(2, len(pluginInfo["fake-plugin-foo"].versions)) + assert.NotNil(pluginInfo["fake-plugin-foo"].versions["v1.0.0"]) + assert.NotNil(pluginInfo["fake-plugin-foo"].versions["v1.1.0"]) + assert.Nil(pluginInfo["fake-plugin-foo"].versions["v1.2.0"]) + + // should include 4 os-arch for version v1.0.0. "darwin-amd64", "darwin-arm32", "windows-amd64", "windows-arm32" + assert.Equal(4, len(pluginInfo["fake-plugin-foo"].versions["v1.0.0"])) + arrOsArch := convertVersionToStringArray(pluginInfo["fake-plugin-foo"].versions["v1.0.0"]) + assert.Contains(arrOsArch, "darwin-amd64") + assert.Contains(arrOsArch, "darwin-arm32") + assert.Contains(arrOsArch, "windows-amd64") + assert.Contains(arrOsArch, "windows-arm32") + + // should include 2 os-arch for version v1.1.0. "darwin-amd64", "darwin-amd64" + assert.Equal(2, len(pluginInfo["fake-plugin-foo"].versions["v1.1.0"])) + arrOsArch = convertVersionToStringArray(pluginInfo["fake-plugin-foo"].versions["v1.1.0"]) + assert.Contains(arrOsArch, "darwin-amd64") + assert.Contains(arrOsArch, "darwin-arm32") + assert.NotContains(arrOsArch, "windows-amd64") + assert.NotContains(arrOsArch, "windows-arm32") + + assert.Equal("v1.1.0", pluginInfo["fake-plugin-foo"].recommendedVersion) + assert.Equal("fake description", pluginInfo["fake-plugin-foo"].description) +} + +func convertVersionToStringArray(arrOsArchInfo []osArch) []string { + oa := []string{} + for idx := range arrOsArchInfo { + oa = append(oa, arrOsArchInfo[idx].os+"-"+arrOsArchInfo[idx].arch) + } + return oa +} + +func createDummyArtifactDir(directoryBasePath, pluginName, recommendedVersion, description string, versions []string) { //nolint:unparam // `pluginName` always receives `"fake-plugin-foo" + var err error + + for _, v := range versions { + err = fs.MkdirAll(filepath.Join(directoryBasePath, pluginName, v), 0755) + if err != nil { + log.Fatal(err) + } + } + + data := `name: %s +description: %s +version: %s` + + err = afero.WriteFile(fs, filepath.Join(directoryBasePath, pluginName, "plugin.yaml"), []byte(fmt.Sprintf(data, pluginName, description, recommendedVersion)), 0644) + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/v1/builder/command/publish/local.go b/pkg/v1/builder/command/publish/local.go new file mode 100644 index 0000000000..683d89887d --- /dev/null +++ b/pkg/v1/builder/command/publish/local.go @@ -0,0 +1,42 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package publish + +import ( + "path/filepath" + + "github.com/otiai10/copy" +) + +// LocalPublisher defines local publisher configuration +type LocalPublisher struct { + LocalDistributionPath string +} + +// NewLocalPublisher create new local publisher +func NewLocalPublisher(localDistributionPath string) Publisher { + return &LocalPublisher{ + LocalDistributionPath: localDistributionPath, + } +} + +// PublishPlugin publishes plugin binaries to local distribution directory +func (l *LocalPublisher) PublishPlugin(sourcePath, version, os, arch, plugin string) (string, error) { + destPath := filepath.Join(l.LocalDistributionPath, os, arch, "cli", plugin, version, "tanzu-"+plugin+"-"+os+"_"+arch) + if os == osTypeWindows { + destPath += fileExtensionWindows + } + + _ = ensureResourceDir(filepath.Dir(destPath), false) + err := copy.Copy(sourcePath, destPath) + if err != nil { + return "", err + } + return destPath, nil +} + +// PublishDiscovery publishes the CLIPlugin resources YAML to a local discovery directory +func (l *LocalPublisher) PublishDiscovery() error { + return nil +} diff --git a/pkg/v1/builder/command/publish/oci.go b/pkg/v1/builder/command/publish/oci.go new file mode 100644 index 0000000000..43a9279225 --- /dev/null +++ b/pkg/v1/builder/command/publish/oci.go @@ -0,0 +1,37 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package publish + +// TODO: to be implemented as part of https://github.com/vmware-tanzu/tanzu-framework/issues/946 + +// OCIPublisher defines OCI publisher configuration +type OCIPublisher struct { + OCIDiscoveryImage string + OCIDistributionImageRepository string + + LocalDiscoveryPath string +} + +// NewOCIPublisher create new OCI based publisher +func NewOCIPublisher( + ociDiscoveryImage, + ociDistributionImageRepository, + localDiscoveryPath string) Publisher { + + return &OCIPublisher{ + OCIDiscoveryImage: ociDiscoveryImage, + OCIDistributionImageRepository: ociDistributionImageRepository, + LocalDiscoveryPath: localDiscoveryPath, + } +} + +// PublishPlugin publishes plugin binaries to OCI based distribution directory +func (o *OCIPublisher) PublishPlugin(version, os, arch, plugin, sourcePath string) (string, error) { + return "", nil +} + +// PublishDiscovery publishes the CLIPlugin resources YAML to a OCI based discovery container image +func (o *OCIPublisher) PublishDiscovery() error { + return nil +} diff --git a/pkg/v1/builder/command/publish/publisher.go b/pkg/v1/builder/command/publish/publisher.go new file mode 100644 index 0000000000..48c9fe4878 --- /dev/null +++ b/pkg/v1/builder/command/publish/publisher.go @@ -0,0 +1,80 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package publish implements plugin and plugin api publishing related function +package publish + +import ( + "path/filepath" + + "github.com/aunum/log" + "github.com/pkg/errors" + + "github.com/vmware-tanzu/tanzu-framework/apis/cli/v1alpha1" + "github.com/vmware-tanzu/tanzu-framework/pkg/v1/cli/common" +) + +// Publisher is an interface to publish plugin and CLIPlugin resource files to discovery +type Publisher interface { + // PublishPlugin publishes plugin binaries to distribution + PublishPlugin(version, os, arch, plugin, sourcePath string) (string, error) + // PublishDiscovery publishes the CLIPlugin resources YAML to a discovery + PublishDiscovery() error +} + +// Metadata defines metadata required for plugins publishing +type Metadata struct { + Plugins []string + InputArtifactDir string + OSArch []string + RecommendedVersion string + LocalDiscoveryPath string + PublisherInterface Publisher +} + +// PublishPlugins publishes the plugin based on provided metadata +// This function is responsible for auto-detecting the available plugin versions +// as well as os-arch and publishing artifacts to correct discovery and distribution +// based on the publisher type +func PublishPlugins(pm *Metadata) error { //nolint:golint // ignore stutters warning for 'publish.PublishPlugins' + _ = ensureResourceDir(pm.LocalDiscoveryPath, true) + + availablePluginInfo, err := detectAvailablePluginInfo(pm.InputArtifactDir, pm.Plugins, pm.OSArch, pm.RecommendedVersion) + if err != nil { + return err + } + + for plugin, pluginInfo := range availablePluginInfo { + log.Info("Processing plugin:", plugin) + mapVersionArtifactList := make(map[string]v1alpha1.ArtifactList) + + // Create version based artifact list + for version, arrOSArch := range pluginInfo.versions { + artifacts := make([]v1alpha1.Artifact, 0) + for _, oa := range arrOSArch { + sourcePath, digest, err := getPluginPathAndDigestFromMetadata(pm.InputArtifactDir, plugin, version, oa.os, oa.arch) + if err != nil { + return err + } + + destPath, err := pm.PublisherInterface.PublishPlugin(sourcePath, version, oa.os, oa.arch, plugin) + if err != nil { + return err + } + + artifacts = append(artifacts, newArtifactObject(oa.os, oa.arch, common.DistributionTypeLocal, digest, destPath)) + } + mapVersionArtifactList[version] = artifacts + } + + // Create new CLIPlugin resource based on plugin and artifact info + cliPlugin := newCLIPluginResource(plugin, pluginInfo.description, pluginInfo.recommendedVersion, mapVersionArtifactList) + + err := saveCLIPluginResource(&cliPlugin, filepath.Join(pm.LocalDiscoveryPath, plugin+".yaml")) + if err != nil { + return errors.Wrap(err, "could not write CLIPlugin to file") + } + } + + return pm.PublisherInterface.PublishDiscovery() +} diff --git a/pkg/v1/cli/command/core/plugin_manager.go b/pkg/v1/cli/command/core/plugin_manager.go index 54b46f31f1..e88c2e0f8d 100644 --- a/pkg/v1/cli/command/core/plugin_manager.go +++ b/pkg/v1/cli/command/core/plugin_manager.go @@ -233,15 +233,10 @@ var installPluginCmd = &cobra.Command{ pluginVersion := version if pluginVersion == cli.VersionLatest { - availablePlugins, err := pluginmanager.AvailablePlugins(server.Name) + pluginVersion, err = pluginmanager.GetRecommendedVersionOfPlugin(server.Name, name) if err != nil { return err } - for i := range availablePlugins { - if availablePlugins[i].Name == name { - pluginVersion = availablePlugins[i].RecommendedVersion - } - } } err = pluginmanager.InstallPlugin(server.Name, name, pluginVersion) @@ -292,16 +287,11 @@ var upgradePluginCmd = &cobra.Command{ if err != nil { return err } - availablePlugins, err := pluginmanager.AvailablePlugins(server.Name) + + pluginVersion, err := pluginmanager.GetRecommendedVersionOfPlugin(server.Name, name) if err != nil { return err } - pluginVersion := "" - for i := range availablePlugins { - if availablePlugins[i].Name == name { - pluginVersion = availablePlugins[i].RecommendedVersion - } - } err = pluginmanager.UpgradePlugin(server.Name, name, pluginVersion) if err != nil { diff --git a/pkg/v1/cli/common/arch.go b/pkg/v1/cli/common/arch.go index f5c702d181..16d3cb312f 100644 --- a/pkg/v1/cli/common/arch.go +++ b/pkg/v1/cli/common/arch.go @@ -8,6 +8,11 @@ import ( "runtime" ) +const ( + // DefaultOSArch defines default OS/ARCH + DefaultOSArch = "darwin-amd64 linux-amd64 windows-amd64" +) + // Arch represents a system architecture. type Arch string diff --git a/pkg/v1/cli/common/constants.go b/pkg/v1/cli/common/constants.go new file mode 100644 index 0000000000..0fa44999bf --- /dev/null +++ b/pkg/v1/cli/common/constants.go @@ -0,0 +1,29 @@ +// Copyright 2021 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package common defines generic constants and structs +package common + +// Plugin status and scope constants +const ( + PluginStatusInstalled = "installed" + PluginStatusNotInstalled = "not installed" + PluginScopeStandalone = "Standalone" + PluginScopeContext = "Context" +) + +// DiscoveryType constants +const ( + DiscoveryTypeOCI = "oci" + DiscoveryTypeLocal = "local" + DiscoveryTypeGCP = "gcp" + DiscoveryTypeKubernetes = "kubernetes" + DiscoveryTypeREST = "rest" +) + +// DistributionType constants +const ( + DistributionTypeOCI = "oci" + DistributionTypeLocal = "local" + DistributionTypeGCP = "gcp" +) diff --git a/pkg/v1/cli/pluginmanager/manager.go b/pkg/v1/cli/pluginmanager/manager.go index de5cb3be7c..51e4d837ac 100644 --- a/pkg/v1/cli/pluginmanager/manager.go +++ b/pkg/v1/cli/pluginmanager/manager.go @@ -33,14 +33,6 @@ const ( exe = ".exe" ) -// Plugin status and scope constants -const ( - PluginStatusInstalled = "installed" - PluginStatusNotInstalled = "not installed" - PluginScopeStandalone = "Stand-Alone" - PluginScopeContext = "Context" -) - var execCommand = exec.Command // ValidatePlugin validates the plugin descriptor. @@ -103,8 +95,8 @@ func DiscoverStandalonePlugins() (plugins []plugin.Discovered, err error) { } for i := range plugins { - plugins[i].Scope = PluginScopeStandalone - plugins[i].Status = PluginStatusNotInstalled + plugins[i].Scope = common.PluginScopeStandalone + plugins[i].Status = common.PluginStatusNotInstalled } return } @@ -126,8 +118,8 @@ func DiscoverServerPlugins(serverName string) (plugins []plugin.Discovered, err return } for i := range plugins { - plugins[i].Scope = PluginScopeContext - plugins[i].Status = PluginStatusNotInstalled + plugins[i].Scope = common.PluginScopeContext + plugins[i].Status = common.PluginStatusNotInstalled } return } @@ -180,7 +172,7 @@ func AvailablePlugins(serverName string) ([]plugin.Discovered, error) { if installedSeverPluginDesc[i].Name == availablePlugins[j].Name && installedSeverPluginDesc[i].Discovery == availablePlugins[j].Source { // Match found, Check for update available and update status - availablePlugins[j].Status = PluginStatusInstalled + availablePlugins[j].Status = common.PluginStatusInstalled } } } @@ -190,7 +182,7 @@ func AvailablePlugins(serverName string) ([]plugin.Discovered, error) { if installedStandalonePluginDesc[i].Name == availablePlugins[j].Name && installedStandalonePluginDesc[i].Discovery == availablePlugins[j].Source { // Match found, Check for update available and update status - availablePlugins[j].Status = PluginStatusInstalled + availablePlugins[j].Status = common.PluginStatusInstalled } } } @@ -262,7 +254,7 @@ func InstallPlugin(serverName, pluginName, version string) error { } for i := range availablePlugins { if availablePlugins[i].Name == pluginName { - if availablePlugins[i].Scope == PluginScopeStandalone { + if availablePlugins[i].Scope == common.PluginScopeStandalone { serverName = "" } return installOrUpgradePlugin(serverName, &availablePlugins[i], version) @@ -280,7 +272,7 @@ func UpgradePlugin(serverName, pluginName, version string) error { } for i := range availablePlugins { if availablePlugins[i].Name == pluginName { - if availablePlugins[i].Scope == PluginScopeStandalone { + if availablePlugins[i].Scope == common.PluginScopeStandalone { serverName = "" } return installOrUpgradePlugin(serverName, &availablePlugins[i], version) @@ -290,6 +282,20 @@ func UpgradePlugin(serverName, pluginName, version string) error { return errors.Errorf("unable to find plugin '%v'", pluginName) } +// GetRecommendedVersionOfPlugin returns recommended version of the plugin +func GetRecommendedVersionOfPlugin(serverName, pluginName string) (string, error) { + availablePlugins, err := AvailablePlugins(serverName) + if err != nil { + return "", err + } + for i := range availablePlugins { + if availablePlugins[i].Name == pluginName { + return availablePlugins[i].RecommendedVersion, nil + } + } + return "", errors.Errorf("unable to find plugin '%v'", pluginName) +} + func installOrUpgradePlugin(serverName string, p *plugin.Discovered, version string) error { b, err := p.Distribution.Fetch(version, runtime.GOOS, runtime.GOARCH) if err != nil { diff --git a/pkg/v1/cli/pluginmanager/manager_test.go b/pkg/v1/cli/pluginmanager/manager_test.go index 79fb79cd61..584a460186 100644 --- a/pkg/v1/cli/pluginmanager/manager_test.go +++ b/pkg/v1/cli/pluginmanager/manager_test.go @@ -100,18 +100,18 @@ func Test_AvailablePlugins(t *testing.T) { assert.Nil(err) assert.Equal(1, len(discovered)) assert.Equal("login", discovered[0].Name) - assert.Equal(PluginScopeStandalone, discovered[0].Scope) - assert.Equal(PluginStatusNotInstalled, discovered[0].Status) + assert.Equal(common.PluginScopeStandalone, discovered[0].Scope) + assert.Equal(common.PluginStatusNotInstalled, discovered[0].Status) discovered, err = AvailablePlugins("mgmt") assert.Nil(err) assert.Equal(2, len(discovered)) assert.Equal("cluster", discovered[0].Name) - assert.Equal(PluginScopeContext, discovered[0].Scope) - assert.Equal(PluginStatusNotInstalled, discovered[0].Status) + assert.Equal(common.PluginScopeContext, discovered[0].Scope) + assert.Equal(common.PluginStatusNotInstalled, discovered[0].Status) assert.Equal("login", discovered[1].Name) - assert.Equal(PluginScopeStandalone, discovered[1].Scope) - assert.Equal(PluginStatusNotInstalled, discovered[1].Status) + assert.Equal(common.PluginScopeStandalone, discovered[1].Scope) + assert.Equal(common.PluginStatusNotInstalled, discovered[1].Status) // Install login, cluster package mockInstallPlugin(assert, "", "login", "v0.2.0") @@ -122,11 +122,11 @@ func Test_AvailablePlugins(t *testing.T) { assert.Nil(err) assert.Equal(2, len(discovered)) assert.Equal("cluster", discovered[0].Name) - assert.Equal(PluginScopeContext, discovered[0].Scope) - assert.Equal(PluginStatusInstalled, discovered[0].Status) + assert.Equal(common.PluginScopeContext, discovered[0].Scope) + assert.Equal(common.PluginStatusInstalled, discovered[0].Status) assert.Equal("login", discovered[1].Name) - assert.Equal(PluginScopeStandalone, discovered[1].Scope) - assert.Equal(PluginStatusInstalled, discovered[1].Status) + assert.Equal(common.PluginScopeStandalone, discovered[1].Scope) + assert.Equal(common.PluginStatusInstalled, discovered[1].Status) } func Test_DescribePlugin(t *testing.T) { From 41fd8fa5b89e81d1910114a2e2f5bd32b76f388d Mon Sep 17 00:00:00 2001 From: Anuj Chaudhari Date: Thu, 28 Oct 2021 07:57:23 -0700 Subject: [PATCH 4/4] Add base documentation for context-aware plugin discovery --- .../context-aware-plugin-discovery-design.md | 44 +++++++++++++++++++ docs/dev/build.md | 8 ++++ 2 files changed, 52 insertions(+) create mode 100644 docs/design/context-aware-plugin-discovery-design.md diff --git a/docs/design/context-aware-plugin-discovery-design.md b/docs/design/context-aware-plugin-discovery-design.md new file mode 100644 index 0000000000..3ddb215421 --- /dev/null +++ b/docs/design/context-aware-plugin-discovery-design.md @@ -0,0 +1,44 @@ +# Context-aware API-driven Plugin Discovery + +## Abstract + +The Tanzu CLI is an amalgamation of all the Tanzu infrastructure elements under one unified core CLI experience. The core CLI supports a plugin model where the developers of different Tanzu services (bundled or SaaS) can distribute plugins that target functionalities of the services they own. When users switch between different services via the CLI context, we want to surface only the relevant plugins for the given context for a crisp user experience. + +## Key Concepts + +- CLI - The Tanzu command line interface, built to be pluggable. +- Service - Any tanzu service, user-managed or SaaS. E.g., TKG, TCE, TMC, etc +- Server - An instance of service. E.g., A single TKG management-cluster, a specific TMC endpoint, etc. +- Context - an isolated scope of relevant client-side configurations for a combination of user identity and server identity. There can be multiple contexts for the same combination of {user, server}. This is currently referred to as `Server` in the Tanzu CLI, which can also mean an instance of a service. Hence, we shall use `Context` to avoid confusion. +- Plugin - A scoped piece of functionality that can be used to extend the CLI. Usually refers to a single binary that is invoked by the root Tanzu CLI. +- Scope - the context association level of a plugin +- Stand-alone - independent of the CLI context +- Context-scoped - scoped to one or more contexts +- Discovery - the interface to fetch the list of available plugins and their supported versions +- Distribution - the interface to deliver a plugin for user download +- Scheme - the specific mechanism to discover or download a plugin +- Discovery Scheme - e.g., REST API, CLIPlugin kubernetes API, manifest YAML +- Distribution Scheme - e.g., OCI image, Google Cloud Storage, S3 +- Discovery Source - the source server of a plugin metadata for discovery, e.g., a REST API server, a management cluster, a local manifest file, OCI compliant image containing manifest file +- Distribution Repository - the repository of plugin binary for distribution, e.g., an OCI compliant image registry, Google Cloud Storage, an S3 compatible object storage server +- Plugin Descriptor - the metadata about a single plugin version that is installed locally and is used by the core to construct a sub-command under `tanzu` + +## Background + +## Goals + +## Non Goals + +## High-Level Design + +## Detailed Design + +## Alternatives Considered + +## Security Considerations + +## Compatibility + +## Implementation + +## Open Issues diff --git a/docs/dev/build.md b/docs/dev/build.md index e208920324..1e3ab91381 100644 --- a/docs/dev/build.md +++ b/docs/dev/build.md @@ -81,3 +81,11 @@ safely. With these powerful APIs the teams can modify the system behavior withou experimentation over the lifecyle of features, these can be incredibly useful for agile management style environments. For more detailed information on these APIs check out this [doc](../api-machinery/features-and-featuregates.md) + +### Context-aware API-driven Plugin Discovery + +The Tanzu CLI is an amalgamation of all the Tanzu infrastructure elements under one unified core CLI experience. The core CLI supports a plugin model where the developers of different Tanzu services (bundled or SaaS) can distribute plugins that target functionalities of the services they own. When users switch between different services via the CLI context, we want to surface only the relevant plugins for the given context for a crisp user experience. + +This feature is gated by `features.global.context-aware-discovery` CLI feature flag and can be turned on/off as described [here](../cli/config-features.md). When this feature is enabled, CLI will not automatically use already installed plugins with context-aware discovery and users will need to install plugins again with `tanzu plugin install` command. + +For more detailed information on these design check out this [doc](../design/context-aware-plugin-discovery.md)