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