From 804e3f1a4931b64b276507034607ae63673bff74 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 | 14 +- pkg/common/constants.go | 3 +- pkg/pluginmanager/manager.go | 58 +++++- pkg/pluginmanager/manager_test.go | 81 ++++++++ test/e2e/Makefile | 1 - .../tmc/plugin_sync_tmc_lifecycle_test.go | 178 +++++++++++++++++- 7 files changed, 327 insertions(+), 16 deletions(-) diff --git a/pkg/command/context.go b/pkg/command/context.go index 0b97a677c..beb317fe8 100644 --- a/pkg/command/context.go +++ b/pkg/command/context.go @@ -245,13 +245,13 @@ func createCtx(_ *cobra.Command, args []string) (err error) { } // Sync all required plugins - _ = syncContextPlugins() + _ = syncContextPlugins(ctx.ContextType) return nil } -func syncContextPlugins() error { - err := pluginmanager.SyncPlugins() +func syncContextPlugins(contextType configtypes.ContextType) error { + err := pluginmanager.SyncPluginsForContextType(contextType) 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()) } @@ -995,7 +995,7 @@ func useCtx(_ *cobra.Command, args []string) error { } // Sync all required plugins - _ = syncContextPlugins() + _ = syncContextPlugins(ctx.ContextType) return nil } diff --git a/pkg/command/plugin.go b/pkg/command/plugin.go index 5aa14a0f8..c7cfb267e 100644 --- a/pkg/command/plugin.go +++ b/pkg/command/plugin.go @@ -41,6 +41,7 @@ var ( const ( invalidTargetMsg = "invalid target specified. Please specify a correct value for the `--target/-t` flag from '" + common.TargetList + "'" + invalidContextTypeMsg = "invalid contextType specified. Please specify a correct value for the `--contextType/-ct` flag from '" + common.ContextTypeList + "'" errorWhileDiscoveringPlugins = "there was an error while discovering plugins, error information: '%v'" errorWhileGettingContextPlugins = "there was an error while getting installed context plugins, error information: '%v'" pluginNameCaps = "PLUGIN_NAME" @@ -108,6 +109,9 @@ func newPluginCmd() *cobra.Command { describePluginCmd.Flags().StringVarP(&targetStr, "target", "t", "", targetFlagDesc) utils.PanicOnErr(describePluginCmd.RegisterFlagCompletionFunc("target", completeTargets)) + contextTypeFlagDescForSync := fmt.Sprintf("sync plugins only for specific ContextType (%s)", common.ContextTypeList) + syncPluginCmd.Flags().StringVarP(&contextTypeStr, "type", "t", "", contextTypeFlagDescForSync) + installPluginCmd.MarkFlagsMutuallyExclusive("group", "local") installPluginCmd.MarkFlagsMutuallyExclusive("group", "local-source") installPluginCmd.MarkFlagsMutuallyExclusive("group", "version") @@ -402,7 +406,15 @@ func newSyncPluginCmd() *cobra.Command { Plugins installed with this command will only be available while the context remains active.`, ValidArgsFunction: cobra.NoFileCompletions, RunE: func(cmd *cobra.Command, args []string) (err error) { - err = pluginmanager.SyncPlugins() + if contextTypeStr != "" { + if !configtypes.IsValidContextType(contextTypeStr) { + return errors.New(invalidContextTypeMsg) + } + err = pluginmanager.SyncPluginsForContextType(getContextType()) + } else { + err = pluginmanager.SyncPlugins() + } + if err != nil { return err } diff --git a/pkg/common/constants.go b/pkg/common/constants.go index d59a47786..221a42df1 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -30,7 +30,8 @@ const ( // Shared strings const ( - TargetList = "kubernetes[k8s]/mission-control[tmc]/global" + TargetList = "kubernetes[k8s]/mission-control[tmc]/global" + ContextTypeList = "kubernetes[k8s]/mission-control[tmc]/application-engine[tae]" ) // CoreName is the name of the core binary. diff --git a/pkg/pluginmanager/manager.go b/pkg/pluginmanager/manager.go index c562df441..beb3f991a 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.GetAllActiveContextsMap() 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,42 @@ 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) + } + return kerrors.NewAggregate(errList) +} + +// SyncPluginsForContextType installs the plugins for given ContextType +func SyncPluginsForContextType(contextType configtypes.ContextType) error { + currentContextMap, err := configlib.GetAllActiveContextsMap() + if err != nil { + return err + } + ctx, ok := currentContextMap[contextType] + if !ok { + return fmt.Errorf("there is no active context for the contextType %v ", contextType) + } + 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) + } + return kerrors.NewAggregate(errList) +} + +// 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..d691815df 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 ContextType only +func Test_SyncPlugins_ForK8SSpecificContextType(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 k8sContextPlugins []*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 { + k8sContextPlugins = append(k8sContextPlugins, p) + } + } + + // Sync all available plugins + err = SyncPluginsForContextType(configtypes.ContextTypeK8s) + 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(k8sContextPlugins)) + + for _, isp := range installedServerPlugins { + p := findDiscoveredPlugin(serverPlugins, isp.Name, isp.Target) + assertions.NotNil(p) + } +} + +// Test_SyncPlugins_ForTMCSpecificContextType tests to sync plugins for tmc specific ContextType only +func Test_SyncPlugins_ForTMCSpecificContextType(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 = SyncPluginsForContextType(configtypes.ContextTypeTMC) + 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..23ae3f28d 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 contextType 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 contextType specific sync + _, _, err = tf.PluginCmd.Sync(f.AddAdditionalFlagAndValue("--type k8s")) + Expect(err).To(BeNil(), "there should be an error for plugin sync for k8s context") + // k8s contextType 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 contextType 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 contextType specific sync (tmc type) + 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 contextType specific sync + _, _, err = tf.PluginCmd.Sync(f.AddAdditionalFlagAndValue("--type tmc")) + Expect(err).To(BeNil(), "there should be an error for plugin sync for k8s context") + // tmc contextType 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 contextType 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