diff --git a/.gometalinter.json b/.gometalinter.json index bdeedebf8b..f1bcfbbc8a 100644 --- a/.gometalinter.json +++ b/.gometalinter.json @@ -12,6 +12,8 @@ "^pkg\/testutils\/(ginkgo|client).go", "^pkg\/testutils\/mockprovider\/mock_provider.go", "^pkg\/cfn\/template\/matchers\/matchers.go", + "^integration\/matchers\/matchers.go", + "^integration\/runner\/runner.go", "^pkg\/drain", "^vendor\/github.com\/awslabs\/goformation" ], diff --git a/Makefile b/Makefile index 03a65d274e..d477880a1b 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ unit-test-race: ## Run unit test with race detection .PHONY: build-integration-test build-integration-test: $(GENERATED_GO_FILES) ## Build integration test binary - time go test -tags integration ./integration/... -c -o eksctl-integration-test + time go test -tags integration ./integration/ -c -o eksctl-integration-test .PHONY: integration-test integration-test: build build-integration-test ## Run the integration tests (with cluster creation and cleanup) diff --git a/integration/common_test.go b/integration/common_test.go index 1b86811ee6..8819014787 100644 --- a/integration/common_test.go +++ b/integration/common_test.go @@ -3,17 +3,10 @@ package integration_test import ( - "fmt" - "os" - "os/exec" - "time" - harness "github.com/dlespiau/kube-test-harness" "github.com/dlespiau/kube-test-harness/logger" . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gexec" ) type tHelper struct{ GinkgoTInterface } @@ -35,59 +28,3 @@ func newKubeTest() (*harness.Test, error) { test.Setup() return test, nil } - -type params struct { - Args []string - Env []string -} - -func eksctl(params params) *gexec.Session { - command := exec.Command(eksctlPath, params.Args...) - params.Env = append(params.Env, "EKSCTL_EXPERIMENTAL=true") - command.Env = append(os.Environ(), params.Env...) - fmt.Fprintf(GinkgoWriter, "calling %q with %v and %v\n", eksctlPath, params.Env, params.Args) - session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) - Expect(err).To(BeNil()) - - t := time.Minute - switch params.Args[0] { - case "create": - t *= 25 - case "delete": - t *= 15 - case "get": - t *= 1 - case "scale": - t *= 5 - default: - t *= 30 - } - session.Wait(t) - return session -} - -func eksctlSuccess(args ...string) *gexec.Session { - return eksctlSuccessWith(params{Args: args}) -} - -func eksctlSuccessWith(params params) *gexec.Session { - session := eksctl(params) - Expect(session.ExitCode()).To(BeZero()) - return session -} - -func eksctlFail(args ...string) *gexec.Session { - session := eksctl(params{Args: args}) - Expect(session.ExitCode()).ToNot(BeZero()) - return session -} - -//eksctlStart starts running an eksctl command but doesn't wait for it to finish the command -//This is primarily so that we can run eksctl create and then subsequently call eksctl delete -//on the same cluster, but might be useful for other test scenarios as well. -func eksctlStart(args ...string) { - fmt.Fprintf(GinkgoWriter, "calling %q with %v\n", eksctlPath, args) - cmd := exec.Command(eksctlPath, args...) - err := cmd.Start() - Expect(err).To(BeNil()) -} diff --git a/integration/createdeletebeforeactive_test.go b/integration/createdeletebeforeactive_test.go index 9469bcc41c..c9e6729c2a 100644 --- a/integration/createdeletebeforeactive_test.go +++ b/integration/createdeletebeforeactive_test.go @@ -9,9 +9,10 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + . "github.com/weaveworks/eksctl/integration/matchers" + . "github.com/weaveworks/eksctl/integration/runner" + "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" - "github.com/weaveworks/eksctl/pkg/testutils/aws" - . "github.com/weaveworks/eksctl/pkg/testutils/matchers" ) const ( @@ -33,7 +34,8 @@ var _ = Describe("(Integration) Create & Delete before Active", func() { Context("when creating a new cluster", func() { It("should not return an error", func() { - eksctlStart("create", "cluster", + cmd := eksctlCreateCmd.WithArgs( + "cluster", "--verbose", "4", "--name", delBeforeActiveName, "--tags", "alpha.eksctl.io/description=eksctl delete before active test", @@ -44,10 +46,11 @@ var _ = Describe("(Integration) Create & Delete before Active", func() { "--region", region, "--version", version, ) + cmd.Start() }) It("should eventually show up as creating", func() { - awsSession := aws.NewSession(region) + awsSession := NewSession(region) Eventually(awsSession, timeOut, pollInterval).Should( HaveExistingCluster(delBeforeActiveName, awseks.ClusterStatusCreating, version)) }) @@ -55,17 +58,17 @@ var _ = Describe("(Integration) Create & Delete before Active", func() { Context("when deleting the cluster in process of being created", func() { It("deleting cluster should have a zero exitcode", func() { - eksctlSuccess("delete", "cluster", - "--verbose", "4", + cmd := eksctlDeleteClusterCmd.WithArgs( "--name", delBeforeActiveName, "--region", region, ) + Expect(cmd).To(RunSuccessfully()) }) }) Context("after the delete of the cluster in progress has been initiated", func() { It("should eventually delete the EKS cluster and both CloudFormation stacks", func() { - awsSession := aws.NewSession(region) + awsSession := NewSession(region) Eventually(awsSession, timeOut, pollInterval).ShouldNot( HaveExistingCluster(delBeforeActiveName, awseks.ClusterStatusActive, version)) Eventually(awsSession, timeOut, pollInterval).ShouldNot( @@ -77,11 +80,11 @@ var _ = Describe("(Integration) Create & Delete before Active", func() { Context("when trying to delete the cluster again", func() { It("should return an a non-zero exit code", func() { - eksctlFail("delete", "cluster", - "--verbose", "4", + cmd := eksctlDeleteClusterCmd.WithArgs( "--name", delBeforeActiveName, "--region", region, ) + Expect(cmd).To(RunSuccessfully()) }) }) }) diff --git a/integration/creategetdelete_test.go b/integration/creategetdelete_test.go index 77b09031d2..57dbaa42da 100644 --- a/integration/creategetdelete_test.go +++ b/integration/creategetdelete_test.go @@ -18,13 +18,14 @@ import ( "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/yaml" + . "github.com/weaveworks/eksctl/integration/matchers" + . "github.com/weaveworks/eksctl/integration/runner" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/authconfigmap" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" "github.com/weaveworks/eksctl/pkg/eks" "github.com/weaveworks/eksctl/pkg/iam" - "github.com/weaveworks/eksctl/pkg/testutils/aws" - . "github.com/weaveworks/eksctl/pkg/testutils/matchers" "github.com/weaveworks/eksctl/pkg/utils/file" ) @@ -62,12 +63,13 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { if !file.Exists(kubeconfigPath) { // Generate the Kubernetes configuration that eksctl create // would have generated otherwise: - eksctlSuccess("utils", "write-kubeconfig", + cmd := eksctlUtilsCmd.WithArgs( + "write-kubeconfig", "--verbose", "4", "--name", clusterName, - "--region", region, "--kubeconfig", kubeconfigPath, ) + Expect(cmd).To(RunSuccessfully()) } return } @@ -78,7 +80,8 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { clusterName = cmdutils.ClusterName("", "") } - eksctlSuccess("create", "cluster", + cmd := eksctlCreateCmd.WithArgs( + "cluster", "--verbose", "4", "--name", clusterName, "--tags", "alpha.eksctl.io/description=eksctl integration test", @@ -86,15 +89,14 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { "--node-labels", "ng-name="+initNG, "--node-type", "t2.medium", "--nodes", "1", - "--region", region, "--version", version, "--kubeconfig", kubeconfigPath, ) - + Expect(cmd).To(RunSuccessfully()) }) It("should have created an EKS cluster and two CloudFormation stacks", func() { - awsSession := aws.NewSession(region) + awsSession := NewSession(region) Expect(awsSession).To(HaveExistingCluster(clusterName, awseks.ClusterStatusActive, version)) @@ -116,8 +118,8 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { Context("and listing clusters", func() { It("should return the previously created cluster", func() { - cmdSession := eksctlSuccess("get", "clusters", "--all-regions") - Expect(string(cmdSession.Buffer().Contents())).To(ContainSubstring(clusterName)) + cmd := eksctlGetCmd.WithArgs("clusters", "--all-regions") + Expect(cmd).To(RunSuccessfullyWithOutputString(ContainSubstring(clusterName))) }) }) @@ -132,17 +134,15 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { assertFluxManifestsAbsentInGit(branch) assertFluxPodsAbsentInKubernetes(kubeconfigPath) - eksctlSuccessWith(params{ - Args: []string{"install", "flux", - "--git-url", Repository, - "--git-email", Email, - "--git-private-ssh-key-path", privateSSHKeyPath, - "--git-branch", branch, - "--name", clusterName, - "--region", region, - }, - Env: []string{"EKSCTL_EXPERIMENTAL=true"}, - }) + cmd := eksctlExperimentalCmd.WithArgs( + "install", "flux", + "--git-url", Repository, + "--git-email", Email, + "--git-private-ssh-key-path", privateSSHKeyPath, + "--git-branch", branch, + "--name", clusterName, + ) + Expect(cmd).To(RunSuccessfully()) assertFluxManifestsPresentInGit(branch) assertFluxPodsPresentInKubernetes(kubeconfigPath) @@ -163,8 +163,8 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { deleteFluxInstallation(kubeconfigPath) assertFluxPodsAbsentInKubernetes(kubeconfigPath) - eksctlSuccessWith(params{ - Args: []string{"gitops", "apply", + cmd := eksctlExperimentalCmd.WithArgs( + "gitops", "apply", "--git-url", Repository, "--git-email", Email, "--git-branch", branch, @@ -173,9 +173,8 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { "--quickstart-profile", "app-dev", "--cluster", clusterName, "--region", region, - }, - Env: []string{"EKSCTL_EXPERIMENTAL=true"}, - }) + ) + Expect(cmd).To(RunSuccessfully()) assertQuickStartComponentsPresentInGit(branch) assertFluxManifestsPresentInGit(branch) @@ -185,13 +184,12 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { Context("and scale the initial nodegroup", func() { It("should not return an error", func() { - eksctlSuccess("scale", "nodegroup", - "--verbose", "4", + cmd := eksctlScaleNodeGroupCmd.WithArgs( "--cluster", clusterName, - "--region", region, "--nodes", "4", "--name", initNG, ) + Expect(cmd).To(RunSuccessfully()) }) It("{FLAKY: https://github.com/weaveworks/eksctl/issues/717} should make it 4 nodes total", func() { @@ -211,45 +209,51 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { Context("and add the second nodegroup", func() { It("should not return an error", func() { - eksctlSuccess("create", "nodegroup", + cmd := eksctlCreateCmd.WithArgs( + "nodegroup", "--cluster", clusterName, - "--region", region, "--nodes", "4", "--node-private-networking", testNG, ) + Expect(cmd).To(RunSuccessfully()) }) It("should be able to list nodegroups", func() { { - cmdSession := eksctlSuccess("get", "nodegroup", + cmd := eksctlGetCmd.WithArgs( + "nodegroup", "--cluster", clusterName, "--region", region, initNG, ) - output := string(cmdSession.Buffer().Contents()) - Expect(output).To(ContainSubstring(initNG)) - Expect(output).ToNot(ContainSubstring(testNG)) - + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring(initNG)), + Not(ContainElement(ContainSubstring(testNG))), + )) } { - cmdSession := eksctlSuccess("get", "nodegroup", + cmd := eksctlGetCmd.WithArgs( + "nodegroup", "--cluster", clusterName, "--region", region, testNG, ) - output := string(cmdSession.Buffer().Contents()) - Expect(output).To(ContainSubstring(testNG)) - Expect(output).ToNot(ContainSubstring(initNG)) + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring(testNG)), + Not(ContainElement(ContainSubstring(initNG))), + )) } { - cmdSession := eksctlSuccess("get", "nodegroup", + cmd := eksctlGetCmd.WithArgs( + "nodegroup", "--cluster", clusterName, "--region", region, ) - output := string(cmdSession.Buffer().Contents()) - Expect(output).To(ContainSubstring(initNG)) - Expect(output).To(ContainSubstring(testNG)) + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring(testNG)), + ContainElement(ContainSubstring(initNG)), + )) } }) @@ -354,11 +358,13 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("should plan to enable two of the types using flags", func() { - eksctlSuccess("utils", "update-cluster-logging", + cmd := eksctlUtilsCmd.WithArgs( + "update-cluster-logging", "--name", clusterName, - "--region", region, "--enable-types", "api,controllerManager", ) + Expect(cmd).To(RunSuccessfully()) + enabled, disable, err := ctl.GetCurrentClusterConfigForLogging(cfg.Metadata) Expect(err).ShouldNot(HaveOccurred()) Expect(enabled.List()).To(HaveLen(0)) @@ -366,12 +372,14 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("should enable two of the types using flags", func() { - eksctlSuccess("utils", "update-cluster-logging", + cmd := eksctlUtilsCmd.WithArgs( + "update-cluster-logging", "--name", clusterName, - "--region", region, "--approve", "--enable-types", "api,controllerManager", ) + Expect(cmd).To(RunSuccessfully()) + enabled, disable, err := ctl.GetCurrentClusterConfigForLogging(cfg.Metadata) Expect(err).ShouldNot(HaveOccurred()) Expect(enabled.List()).To(HaveLen(2)) @@ -381,12 +389,14 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("should enable all of the types with --enable-types=all", func() { - eksctlSuccess("utils", "update-cluster-logging", + cmd := eksctlUtilsCmd.WithArgs( + "update-cluster-logging", "--name", clusterName, - "--region", region, "--approve", "--enable-types", "all", ) + Expect(cmd).To(RunSuccessfully()) + enabled, disable, err := ctl.GetCurrentClusterConfigForLogging(cfg.Metadata) Expect(err).ShouldNot(HaveOccurred()) Expect(enabled.List()).To(HaveLen(5)) @@ -394,13 +404,15 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("should enable all but one type", func() { - eksctlSuccess("utils", "update-cluster-logging", + cmd := eksctlUtilsCmd.WithArgs( + "update-cluster-logging", "--name", clusterName, - "--region", region, "--approve", "--enable-types", "all", "--disable-types", "controllerManager", ) + Expect(cmd).To(RunSuccessfully()) + enabled, disable, err := ctl.GetCurrentClusterConfigForLogging(cfg.Metadata) Expect(err).ShouldNot(HaveOccurred()) Expect(enabled.List()).To(HaveLen(4)) @@ -410,13 +422,15 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("should disable all but one type", func() { - eksctlSuccess("utils", "update-cluster-logging", + cmd := eksctlUtilsCmd.WithArgs( + "update-cluster-logging", "--name", clusterName, - "--region", region, "--approve", "--disable-types", "all", "--enable-types", "controllerManager", ) + Expect(cmd).To(RunSuccessfully()) + enabled, disable, err := ctl.GetCurrentClusterConfigForLogging(cfg.Metadata) Expect(err).ShouldNot(HaveOccurred()) Expect(disable.List()).To(HaveLen(4)) @@ -426,12 +440,14 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("should disable all of the types with --disable-types=all", func() { - eksctlSuccess("utils", "update-cluster-logging", + cmd := eksctlUtilsCmd.WithArgs( + "update-cluster-logging", "--name", clusterName, - "--region", region, "--approve", "--disable-types", "all", ) + Expect(cmd).To(RunSuccessfully()) + enabled, disable, err := ctl.GetCurrentClusterConfigForLogging(cfg.Metadata) Expect(err).ShouldNot(HaveOccurred()) Expect(enabled.List()).To(HaveLen(0)) @@ -473,111 +489,122 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { }) It("fails getting unknown mapping", func() { - eksctlFail("get", "iamidentitymapping", + cmd := eksctlGetCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", "idontexist", "-o", "yaml", ) + Expect(cmd).ToNot(RunSuccessfully()) }) It("creates mapping", func() { - eksctlSuccess("create", "iamidentitymapping", + createCmd := eksctlCreateCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role0.RoleARN, "--username", role0.Username, "--group", role0.Groups[0], "--group", role0.Groups[1], ) + Expect(createCmd).To(RunSuccessfully()) - get := eksctlSuccess("get", "iamidentitymapping", + getCmd := eksctlGetCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role0.RoleARN, "-o", "yaml", ) - Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0)) + Expect(getCmd).To(RunSuccessfullyWithOutputString(MatchYAML(exp0))) }) It("creates a duplicate mapping", func() { - eksctlSuccess("create", "iamidentitymapping", + createCmd := eksctlCreateCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role0.RoleARN, "--username", role0.Username, "--group", role0.Groups[0], "--group", role0.Groups[1], ) + Expect(createCmd).To(RunSuccessfully()) - get := eksctlSuccess("get", "iamidentitymapping", + getCmd := eksctlGetCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role0.RoleARN, "-o", "yaml", ) - Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0 + exp0)) + Expect(getCmd).To(RunSuccessfullyWithOutputString(MatchYAML(exp0 + exp0))) }) It("creates a duplicate mapping with different identity", func() { - eksctlSuccess("create", "iamidentitymapping", + createCmd := eksctlCreateCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role1.RoleARN, "--group", role1.Groups[0], ) + Expect(createCmd).To(RunSuccessfully()) - get := eksctlSuccess("get", "iamidentitymapping", + getCmd := eksctlGetCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role1.RoleARN, "-o", "yaml", ) - Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0 + exp0 + exp1)) + Expect(getCmd).To(RunSuccessfullyWithOutputString(MatchYAML(exp0 + exp0 + exp1))) }) It("deletes a single mapping fifo", func() { - eksctlSuccess("delete", "iamidentitymapping", + deleteCmd := eksctlDeleteCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role, ) + Expect(deleteCmd).To(RunSuccessfully()) - get := eksctlSuccess("get", "iamidentitymapping", + getCmd := eksctlGetCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, + "--role", role, "-o", "yaml", ) - Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0 + exp1)) + Expect(getCmd).To(RunSuccessfullyWithOutputString(MatchYAML(exp0 + exp1))) }) It("fails when deleting unknown mapping", func() { - eksctlFail("delete", "iamidentitymapping", + deleteCmd := eksctlDeleteCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", "idontexist", ) + Expect(deleteCmd).ToNot(RunSuccessfully()) }) It("deletes duplicate mappings with --all", func() { - eksctlSuccess("delete", "iamidentitymapping", + deleteCmd := eksctlDeleteCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role, "--all", ) - eksctlFail("get", "iamidentitymapping", + Expect(deleteCmd).To(RunSuccessfully()) + + getCmd := eksctlGetCmd.WithArgs( + "iamidentitymapping", "--name", clusterName, - "--region", region, "--role", role, "-o", "yaml", ) + Expect(getCmd).ToNot(RunSuccessfully()) }) }) Context("and delete the second nodegroup", func() { It("should not return an error", func() { - eksctlSuccess("delete", "nodegroup", + cmd := eksctlDeleteCmd.WithArgs( + "nodegroup", "--verbose", "4", "--cluster", clusterName, - "--region", region, testNG, ) + Expect(cmd).To(RunSuccessfully()) }) It("{FLAKY: https://github.com/weaveworks/eksctl/issues/717} should make it 4 nodes total", func() { @@ -599,13 +626,12 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { Context("and scale the initial nodegroup back to 1 node", func() { It("should not return an error", func() { - eksctlSuccess("scale", "nodegroup", - "--verbose", "4", + cmd := eksctlScaleNodeGroupCmd.WithArgs( "--cluster", clusterName, - "--region", region, "--nodes", "1", "--name", initNG, ) + Expect(cmd).To(RunSuccessfully()) }) It("{FLAKY: https://github.com/weaveworks/eksctl/issues/717} should make it 1 nodes total", func() { @@ -630,12 +656,11 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { Skip("will not delete cluster " + clusterName) } - eksctlSuccess("delete", "cluster", - "--verbose", "4", + cmd := eksctlDeleteClusterCmd.WithArgs( "--name", clusterName, - "--region", region, "--wait", ) + Expect(cmd).To(RunSuccessfully()) }) It("{FLAKY: https://github.com/weaveworks/eksctl/issues/536} should have deleted the EKS cluster and both CloudFormation stacks", func() { @@ -643,7 +668,7 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() { Skip("will not delete cluster " + clusterName) } - awsSession := aws.NewSession(region) + awsSession := NewSession(region) Expect(awsSession).ToNot(HaveExistingCluster(clusterName, awseks.ClusterStatusActive, version)) diff --git a/integration/gitopscreateprofile_test.go b/integration/gitopscreateprofile_test.go index 70ba1e050c..6d20653f00 100644 --- a/integration/gitopscreateprofile_test.go +++ b/integration/gitopscreateprofile_test.go @@ -8,9 +8,11 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/spf13/afero" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" + . "github.com/weaveworks/eksctl/integration/runner" ) var _ = Describe("(Integration) generate profile", func() { @@ -22,13 +24,14 @@ var _ = Describe("(Integration) generate profile", func() { clusterName = cmdutils.ClusterName("", "") } - eksctlSuccess("generate", "profile", + cmd := eksctlExperimentalCmd.WithArgs( + "generate", "profile", "--verbose", "4", "--name", clusterName, - "--region", region, "--git-url", "git@github.com:eksctl-bot/eksctl-profile-integration-tests.git", "--profile-path", testDirectory, ) + Expect(cmd).To(RunSuccessfully()) fs := afero.Afero{ Fs: afero.NewOsFs(), diff --git a/integration/integration_test.go b/integration/integration_test.go index ee085e4864..26f9f01880 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -6,7 +6,9 @@ import ( "flag" "fmt" "testing" + "time" + "github.com/weaveworks/eksctl/integration/runner" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/testutils" ) @@ -27,6 +29,12 @@ var ( testDirectory = "test_profile" // privateSSHKeyPath is the SSH key to use for Git operations. privateSSHKeyPath string + + eksctlCmd, eksctlCreateCmd, eksctlGetCmd, eksctlDeleteCmd runner.Cmd + + eksctlDeleteClusterCmd, eksctlScaleNodeGroupCmd runner.Cmd + + eksctlUtilsCmd, eksctlExperimentalCmd runner.Cmd ) const ( @@ -45,6 +53,37 @@ func init() { flag.BoolVar(&doDelete, "eksctl.delete", true, "Skip the cleanup after the tests have run") flag.StringVar(&kubeconfigPath, "eksctl.kubeconfig", "", "Path to kubeconfig (default: create it a temporary file)") flag.StringVar(&privateSSHKeyPath, "eksctl.git.sshkeypath", defaultPrivateSSHKeyPath, fmt.Sprintf("Path to the SSH key to use for Git operations (default: %s)", defaultPrivateSSHKeyPath)) + + eksctlCmd = runner.NewCmd(eksctlPath). + WithArgs("--region", region). + WithTimeout(30 * time.Minute) + + eksctlCreateCmd = eksctlCmd. + WithArgs("create"). + WithTimeout(25 * time.Minute) + + eksctlGetCmd = eksctlCmd. + WithArgs("get"). + WithTimeout(1 * time.Minute) + + eksctlDeleteCmd = eksctlCmd. + WithArgs("delete"). + WithTimeout(15 * time.Minute) + + eksctlDeleteClusterCmd = eksctlDeleteCmd. + WithArgs("cluster", "--verbose", "4") + + eksctlScaleNodeGroupCmd = eksctlCmd. + WithArgs("scale", "nodegroup", "--verbose", "4"). + WithTimeout(5 * time.Minute) + + eksctlUtilsCmd = eksctlCmd. + WithArgs("utils"). + WithTimeout(5 * time.Minute) + + eksctlExperimentalCmd = eksctlCmd. + WithEnv("EKSCTL_EXPERIMENTAL=true") + } func TestSuite(t *testing.T) { diff --git a/pkg/testutils/aws/cloudformation.go b/integration/matchers/cloudformation.go similarity index 60% rename from pkg/testutils/aws/cloudformation.go rename to integration/matchers/cloudformation.go index 67fd2af94b..9799a1796f 100644 --- a/pkg/testutils/aws/cloudformation.go +++ b/integration/matchers/cloudformation.go @@ -1,4 +1,4 @@ -package aws +package matchers import ( "fmt" @@ -13,8 +13,8 @@ const ( errorMessageTemplate = "Stack with id %s does not exist" ) -// StackExists checks to see if a CloudFormation stack exists -func StackExists(stackName string, session *session.Session) (bool, error) { +// stackExists checks to see if a CloudFormation stack exists +func stackExists(stackName string, session *session.Session) (bool, error) { cfn := cloudformation.New(session) input := &cloudformation.ListStackResourcesInput{ @@ -34,17 +34,3 @@ func StackExists(stackName string, session *session.Session) (bool, error) { return true, nil } - -// DeleteStack deletes a cloudformation stack -func DeleteStack(stackName string, session *session.Session) error { - cfn := cloudformation.New(session) - - input := &cloudformation.DeleteStackInput{ - StackName: aws.String(stackName), - } - - _, err := cfn.DeleteStack(input) - - return err - -} diff --git a/pkg/testutils/matchers/have_eks_cluster_matcher.go b/integration/matchers/matchers.go similarity index 69% rename from pkg/testutils/matchers/have_eks_cluster_matcher.go rename to integration/matchers/matchers.go index 013e216731..ecf3d1e98f 100644 --- a/pkg/testutils/matchers/have_eks_cluster_matcher.go +++ b/integration/matchers/matchers.go @@ -10,9 +10,46 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + awseks "github.com/aws/aws-sdk-go/service/eks" ) +// HaveExistingStack returns a GoMega matcher that will check for the existence of an cloudformation stack +func HaveExistingStack(expectedStackName string) types.GomegaMatcher { + return &existingStack{expectedStackName: expectedStackName} +} + +type existingStack struct { + expectedStackName string + stackNotFound bool +} + +func (m *existingStack) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return false, errors.New("input is nil") + } + + if reflect.TypeOf(actual).String() != "*session.Session" { + return false, errors.New("not a AWS session") + } + + found, err := stackExists(m.expectedStackName, actual.(*session.Session)) + if err != nil { + return false, err + } + + m.stackNotFound = !found + return found, nil +} + +func (m *existingStack) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected to find a Cloudformation stack named %s but it was NOT found", m.expectedStackName) +} + +func (m *existingStack) NegatedFailureMessage(_ interface{}) (message string) { + return fmt.Sprintf("Expected NOT to find a Cloudformation stack named %s but it was found", m.expectedStackName) +} + // HaveExistingCluster returns a GoMega matcher that will check for the existence of an EKS cluster func HaveExistingCluster(expectedName string, expectedStatus string, expectedVersion string) types.GomegaMatcher { return &existingCluster{expectedName: expectedName, expectedStatus: expectedStatus, expectedVersion: expectedVersion} @@ -82,7 +119,7 @@ func (m *existingCluster) FailureMessage(actual interface{}) (message string) { return fmt.Sprintf("Expected EKS cluster version: %s to equal actual EKS cluster version: %s", m.expectedVersion, m.actualVersion) } - return fmt.Sprintf("Expected to find a cluster named %s but it wasn't found", m.expectedName) + return fmt.Sprintf("Expected to find a cluster named %s but it was NOT found", m.expectedName) } func (m *existingCluster) NegatedFailureMessage(_ interface{}) (message string) { @@ -93,5 +130,5 @@ func (m *existingCluster) NegatedFailureMessage(_ interface{}) (message string) return fmt.Sprintf("Expected EKS cluster version: %s NOT to equal actual EKS cluster version: %s", m.expectedVersion, m.actualVersion) } - return fmt.Sprintf("Expected NOT to find a cluster named %s but it found", m.expectedName) + return fmt.Sprintf("Expected NOT to find a cluster named %s but it was found", m.expectedName) } diff --git a/pkg/testutils/aws/session.go b/integration/matchers/session.go similarity index 95% rename from pkg/testutils/aws/session.go rename to integration/matchers/session.go index bfe21d7b1a..e61ca0a9b0 100644 --- a/pkg/testutils/aws/session.go +++ b/integration/matchers/session.go @@ -1,4 +1,4 @@ -package aws +package matchers import ( "github.com/aws/aws-sdk-go/aws" diff --git a/integration/runner/runner.go b/integration/runner/runner.go new file mode 100644 index 0000000000..89efa7da7b --- /dev/null +++ b/integration/runner/runner.go @@ -0,0 +1,205 @@ +package runner + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + . "github.com/onsi/ginkgo" + + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/types" +) + +// Cmd holds definition of a command +type Cmd struct { + execPath string + args []string + env []string + cleanEnv bool + timeout time.Duration +} + +// NewCmd constructs a new command +func NewCmd(execPath string) Cmd { + return Cmd{ + execPath: execPath, + timeout: 20 * time.Second, + } +} + +// NewCmd presents a the command as a string; each argument is quoted, +// so that the command can be copied and called from a shell +func (c Cmd) String() string { + s := c.execPath + for _, arg := range c.args { + s += fmt.Sprintf(" %q", arg) + } + return s +} + +// WithArgs returns a copy of the command with new arguments +func (c Cmd) WithArgs(args ...string) Cmd { + c.args = append(c.args, args...) + return c +} + +// WithEnv returns a copy of the command with new environment variables +func (c Cmd) WithEnv(env ...string) Cmd { + c.env = append(c.env, env...) + return c +} + +// WithCleanEnv returns a copy of the command with all environment variables +// reset (including OS environment variables) +func (c Cmd) WithCleanEnv() Cmd { + c.env = []string{} + c.cleanEnv = true + return c +} + +// WithTimeout returns a copy of the command with new timeout value +func (c Cmd) WithTimeout(timeout time.Duration) Cmd { + c.timeout = timeout + return c +} + +// Start the command and returns underlying session +func (c Cmd) Start() *gexec.Session { + command := exec.Command(c.execPath, c.args...) + + if c.cleanEnv { + command.Env = []string{} + } else { + command.Env = os.Environ() + } + command.Env = append(command.Env, c.env...) + + fmt.Fprintf(GinkgoWriter, "starting '%s'\n", c.String()) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + if err != nil { + Fail(fmt.Sprintf("error starting process: %v\n", err), 1) + } + + return session +} + +// Run the command and wait for it to return +func (c Cmd) Run() *gexec.Session { + session := c.Start() + session.Wait(c.timeout) + return session +} + +type runCmdMatcher struct { + session *gexec.Session + args []string + + failedCmd string +} + +func (m *runCmdMatcher) run(cmd Cmd) bool { + m.session = cmd.Run() + if m.session.ExitCode() == 0 { + return true + } + m.failedCmd = cmd.String() + return false +} + +// RunSuccessfully matches successful excution of a command +func RunSuccessfully() types.GomegaMatcher { + return &runCmdMatcher{} +} + +func (m *runCmdMatcher) Match(actual interface{}) (bool, error) { + switch actual.(type) { + case Cmd: + return m.run(actual.(Cmd)), nil + case []Cmd: + for _, cmd := range actual.([]Cmd) { + if !m.run(cmd) { + return false, nil + } + } + return true, nil + default: + return false, fmt.Errorf("not a Cmd or []Cmd") + } +} + +func (m *runCmdMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf("Expected '%s' to succeed, got return code %d", m.failedCmd, m.session.ExitCode()) +} + +func (m *runCmdMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf("Expected command NOT to succeed") +} + +type runCmdOutputMatcher struct { + outputMatchers []types.GomegaMatcher + failureMessage string + splitLines bool + *runCmdMatcher +} + +// RunSuccessfullyWithOutputString matches successful excution of a command and passes the output string +// to another matcher (the string will include stdout and stderr) +func RunSuccessfullyWithOutputString(outputMatchers ...types.GomegaMatcher) types.GomegaMatcher { + return &runCmdOutputMatcher{ + outputMatchers: outputMatchers, + splitLines: false, + runCmdMatcher: &runCmdMatcher{}, + } +} + +// RunSuccessfullyWithOutputStringLines matches successful excution of a command and passes the output string +// to another matcher split into lines (the string will include stdout and stderr) +func RunSuccessfullyWithOutputStringLines(outputMatchers ...types.GomegaMatcher) types.GomegaMatcher { + return &runCmdOutputMatcher{ + outputMatchers: outputMatchers, + splitLines: true, + runCmdMatcher: &runCmdMatcher{}, + } +} + +func (m *runCmdOutputMatcher) Match(actual interface{}) (bool, error) { + cmd, ok := actual.(Cmd) + if !ok { + return false, fmt.Errorf("not a Cmd") + } + + if !m.runCmdMatcher.run(cmd) { + m.failureMessage = m.runCmdMatcher.FailureMessage(nil) + return false, nil + } + + outputString := string(m.runCmdMatcher.session.Buffer().Contents()) + + var output interface{} + if m.splitLines { + output = strings.Split(outputString, "\n") + } else { + output = string(outputString) + } + + for _, outputMatcher := range m.outputMatchers { + if ok, err := outputMatcher.Match(output); !ok { + m.failureMessage = outputMatcher.FailureMessage(output) + return false, err + } + } + + return true, nil +} + +func (m *runCmdOutputMatcher) FailureMessage(_ interface{}) string { + return m.failureMessage +} + +func (m *runCmdOutputMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf("Expected command NOT to succeed") +} diff --git a/integration/runner/runner_suite_test.go b/integration/runner/runner_suite_test.go new file mode 100644 index 0000000000..01838c0f5d --- /dev/null +++ b/integration/runner/runner_suite_test.go @@ -0,0 +1,11 @@ +package runner + +import ( + "testing" + + "github.com/weaveworks/eksctl/pkg/testutils" +) + +func TestSuite(t *testing.T) { + testutils.RegisterAndRun(t) +} diff --git a/integration/runner/runner_test.go b/integration/runner/runner_test.go new file mode 100644 index 0000000000..19e73e3e2a --- /dev/null +++ b/integration/runner/runner_test.go @@ -0,0 +1,87 @@ +// +build !windows + +package runner + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("command runner", func() { + It("can declare commands", func() { + c1 := NewCmd("foo") + + c2 := c1.WithArgs("bar1", "bar2") + + c3 := c2.WithArgs("bar3").WithEnv("bar=3") + + c4 := c3.WithCleanEnv() + + Expect(c1.execPath).To(Equal("foo")) + Expect(c1.args).To(BeEmpty()) + Expect(c1.String()).To(Equal("foo")) + + Expect(c2.execPath).To(Equal("foo")) + Expect(c2.args).To(ConsistOf("bar1", "bar2")) + + Expect(c3.execPath).To(Equal("foo")) + Expect(c3.args).To(ConsistOf("bar1", "bar2", "bar3")) + + Expect(c3.String()).To(Equal(`foo "bar1" "bar2" "bar3"`)) + + Expect(c3.cleanEnv).To(BeFalse()) + Expect(c3.env).To(ConsistOf("bar=3")) + + Expect(c4.cleanEnv).To(BeTrue()) + Expect(c4.env).To(BeEmpty()) + }) + + It("can run commands with env vars and match output", func() { + c4 := NewCmd("env").WithEnv("FOO=bar") + + Expect(c4).To(RunSuccessfullyWithOutputString(ContainSubstring("FOO=bar\n"))) + Expect(c4).To(RunSuccessfullyWithOutputString(ContainSubstring("PATH="))) + + Expect(c4).To(RunSuccessfullyWithOutputStringLines( + ContainElement(MatchRegexp(".*")), + ContainElement("FOO=bar"), + Not(ContainElement("FOO=baz")), + ContainElement(MatchRegexp("PATH=.*")), + )) + + c5 := c4.WithCleanEnv().WithEnv("BAR=foo") + + Expect(c5).To(RunSuccessfullyWithOutputString(Equal("BAR=foo\n"))) + Expect(c5).To(RunSuccessfullyWithOutputString(Not(ContainSubstring("PATH=")))) + + Expect(c5).To(RunSuccessfullyWithOutputStringLines( + ConsistOf("BAR=foo", ""), + )) + + c6 := NewCmd("echo").WithArgs("{}") + + Expect(c6).To(RunSuccessfullyWithOutputString(MatchJSON("{}"))) + }) + + It("can run multiple commands", func() { + willSucceed := []Cmd{ + NewCmd("echo"), + NewCmd("true"), + } + + Expect(willSucceed).To(RunSuccessfully()) + + willFail := []Cmd{ + NewCmd("true"), + NewCmd("false"), + } + Expect(willFail).ToNot(RunSuccessfully()) + }) + + It("can start a command and interrupt it", func() { + session := NewCmd("sleep").WithArgs("20").Start() + Expect(session.Command.Process).ToNot(BeNil()) + session.Interrupt().Wait() + Expect(session.ExitCode()).ToNot(BeZero()) + }) +}) diff --git a/pkg/testutils/matchers/have_cfn_stack_matcher.go b/pkg/testutils/matchers/have_cfn_stack_matcher.go deleted file mode 100644 index 35c744ac11..0000000000 --- a/pkg/testutils/matchers/have_cfn_stack_matcher.go +++ /dev/null @@ -1,48 +0,0 @@ -package matchers - -import ( - "errors" - "fmt" - "reflect" - - "github.com/aws/aws-sdk-go/aws/session" - "github.com/onsi/gomega/types" - "github.com/weaveworks/eksctl/pkg/testutils/aws" -) - -// HaveExistingStack returns a GoMega matcher that will check for the existence of an cloudformation stack -func HaveExistingStack(expectedStackName string) types.GomegaMatcher { - return &existingStack{expectedStackName: expectedStackName} -} - -type existingStack struct { - expectedStackName string - stackNotFound bool -} - -func (m *existingStack) Match(actual interface{}) (success bool, err error) { - if actual == nil { - return false, errors.New("input is nil") - } - - if reflect.TypeOf(actual).String() != "*session.Session" { - return false, errors.New("not a AWS session") - } - - found, err := aws.StackExists(m.expectedStackName, actual.(*session.Session)) - - if err != nil { - return false, err - } - - m.stackNotFound = !found - return found, nil -} - -func (m *existingStack) FailureMessage(actual interface{}) (message string) { - return fmt.Sprintf("Expected to find a Cloudformation stack named %s but it wasn't found", m.expectedStackName) -} - -func (m *existingStack) NegatedFailureMessage(_ interface{}) (message string) { - return fmt.Sprintf("Expected NOT to find a Cloudformation stack named %s but it found", m.expectedStackName) -}