From f1e1f3c235f9b644a1a7ce19d49a9b678183e830 Mon Sep 17 00:00:00 2001 From: Chandra Pamuluri Date: Wed, 4 Oct 2023 12:38:09 -0500 Subject: [PATCH] Sync Single Target Plugins Only This pull request updates existing functionality as follows: 1. For 'tanzu plugin sync', a new optional '--target' parameter has been added. If a user provides a value for '--target', the sync operation is performed only for the specified target, not for all active contexts. 2. 'tanzu context create' now performs 'plugin sync' only for the newly created context, not for all active contexts. 3. 'tanzu context use' now triggers 'plugin sync' only for the context being set as active, not for all currently active contexts. --- pkg/command/context.go | 8 +- pkg/command/plugin.go | 12 +- pkg/pluginmanager/manager.go | 66 ++++++- pkg/pluginmanager/manager_test.go | 81 ++++++++ test/e2e/Makefile | 1 - .../tmc/plugin_sync_tmc_lifecycle_test.go | 178 +++++++++++++++++- 6 files changed, 331 insertions(+), 15 deletions(-) diff --git a/pkg/command/context.go b/pkg/command/context.go index 27c583ab0..a3989efd0 100644 --- a/pkg/command/context.go +++ b/pkg/command/context.go @@ -200,13 +200,13 @@ func createCtx(_ *cobra.Command, args []string) (err error) { } // Sync all required plugins - _ = syncContextPlugins() + _ = syncContextPlugins(ctx.Target) return nil } -func syncContextPlugins() error { - err := pluginmanager.SyncPlugins() +func syncContextPlugins(target configtypes.Target) error { + err := pluginmanager.SyncPluginsForTarget(target) if err != nil { log.Warningf("unable to automatically sync the plugins from target context. Please run 'tanzu plugin sync' command to sync plugins manually, error: '%v'", err.Error()) } @@ -898,7 +898,7 @@ func useCtx(_ *cobra.Command, args []string) error { } // Sync all required plugins - _ = syncContextPlugins() + _ = syncContextPlugins(ctx.Target) return nil } diff --git a/pkg/command/plugin.go b/pkg/command/plugin.go index a95035ac1..82618ab03 100644 --- a/pkg/command/plugin.go +++ b/pkg/command/plugin.go @@ -94,6 +94,8 @@ func newPluginCmd() *cobra.Command { upgradePluginCmd.Flags().StringVarP(&targetStr, "target", "t", "", targetFlagDesc) deletePluginCmd.Flags().StringVarP(&targetStr, "target", "t", "", targetFlagDesc) describePluginCmd.Flags().StringVarP(&targetStr, "target", "t", "", targetFlagDesc) + targetFlagDescForSync := fmt.Sprintf("sync plugins only for specific target (%s)", common.TargetList) + syncPluginCmd.Flags().StringVarP(&targetStr, "target", "t", "", targetFlagDescForSync) installPluginCmd.MarkFlagsMutuallyExclusive("group", "local") installPluginCmd.MarkFlagsMutuallyExclusive("group", "local-source") @@ -382,7 +384,15 @@ func newSyncPluginCmd() *cobra.Command { Long: `Installs all plugins recommended by the active contexts. Plugins installed with this command will only be available while the context remains active.`, RunE: func(cmd *cobra.Command, args []string) (err error) { - err = pluginmanager.SyncPlugins() + if targetStr != "" { + if !configtypes.IsValidTarget(targetStr, false, true) { + return errors.New(invalidTargetMsg) + } + err = pluginmanager.SyncPluginsForTarget(getTarget()) + } else { + err = pluginmanager.SyncPlugins() + } + if err != nil { return err } diff --git a/pkg/pluginmanager/manager.go b/pkg/pluginmanager/manager.go index 9256ceb38..8dc2ae89c 100644 --- a/pkg/pluginmanager/manager.go +++ b/pkg/pluginmanager/manager.go @@ -160,20 +160,27 @@ func GetAdditionalTestPluginDiscoveries() []configtypes.PluginDiscovery { return testDiscoveries } -// DiscoverServerPlugins returns the available plugins associated all the active contexts +// DiscoverServerPlugins returns the available discovered plugins associated with all active contexts func DiscoverServerPlugins() ([]discovery.Discovered, error) { - var plugins []discovery.Discovered - var errList []error - currentContextMap, err := configlib.GetAllCurrentContextsMap() if err != nil { return nil, err } - if len(currentContextMap) == 0 { - return plugins, nil + contexts := make([]*configtypes.Context, 0) + for _, context := range currentContextMap { + contexts = append(contexts, context) } + return DiscoverServerPluginsForGivenContexts(contexts) +} - for _, context := range currentContextMap { +// DiscoverServerPluginsForGivenContexts returns the available discovered plugins associated with specific contexts +func DiscoverServerPluginsForGivenContexts(contexts []*configtypes.Context) ([]discovery.Discovered, error) { + var plugins []discovery.Discovered + var errList []error + if len(contexts) == 0 { + return plugins, nil + } + for _, context := range contexts { var discoverySources []configtypes.PluginDiscovery discoverySources = append(discoverySources, context.DiscoverySources...) discoverySources = append(discoverySources, defaultDiscoverySourceBasedOnContext(context)...) @@ -989,7 +996,50 @@ func SyncPlugins() error { if err != nil { errList = append(errList, err) } - if installedPlugins, err := pluginsupplier.GetInstalledServerPlugins(); err == nil { + err = installDiscoveredContextPlugins(plugins) + if err != nil { + errList = append(errList, err) + } + err = kerrors.NewAggregate(errList) + if err != nil { + return err + } + return nil +} + +// SyncPluginsForTarget installs the plugins for given target +func SyncPluginsForTarget(target configtypes.Target) error { + currentContextMap, err := configlib.GetAllCurrentContextsMap() + if err != nil { + return err + } + ctx, ok := currentContextMap[target] + if !ok { + return fmt.Errorf("there is no active context for the target %v ", target) + } + log.Infof("Checking for required plugins for context %s...", ctx.Name) + errList := make([]error, 0) + plugins, err := DiscoverServerPluginsForGivenContexts([]*configtypes.Context{ctx}) + if err != nil { + errList = append(errList, err) + } + err = installDiscoveredContextPlugins(plugins) + if err != nil { + errList = append(errList, err) + } + err = kerrors.NewAggregate(errList) + if err != nil { + return err + } + return nil +} + +// installDiscoveredContextPlugins installs the given context scope plugins +func installDiscoveredContextPlugins(plugins []discovery.Discovered) error { + var errList []error + var err error + var installedPlugins []cli.PluginInfo + if installedPlugins, err = pluginsupplier.GetInstalledServerPlugins(); err == nil { setAvailablePluginsStatus(plugins, installedPlugins) } diff --git a/pkg/pluginmanager/manager_test.go b/pkg/pluginmanager/manager_test.go index 2cf13a207..f4fd4382a 100644 --- a/pkg/pluginmanager/manager_test.go +++ b/pkg/pluginmanager/manager_test.go @@ -673,6 +673,87 @@ func Test_SyncPlugins(t *testing.T) { } } +// Test_SyncPlugins_ForK8SSpecificTarget tests to sync plugins for k8s specific target only +func Test_SyncPlugins_ForK8SSpecificTarget(t *testing.T) { + assertions := assert.New(t) + + defer setupPluginSourceForTesting()() + execCommand = fakeInfoExecCommand + defer func() { execCommand = exec.Command }() + + // Get the server plugins (they are not installed yet) + serverPlugins, err := DiscoverServerPlugins() + assertions.NotNil(err) + // There is an error for the kubernetes discovery since we don't have a cluster + // but other server plugins will be found, so we use those + assertions.Contains(err.Error(), `Failed to load Kubeconfig file from "config"`) + assertions.Equal(len(expectedDiscoveredContextPlugins), len(serverPlugins)) + var k8sTargetPlugins []*discovery.Discovered + for _, edp := range expectedDiscoveredContextPlugins { + p := findDiscoveredPlugin(serverPlugins, edp.Name, edp.Target) + assertions.NotNil(p) + assertions.Equal(common.PluginStatusNotInstalled, p.Status) + if p.Target == configtypes.TargetK8s { + k8sTargetPlugins = append(k8sTargetPlugins, p) + } + } + + // Sync all available plugins + err = SyncPluginsForTarget(configtypes.TargetK8s) + assertions.NotNil(err) + // There is an error for the kubernetes discovery since we don't have a cluster + // but other server plugins will be found, so we use those + assertions.Contains(err.Error(), `Failed to load Kubeconfig file from "config"`) + + installedServerPlugins, err := pluginsupplier.GetInstalledServerPlugins() + assertions.Nil(err) + assertions.Equal(len(installedServerPlugins), len(k8sTargetPlugins)) + + for _, isp := range installedServerPlugins { + p := findDiscoveredPlugin(serverPlugins, isp.Name, isp.Target) + assertions.NotNil(p) + } +} + +// Test_SyncPlugins_ForTMCSpecificTarget tests to sync plugins for tmc specific target only +func Test_SyncPlugins_ForTMCSpecificTarget(t *testing.T) { + assertions := assert.New(t) + + defer setupPluginSourceForTesting()() + execCommand = fakeInfoExecCommand + defer func() { execCommand = exec.Command }() + + // Get the server plugins (they are not installed yet) + serverPlugins, err := DiscoverServerPlugins() + assertions.NotNil(err) + // There is an error for the kubernetes discovery since we don't have a cluster + // but other server plugins will be found, so we use those + assertions.Contains(err.Error(), `Failed to load Kubeconfig file from "config"`) + assertions.Equal(len(expectedDiscoveredContextPlugins), len(serverPlugins)) + var tmcTargetPlugins []*discovery.Discovered + for _, edp := range expectedDiscoveredContextPlugins { + p := findDiscoveredPlugin(serverPlugins, edp.Name, edp.Target) + assertions.NotNil(p) + assertions.Equal(common.PluginStatusNotInstalled, p.Status) + if p.Target == configtypes.TargetTMC { + tmcTargetPlugins = append(tmcTargetPlugins, p) + } + } + + // Sync all available plugins + err = SyncPluginsForTarget(configtypes.TargetTMC) + assertions.Nil(err) + + installedServerPlugins, err := pluginsupplier.GetInstalledServerPlugins() + assertions.Nil(err) + assertions.Equal(len(installedServerPlugins), len(tmcTargetPlugins)) + + for _, isp := range installedServerPlugins { + p := findDiscoveredPlugin(serverPlugins, isp.Name, isp.Target) + assertions.NotNil(p) + } +} + func Test_setAvailablePluginsStatus(t *testing.T) { assertions := assert.New(t) diff --git a/test/e2e/Makefile b/test/e2e/Makefile index 430c0d034..cdd6443ac 100644 --- a/test/e2e/Makefile +++ b/test/e2e/Makefile @@ -57,7 +57,6 @@ TANZU_CLI_E2E_AIRGAPPED_REPO_WITH_AUTH_PASSWORD = testpassword endif - # Set the plugin group name for the plugins used to execute E2E test cases. E2E_TEST_USE_PLGINS_FROM_PLUGIN_GROUP_FOR_TMC ?= vmware-tmc/tmc-user:v9.9.9 E2E_TEST_USE_PLGINS_FROM_PLUGIN_GROUP_FOR_K8S ?= vmware-tkg/default:v9.9.9 diff --git a/test/e2e/plugin_sync/tmc/plugin_sync_tmc_lifecycle_test.go b/test/e2e/plugin_sync/tmc/plugin_sync_tmc_lifecycle_test.go index 6a97586e8..5ca307ffc 100644 --- a/test/e2e/plugin_sync/tmc/plugin_sync_tmc_lifecycle_test.go +++ b/test/e2e/plugin_sync/tmc/plugin_sync_tmc_lifecycle_test.go @@ -666,7 +666,183 @@ var _ = f.CLICoreDescribe("[Tests:E2E][Feature:Plugin-Sync-TMC-lifecycle]", func }) }) - // Use Case 7: Plugin List, sync, search and install functionalities with Context Issues + // Use case 7: Sync for single target specific plugins, and validate the plugin list + // run context create (make sure another target context is active, but yet to install plugins), it should not perform the sync for all active contexts + // run target specific plugin sync (for k8s target), make sync should not happen for tmc context even though its active + // run target specific plugin sync (for tmc target), make sync should not happen for k8s context even though its active + Context("Use case: create k8s and tmc specific contexts, validate plugins list and perform pluin sync, and perform context switch", func() { + var clusterInfo *f.ClusterInfo + var pluginCRFilePaths []string + var pluginsInfoForCRsApplied, installedPluginsListK8s []*f.PluginInfo + var contextNameK8s string + contexts := make([]string, 0) + totalInstalledPlugins := 1 // telemetry plugin that is part of essentials plugin group will always be installed + var err error + // Test case: a. k8s: create KIND cluster, apply CRD + It("create KIND cluster", func() { + // Create KIND cluster, which is used in test cases to create context's + clusterInfo, err = f.CreateKindCluster(tf, f.ContextPrefixK8s+f.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + }) + // Test case: b. k8s: apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins + It("apply CRD and CRs to KIND cluster", func() { + err = f.ApplyConfigOnKindCluster(tf, clusterInfo, append(make([]string, 0), f.K8SCRDFilePath)) + Expect(err).To(BeNil(), "should not get any error for config apply") + + pluginsToGenerateCRs, ok := pluginGroupToPluginListMap[usePluginsFromK8sPluginGroup] + Expect(ok).To(BeTrue(), "plugin group is not exist in the map") + Expect(len(pluginsToGenerateCRs) > numberOfPluginsToInstall).To(BeTrue(), "we don't have enough plugins in local test central repo") + pluginsInfoForCRsApplied, pluginCRFilePaths, err = f.CreateTemporaryCRsFromPluginInfos(pluginsToGenerateCRs[:numberOfPluginsToInstall]) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + err = f.ApplyConfigOnKindCluster(tf, clusterInfo, pluginCRFilePaths) + Expect(err).To(BeNil(), "should not get any error for config apply") + totalInstalledPlugins += numberOfPluginsToInstall + }) + + // Test case: c. k8s: create context and make sure context has created + It("create context with kubeconfig and context", func() { + By("create context with kubeconfig and context") + contextNameK8s = f.ContextPrefixK8s + f.RandomString(4) + err := tf.ContextCmd.CreateContextWithKubeconfig(contextNameK8s, clusterInfo.KubeConfigPath, clusterInfo.ClusterKubeContext) + Expect(err).To(BeNil(), "context should create without any error") + active, err := tf.ContextCmd.GetActiveContext(string(types.TargetK8s)) + Expect(err).To(BeNil(), "there should be a active context") + Expect(active).To(Equal(contextNameK8s), "the active context should be recently added context") + contexts = append(contexts, contextNameK8s) + }) + // Test case: d. k8s: list plugins and validate plugins info, make sure all plugins are installed for which CRs were present on the cluster + It("Test case: d; list plugins and validate plugins being installed after context being created", func() { + installedPluginsListK8s, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameK8s, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(f.CheckAllPluginsExists(installedPluginsListK8s, pluginsInfoForCRsApplied)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + + var pluginsToGenerateMockResponseTMC, installedPluginsListTMC []*f.PluginInfo + var contextNameTMC string + var ok bool + + // Test case: e. TMC: mock tmc endpoint with plugins info, start the mock server + It("mock tmc endpoint with expected plugins response and restart REST API mock server", func() { + // get plugins from a group + pluginsToGenerateMockResponseTMC, ok = pluginGroupToPluginListMap[usePluginsFromTmcPluginGroup] + Expect(ok).To(BeTrue(), pluginGroupShouldExists) + Expect(len(pluginsToGenerateMockResponseTMC) > numberOfPluginsToInstall).To(BeTrue(), testRepoDoesNotHaveEnoughPlugins) + // mock tmc endpoint with only specific number of plugins info + pluginsToGenerateMockResponseTMC = pluginsToGenerateMockResponseTMC[:numberOfPluginsToInstall] + mockReqResMapping, err := f.ConvertPluginsInfoToTMCEndpointMockResponse(pluginsToGenerateMockResponseTMC[:numberOfPluginsToInstall]) + Expect(err).To(BeNil(), noErrorForMockResponsePreparation) + err = f.WriteToFileInJSONFormat(mockReqResMapping, tmcPluginsMockFilePath) + Expect(err).To(BeNil(), noErrorForMockResponseFileUpdate) + + // start http mock server + err = f.StartMockServer(tf, tmcConfigFolderPath, f.HttpMockServerName) + Expect(err).To(BeNil(), mockServerShouldStartWithoutError) + var mockResPluginsInfo f.TMCPluginsInfo + // check the tmc mocked endpoint is working as expected + err = f.GetHTTPCall(f.TMCPluginsMockServerEndpoint, &mockResPluginsInfo) + Expect(err).To(BeNil(), "there should not be any error for GET http call on mockapi endpoint:"+f.TMCPluginsMockServerEndpoint) + Expect(len(mockResPluginsInfo.Plugins)).Should(Equal(len(pluginsToGenerateMockResponseTMC)), "the number of plugins in endpoint response and initially mocked should be same") + totalInstalledPlugins += numberOfPluginsToInstall + }) + // Test case: f. TMC: create context and make sure context has created + It("create context for TMC target with http mock server URL as endpoint", func() { + // Clean K8s context specific plugins + err = tf.PluginCmd.CleanPlugins() + Expect(err).To(BeNil(), "plugin clean should not return any error") + + contextNameTMC = f.ContextPrefixTMC + f.RandomString(4) + _, _, err = tf.ContextCmd.CreateContextWithEndPointStaging(contextNameTMC, f.TMCMockServerEndpoint, f.AddAdditionalFlagAndValue(forceCSPFlag)) + Expect(err).To(BeNil(), noErrorWhileCreatingContext) + active, err := tf.ContextCmd.GetActiveContext(string(types.TargetTMC)) + Expect(err).To(BeNil(), activeContextShouldExists) + Expect(active).To(Equal(contextNameTMC), activeContextShouldBeRecentlyAddedOne) + contexts = append(contexts, contextNameTMC) + }) + + // Test case: g. TMC: list plugins and validate plugins info, make sure all plugins are installed as per mock response + // there should not be any k8s specific plugins should be installed/sync as part of tmc context creation + It("Test case: g: list plugins and validate plugins being installed after context being created", func() { + installedPluginsListTMC, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameTMC, true) + Expect(err).To(BeNil(), noErrorForPluginList) + Expect(len(installedPluginsListTMC)).Should(Equal(len(pluginsToGenerateMockResponseTMC)), numberOfPluginsSameAsNoOfPluginsInfoMocked) + Expect(f.CheckAllPluginsExists(installedPluginsListTMC, pluginsToGenerateMockResponseTMC)).Should(BeTrue(), pluginsInstalledAndMockedShouldBeSame) + + // Sync should not happen for the k8s context specific plugins + installedPluginsListK8S, err := tf.PluginCmd.ListPluginsForGivenContext(contextNameK8s, true) + Expect(len(installedPluginsListK8S)).Should(Equal(0)) + Expect(err).To(BeNil(), noErrorForPluginList) + }) + + // Test case: i. set both k8s and tmc context as active + // clean plugins + // perform target specific sync (k8s specific) + It("use first context, check plugin list", func() { + err = tf.ContextCmd.UseContext(contextNameK8s) + Expect(err).To(BeNil(), "use context should not return any error") + + err = tf.ContextCmd.UseContext(contextNameTMC) + Expect(err).To(BeNil(), "use context should not return any error") + + err = tf.PluginCmd.CleanPlugins() + Expect(err).To(BeNil(), "plugin clean should not return any error") + + // run target specific sync + _, _, err = tf.PluginCmd.Sync(f.AddAdditionalFlagAndValue("--target k8s")) + Expect(err).To(BeNil(), "there should be an error for plugin sync for k8s context") + // k8s target specific plugins only should be installed + installedPluginsListK8s, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameK8s, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(f.CheckAllPluginsExists(installedPluginsListK8s, pluginsInfoForCRsApplied)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + + // Sync should not happen for the tmc context specific plugins, as its target specific sync + installedPluginsListTMC, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameTMC, true) + Expect(len(installedPluginsListTMC)).Should(Equal(0)) + Expect(err).To(BeNil(), noErrorForPluginList) + }) + + // Test case: set both k8s and tmc context as active + // clean plugins + // perform target specific sync (tmc target) + It("use first context, check plugin list", func() { + err = tf.ContextCmd.UseContext(contextNameK8s) + Expect(err).To(BeNil(), "use context should not return any error") + + err = tf.ContextCmd.UseContext(contextNameTMC) + Expect(err).To(BeNil(), "use context should not return any error") + + err = tf.PluginCmd.CleanPlugins() + Expect(err).To(BeNil(), "plugin clean should not return any error") + + // run target specific sync + _, _, err = tf.PluginCmd.Sync(f.AddAdditionalFlagAndValue("--target tmc")) + Expect(err).To(BeNil(), "there should be an error for plugin sync for k8s context") + // tmc target specific plugins only should be installed + installedPluginsListTMC, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameTMC, true) + Expect(err).To(BeNil(), noErrorForPluginList) + Expect(len(installedPluginsListTMC)).Should(Equal(len(pluginsToGenerateMockResponseTMC)), numberOfPluginsSameAsNoOfPluginsInfoMocked) + Expect(f.CheckAllPluginsExists(installedPluginsListTMC, pluginsToGenerateMockResponseTMC)).Should(BeTrue(), pluginsInstalledAndMockedShouldBeSame) + + // Sync should not happen for the k8s context specific plugins, as its target specific sync + installedPluginsListK8s, err := tf.PluginCmd.ListPluginsForGivenContext(contextNameK8s, true) + Expect(len(installedPluginsListK8s)).Should(Equal(0)) + Expect(err).To(BeNil(), noErrorForPluginList) + }) + + // Test case: l. delete tmc/k8s contexts and the KIND cluster + It("delete tmc/k8s contexts and the KIND cluster", func() { + _, _, err = tf.ContextCmd.DeleteContext(contextNameTMC) + Expect(err).To(BeNil(), "context should be deleted without error") + err = f.StopContainer(tf, f.HttpMockServerName) + Expect(err).To(BeNil(), mockServerShouldStopWithoutError) + + _, _, err = tf.ContextCmd.DeleteContext(contextNameK8s) + Expect(err).To(BeNil(), "context should be deleted without error") + _, err := tf.KindCluster.DeleteCluster(clusterInfo.Name) + Expect(err).To(BeNil(), "kind cluster should be deleted without any error") + }) + }) + + // Use Case 8: Plugin List, sync, search and install functionalities with Context Issues // Use case details: In this use case, we will create one Tanzu Mission Control (TMC) context and one Kubernetes contexts. // The active K8s context will be associated with a kind cluster that has been deleted. As a result, there will be an issue // when attempting to discover plugins for this context. However, despite the issue, the plugin list and plugin sync commands