From 8b20bb9865d9eb2032445d2cd1997c07d41e2b79 Mon Sep 17 00:00:00 2001 From: Chandra Pamuluri Date: Thu, 9 Mar 2023 21:42:05 -0600 Subject: [PATCH] E2E: plugin group life cycle e2e test cases and e2e framework tooling implementation --- Makefile | 3 +- test/e2e/config/config_server_test.go | 19 +- test/e2e/config/config_suite_test.go | 6 +- .../e2e/context/context_k8s_lifecycle_test.go | 29 +- test/e2e/context/context_lifecycle_helper.go | 25 -- test/e2e/context/context_suite_test.go | 7 +- .../context/tmc/context_tmc_lifecycle_test.go | 23 +- test/e2e/framework/cluster_interface.go | 12 +- test/e2e/framework/cluster_kind.go | 13 + .../cli.tanzu.vmware.com_cliplugins.yaml | 105 +++++ .../framework/context_lifecycle_operations.go | 2 +- test/e2e/framework/framework.go | 54 ++- test/e2e/framework/framework_helper.go | 186 ++++++++- test/e2e/framework/output_handling.go | 1 + .../framework/plugin_lifecycle_operations.go | 64 ++- test/e2e/framework/templates.go | 10 + .../plugin_group_lifecycle_test.go | 4 +- .../plugin_lifecycle_helper.go | 93 +---- .../plugin_lifecycle_suite_test.go | 12 +- .../plugin_lifecycle/plugin_lifecycle_test.go | 10 +- .../plugin_sync_lifecycle_suite_test.go | 80 ++++ .../plugin_sync/plugin_sync_lifecycle_test.go | 372 ++++++++++++++++++ 22 files changed, 944 insertions(+), 186 deletions(-) create mode 100644 test/e2e/framework/config/cli.tanzu.vmware.com_cliplugins.yaml create mode 100644 test/e2e/plugin_sync/plugin_sync_lifecycle_suite_test.go create mode 100644 test/e2e/plugin_sync/plugin_sync_lifecycle_test.go diff --git a/Makefile b/Makefile index a07d2cd23..a5ae5bf8f 100644 --- a/Makefile +++ b/Makefile @@ -190,7 +190,7 @@ test: fmt ## Run Tests .PHONY: e2e-cli-core ## Execute all CLI Core E2E Tests e2e-cli-core: e2e-cli-plugin-compatibility-test e2e-cli-tmc-test e2e-cli-plugin-lifecycle-test export TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER="Yes" ; \ - ${GO} test `go list ./test/e2e/... | grep -v test/e2e/context/tmc | grep -v test/e2e/plugins_compatibility | grep -v test/e2e/plugin_lifecycle` -timeout 60m -race -coverprofile coverage.txt ${GOTEST_VERBOSE} + ${GO} test `go list ./test/e2e/... | grep -v test/e2e/context/tmc | grep -v test/e2e/plugins_compatibility | grep -v test/e2e/plugin_lifecycle | grep -v test/e2e/plugin_sync` -timeout 60m -race -coverprofile coverage.txt ${GOTEST_VERBOSE} .PHONY: e2e-cli-plugin-compatibility-test ## Execute CLI Core Plugin Compatibility E2E test cases e2e-cli-plugin-compatibility-test: @@ -213,6 +213,7 @@ e2e-cli-plugin-lifecycle-test: export TANZU_CLI_PLUGIN_DISCOVERY_IMAGE_SIGNATURE_VERIFICATION_SKIP_LIST=$(TANZU_CLI_E2E_TEST_LOCAL_CENTRAL_REPO_URL) ; \ export TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER="Yes" ; \ ${GO} test ./test/e2e/plugin_lifecycle -timeout 60m -race -coverprofile coverage.txt ${GOTEST_VERBOSE} ; \ + ${GO} test ./test/e2e/plugin_sync -timeout 60m -race -coverprofile coverage.txt ${GOTEST_VERBOSE} ; \ fi .PHONY: e2e-cli-tmc-test ## Execute CLI Core TMC Specific E2E test cases diff --git a/test/e2e/config/config_server_test.go b/test/e2e/config/config_server_test.go index 78a27a60d..4e78f7f5c 100644 --- a/test/e2e/config/config_server_test.go +++ b/test/e2e/config/config_server_test.go @@ -19,6 +19,17 @@ const ContextNameConfigPrefix = "config-k8s-" // tests the 'tanzu config server list' and 'tanzu config server delete' commands var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Config-Server]", func() { Context("tanzu config server command test cases ", func() { + // Test case: delete config files and initialize config + It("should initialize configuration successfully", func() { + // delete config files + err := tf.Config.DeleteCLIConfigurationFiles() + Expect(err).To(BeNil()) + // call init + err = tf.Config.ConfigInit() + Expect(err).To(BeNil()) + // should create config files + Expect(tf.Config.IsCLIConfigurationFilesExists()).To(BeTrue()) + }) // Test case: delete servers if any exists, with command 'tanzu config server delete' It("list and delete servers if any exists before running tests", func() { By("delete servers if any exists before running tests") @@ -36,18 +47,18 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Config-Server]", func() { It("create context with kubeconfig and context", func() { By("create context with kubeconfig and context") ctxName := ContextNameConfigPrefix + framework.RandomString(4) - err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, clusterInfo.KubeConfigPath, clusterInfo.ClusterContext) + err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, clusterInfo.KubeConfigPath, clusterInfo.ClusterKubeContext) Expect(err).To(BeNil(), "context should create without any error") - Expect(context.IsContextExists(tf, ctxName)).To(BeTrue(), "context should be available") + Expect(framework.IsContextExists(tf, ctxName)).To(BeTrue(), "context should be available") contextNames = append(contextNames, ctxName) }) // Test case: Create context for k8s target with "default" kubeconfig and its context only as input value It("create context with default kubeconfig and context", func() { By("create context with default kubeconfig and context") ctxName := "context-defaultConfig-" + framework.RandomString(4) - err := tf.ContextCmd.CreateContextWithDefaultKubeconfig(ctxName, clusterInfo.ClusterContext) + err := tf.ContextCmd.CreateContextWithDefaultKubeconfig(ctxName, clusterInfo.ClusterKubeContext) Expect(err).To(BeNil(), "context should create without any error") - Expect(context.IsContextExists(tf, ctxName)).To(BeTrue(), "context should be available") + Expect(framework.IsContextExists(tf, ctxName)).To(BeTrue(), "context should be available") contextNames = append(contextNames, ctxName) }) // Test case: test 'tanzu config server list' command, should list all contexts created as servers diff --git a/test/e2e/config/config_suite_test.go b/test/e2e/config/config_suite_test.go index 21842bece..73ca2bbea 100644 --- a/test/e2e/config/config_suite_test.go +++ b/test/e2e/config/config_suite_test.go @@ -10,7 +10,6 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/vmware-tanzu/tanzu-cli/test/e2e/context" "github.com/vmware-tanzu/tanzu-cli/test/e2e/framework" ) @@ -23,6 +22,7 @@ var ( tf *framework.Framework clusterInfo *framework.ClusterInfo contextNames []string + err error ) // BeforeSuite creates KIND cluster needed to test 'tanzu config server' use cases @@ -30,7 +30,9 @@ var ( var _ = BeforeSuite(func() { tf = framework.NewFramework() // Create KIND cluster, which is used in test cases to create server's/context's - clusterInfo = context.CreateKindCluster(tf, "config-e2e-"+framework.RandomNumber(4)) + clusterInfo, err = framework.CreateKindCluster(tf, "config-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + contextNames = make([]string, 0) }) diff --git a/test/e2e/context/context_k8s_lifecycle_test.go b/test/e2e/context/context_k8s_lifecycle_test.go index 52e499401..9707f0794 100644 --- a/test/e2e/context/context_k8s_lifecycle_test.go +++ b/test/e2e/context/context_k8s_lifecycle_test.go @@ -20,6 +20,17 @@ const ContextNameConfigPrefix = "context-config-k8s-" // Test suite tests the context life cycle use cases for the k8s target var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-k8s]", func() { Context("Context lifecycle tests for k8s target", func() { + // Test case: delete config files and initialize config + It("should initialize configuration successfully", func() { + // delete config files + err := tf.Config.DeleteCLIConfigurationFiles() + Expect(err).To(BeNil()) + // call init + err = tf.Config.ConfigInit() + Expect(err).To(BeNil()) + // should create config files + Expect(tf.Config.IsCLIConfigurationFilesExists()).To(BeTrue()) + }) // Test case: list and delete context's if any exists already, before running test cases. It("list and delete contexts if any exists already", func() { By("list and delete contexts if any exists already before running test cases") @@ -28,7 +39,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-k8s]", for _, ctx := range list { err := tf.ContextCmd.DeleteContext(ctx.Name) Expect(err).To(BeNil(), "delete context should delete context without any error") - Expect(IsContextExists(tf, ctx.Name)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctx.Name)) + Expect(framework.IsContextExists(tf, ctx.Name)).To(BeFalse(), fmt.Sprintf(framework.ContextShouldNotExists, ctx.Name)) } list, err = tf.ContextCmd.ListContext() Expect(err).To(BeNil(), "list context should not return any error") @@ -38,19 +49,19 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-k8s]", It("create context with kubeconfig and context", func() { By("create context with kubeconfig and context") ctxName := ContextNameConfigPrefix + framework.RandomString(4) - err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, clusterInfo.KubeConfigPath, clusterInfo.ClusterContext) + err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, clusterInfo.KubeConfigPath, clusterInfo.ClusterKubeContext) Expect(err).To(BeNil(), "context should create without any error") - Expect(IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(ContextShouldExistsAsCreated, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(framework.ContextShouldExistsAsCreated, ctxName)) contextNames = append(contextNames, ctxName) }) // Test case: (negative test) Create context for k8s target with incorrect kubeconfig file path and its context as input It("create context with incorrect kubeconfig and context", func() { By("create context with incorrect kubeconfig and context") ctxName := ContextNameConfigPrefix + framework.RandomString(4) - err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, framework.RandomString(4), clusterInfo.ClusterContext) + err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, framework.RandomString(4), clusterInfo.ClusterKubeContext) Expect(err).ToNot(BeNil()) Expect(strings.Contains(err.Error(), framework.FailedToCreateContext)).To(BeTrue()) - Expect(IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(framework.ContextShouldNotExists, ctxName)) }) // Test case: (negative test) Create context for k8s target with kubeconfig file path and incorrect context as input It("create context with kubeconfig and incorrect context", func() { @@ -59,15 +70,15 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-k8s]", err := tf.ContextCmd.CreateContextWithKubeconfig(ctxName, clusterInfo.KubeConfigPath, framework.RandomString(4)) Expect(err).ToNot(BeNil()) Expect(strings.Contains(err.Error(), framework.FailedToCreateContext)).To(BeTrue()) - Expect(IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(framework.ContextShouldNotExists, ctxName)) }) // Test case: Create context for k8s target with "default" kubeconfig and its context only as input value It("create context with kubeconfig and context", func() { By("create context with kubeconfig and context") ctxName := "context-defaultConfig-" + framework.RandomString(4) - err := tf.ContextCmd.CreateContextWithDefaultKubeconfig(ctxName, clusterInfo.ClusterContext) + err := tf.ContextCmd.CreateContextWithDefaultKubeconfig(ctxName, clusterInfo.ClusterKubeContext) Expect(err).To(BeNil(), "context should create without any error") - Expect(IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(ContextShouldExistsAsCreated, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(framework.ContextShouldExistsAsCreated, ctxName)) contextNames = append(contextNames, ctxName) active, err := tf.ContextCmd.GetActiveContext(string(types.TargetK8s)) Expect(err).To(BeNil(), "there should be a active context") @@ -98,7 +109,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-k8s]", By("delete all contexts created in previous tests") for _, ctx := range contextNames { err := tf.ContextCmd.DeleteContext(ctx) - Expect(IsContextExists(tf, ctx)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctx)) + Expect(framework.IsContextExists(tf, ctx)).To(BeFalse(), fmt.Sprintf(framework.ContextShouldNotExists, ctx)) Expect(err).To(BeNil(), "delete context should delete context without any error") } list := GetAvailableContexts(tf, contextNames) diff --git a/test/e2e/context/context_lifecycle_helper.go b/test/e2e/context/context_lifecycle_helper.go index d714ec81b..8d4c0f0e4 100644 --- a/test/e2e/context/context_lifecycle_helper.go +++ b/test/e2e/context/context_lifecycle_helper.go @@ -12,19 +12,6 @@ import ( "github.com/vmware-tanzu/tanzu-cli/test/e2e/framework" ) -// CreateKindCluster create the k8s KIND cluster in the local Docker environment -func CreateKindCluster(tf *framework.Framework, name string) *framework.ClusterInfo { - ci := &framework.ClusterInfo{Name: name} - _, err := tf.KindCluster.CreateCluster(name) - gomega.Expect(err).To(gomega.BeNil(), "the kind cluster creation should be successful without any error") - endpoint, err := tf.KindCluster.GetClusterEndpoint(name) - gomega.Expect(err).To(gomega.BeNil(), "we need cluster endpoint") - ci.EndPoint = endpoint - ci.ClusterContext = tf.KindCluster.GetClusterContext(name) - ci.KubeConfigPath = tf.KindCluster.GetKubeconfigPath() - return ci -} - // GetTMCClusterInfo returns the TMC cluster info by reading environment variables TANZU_CLI_TMC_UNSTABLE_URL and TANZU_API_TOKEN func GetTMCClusterInfo() *framework.ClusterInfo { return &framework.ClusterInfo{EndPoint: os.Getenv(framework.TanzuCliTmcUnstableURL), APIKey: os.Getenv(framework.TanzuAPIToken)} @@ -44,18 +31,6 @@ func GetAvailableContexts(tf *framework.Framework, contextNames []string) []stri return available } -// IsContextExists checks the given context is exists in the config file by listing the existing contexts in the config file -func IsContextExists(tf *framework.Framework, contextName string) bool { - list, err := tf.ContextCmd.ListContext() - gomega.Expect(err).To(gomega.BeNil(), "list context should not return any error") - for _, context := range list { - if context.Name == contextName { - return true - } - } - return false -} - // GetAvailableServers takes list of servers and returns which are available in the 'tanzu config server list' command func GetAvailableServers(tf *framework.Framework, serverNames []string) []string { var available []string diff --git a/test/e2e/context/context_suite_test.go b/test/e2e/context/context_suite_test.go index 7bb1fac56..071c34bb0 100644 --- a/test/e2e/context/context_suite_test.go +++ b/test/e2e/context/context_suite_test.go @@ -16,20 +16,19 @@ func TestContext(t *testing.T) { RunSpecs(t, "Context-K8S Suite") } -const ContextShouldNotExists = "the context %s should not exists" -const ContextShouldExistsAsCreated = "the context %s should exists as its been created" - var ( tf *framework.Framework clusterInfo *framework.ClusterInfo contextNames []string + err error ) // BeforeSuite created KIND cluster var _ = BeforeSuite(func() { tf = framework.NewFramework() // Create KIND cluster, which is used in test cases to create context's - clusterInfo = CreateKindCluster(tf, "context-e2e-"+framework.RandomNumber(4)) + clusterInfo, err = framework.CreateKindCluster(tf, "context-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") contextNames = make([]string, 0) }) diff --git a/test/e2e/context/tmc/context_tmc_lifecycle_test.go b/test/e2e/context/tmc/context_tmc_lifecycle_test.go index 937260673..b67b35fa1 100644 --- a/test/e2e/context/tmc/context_tmc_lifecycle_test.go +++ b/test/e2e/context/tmc/context_tmc_lifecycle_test.go @@ -37,13 +37,24 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-tmc]", contextNames = make([]string, 0) }) Context("Context lifecycle tests for TMC target", func() { + // Test case: delete config files and initialize config + It("should initialize configuration successfully", func() { + // delete config files + err := tf.Config.DeleteCLIConfigurationFiles() + Expect(err).To(BeNil()) + // call init + err = tf.Config.ConfigInit() + Expect(err).To(BeNil()) + // should create config files + Expect(tf.Config.IsCLIConfigurationFilesExists()).To(BeTrue()) + }) // Test case: list and delete context's if any exists It("delete context command", func() { list, err := tf.ContextCmd.ListContext() Expect(err).To(BeNil(), "list context should not return any error") for _, ctx := range list { err := tf.ContextCmd.DeleteContext(ctx.Name) - Expect(context.IsContextExists(tf, ctx.Name)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctx.Name)) + Expect(framework.IsContextExists(tf, ctx.Name)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctx.Name)) Expect(err).To(BeNil(), "delete context should delete context without any error") } list, err = tf.ContextCmd.ListContext() @@ -55,7 +66,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-tmc]", ctxName := ContextNamePrefix + framework.RandomString(4) err := tf.ContextCmd.CreateContextWithEndPointStaging(ctxName, clusterInfo.EndPoint) Expect(err).To(BeNil(), "context should create without any error") - Expect(context.IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(ContextShouldExistsAsCreated, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(ContextShouldExistsAsCreated, ctxName)) contextNames = append(contextNames, ctxName) }) // Test case: (negative test) Create context for TMC target with TMC cluster "incorrect" URL as endpoint @@ -64,7 +75,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-tmc]", err := tf.ContextCmd.CreateContextWithEndPointStaging(ctxName, framework.RandomString(4)) Expect(err).ToNot(BeNil()) Expect(strings.Contains(err.Error(), framework.FailedToCreateContext)).To(BeTrue()) - Expect(context.IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctxName)) }) // Test case: (negative test) Create context for TMC target with TMC cluster URL as endpoint when api token set as incorrect It("create tmc context with endpoint and with incorrect api token", func() { @@ -74,14 +85,14 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-tmc]", os.Setenv(framework.TanzuAPIToken, clusterInfo.APIKey) Expect(err).ToNot(BeNil()) Expect(strings.Contains(err.Error(), framework.FailedToCreateContext)).To(BeTrue()) - Expect(context.IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists, ctxName)) }) // Test case: Create context for TMC target with TMC cluster URL as endpoint, and validate the active context, should be recently create context It("create tmc context with endpoint and check active context", func() { ctxName := ContextNamePrefix + framework.RandomString(4) err := tf.ContextCmd.CreateContextWithEndPointStaging(ctxName, clusterInfo.EndPoint) Expect(err).To(BeNil(), "context should create without any error") - Expect(context.IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(ContextShouldExistsAsCreated, ctxName)) + Expect(framework.IsContextExists(tf, ctxName)).To(BeTrue(), fmt.Sprintf(ContextShouldExistsAsCreated, ctxName)) contextNames = append(contextNames, ctxName) active, err := tf.ContextCmd.GetActiveContext(string(types.TargetTMC)) Expect(err).To(BeNil(), "there should be a active context") @@ -109,7 +120,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Context-lifecycle-tmc]", It("delete context command", func() { for _, ctx := range contextNames { err := tf.ContextCmd.DeleteContext(ctx) - Expect(context.IsContextExists(tf, ctx)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists+" as been deleted", ctx)) + Expect(framework.IsContextExists(tf, ctx)).To(BeFalse(), fmt.Sprintf(ContextShouldNotExists+" as been deleted", ctx)) Expect(err).To(BeNil(), "delete context should delete context without any error") } list := context.GetAvailableContexts(tf, contextNames) diff --git a/test/e2e/framework/cluster_interface.go b/test/e2e/framework/cluster_interface.go index cabf94c77..d4472e6a7 100644 --- a/test/e2e/framework/cluster_interface.go +++ b/test/e2e/framework/cluster_interface.go @@ -17,13 +17,15 @@ type ClusterOps interface { GetClusterContext(clusterName string) string // GetKubeconfigPath returns the default kubeconfig path GetKubeconfigPath() string + // ApplyConfig applies the given configFilePath on to the given contextName cluster context + ApplyConfig(contextName, configFilePath string) error } // ClusterInfo holds the general cluster details type ClusterInfo struct { - Name string - ClusterContext string - EndPoint string - KubeConfigPath string - APIKey string + Name string + ClusterKubeContext string + EndPoint string + KubeConfigPath string + APIKey string } diff --git a/test/e2e/framework/cluster_kind.go b/test/e2e/framework/cluster_kind.go index f94feb659..37dca499e 100644 --- a/test/e2e/framework/cluster_kind.go +++ b/test/e2e/framework/cluster_kind.go @@ -9,6 +9,8 @@ import ( "github.com/pkg/errors" + "github.com/vmware-tanzu/tanzu-plugin-runtime/log" + "gopkg.in/yaml.v3" configapi "k8s.io/client-go/tools/clientcmd/api/v1" ) @@ -76,6 +78,17 @@ func (kc *kindCluster) ClusterStatus(kindClusterName string) (output string, err return stdOutBuffer.String(), err } +func (kc *kindCluster) ApplyConfig(contextName, configFilePath string) error { + applyCmd := fmt.Sprintf(KubectlApply, contextName, configFilePath) + stdOut, stdErr, err := kc.CmdOps.Exec(applyCmd) + if err != nil { + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, applyCmd, err.Error(), stdErr.String(), stdOut.String()) + return err + } + log.Infof("the config:%s applied successfully to context:%s", configFilePath, contextName) + return err +} + // GetClusterEndpoint returns given kind cluster control plane endpoint func (kc *kindCluster) GetClusterEndpoint(kindClusterName string) (endpoint string, err error) { stdOut, err := kc.ContainerRuntimeStatus() diff --git a/test/e2e/framework/config/cli.tanzu.vmware.com_cliplugins.yaml b/test/e2e/framework/config/cli.tanzu.vmware.com_cliplugins.yaml new file mode 100644 index 000000000..bb321fed0 --- /dev/null +++ b/test/e2e/framework/config/cli.tanzu.vmware.com_cliplugins.yaml @@ -0,0 +1,105 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: cliplugins.cli.tanzu.vmware.com +spec: + group: cli.tanzu.vmware.com + names: + kind: CLIPlugin + listKind: CLIPluginList + plural: cliplugins + singular: cliplugin + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: CLIPlugin denotes a Tanzu cli plugin. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: CLIPluginSpec defines the desired state of CLIPlugin. + properties: + artifacts: + additionalProperties: + description: ArtifactList contains an Artifact object for every + supported platform of a version. + items: + description: Artifact points to an individual plugin binary specific + to a version and platform. + properties: + arch: + description: Arch is CPU architecture of the plugin binary + in `GOARCH` format. + type: string + digest: + description: SHA256 hash of the plugin binary. + type: string + image: + description: Image is a fully qualified OCI image for the + plugin binary. + type: string + os: + description: OS of the plugin binary in `GOOS` format. + type: string + type: + description: Type of the binary artifact. Valid values are + S3, GCP, OCIImage. + type: string + uri: + description: AssetURI is a URI of the plugin binary. This + can be a fully qualified HTTP path or a local path. + type: string + required: + - arch + - os + - type + type: object + type: array + description: Artifacts contains an artifact list for every supported + version. + type: object + description: + description: Description is the plugin's description. + type: string + optional: + description: Optional specifies whether the plugin is mandatory or + optional If optional, the plugin will not get auto-downloaded as + part of `tanzu login` or `tanzu plugin sync` command To view the + list of plugin, user can use `tanzu plugin list` and to download + a specific plugin run, `tanzu plugin install ` + type: boolean + recommendedVersion: + description: Recommended version that Tanzu CLI should use if available. + The value should be a valid semantic version as defined in https://semver.org/. + E.g., 2.0.1 + type: string + target: + description: Target specifies the target of the plugin. Only needed + for standalone plugins + type: string + required: + - description + - recommendedVersion + type: object + required: + - metadata + - spec + type: object + served: true + storage: true diff --git a/test/e2e/framework/context_lifecycle_operations.go b/test/e2e/framework/context_lifecycle_operations.go index ec41b6e1c..928836854 100644 --- a/test/e2e/framework/context_lifecycle_operations.go +++ b/test/e2e/framework/context_lifecycle_operations.go @@ -90,6 +90,6 @@ func (cc *contextCmdOps) DeleteContext(contextName string) error { log.Infof("failed to delete context:%s", contextName) return errors.Wrapf(err, FailedToDeleteContext+", stderr:%s stdout:%s , ", stdErr.String(), stdOut.String()) } - log.Infof(ContextCreated, contextName) + log.Infof(ContextDeleted, contextName) return err } diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 644bd674c..6407e7d35 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -39,6 +39,7 @@ const ( DescribePluginCmd = "tanzu plugin describe %s" UninstallPLuginCmd = "tanzu plugin delete %s --yes" CleanPluginsCmd = "tanzu plugin clean" + pluginSyncCmd = "tanzu plugin sync" JSONOutput = " -o json" TestPluginsPrefix = "test-plugin-" PluginSubCommand = "tanzu %s" @@ -50,8 +51,9 @@ const ( TanzuCliE2ETestLocalCentralRepositoryURL = "TANZU_CLI_E2E_TEST_LOCAL_CENTRAL_REPO_URL" // General constants - True = "true" - Installed = "installed" + True = "true" + Installed = "installed" + NotInstalled = "not installed" // Context commands CreateContextWithEndPoint = "tanzu context create --endpoint %s --name %s" @@ -65,23 +67,29 @@ const ( TanzuAPIToken = "TANZU_API_TOKEN" //nolint:gosec TanzuCliTmcUnstableURL = "TANZU_CLI_TMC_UNSTABLE_URL" + // context logs + ContextShouldNotExists = "the context %s should not exists" + ContextShouldExistsAsCreated = "the context %s should exists as its been created" + KindClusterCreate = "kind create cluster --name %s" KindClusterStatus = "kubectl cluster-info --context %s" KindClusterDelete = "kind delete cluster --name %s" KindClusterGet = "kind get clusters " KindClusterInfo = "kubectl cluster-info --context %s" + KubectlApply = "kubectl --context %s apply -f %s" + + // specific plugin custom resource file name cr___.yaml to apply on kind cluster + PluginCRFileName = "cr_%s_%s_%s.yaml" KindCreateCluster = "kind create cluster --name " DockerInfo = "docker info" StartDockerUbuntu = "sudo systemctl start docker" StopDockerUbuntu = "sudo systemctl stop docker" - TMC = "tmc" - TKG = "tkg" - TestDir = ".tanzu-cli-e2e" - TestPluginsDir = ".e2e-test-plugins" - SourceType = "oci" - GlobalTarget = "global" + TMC = "tmc" + TKG = "tkg" + SourceType = "oci" + GlobalTarget = "global" // log info ExecutingCommand = "Executing command: %s" @@ -93,7 +101,7 @@ const ( InvalidTargetGlobal = "invalid target for plugin: global" UnknownDiscoverySourceType = "unknown discovery source type" DiscoverySourceNotFound = "cli discovery source not found" - ErrorLogForCommandWithErrAndStdErr = "error while executing command:'%s', error:'%s' stdErr:'%s'" + ErrorLogForCommandWithErrStdErrAndStdOut = "error while executing command:'%s', error:'%s' stdErr:'%s' stdOut: '%s'" FailedToConstructJSONNodeFromOutputAndErrInfo = "failed to construct json node from output:'%s' error:'%s' " FailedToConstructJSONNodeFromOutput = "failed to construct json node from output:'%s'" @@ -106,10 +114,23 @@ const ( FailedToDeleteContext = "failed to delete context" ) +// TestDir is the directory under $HOME, created during framework initialization, and the $HOME updated as $HOME/$TestDir, to create all Tanzu CLI specific files +// and not to disturb any existing Tanzu CLI files +const TestDir = ".tanzu-cli-e2e" + +// TestPluginsDir is the directory under $HOME/$TestDir, to store test plugins for E2E tests +const TestPluginsDir = ".e2e-test-plugins" + +// TempDirInTestDirPath is the directory under $HOME/$TestDir, to create temporary files (if any) for E2E test execution +const TempDirInTestDirPath = "temp" + var ( + // TestDirPath is the absolute directory path for the E2E test execution uses to create all Tanzu CLI specific files (config, local plugins etc) TestDirPath string TestPluginsDirPath string TestStandalonePluginsPath string + // FullPathForTempDir is the absolute path for the temp directory under $TestDir + FullPathForTempDir string ) // PluginsForLifeCycleTests is list of plugins (which are published in local central repo) used in plugin life cycle test cases @@ -118,6 +139,11 @@ var PluginsForLifeCycleTests []*PluginInfo // PluginGroupsForLifeCycleTests is list of plugin groups (which are published in local central repo) used in plugin group life cycle test cases var PluginGroupsForLifeCycleTests []*PluginGroup +// PluginGroupsLatestToOldVersions is plugin group names mapping latest version to old version, of same target, +// we are mapping because the 'tanzu plugin search' is not displaying plugins for old versions, showing only latest version of plugins +// we need this mapping for plugin sync test cases, we want to install same plugins but for different versions +var PluginGroupsLatestToOldVersions map[string]string + // CLICoreDescribe annotates the test with the CLICore label. func CLICoreDescribe(text string, body func()) bool { return ginkgo.Describe(CliCore+text, body) @@ -147,15 +173,23 @@ func NewFramework() *Framework { func init() { homeDir := GetHomeDir() TestDirPath = filepath.Join(homeDir, TestDir) + FullPathForTempDir = filepath.Join(TestDirPath, TempDirInTestDirPath) + // Update $HOME as $HOME/.tanzu-cli-e2e os.Setenv("HOME", TestDirPath) TestPluginsDirPath = filepath.Join(TestDirPath, TestPluginsDir) + // Create a directory (if not exists) $HOME/.tanzu-cli-e2e/.config/tanzu-plugins/discovery/standalone TestStandalonePluginsPath = filepath.Join(filepath.Join(filepath.Join(filepath.Join(TestDirPath, ".config"), "tanzu-plugins"), "discovery"), "standalone") _ = CreateDir(TestStandalonePluginsPath) + // Create a directory (if not exists) $HOME/.tanzu-cli-e2e/test + _ = CreateDir(FullPathForTempDir) // TODO:cpamuluri: need to move plugins info to configuration file with positive and negative use cases - github issue: https://github.com/vmware-tanzu/tanzu-cli/issues/122 PluginsForLifeCycleTests = make([]*PluginInfo, 3) PluginsForLifeCycleTests = []*PluginInfo{{Name: "cluster", Target: "kubernetes", Version: "v9.9.9", Description: "cluster functionality"}, {Name: "cluster", Target: "mission-control", Version: "v9.9.9", Description: "cluster functionality"}, {Name: "pinniped-auth", Target: "global", Version: "v9.9.9", Description: "pinniped-auth functionality"}} // TODO:cpamuluri: need to move Plugin Groups to configuration file with positive and negative use cases - github issue: https://github.com/vmware-tanzu/tanzu-cli/issues/122 PluginGroupsForLifeCycleTests = make([]*PluginGroup, 2) - PluginGroupsForLifeCycleTests = []*PluginGroup{{Group: "vmware-tmc/v9.9.9"}, {Group: "vmware-tkg/v9.9.9"}} + PluginGroupsForLifeCycleTests = []*PluginGroup{{Group: "vmware-tkg/v9.9.9"}, {Group: "vmware-tmc/v9.9.9"}} + PluginGroupsLatestToOldVersions = make(map[string]string) + PluginGroupsLatestToOldVersions["vmware-tmc/v9.9.9"] = "vmware-tmc/v0.0.1" + PluginGroupsLatestToOldVersions["vmware-tkg/v9.9.9"] = "vmware-tkg/v0.0.1" } diff --git a/test/e2e/framework/framework_helper.go b/test/e2e/framework/framework_helper.go index 88f28155d..15f5c9a37 100644 --- a/test/e2e/framework/framework_helper.go +++ b/test/e2e/framework/framework_helper.go @@ -10,10 +10,12 @@ import ( "fmt" "math/big" "os" + "path/filepath" + "github.com/onsi/gomega" "github.com/pkg/errors" - configapi "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" + "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" "github.com/vmware-tanzu/tanzu-plugin-runtime/log" ) @@ -41,11 +43,16 @@ func PluginListToSet(pluginsToInstall []*PluginInfo) map[string]struct{} { func PluginListToMap(pluginsList []*PluginInfo) map[string]*PluginInfo { m := make(map[string]*PluginInfo) for i := range pluginsList { - m[fmt.Sprintf(PluginKey, (pluginsList)[i].Name, (pluginsList)[i].Target, (pluginsList)[i].Version)] = pluginsList[i] + m[GetMapKeyForPlugin((pluginsList)[i])] = pluginsList[i] } return m } +// GetMapKeyForPlugin takes the plugin and returns the map key for the plugin +func GetMapKeyForPlugin(pluginsList *PluginInfo) string { + return fmt.Sprintf(PluginKey, pluginsList.Name, pluginsList.Target, pluginsList.Version) +} + // PluginGroupToMap converts the given slice of PluginGroups to map (PluginGroup name is the key) and PluginGroup is the value func PluginGroupToMap(pluginGroups []*PluginGroup) map[string]*PluginGroup { m := make(map[string]*PluginGroup) @@ -86,14 +93,15 @@ func GetHomeDir() string { } // ExecuteCmdAndBuildJSONOutput is generic function to execute given command and build JSON output and return -func ExecuteCmdAndBuildJSONOutput[T PluginInfo | PluginSearch | PluginGroup | PluginSourceInfo | configapi.ClientConfig | Server | ContextListInfo](cmdExe CmdOps, cmd string) ([]*T, error) { +func ExecuteCmdAndBuildJSONOutput[T PluginInfo | PluginSearch | PluginGroup | PluginSourceInfo | types.ClientConfig | Server | ContextListInfo](cmdExe CmdOps, cmd string) ([]*T, error) { out, stdErr, err := cmdExe.Exec(cmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, cmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, cmd, err.Error(), stdErr.String(), out.String()) return nil, err } jsonStr := out.String() + log.Info(jsonStr) var list []*T err = json.Unmarshal([]byte(jsonStr), &list) if err != nil { @@ -112,3 +120,173 @@ func GetMapKeys[K string, V *PluginInfo](m map[K][]V) []*K { } return keySet } + +// CreateKindCluster create the k8s KIND cluster in the local Docker environment +func CreateKindCluster(tf *Framework, name string) (*ClusterInfo, error) { + ci := &ClusterInfo{Name: name} + _, err := tf.KindCluster.CreateCluster(name) + if err != nil { + return nil, errors.Wrapf(err, "error while creating kind cluster: %s", name) + } + endpoint, err := tf.KindCluster.GetClusterEndpoint(name) + if err != nil { + return nil, errors.Wrapf(err, "error while getting kind cluster %s endpoint", name) + } + ci.EndPoint = endpoint + ci.ClusterKubeContext = tf.KindCluster.GetClusterContext(name) + ci.KubeConfigPath = tf.KindCluster.GetKubeconfigPath() + return ci, nil +} + +// IsContextExists checks the given context is exists in the config file by listing the existing contexts in the config file +func IsContextExists(tf *Framework, contextName string) bool { + list, err := tf.ContextCmd.ListContext() + gomega.Expect(err).To(gomega.BeNil(), "list context should not return any error") + for _, context := range list { + if context.Name == contextName { + return true + } + } + return false +} + +// IsAllPluginGroupsExists takes the two list of PluginGroups (super list and sub list), check if all sub list PluginGroup are exists in super list PluginGroup +func IsAllPluginGroupsExists(superList, subList []*PluginGroup) bool { + superMap := PluginGroupToMap(superList) + subMap := PluginGroupToMap(subList) + for ele := range subMap { + _, exists := superMap[ele] + if !exists { + return false + } + } + return true +} + +// MapPluginsToPluginGroups takes the plugins info (output of: tanzu plugin search) and +// plugins group info (output of: tanzu plugin group search), +// maps the plugins to plugin group, plugin is mapped to plugin group based on plugin target +// (kubernetes to tkg, mission-control to tmc) and version, group name would be vmware-/ +func MapPluginsToPluginGroups(list []*PluginInfo, pg []*PluginGroup) map[string][]*PluginInfo { + m := make(map[string][]*PluginInfo) + for _, pluginGroup := range pg { + m[pluginGroup.Group] = make([]*PluginInfo, 0) + } + for i := range list { + plugin := list[i] + key := "vmware-" + if plugin.Target == string(types.TargetK8s) { + key += TKG + "/" + } else if plugin.Target == string(types.TargetTMC) { + key += TMC + "/" + } + key += plugin.Version + pluginList, ok := m[key] + if ok { + pluginList = append(pluginList, plugin) + m[key] = pluginList + } + } + return m +} + +// CopyPluginsBetweenPluginGroupsAndUpdatePluginsVersion copies list of plugins from fromPluginGroup to toPluginGroup in the map pluginGroupToPluginsMap +// and it does updates the each plugin Version value with pluginsNewVersion +func CopyPluginsBetweenPluginGroupsAndUpdatePluginsVersion(pluginGroupToPluginsMap map[string][]*PluginInfo, fromPluginGroup, toPluginGroup, pluginsNewVersion string) { + pluginGroupToPluginsMap[toPluginGroup] = make([]*PluginInfo, 0) + if plugins, ok := pluginGroupToPluginsMap[fromPluginGroup]; ok { + newPluginList := pluginGroupToPluginsMap[toPluginGroup] + for _, plugin := range plugins { + newPlugin := *plugin + newPlugin.Version = pluginsNewVersion + newPluginList = append(newPluginList, &newPlugin) + } + pluginGroupToPluginsMap[toPluginGroup] = newPluginList + } +} + +// CreateTemporaryCRsForPluginsInGivenPluginGroup takes list of Plugins info and generates temporary CR files(under $FullPathForTempDir), and returns plugins list, CR files and error if any while creating the CR files +func CreateTemporaryCRsForPluginsInGivenPluginGroup(plugins []*PluginInfo) ([]*PluginInfo, []string, error) { + pluginsList := make([]*PluginInfo, 0) + filePaths := make([]string, 0) + for _, plugin := range plugins { + absoluteCRFilePath := filepath.Join(FullPathForTempDir, fmt.Sprintf(PluginCRFileName, plugin.Name, plugin.Target, plugin.Version)) + err := os.WriteFile(absoluteCRFilePath, []byte(fmt.Sprintf(CRTemplate, plugin.Name, plugin.Version)), 0644) + if err != nil { + return pluginsList, filePaths, err + } + filePaths = append(filePaths, absoluteCRFilePath) + pluginsList = append(pluginsList, plugin) + } + return pluginsList, filePaths, nil +} + +// GetPluginFromFirstListButNotExistsInSecondList returns a plugin which is exists in first plugin list but not in second plugin list +func GetPluginFromFirstListButNotExistsInSecondList(first, second []*PluginInfo) (*PluginInfo, error) { + m1 := PluginListToMap(first) + m2 := PluginListToMap(second) + for plugin := range m1 { + if _, ok := m2[plugin]; !ok { + return m1[plugin], nil + } + } + return nil, fmt.Errorf("there is no plugin which is not common in the given pluginInfo's") +} + +// IsPluginSourceExists checks the sourceName is exists in the given list of PluginSourceInfo's +func IsPluginSourceExists(list []*PluginSourceInfo, sourceName string) bool { + for _, val := range list { + if val.Name == sourceName { + return true + } + } + return false +} + +// CheckAllPluginsExists checks all PluginInfo's in subList are available in superList +// superList is the super set, subList is sub set +func CheckAllPluginsExists(superList, subList []*PluginInfo) bool { + superSet := PluginListToMap(superList) + subSet := PluginListToMap(subList) + for key := range subSet { + // val2, ok := superSet[key] + // Plugin's Name, Target and Version are part of map Key, so no need to compare/validate again here if different then we can not find the plugin in superSet map + // TODO: cpamuluri: currently the plugin's description in 'tanzu plugin search' output and 'tanzu plugin list' (after install) are different, ignore comparing description field for now, until we fix the description fields in local test central repository + // if !ok || val1.Description != val2.Description { + // return false + // } + _, ok := superSet[key] + if !ok { + return false + } + } + return true +} + +// GetInstalledPlugins takes list of plugins and returns installed only list of plugins +func GetInstalledPlugins(pluginList []*PluginInfo) []*PluginInfo { + installedPlugin := make([]*PluginInfo, 0) + for i := range pluginList { + if pluginList[i].Status == Installed { + installedPlugin = append(installedPlugin, pluginList[i]) + } + } + return installedPlugin +} + +// IsPluginExists validates the given plugin (with plugin status) is exists in the plugins list or not +func IsPluginExists(pluginList []*PluginInfo, plugin *PluginInfo, pluginInstallationStatus string) bool { + isExist := CheckAllPluginsExists(pluginList, append(make([]*PluginInfo, 0), plugin)) + if isExist { + return plugin.Status == pluginInstallationStatus + } + return isExist +} + +// GetGivenPluginFromTheGivenPluginList takes the plugin list and a plugin +// checks the given plugin exists in the plugin list, if exists then returns the plugin +// otherwise returns nil +func GetGivenPluginFromTheGivenPluginList(pluginList []*PluginInfo, requiredPlugin *PluginInfo) *PluginInfo { + superSet := PluginListToMap(pluginList) + return superSet[GetMapKeyForPlugin(requiredPlugin)] +} diff --git a/test/e2e/framework/output_handling.go b/test/e2e/framework/output_handling.go index f2624a8c8..79a348d25 100644 --- a/test/e2e/framework/output_handling.go +++ b/test/e2e/framework/output_handling.go @@ -11,6 +11,7 @@ type PluginInfo struct { Scope string `json:"scope"` Status string `json:"status"` Version string `json:"version"` + Context string `json:"context"` } type PluginSearch struct { diff --git a/test/e2e/framework/plugin_lifecycle_operations.go b/test/e2e/framework/plugin_lifecycle_operations.go index 23f3c3527..7bc738ca0 100644 --- a/test/e2e/framework/plugin_lifecycle_operations.go +++ b/test/e2e/framework/plugin_lifecycle_operations.go @@ -16,10 +16,16 @@ import ( type PluginBasicOps interface { // ListPlugins lists all plugins by running 'tanzu plugin list' command ListPlugins() ([]*PluginInfo, error) + // ListInstalledPlugins lists all installed plugins + ListInstalledPlugins() ([]*PluginInfo, error) + // ListPluginsForGivenContext lists all plugins for a given context and either installed only or all + ListPluginsForGivenContext(context string, installedOnly bool) ([]*PluginInfo, error) // SearchPlugins searches all plugins for given filter (keyword|regex) by running 'tanzu plugin search' command SearchPlugins(filter string) ([]*PluginInfo, error) // InstallPlugin installs given plugin and flags InstallPlugin(pluginName, target, versions string) error + // Sync performs sync operation + Sync() (string, error) // DescribePlugin describes given plugin and flags DescribePlugin(pluginName, target string) (string, error) // UninstallPlugin uninstalls/deletes given plugin @@ -100,7 +106,7 @@ func (po *pluginCmdOps) DeletePluginDiscoverySource(pluginSourceName string) (st deleteCmd := fmt.Sprintf(DeletePluginSource, pluginSourceName) out, stdErr, err := po.cmdExe.Exec(deleteCmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, deleteCmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, deleteCmd, err.Error(), stdErr.String(), out.String()) } return out.String(), err } @@ -109,6 +115,42 @@ func (po *pluginCmdOps) ListPlugins() ([]*PluginInfo, error) { return ExecuteCmdAndBuildJSONOutput[PluginInfo](po.cmdExe, ListPluginsCmdWithJSONOutputFlag) } +func (po *pluginCmdOps) ListInstalledPlugins() ([]*PluginInfo, error) { + plugins, err := ExecuteCmdAndBuildJSONOutput[PluginInfo](po.cmdExe, ListPluginsCmdWithJSONOutputFlag) + installedPlugins := make([]*PluginInfo, 0) + for i := range plugins { + if plugins[i].Status == Installed { + installedPlugins = append(installedPlugins, plugins[i]) + } + } + return installedPlugins, err +} + +func (po *pluginCmdOps) ListPluginsForGivenContext(context string, installedOnly bool) ([]*PluginInfo, error) { + plugins, err := ExecuteCmdAndBuildJSONOutput[PluginInfo](po.cmdExe, ListPluginsCmdWithJSONOutputFlag) + contextSpecificPlugins := make([]*PluginInfo, 0) + for i := range plugins { + if plugins[i].Context == context { + if installedOnly { + if plugins[i].Status == Installed { + contextSpecificPlugins = append(contextSpecificPlugins, plugins[i]) + } + } else { + contextSpecificPlugins = append(contextSpecificPlugins, plugins[i]) + } + } + } + return contextSpecificPlugins, err +} + +func (po *pluginCmdOps) Sync() (string, error) { + out, stdErr, err := po.cmdExe.Exec(pluginSyncCmd) + if err != nil { + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, pluginSyncCmd, err.Error(), stdErr.String(), out.String()) + } + return out.String(), err +} + func (po *pluginCmdOps) SearchPlugins(filter string) ([]*PluginInfo, error) { searchPluginCmdWithOptions := SearchPluginsCmd if len(strings.TrimSpace(filter)) > 0 { @@ -147,9 +189,9 @@ func (po *pluginCmdOps) InstallPlugin(pluginName, target, versions string) error if len(strings.TrimSpace(versions)) > 0 { installPluginCmd += " --version " + versions } - _, stdErr, err := po.cmdExe.Exec(installPluginCmd) + out, stdErr, err := po.cmdExe.Exec(installPluginCmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, installPluginCmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, installPluginCmd, err.Error(), stdErr.String(), out.String()) } return err } @@ -161,9 +203,9 @@ func (po *pluginCmdOps) InstallPluginsFromGroup(pluginNameORAll, groupName strin } else { installPluginCmd = fmt.Sprintf(InstallAllPluginsFromGroupCmd, groupName) } - _, stdErr, err := po.cmdExe.Exec(installPluginCmd) + out, stdErr, err := po.cmdExe.Exec(installPluginCmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, installPluginCmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, installPluginCmd, err.Error(), stdErr.String(), out.String()) } return err } @@ -176,7 +218,7 @@ func (po *pluginCmdOps) DescribePlugin(pluginName, target string) (string, error stdOut, stdErr, err := po.cmdExe.Exec(installPluginCmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, installPluginCmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, installPluginCmd, err.Error(), stdErr.String(), stdOut.String()) } return stdOut.String(), err } @@ -190,9 +232,9 @@ func (po *pluginCmdOps) UninstallPlugin(pluginName, target string) error { if len(strings.TrimSpace(target)) > 0 { uninstallPluginCmd += " --target " + target } - _, stdErr, err := po.cmdExe.Exec(uninstallPluginCmd) + out, stdErr, err := po.cmdExe.Exec(uninstallPluginCmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, uninstallPluginCmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, uninstallPluginCmd, err.Error(), stdErr.String(), out.String()) } return err } @@ -201,16 +243,16 @@ func (po *pluginCmdOps) ExecuteSubCommand(pluginWithSubCommand string) (string, pluginCmdWithSubCommand := fmt.Sprintf(PluginSubCommand, pluginWithSubCommand) stdOut, stdErr, err := po.cmdExe.Exec(pluginCmdWithSubCommand) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, pluginCmdWithSubCommand, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, pluginCmdWithSubCommand, err.Error(), stdErr.String(), stdOut.String()) return stdOut.String(), errors.Wrap(err, stdErr.String()) } return stdOut.String(), nil } func (po *pluginCmdOps) CleanPlugins() error { - _, stdErr, err := po.cmdExe.Exec(CleanPluginsCmd) + out, stdErr, err := po.cmdExe.Exec(CleanPluginsCmd) if err != nil { - log.Errorf(ErrorLogForCommandWithErrAndStdErr, CleanPluginsCmd, err.Error(), stdErr.String()) + log.Errorf(ErrorLogForCommandWithErrStdErrAndStdOut, CleanPluginsCmd, err.Error(), stdErr.String(), out.String()) } return err } diff --git a/test/e2e/framework/templates.go b/test/e2e/framework/templates.go index d2af039ab..1881fe4ab 100644 --- a/test/e2e/framework/templates.go +++ b/test/e2e/framework/templates.go @@ -54,3 +54,13 @@ const GeneratedValuesTemplate = `#@data/values #@overlay/match-child-defaults missing_ok=True ---` + +const CRTemplate = `apiVersion: cli.tanzu.vmware.com/v1alpha1 +kind: CLIPlugin +metadata: + annotations: + name: %s + namespace: default +spec: + description: Kubernetes cluster operations + recommendedVersion: %s` diff --git a/test/e2e/plugin_lifecycle/plugin_group_lifecycle_test.go b/test/e2e/plugin_lifecycle/plugin_group_lifecycle_test.go index a3cfba859..779141212 100644 --- a/test/e2e/plugin_lifecycle/plugin_group_lifecycle_test.go +++ b/test/e2e/plugin_lifecycle/plugin_group_lifecycle_test.go @@ -1,8 +1,8 @@ // Copyright 2023 VMware, Inc. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// plugin provides plugin command specific E2E test cases -package plugin +// pluginlifecyclee2e provides plugin command specific E2E test cases +package pluginlifecyclee2e import ( . "github.com/onsi/ginkgo" diff --git a/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go b/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go index fb151a880..a571ae80d 100644 --- a/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go +++ b/test/e2e/plugin_lifecycle/plugin_lifecycle_helper.go @@ -1,56 +1,15 @@ // Copyright 2023 VMware, Inc. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package plugin provides plugin command specific E2E test cases -package plugin +// Package pluginlifecyclee2e provides plugin command specific E2E test cases +package pluginlifecyclee2e import ( - "fmt" - "github.com/onsi/gomega" "github.com/vmware-tanzu/tanzu-cli/test/e2e/framework" - "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" ) -// IsPluginSourceExists checks the sourceName is exists in the given list of PluginSourceInfo's -func IsPluginSourceExists(list []*framework.PluginSourceInfo, sourceName string) bool { - for _, val := range list { - if val.Name == sourceName { - return true - } - } - return false -} - -// CheckAllPluginsAvailable checks requiredPlugins are exists in the allPlugins -func CheckAllPluginsAvailable(allPlugins, requiredPlugins []*framework.PluginInfo) bool { - set := framework.PluginListToSet(requiredPlugins) - for _, plugin := range allPlugins { - key := fmt.Sprintf(framework.PluginKey, plugin.Name, plugin.Target, plugin.Version) - _, ok := set[key] - if ok { - delete(set, key) - } - } - return len(set) == 0 -} - -// CheckAllPluginsExists checks all PluginInfo's in subList are available in superList -// superList is the super set, subList is sub set -func CheckAllPluginsExists(superList, subList []*framework.PluginInfo) bool { - superSet := framework.PluginListToMap(superList) - subSet := framework.PluginListToMap(subList) - for key, val := range subSet { - val2, ok := superSet[key] - // Plugin's Name, Target and Version are part of map Key, so no need to compare/validate again here if different then we can not find the plugin in superSet map - if !ok || val.Description != val2.Description { - return false - } - } - return true -} - // SearchAllPlugins runs the plugin search command and returns all the plugins from the search output func SearchAllPlugins(tf *framework.Framework) []*framework.PluginInfo { pluginsSearchList, err := tf.PluginCmd.SearchPlugins("") @@ -64,51 +23,3 @@ func SearchAllPluginGroups(tf *framework.Framework) []*framework.PluginGroup { gomega.Expect(err).To(gomega.BeNil(), "should not get any error for plugin search") return pluginGroups } - -// IsAllPluginGroupsExists takes the two list of PluginGroups (super list and sub list), check if all sub list PluginGroup are exists in super list PluginGroup -func IsAllPluginGroupsExists(superList, subList []*framework.PluginGroup) bool { - superMap := framework.PluginGroupToMap(superList) - subMap := framework.PluginGroupToMap(subList) - for ele := range subMap { - _, exists := superMap[ele] - if !exists { - return false - } - } - return true -} - -func MapPluginsToPluginGroups(list []*framework.PluginInfo, pg []*framework.PluginGroup) map[string][]*framework.PluginInfo { - m := make(map[string][]*framework.PluginInfo) - for _, pluginGroup := range pg { - m[pluginGroup.Group] = make([]*framework.PluginInfo, 0) - } - for i := range list { - plugin := list[i] - key := "vmware-" - if plugin.Target == string(types.TargetK8s) { - key += framework.TKG + "/" - } else if plugin.Target == string(types.TargetTMC) { - key += framework.TMC + "/" - } - key += plugin.Version - pluginList, ok := m[key] - if ok { - pluginList = append(pluginList, plugin) - m[key] = pluginList - } - } - return m -} - -// GetPluginFromFirstListButNotExistsInSecondList returns a plugin which is exists in first plugin list but not in second plugin list -func GetPluginFromFirstListButNotExistsInSecondList(first, second []*framework.PluginInfo) (*framework.PluginInfo, error) { - m1 := framework.PluginListToMap(first) - m2 := framework.PluginListToMap(second) - for plugin := range m1 { - if _, ok := m2[plugin]; !ok { - return m1[plugin], nil - } - } - return nil, fmt.Errorf("there is no plugin which is not common in the given pluginInfo's") -} diff --git a/test/e2e/plugin_lifecycle/plugin_lifecycle_suite_test.go b/test/e2e/plugin_lifecycle/plugin_lifecycle_suite_test.go index 6c817302e..fe754af2d 100644 --- a/test/e2e/plugin_lifecycle/plugin_lifecycle_suite_test.go +++ b/test/e2e/plugin_lifecycle/plugin_lifecycle_suite_test.go @@ -1,8 +1,8 @@ // Copyright 2023 VMware, Inc. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// plugin provides plugin command specific E2E test cases -package plugin +// pluginlifecyclee2e provides plugin command specific E2E test cases +package pluginlifecyclee2e import ( "fmt" @@ -17,7 +17,7 @@ import ( func TestPluginLifecycle(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "PluginLifecycle Suite") + RunSpecs(t, "Plugin-Lifecycle E2E Test Suite") } var ( @@ -42,16 +42,16 @@ var _ = BeforeSuite(func() { pluginGroups = SearchAllPluginGroups(tf) // check all required plugin groups (framework.PluginGroupsForLifeCycleTests) need for life cycle test are available in plugin group search output - Expect(IsAllPluginGroupsExists(pluginGroups, framework.PluginGroupsForLifeCycleTests)).Should(BeTrue(), "all required plugin groups for life cycle tests should exists in plugin group search output") + Expect(framework.IsAllPluginGroupsExists(pluginGroups, framework.PluginGroupsForLifeCycleTests)).Should(BeTrue(), "all required plugin groups for life cycle tests should exists in plugin group search output") // search plugins and make sure there are plugins available pluginsSearchList = SearchAllPlugins(tf) Expect(len(pluginsSearchList)).Should(BeNumerically(">", 0)) // check all required plugins (framework.PluginsForLifeCycleTests) for plugin life cycle e2e are available in plugin search output - CheckAllPluginsAvailable(pluginsSearchList, framework.PluginsForLifeCycleTests) + framework.CheckAllPluginsExists(pluginsSearchList, framework.PluginsForLifeCycleTests) - pluginGroupToPluginListMap = MapPluginsToPluginGroups(pluginsSearchList, framework.PluginGroupsForLifeCycleTests) + pluginGroupToPluginListMap = framework.MapPluginsToPluginGroups(pluginsSearchList, framework.PluginGroupsForLifeCycleTests) // check for every plugin group (in framework.PluginGroupsForLifeCycleTests) there should be plugin's available for pg := range pluginGroupToPluginListMap { diff --git a/test/e2e/plugin_lifecycle/plugin_lifecycle_test.go b/test/e2e/plugin_lifecycle/plugin_lifecycle_test.go index e9f351ebe..d0f1a3085 100644 --- a/test/e2e/plugin_lifecycle/plugin_lifecycle_test.go +++ b/test/e2e/plugin_lifecycle/plugin_lifecycle_test.go @@ -1,8 +1,8 @@ // Copyright 2023 VMware, Inc. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// plugin provides plugin command specific E2E test cases -package plugin +// pluginlifecyclee2e provides plugin command specific E2E test cases +package pluginlifecyclee2e import ( "fmt" @@ -34,7 +34,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Plugin-lifecycle]", func( It("list plugin source and validate previously created plugin source available", func() { list, err := tf.PluginCmd.ListPluginSources() Expect(err).To(BeNil(), "should not get any error for plugin source list") - Expect(IsPluginSourceExists(list, pluginSourceName)).To(BeTrue()) + Expect(framework.IsPluginSourceExists(list, pluginSourceName)).To(BeTrue()) }) // Test case: update plugin source URL It("update previously created plugin source URL", func() { @@ -57,7 +57,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Plugin-lifecycle]", func( Expect(err).To(BeNil(), "should not get any error for plugin source delete") list, err := tf.PluginCmd.ListPluginSources() Expect(err).To(BeNil(), "should not get any error for plugin source list") - Expect(IsPluginSourceExists(list, pluginSourceName)).To(BeFalse()) + Expect(framework.IsPluginSourceExists(list, pluginSourceName)).To(BeFalse()) }) }) Context("plugin use cases: tanzu plugin clean, install and describe, list, delete", func() { @@ -101,7 +101,7 @@ var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Plugin-lifecycle]", func( pluginsList, err := tf.PluginCmd.ListPlugins() Expect(err).To(BeNil(), "should not get any error for plugin list") Expect(len(pluginsList)).Should(Equal(len(framework.PluginsForLifeCycleTests)), "plugins list should return all installed plugins") - Expect(CheckAllPluginsExists(pluginsList, framework.PluginsForLifeCycleTests)).Should(BeTrue(), "the plugin list output is not same as the plugins being installed") + Expect(framework.CheckAllPluginsExists(pluginsList, framework.PluginsForLifeCycleTests)).Should(BeTrue(), "the plugin list output is not same as the plugins being installed") }) // Test case: delete all plugins which are installed, and validate by running list plugin command It("delete all plugins and verify with plugin list", func() { diff --git a/test/e2e/plugin_sync/plugin_sync_lifecycle_suite_test.go b/test/e2e/plugin_sync/plugin_sync_lifecycle_suite_test.go new file mode 100644 index 000000000..d0d5e3542 --- /dev/null +++ b/test/e2e/plugin_sync/plugin_sync_lifecycle_suite_test.go @@ -0,0 +1,80 @@ +// Copyright 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// pluginsynce2e provides plugin sync command specific E2E test cases +package pluginsynce2e + +import ( + "fmt" + "os" + "strings" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/tanzu-cli/test/e2e/framework" + helper "github.com/vmware-tanzu/tanzu-cli/test/e2e/plugin_lifecycle" +) + +func TestPluginSyncLifecycle(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin-Sync-Lifecycle E2E Test Suite") +} + +var ( + tf *framework.Framework + e2eTestLocalCentralRepoURL string + pluginsSearchList []*framework.PluginInfo + pluginGroups []*framework.PluginGroup + pluginGroupToPluginListMap map[string][]*framework.PluginInfo +) + +const CRDFilePath = "../framework/config/cli.tanzu.vmware.com_cliplugins.yaml" +const numberOfPluginsToInstall = 3 + +// BeforeSuite initializes and set up the environment to execute the plugin life cycle and plugin group life cycle end-to-end test cases +var _ = BeforeSuite(func() { + tf = framework.NewFramework() + // check E2E test central repo URL (TANZU_CLI_E2E_TEST_LOCAL_CENTRAL_REPO_URL) + e2eTestLocalCentralRepoURL = os.Getenv(framework.TanzuCliE2ETestLocalCentralRepositoryURL) + Expect(e2eTestLocalCentralRepoURL).NotTo(BeEmpty(), fmt.Sprintf("environment variable %s should set with local central repository URL", framework.TanzuCliE2ETestLocalCentralRepositoryURL)) + // set E2E test central repo URL to TANZU_CLI_PRE_RELEASE_REPO_IMAGE + os.Setenv(framework.CentralRepositoryPreReleaseRepoImage, e2eTestLocalCentralRepoURL) + + // search plugin groups and make sure there plugin groups available + pluginGroups = helper.SearchAllPluginGroups(tf) + + // check all required plugin groups (framework.PluginGroupsForLifeCycleTests) need for life cycle test are available in plugin group search output + Expect(framework.IsAllPluginGroupsExists(pluginGroups, framework.PluginGroupsForLifeCycleTests)).Should(BeTrue(), "all required plugin groups for life cycle tests should exists in plugin group search output") + + // search plugins and make sure there are plugins available + pluginsSearchList = helper.SearchAllPlugins(tf) + Expect(len(pluginsSearchList)).Should(BeNumerically(">", 0)) + + // check all required plugins (framework.PluginsForLifeCycleTests) for plugin life cycle e2e are available in plugin search output + framework.CheckAllPluginsExists(pluginsSearchList, framework.PluginsForLifeCycleTests) + + pluginGroupToPluginListMap = framework.MapPluginsToPluginGroups(pluginsSearchList, framework.PluginGroupsForLifeCycleTests) + for pluginGroupLatest := range framework.PluginGroupsLatestToOldVersions { + framework.CopyPluginsBetweenPluginGroupsAndUpdatePluginsVersion(pluginGroupToPluginListMap, pluginGroupLatest, framework.PluginGroupsLatestToOldVersions[pluginGroupLatest], strings.Split(framework.PluginGroupsLatestToOldVersions[pluginGroupLatest], "/")[1]) + } + + // check for every plugin group (in framework.PluginGroupsForLifeCycleTests) there should be plugins available + for pg := range pluginGroupToPluginListMap { + Expect(len(pluginGroupToPluginListMap[pg])).Should(BeNumerically(">", 0), "there should be at least one plugin available for each plugin group in plugin group life cycle list") + } +}) + +// AfterSuite deletes the temp directory created during test cases execution +var _ = AfterSuite(func() { + err := os.RemoveAll(framework.FullPathForTempDir) // delete an entire directory + Expect(err).To(BeNil(), "should not get any error while deleting temp directory") +}) + +func ApplyConfigOnKindCluster(tf *framework.Framework, clusterInfo *framework.ClusterInfo, confFilePaths []string) { + for _, pluginCRFilePaths := range confFilePaths { + err := tf.KindCluster.ApplyConfig(clusterInfo.ClusterKubeContext, pluginCRFilePaths) + Expect(err).To(BeNil(), "should not get any error for config apply") + } +} diff --git a/test/e2e/plugin_sync/plugin_sync_lifecycle_test.go b/test/e2e/plugin_sync/plugin_sync_lifecycle_test.go new file mode 100644 index 000000000..31539568f --- /dev/null +++ b/test/e2e/plugin_sync/plugin_sync_lifecycle_test.go @@ -0,0 +1,372 @@ +// Copyright 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// pluginsynce2e provides plugin sync command specific E2E test cases +package pluginsynce2e + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/tanzu-cli/test/e2e/framework" + "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" +) + +var _ = framework.CLICoreDescribe("[Tests:E2E][Feature:Plugin-Group-lifecycle]", func() { + + // Use case 1: create a KIND cluster, don't apply CRD and CRs, create context, make sure no plugins are installed + // a. create k8s context for the KIND cluster + // b. create context and validate current active context + // c. list plugins and make sure no plugins installed + // d. delete current context and KIND cluster + Context("plugin install from group: install a plugin from a specific plugin group", func() { + var clusterInfo *framework.ClusterInfo + var contextName string + var err error + // Test case: a. create k8s context for the KIND cluster + It("create KIND cluster", func() { + // Create KIND cluster, which is used in test cases to create context's + clusterInfo, err = framework.CreateKindCluster(tf, "sync-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + }) + // Test case: b. create context and validate current active context + It("create context with kubeconfig and context", func() { + By("create context with kubeconfig and context") + contextName = "sync-e2e-" + framework.RandomString(4) + err := tf.ContextCmd.CreateContextWithKubeconfig(contextName, 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(contextName), "the active context should be recently added context") + }) + // Test case: c. list plugins and make sure no plugins installed + It("list plugins and check number plugins should be same as installed in previous test", func() { + pluginsList, err := tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsList)).Should(Equal(0), "there should not be any context specific plugins") + }) + // Test case: d. delete current context and KIND cluster + It("delete current context and the KIND cluster", func() { + err = tf.ContextCmd.DeleteContext(contextName) + 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 2: Create kind cluster, apply CRD and CRs, create context, should install all plugins, uninstall the specific plugin, and perform plugin sync: + // Steps: + // a. create KIND cluster, apply CRD + // b. apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins + // c. create context and make sure context has created + // d. list plugins and validate plugins info, make sure all plugins installed for which CR's has applied to KIND cluster + // e. uninstall one of the installed plugin, make sure plugin is uninstalled, + // run plugin sync, make sure the uninstalled plugin has installed again. + // f. delete current context and KIND cluster + Context("Use case: Install KIND Cluster, Apply CRD, Apply specific plugin CRs, create context and validate plugin sync", func() { + var clusterInfo *framework.ClusterInfo + var pluginCRFilePaths []string + var pluginsInfoForCRsApplied, installedPluginsList []*framework.PluginInfo + var contextName string + var err error + // Test case: a. 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 = framework.CreateKindCluster(tf, "sync-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + }) + // Test case: b. apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins + It("apply CRD and CRs to KIND cluster", func() { + ApplyConfigOnKindCluster(tf, clusterInfo, append(make([]string, 0), CRDFilePath)) + + pluginsToGenerateCRs, ok := pluginGroupToPluginListMap[framework.PluginGroupsForLifeCycleTests[0].Group] + 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 = framework.CreateTemporaryCRsForPluginsInGivenPluginGroup(pluginsToGenerateCRs[:numberOfPluginsToInstall]) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + ApplyConfigOnKindCluster(tf, clusterInfo, pluginCRFilePaths) + }) + + // Test case: c. create context and make sure context has created + It("create context with kubeconfig and context", func() { + By("create context with kubeconfig and context") + contextName = "sync-e2e-" + framework.RandomString(4) + err := tf.ContextCmd.CreateContextWithKubeconfig(contextName, 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(contextName), "the active context should be recently added context") + }) + // Test case: d. list plugins and validate plugins info, make sure all plugins are installed for which CRs were present on the cluster + It("list plugins and validate plugins being installed after context being created", func() { + installedPluginsList, err = tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(installedPluginsList)).Should(Equal(len(pluginsInfoForCRsApplied)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(installedPluginsList, pluginsInfoForCRsApplied)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + + // Test case: e. uninstall one of the installed plugin, make sure plugin is uninstalled, + // run plugin sync, make sure the uninstalled plugin has installed again. + It("Uninstall one of the installed plugin", func() { + pluginToUninstall := pluginsInfoForCRsApplied[0] + err := tf.PluginCmd.UninstallPlugin(pluginToUninstall.Name, pluginToUninstall.Target) + Expect(err).To(BeNil(), "should not get any error for plugin uninstall") + + latestPluginsInstalledList := pluginsInfoForCRsApplied[1:] + allPluginsList, err := tf.PluginCmd.ListPluginsForGivenContext(contextName, false) + Expect(err).To(BeNil(), "should not get any error for plugin list") + installedPluginsList = framework.GetInstalledPlugins(allPluginsList) + Expect(framework.IsPluginExists(allPluginsList, framework.GetGivenPluginFromTheGivenPluginList(allPluginsList, pluginToUninstall), framework.NotInstalled)).To(BeTrue(), "uninstalled plugin should be listed as not installed") + Expect(len(installedPluginsList)).Should(Equal(len(latestPluginsInstalledList)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(installedPluginsList, latestPluginsInstalledList)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + + _, err = tf.PluginCmd.Sync() + Expect(err).To(BeNil(), "should not get any error for plugin sync") + + installedPluginsList, err = tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(installedPluginsList)).Should(Equal(len(pluginsInfoForCRsApplied)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(installedPluginsList, pluginsInfoForCRsApplied)).Should(BeTrue(), "plugins being installed and plugins info for which CRs applied should be same") + }) + // f. delete current context and the KIND cluster + It("delete current context and the KIND cluster", func() { + err = tf.ContextCmd.DeleteContext(contextName) + 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 3: Test plugin sync when central repo does not have all plugin CRs being applied in KIND cluster + // Steps: + // a. create KIND cluster + // b. apply CRD (cluster resource definition) and CRs (cluster resource) for a few plugins which are available in the central repo and CRs for plugins which are not available in the central repo + // c. create context and make sure context has been created + // d. list plugins and validate plugins info, make sure all plugins installed for which CRs have applied to the KIND cluster and are available in the central repo + // e. run plugin sync and validate the plugin list + // f. delete the KIND cluster + Context("Use case: Install KIND Cluster, Apply CRD, Apply specific plugin CRs, create context and validate plugin sync", func() { + var clusterInfo *framework.ClusterInfo + var pluginCRFilePaths, pluginWithIncorrectVerCRFilePaths []string + var pluginsInfoForCRsApplied, pluginsWithIncorrectVer, pluginsList []*framework.PluginInfo + var contextName string + var err error + // Test case: a. create KIND cluster + It("create KIND cluster", func() { + // Create KIND cluster, which is used in test cases to create context's + clusterInfo, err = framework.CreateKindCluster(tf, "sync-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + }) + // Test case: b. apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins which are available in centra repo + // and CR's for plugins which are not available in central repo + It("apply CRD and CRs to KIND cluster", func() { + ApplyConfigOnKindCluster(tf, clusterInfo, append(make([]string, 0), CRDFilePath)) + pluginsToGenerateCRs, ok := pluginGroupToPluginListMap[framework.PluginGroupsForLifeCycleTests[0].Group] + 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 = framework.CreateTemporaryCRsForPluginsInGivenPluginGroup(pluginsToGenerateCRs[:numberOfPluginsToInstall]) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + + pluginWithIncorrectVersion := *pluginsToGenerateCRs[numberOfPluginsToInstall] + pluginWithIncorrectVersion.Version = pluginWithIncorrectVersion.Version + framework.RandomNumber(2) + pluginsWithIncorrectVer, pluginWithIncorrectVerCRFilePaths, err = framework.CreateTemporaryCRsForPluginsInGivenPluginGroup(append(make([]*framework.PluginInfo, 0), &pluginWithIncorrectVersion)) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + + ApplyConfigOnKindCluster(tf, clusterInfo, pluginCRFilePaths) + ApplyConfigOnKindCluster(tf, clusterInfo, pluginWithIncorrectVerCRFilePaths) + }) + + // Test case: c. create context and make sure context has created + It("create context with kubeconfig and context", func() { + By("create context with kubeconfig and context") + contextName = "sync-e2e-" + framework.RandomString(4) + err := tf.ContextCmd.CreateContextWithKubeconfig(contextName, 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(contextName), "the active context should be recently added context") + }) + // Test case: d. list plugins and validate plugins info, make sure all plugins installed for which CR's has applied to KIND cluster and available in central repo + It("list plugins and validate plugins being installed after context being created", func() { + pluginsList, err = tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsList)).Should(Equal(len(pluginsInfoForCRsApplied)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsList, pluginsInfoForCRsApplied)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + + // Test case: e. run plugin sync and validate the plugin list + It("Uninstall one of the installed plugin", func() { + _, err = tf.PluginCmd.Sync() + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf(framework.UnableToFindPluginForTarget, pluginsWithIncorrectVer[0].Name, pluginsWithIncorrectVer[0].Target))) + + pluginsList, err = tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsList)).Should(Equal(len(pluginsInfoForCRsApplied)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsList, pluginsInfoForCRsApplied)).Should(BeTrue(), "plugins being installed and plugins info for which CRs applied should be same") + }) + // f. delete the KIND cluster + It("delete the KIND cluster", func() { + err = tf.ContextCmd.DeleteContext(contextName) + 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 4: test delete context use case, it should uninstall plugins installed for the context + // Steps: + // a. create KIND cluster + // b. apply CRD (cluster resource definition) and CRs (cluster resource) for few plugins + // c. create context and make sure context gets created, list plugins, make sure all + // plugins installed for which CRs are applied in KIND cluster + // d. delete the context, make sure all context specific plugins are uninstalled + // e. delete the KIND cluster + Context("Use case: Install KIND Cluster, Apply CRD, Apply specific plugin CRs, create context and validate plugin sync", func() { + var clusterInfo *framework.ClusterInfo + var pluginCRFilePaths []string + var pluginsInfoForCRsApplied, pluginsList []*framework.PluginInfo + var contextName string + var err error + // Test case: a. create KIND cluster + It("create KIND cluster", func() { + // Create KIND cluster, which is used in test cases to create context's + clusterInfo, err = framework.CreateKindCluster(tf, "sync-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + }) + // Test case: b. apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins which are available in centra repo + // and CR's for plugins which are not available in central repo + It("apply CRD and CRs to KIND cluster", func() { + ApplyConfigOnKindCluster(tf, clusterInfo, append(make([]string, 0), CRDFilePath)) + pluginsToGenerateCRs, ok := pluginGroupToPluginListMap[framework.PluginGroupsForLifeCycleTests[0].Group] + 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 = framework.CreateTemporaryCRsForPluginsInGivenPluginGroup(pluginsToGenerateCRs[:numberOfPluginsToInstall]) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + ApplyConfigOnKindCluster(tf, clusterInfo, pluginCRFilePaths) + }) + + // Test case: c. create context and make sure context has created, list plugins, make sure all plugins installed for which CR's are applied in KIND cluster + It("create context and validate installed plugins list, should installed all plugins for which CRs has applied in KIND cluster", func() { + By("create context with kubeconfig and context") + contextName = "sync-e2e-" + framework.RandomString(4) + err = tf.ContextCmd.CreateContextWithKubeconfig(contextName, clusterInfo.KubeConfigPath, clusterInfo.ClusterKubeContext) + Expect(err).To(BeNil(), "context should create without any error") + pluginsList, err = tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsList)).Should(Equal(len(pluginsInfoForCRsApplied)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsList, pluginsInfoForCRsApplied)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + // Test case: d. delete the context, make sure all context specific plugins are uninstalled + It("delete context, validate installed plugins list, should uninstalled all context plugins", func() { + err = tf.ContextCmd.DeleteContext(contextName) + Expect(err).To(BeNil(), "there should be no error for delete context") + + pluginsList, err = tf.PluginCmd.ListPluginsForGivenContext(contextName, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsList)).Should(Equal(0), "all context plugins should be uninstalled as context delete") + }) + + // Test case: e. delete the KIND cluster + It("delete the KIND cluster", func() { + 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 5: test switch context use case, make installed plugins should be updated as per the context + // Steps: + // a. create KIND clusters + // b. for both clusters, apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins + // c. for cluster one, create random context and validate the plugin list should show all plugins for which CRs are applied + // d. for cluster two, create random context and validate the plugin list should show all plugins for which CRs are applied + // e. switch context's, make sure installed plugins also updated + // f. delete the KIND clusters + Context("Use case: Install KIND Cluster, Apply CRD, Apply specific plugin CRs, create context and validate plugin sync", func() { + var clusterOne, clusterTwo *framework.ClusterInfo + var pluginCRFilePathsClusterOne, pluginCRFilePathsClusterTwo []string + var pluginsInfoForCRsAppliedClusterOne, pluginsListClusterOne []*framework.PluginInfo + var pluginsInfoForCRsAppliedClusterTwo, pluginsListClusterTwo []*framework.PluginInfo + var contextNameClusterOne, contextNameClusterTwo string + var err error + + // Test case: a. create KIND clusters + It("create KIND cluster", func() { + // Create KIND cluster, which is used in test cases to create context's + clusterOne, err = framework.CreateKindCluster(tf, "sync-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + clusterTwo, err = framework.CreateKindCluster(tf, "sync-e2e-"+framework.RandomNumber(4)) + Expect(err).To(BeNil(), "should not get any error for KIND cluster creation") + }) + // Test case: b. for both clusters, apply CRD (cluster resource definition) and CR's (cluster resource) for few plugins + It("apply CRD and CRs to KIND cluster", func() { + ApplyConfigOnKindCluster(tf, clusterOne, append(make([]string, 0), CRDFilePath)) + pluginsToGenerateCRs, ok := pluginGroupToPluginListMap[framework.PluginGroupsForLifeCycleTests[0].Group] + Expect(ok).To(BeTrue(), "plugin group is not exist in the map") + Expect(len(pluginsToGenerateCRs) > numberOfPluginsToInstall*2).To(BeTrue(), "we don't have enough plugins in local test central repo") + + pluginsInfoForCRsAppliedClusterOne, pluginCRFilePathsClusterOne, err = framework.CreateTemporaryCRsForPluginsInGivenPluginGroup(pluginsToGenerateCRs[:numberOfPluginsToInstall]) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + ApplyConfigOnKindCluster(tf, clusterOne, pluginCRFilePathsClusterOne) + + ApplyConfigOnKindCluster(tf, clusterTwo, append(make([]string, 0), CRDFilePath)) + pluginsInfoForCRsAppliedClusterTwo, pluginCRFilePathsClusterTwo, err = framework.CreateTemporaryCRsForPluginsInGivenPluginGroup(pluginsToGenerateCRs[numberOfPluginsToInstall : numberOfPluginsToInstall*2]) + Expect(err).To(BeNil(), "should not get any error while generating CR files") + ApplyConfigOnKindCluster(tf, clusterTwo, pluginCRFilePathsClusterTwo) + }) + + // Test case: c. for cluster one, create random context and validate the plugin list should show all plugins for which CRs are applied + It("create context and validate installed plugins list, should installed all plugins for which CRs has applied in KIND cluster", func() { + By("create context with kubeconfig and context") + contextNameClusterOne = "sync-e2e-" + framework.RandomString(4) + err = tf.ContextCmd.CreateContextWithKubeconfig(contextNameClusterOne, clusterOne.KubeConfigPath, clusterOne.ClusterKubeContext) + Expect(err).To(BeNil(), "context should create without any error") + pluginsListClusterOne, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameClusterOne, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsListClusterOne)).Should(Equal(len(pluginsInfoForCRsAppliedClusterOne)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsListClusterOne, pluginsInfoForCRsAppliedClusterOne)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + + // Test case: d. for cluster two, create random context and validate the plugin list should show all plugins for which CRs are applied + It("create context and validate installed plugins list, should installed all plugins for which CRs has applied in KIND cluster", func() { + By("create context with kubeconfig and context") + contextNameClusterTwo = "sync-e2e-" + framework.RandomString(4) + err = tf.ContextCmd.CreateContextWithKubeconfig(contextNameClusterTwo, clusterTwo.KubeConfigPath, clusterTwo.ClusterKubeContext) + Expect(err).To(BeNil(), "context should create without any error") + pluginsListClusterTwo, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameClusterTwo, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsListClusterTwo)).Should(Equal(len(pluginsInfoForCRsAppliedClusterTwo)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsListClusterTwo, pluginsInfoForCRsAppliedClusterTwo)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + + // Test case: e. switch context's, make sure installed plugins also updated + It("switch context, make sure installed plugins also updated", func() { + err = tf.ContextCmd.UseContext(contextNameClusterTwo) + Expect(err).To(BeNil(), "there should not be any error for use context") + pluginsListClusterTwo, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameClusterTwo, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsListClusterTwo)).Should(Equal(len(pluginsInfoForCRsAppliedClusterTwo)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsListClusterTwo, pluginsInfoForCRsAppliedClusterTwo)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + + err = tf.ContextCmd.UseContext(contextNameClusterOne) + Expect(err).To(BeNil(), "there should not be any error for use context") + pluginsListClusterOne, err = tf.PluginCmd.ListPluginsForGivenContext(contextNameClusterOne, true) + Expect(err).To(BeNil(), "should not get any error for plugin list") + Expect(len(pluginsListClusterOne)).Should(Equal(len(pluginsInfoForCRsAppliedClusterOne)), "number of plugins should be same as number of plugins CRs applied") + Expect(framework.CheckAllPluginsExists(pluginsListClusterOne, pluginsInfoForCRsAppliedClusterOne)).Should(BeTrue(), " plugins being installed and plugins info for which CRs applied should be same") + }) + + // Test case: f. delete the KIND clusters + It("delete the KIND cluster", func() { + err = tf.ContextCmd.DeleteContext(contextNameClusterOne) + Expect(err).To(BeNil(), "context should be deleted without error") + err = tf.ContextCmd.DeleteContext(contextNameClusterTwo) + Expect(err).To(BeNil(), "context should be deleted without error") + _, err = tf.KindCluster.DeleteCluster(clusterOne.Name) + Expect(err).To(BeNil(), "kind cluster should be deleted without any error") + _, err = tf.KindCluster.DeleteCluster(clusterTwo.Name) + Expect(err).To(BeNil(), "kind cluster should be deleted without any error") + }) + }) +})