diff --git a/.travis.yml b/.travis.yml index cd04cb1d4c3..077618b72a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,13 +18,17 @@ git: go_import_path: sigs.k8s.io/kubebuilder +services: +- docker + before_install: - go get -u github.com/golang/dep/cmd/dep - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then curl -sL https://github.com/git-lfs/git-lfs/releases/download/v2.7.2/git-lfs-darwin-amd64-v2.7.2.tar.gz | tar -xz git-lfs; fi +- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then GO111MODULE=on scripts/install_and_setup.sh; fi before_script: - - git lfs install - - git lfs pull +- git lfs install +- git lfs pull # Install must be set to prevent default `go get` to run. # The dependencies have already been vendored by `dep` so @@ -33,6 +37,7 @@ install: - script: +- if [ "$TRAVIS_OS_NAME" = "linux" ]; then GO111MODULE=on TRACE=1 PATH=$PATH:$(pwd) ./test_e2e.sh ; fi - GO111MODULE=on TRACE=1 ./test.sh - ./scripts/install_test.sh diff --git a/common.sh b/common.sh index 2d292ff22f1..2ac8f661c9a 100644 --- a/common.sh +++ b/common.sh @@ -157,6 +157,7 @@ function setup_envs { export TEST_ASSET_KUBE_APISERVER=$tmp_root/kubebuilder/bin/kube-apiserver export TEST_ASSET_ETCD=$tmp_root/kubebuilder/bin/etcd export TEST_DEP=$tmp_root/kubebuilder/init_project + export KUBECONFIG="$(kind get kubeconfig-path --name="kind")" } function restore_go_deps { diff --git a/generate_vendor.sh b/generate_vendor.sh index 99287c39f72..2b1d125a152 100755 --- a/generate_vendor.sh +++ b/generate_vendor.sh @@ -58,4 +58,4 @@ generate_vendor() { } build_kb && \ -generate_vendor $1 +generate_vendor 1 diff --git a/go.mod b/go.mod index 4a6c8e54bce..bbb594bfd21 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/afero v1.2.2 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 - golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c // indirect + golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect golang.org/x/sys v0.0.0-20190506115046-ca7f33d4116e // indirect golang.org/x/text v0.3.2 // indirect golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c diff --git a/go.sum b/go.sum index defdc58da21..b44ec55826e 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190514140710-3ec191127204 h1:4yG6GqBtw9C+UrLp6s2wtSniayy/Vd/3F7ffLE427XI= +golang.org/x/net v0.0.0-20190514140710-3ec191127204/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/scaffold/v1/manager/config.go b/pkg/scaffold/v1/manager/config.go index 3d9add84a90..9de6602b8bb 100644 --- a/pkg/scaffold/v1/manager/config.go +++ b/pkg/scaffold/v1/manager/config.go @@ -89,7 +89,6 @@ spec: - command: - /manager image: {{ .Image }} - imagePullPolicy: Always name: manager env: - name: POD_NAMESPACE diff --git a/pkg/scaffold/v2/manager/config.go b/pkg/scaffold/v2/manager/config.go index 351edb388c8..578840b6c28 100644 --- a/pkg/scaffold/v2/manager/config.go +++ b/pkg/scaffold/v2/manager/config.go @@ -89,7 +89,6 @@ spec: - command: - /manager image: {{ .Image }} - imagePullPolicy: Always name: manager resources: limits: diff --git a/scripts/install_and_setup.sh b/scripts/install_and_setup.sh new file mode 100755 index 00000000000..bb970163e8c --- /dev/null +++ b/scripts/install_and_setup.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +go get sigs.k8s.io/kind@v0.3.0 +# TODO(mengqiy): use a released version after v2.1.0 is out. +go get sigs.k8s.io/kustomize@f9c631e9eec7a2d6e46eb9e1bf5122f68b97d12d + +# You can use --image flag to specify the cluster version you want, e.g --image=kindest/node:v1.13.6, the supported version are listed at https://hub.docker.com/r/kindest/node/tags +kind create cluster --config test/kind-config.yaml --image=kindest/node:v1.14.1 diff --git a/test/e2e/config.go b/test/e2e/config.go new file mode 100644 index 00000000000..918d246c4c5 --- /dev/null +++ b/test/e2e/config.go @@ -0,0 +1,50 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "path/filepath" +) + +// runtime config specified to run e2e tests +type config struct { + domain string + group string + version string + kind string + controllerImageName string + workDir string +} + +// configWithSuffix init with a random suffix for test config stuff, +// to avoid conflict when running tests synchronously. +func configWithSuffix(testSuffix string) (*config, error) { + testGroup := "bar" + testSuffix + path, err := filepath.Abs("e2e-" + testSuffix) + if err != nil { + return nil, err + } + + return &config{ + domain: "example.com" + testSuffix, + group: testGroup, + version: "v1alpha1", + kind: "Foo" + testSuffix, + controllerImageName: "e2e-test/controller-manager:" + testSuffix, + workDir: path, + }, nil +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 00000000000..8f31c99398a --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + fmt.Fprintf(GinkgoWriter, "Starting kubebuilder suite\n") + RunSpecs(t, "Kubebuilder e2e suite") +} diff --git a/test/e2e/e2ev1.go b/test/e2e/e2ev1.go new file mode 100644 index 00000000000..65576d2899a --- /dev/null +++ b/test/e2e/e2ev1.go @@ -0,0 +1,179 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("kubebuilder", func() { + Context("with v1 scaffolding", func() { + imageName := "controller:v0.0.1" + var testSuffix string + var c *config + var kbTest *kubebuilderTest + + BeforeEach(func() { + var err error + testSuffix, err = randomSuffix() + Expect(err).NotTo(HaveOccurred()) + c, err = configWithSuffix(testSuffix) + Expect(err).NotTo(HaveOccurred()) + kbTest = &kubebuilderTest{ + Dir: c.workDir, + Env: []string{"GO111MODULE=off"}, + } + prepare(c.workDir) + }) + + AfterEach(func() { + By("clean up created API objects during test process") + resources, err := kbTest.RunKustomizeCommand("build", filepath.Join("config", "default")) + if err != nil { + fmt.Fprintf(GinkgoWriter, "error when running kustomize build during cleaning up: %v\n", err) + } + if _, err = kbTest.RunKubectlCommandWithInput(resources, "delete", "--recursive", "-f", "-"); err != nil { + fmt.Fprintf(GinkgoWriter, "error when running kubectl delete during cleaning up: %v\n", err) + } + if _, err = kbTest.RunKubectlCommand( + "delete", "--recursive", + "-f", filepath.Join("config", "crds"), + ); err != nil { + fmt.Fprintf(GinkgoWriter, "error when running kubectl delete during cleaning up crd: %v\n", err) + } + + By("remove container image created during test") + kbTest.CleanupImage(c.controllerImageName) + + By("remove test work dir") + os.RemoveAll(c.workDir) + }) + + It("should generate a runnable project", func() { + // prepare v1 vendor + By("untar the vendor tarball") + cmd := exec.Command("tar", "-zxf", "../../../testdata/vendor.v1.tgz") + cmd.Dir = c.workDir + err := cmd.Run() + Expect(err).Should(Succeed()) + + var controllerPodName string + + By("init v1 project") + err = kbTest.Init( + "--project-version", "1", + "--domain", c.domain, + "--dep=false") + Expect(err).Should(Succeed()) + + By("creating api definition") + err = kbTest.CreateAPI( + "--group", c.group, + "--version", c.version, + "--kind", c.kind, + "--namespaced", + "--resource", + "--controller", + "--make=false") + Expect(err).Should(Succeed()) + + By("creating core-type resource controller") + err = kbTest.CreateAPI( + "--group", "apps", + "--version", "v1", + "--kind", "Deployment", + "--namespaced", + "--resource=false", + "--controller", + "--make=false") + Expect(err).Should(Succeed()) + + By("building image") + err = kbTest.Make("docker-build", "IMG="+imageName) + Expect(err).Should(Succeed()) + + By("loading docker image into kind cluster") + err = kbTest.LoadImageToKindCluster(imageName) + Expect(err).Should(Succeed()) + + // NOTE: If you want to run the test against a GKE cluster, you will need to grant yourself permission. + // Otherwise, you may see "... is forbidden: attempt to grant extra privileges" + // $ kubectl create clusterrolebinding myname-cluster-admin-binding --clusterrole=cluster-admin --user=myname@mycompany.com + // https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control + By("deploying controller manager") + err = kbTest.Make("deploy") + Expect(err).Should(Succeed()) + + By("validate the controller-manager pod running as expected") + verifyControllerUp := func() error { + // Get pod name + podOutput, err := kbTest.RunKubectlGetPodsInNamespace( + testSuffix, + "-l", "control-plane=controller-manager", + "-o", "go-template={{ range .items }}{{ if not .metadata.deletionTimestamp }}{{ .metadata.name }}{{ \"\\n\" }}{{ end }}{{ end }}", + ) + Expect(err).NotTo(HaveOccurred()) + podNames := getNonEmptyLines(podOutput) + if len(podNames) != 1 { + return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) + } + controllerPodName = podNames[0] + Expect(controllerPodName).Should(ContainSubstring("controller-manager")) + + // Validate pod status + status, err := kbTest.RunKubectlGetPodsInNamespace( + testSuffix, + controllerPodName, "-o", "jsonpath={.status.phase}", + ) + Expect(err).NotTo(HaveOccurred()) + if status != "Running" { + return fmt.Errorf("controller pod in %s status", status) + } + + return nil + } + Eventually(verifyControllerUp, 2*time.Minute, time.Second).Should(Succeed()) + + By("creating an instance of CR") + inputFile := filepath.Join("config", "samples", fmt.Sprintf("%s_%s_%s.yaml", c.group, c.version, strings.ToLower(c.kind))) + _, err = kbTest.RunKubectlCommand("apply", "-f", inputFile) + Expect(err).NotTo(HaveOccurred()) + + By("validate the created resource object gets reconciled in controller") + controllerContainerLogs := func() string { + // Check container log to validate that the created resource object gets reconciled in controller + logOutput, err := kbTest.RunKubectlCommand( + "logs", controllerPodName, + "-c", "manager", + "-n", fmt.Sprintf("e2e-%s-system", testSuffix), + ) + Expect(err).NotTo(HaveOccurred()) + + return logOutput + } + Eventually(controllerContainerLogs, 2*time.Minute, time.Second).Should(ContainSubstring("Updating")) + }) + }) +}) diff --git a/test/e2e/kubebuildertest.go b/test/e2e/kubebuildertest.go new file mode 100644 index 00000000000..b5eead0765a --- /dev/null +++ b/test/e2e/kubebuildertest.go @@ -0,0 +1,105 @@ +package e2e + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo" +) + +// kubebuilderTest contains context to run commands for kubebuilder +type kubebuilderTest struct { + Dir string + // environment variables in k=v format. + Env []string +} + +// Init is for running `kubebuilder init` +func (kt *kubebuilderTest) Init(initOptions ...string) error { + initOptions = append([]string{"init"}, initOptions...) + cmd := exec.Command("kubebuilder", initOptions...) + _, err := kt.runCommand(cmd) + return err +} + +// CreateAPI is for running `kubebuilder create api` +func (kt *kubebuilderTest) CreateAPI(resourceOptions ...string) error { + resourceOptions = append([]string{"create", "api"}, resourceOptions...) + cmd := exec.Command("kubebuilder", resourceOptions...) + _, err := kt.runCommand(cmd) + return err +} + +// Make is for running `make` with various targets +func (kt *kubebuilderTest) Make(makeOptions ...string) error { + cmd := exec.Command("make", makeOptions...) + _, err := kt.runCommand(cmd) + return err +} + +// CleanupImage is for cleaning up the docker images for testing +func (kt *kubebuilderTest) CleanupImage(imageOptions ...string) error { + imageOptions = append([]string{"rmi", "-f"}, imageOptions...) + cmd := exec.Command("docker", imageOptions...) + _, err := kt.runCommand(cmd) + return err +} + +// RunKubectlCommand is a general func to run kubectl commands +func (kt *kubebuilderTest) RunKubectlCommand(cmdOptions ...string) (string, error) { + cmd := exec.Command("kubectl", cmdOptions...) + output, err := kt.runCommand(cmd) + return string(output), err +} + +// RunKubectlCommandWithInput is a general func to run kubectl commands with input +func (kt *kubebuilderTest) RunKubectlCommandWithInput(stdinInput string, cmdOptions ...string) (string, error) { + cmd := exec.Command("kubectl", cmdOptions...) + stdin, err := cmd.StdinPipe() + if err != nil { + return "", err + } + go func() { + defer stdin.Close() + io.WriteString(stdin, stdinInput) + }() + output, err := kt.runCommand(cmd) + return string(output), err +} + +// RunKubectlGetPodsInNamespace is a func to run kubectl get pods -n commands +func (kt *kubebuilderTest) RunKubectlGetPodsInNamespace(testSuffix string, cmdOptions ...string) (string, error) { + getPodsOptions := []string{"get", "pods", "-n", fmt.Sprintf("e2e-%s-system", testSuffix)} + return kt.RunKubectlCommand(append(getPodsOptions, cmdOptions...)...) +} + +// LoadImageToKindCluster loads a local docker image to the kind cluster +func (kt *kubebuilderTest) LoadImageToKindCluster(imageName string) error { + kindOptions := []string{"load", "docker-image", imageName} + cmd := exec.Command("kind", kindOptions...) + _, err := kt.runCommand(cmd) + return err +} + +// RunKustomizeCommand is a general func to run kustomize commands +func (kt *kubebuilderTest) RunKustomizeCommand(kustomizeOptions ...string) (string, error) { + cmd := exec.Command("kustomize", kustomizeOptions...) + output, err := kt.runCommand(cmd) + return string(output), err +} + +func (kt *kubebuilderTest) runCommand(cmd *exec.Cmd) ([]byte, error) { + cmd.Dir = kt.Dir + cmd.Env = append(os.Environ(), kt.Env...) + command := strings.Join(cmd.Args, " ") + fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + output, err := cmd.CombinedOutput() + if err != nil { + return output, fmt.Errorf("%s failed with error: %s", command, string(output)) + } + + return output, nil +} diff --git a/test/e2e/util.go b/test/e2e/util.go new file mode 100644 index 00000000000..aed94dc3780 --- /dev/null +++ b/test/e2e/util.go @@ -0,0 +1,62 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "crypto/rand" + "math/big" + "os" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +// randomSuffix returns a 4-letter string. +func randomSuffix() (string, error) { + source := []rune("abcdefghijklmnopqrstuvwxyz") + res := make([]rune, 4) + for i := range res { + bi := new(big.Int) + r, err := rand.Int(rand.Reader, bi.SetInt64(int64(len(source)))) + if err != nil { + return "", err + } + res[i] = source[r.Int64()] + } + return string(res), nil +} + +// getNonEmptyLines converts given command output string into individual objects +// according to line breakers, and ignores the empty elements in it. +func getNonEmptyLines(output string) []string { + var res []string + elements := strings.Split(output, "\n") + for _, element := range elements { + if element != "" { + res = append(res, element) + } + } + + return res +} + +func prepare(workDir string) { + By("create a path under given project dir, as the test work dir") + err := os.MkdirAll(workDir, 0755) + Expect(err).NotTo(HaveOccurred()) +} diff --git a/test/kind-config.yaml b/test/kind-config.yaml new file mode 100644 index 00000000000..3c696b75df6 --- /dev/null +++ b/test/kind-config.yaml @@ -0,0 +1,6 @@ +kind: Cluster +apiVersion: kind.sigs.k8s.io/v1alpha3 +nodes: + - role: control-plane + - role: worker + - role: worker diff --git a/test_e2e.sh b/test_e2e.sh new file mode 100755 index 00000000000..00fbf6e58d7 --- /dev/null +++ b/test_e2e.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Copyright 2018 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +source common.sh + +fetch_tools +build_kb + +setup_envs + +docker pull gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0 +kind load docker-image gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0 + +go test ./test/e2e diff --git a/testdata/gopath/src/project/config/manager/manager.yaml b/testdata/gopath/src/project/config/manager/manager.yaml index 3ddc1ac1793..2e57718d1dc 100644 --- a/testdata/gopath/src/project/config/manager/manager.yaml +++ b/testdata/gopath/src/project/config/manager/manager.yaml @@ -47,7 +47,6 @@ spec: - command: - /manager image: controller:latest - imagePullPolicy: Always name: manager env: - name: POD_NAMESPACE diff --git a/testdata/project-v2/config/manager/manager.yaml b/testdata/project-v2/config/manager/manager.yaml index fe765f55dd9..abb214322e9 100644 --- a/testdata/project-v2/config/manager/manager.yaml +++ b/testdata/project-v2/config/manager/manager.yaml @@ -47,7 +47,6 @@ spec: - command: - /manager image: controller:latest - imagePullPolicy: Always name: manager resources: limits: