diff --git a/.travis.yml b/.travis.yml index 121f0956..0d3f871e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,9 @@ script: - hack/verify-boilerplate.sh - hack/verify-gofmt.sh - hack/run-lint.sh -- go test -v -coverprofile=coverage.txt -covermode=atomic ./... +- go test -v -short -coverprofile=coverage.txt -covermode=atomic ./... - hack/make-all.sh +- hack/run-integration-tests.sh after_success: - bash <(curl -s https://codecov.io/bash) env: diff --git a/cmd/krew/cmd/update.go b/cmd/krew/cmd/update.go index 995d05a3..02622969 100644 --- a/cmd/krew/cmd/update.go +++ b/cmd/krew/cmd/update.go @@ -19,15 +19,12 @@ import ( "os" "github.com/golang/glog" - "sigs.k8s.io/krew/pkg/gitutil" - "github.com/pkg/errors" "github.com/spf13/cobra" + "sigs.k8s.io/krew/pkg/constants" + "sigs.k8s.io/krew/pkg/gitutil" ) -// IndexURI points to the upstream index. -const IndexURI = "https://github.com/kubernetes-sigs/krew-index.git" - // updateCmd represents the update command var updateCmd = &cobra.Command{ Use: "update", @@ -45,7 +42,7 @@ Remarks: func ensureIndexUpdated(_ *cobra.Command, _ []string) error { glog.V(1).Infof("Updating the local copy of plugin index (%s)", paths.IndexPath()) - if err := gitutil.EnsureUpdated(IndexURI, paths.IndexPath()); err != nil { + if err := gitutil.EnsureUpdated(constants.IndexURI, paths.IndexPath()); err != nil { return errors.Wrap(err, "failed to update the local index") } fmt.Fprintln(os.Stderr, "Updated the local copy of plugin index.") diff --git a/cmd/krew/cmd/version.go b/cmd/krew/cmd/version.go index 9e66591d..3aa25cd9 100644 --- a/cmd/krew/cmd/version.go +++ b/cmd/krew/cmd/version.go @@ -21,6 +21,7 @@ import ( "github.com/golang/glog" "github.com/pkg/errors" "github.com/spf13/cobra" + "sigs.k8s.io/krew/pkg/constants" "sigs.k8s.io/krew/pkg/environment" "sigs.k8s.io/krew/pkg/version" ) @@ -58,7 +59,7 @@ Remarks: {"ExecutedVersion", executedVersion}, {"GitTag", version.GitTag()}, {"GitCommit", version.GitCommit()}, - {"IndexURI", IndexURI}, + {"IndexURI", constants.IndexURI}, {"BasePath", paths.BasePath()}, {"IndexPath", paths.IndexPath()}, {"InstallPath", paths.InstallPath()}, diff --git a/docs/CONTRIBUTOR_GUIDE.md b/docs/CONTRIBUTOR_GUIDE.md index c0aff06f..dfa910a1 100644 --- a/docs/CONTRIBUTOR_GUIDE.md +++ b/docs/CONTRIBUTOR_GUIDE.md @@ -47,9 +47,28 @@ To run tests locally, the easiest way to get started is with hack/run-tests.sh ``` +This will run all unit tests and code quality tools. To run a single tool independently of the other code checks, have a look at the other scripts in [`hack/`](../hack). +In addition, there are integration tests to cover high-level krew functionality. +To run integration tests, you will need to build the `krew` binary beforehand: + +```bash +hack/make-binaries.sh +hack/run-integration-tests.sh +``` + +The above builds binaries for all supported platforms. +Building only for your platform produces a slightly different image but will +work in most circumstances: + +```bash +go build ./cmd/krew +# you need to specify your local krew binary when using this method: +hack/run-integration-tests.sh ./krew +``` + ## Testing `krew` in a sandbox After making changes to krew, you should also check that it behaves as expected. diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh new file mode 100755 index 00000000..b6aa6648 --- /dev/null +++ b/hack/run-integration-tests.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# 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. + +set -euo pipefail + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BINDIR="${SCRIPTDIR}/../out/bin" +KREW_BINARY_DEFAULT="$BINDIR/krew-linux_amd64" + +if [[ "$#" -gt 0 && ( "$1" = '-h' || "$1" = '--help' ) ]]; then + cat </dev/null; then + echo 'using kubectl from the host system' + else + # install kubectl + local -r KUBECTL_VERSION='v1.14.2' + local -r KUBECTL_BINARY="$BINDIR/kubectl" + curl -fSsLo "$KUBECTL_BINARY" https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl + chmod +x "$KUBECTL_BINARY" + export PATH="$BINDIR:$PATH" + fi +} + +install_kubectl_if_needed +KREW_BINARY=$(readlink -f "${1:-$KREW_BINARY_DEFAULT}") # needed for `kubectl krew` in tests +export KREW_BINARY + +go test -v ./... diff --git a/hack/run-lint.sh b/hack/run-lint.sh index ff61abf3..95710c61 100755 --- a/hack/run-lint.sh +++ b/hack/run-lint.sh @@ -16,7 +16,7 @@ set -euo pipefail -HACK=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +SCRIPTDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) if ! [[ -x "$GOPATH/bin/golangci-lint" ]] then diff --git a/hack/run-tests.sh b/hack/run-tests.sh index 6bc1c339..3c06f53a 100755 --- a/hack/run-tests.sh +++ b/hack/run-tests.sh @@ -44,7 +44,7 @@ print_with_color "$color_blue" 'Running gofmt' "$SCRIPTDIR"/verify-gofmt.sh print_with_color "$color_blue" 'Running tests' -go test -v -race sigs.k8s.io/krew/... +go test -v -short -race sigs.k8s.io/krew/... print_with_color "$color_blue" 'Running linter' "$SCRIPTDIR"/run-lint.sh diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index a1e9c0cd..66f93b64 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -17,4 +17,7 @@ package constants const ( CurrentAPIVersion = "krew.googlecontainertools.github.com/v1alpha2" PluginKind = "Plugin" + + // IndexURI points to the upstream index. + IndexURI = "https://github.com/kubernetes-sigs/krew-index.git" ) diff --git a/test/krew/index.go b/test/krew/index.go new file mode 100644 index 00000000..b727c18e --- /dev/null +++ b/test/krew/index.go @@ -0,0 +1,99 @@ +// 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. + +package krew + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sync" + + "github.com/golang/glog" + "sigs.k8s.io/krew/pkg/constants" +) + +const ( + persistentIndexCache = "krew-persistent-index-cache" +) + +var ( + once sync.Once + indexTar []byte +) + +// InitializeIndex initializes the krew index in `$root/index` with the actual krew-index. +// It caches the index tree as in-memory tar after the first run. +func (it *ITest) initializeIndex() { + once.Do(func() { + persistentCacheFile := filepath.Join(os.TempDir(), persistentIndexCache) + fileInfo, err := os.Stat(persistentCacheFile) + + if err == nil && fileInfo.Mode().IsRegular() { + it.t.Logf("Using persistent index cache from file %q", persistentCacheFile) + if indexTar, err = ioutil.ReadFile(persistentCacheFile); err == nil { + return + } + } + + if indexTar, err = initFromGitClone(); err != nil { + it.t.Fatalf("cannot clone repository: %s", err) + } + + ioutil.WriteFile(persistentCacheFile, indexTar, 0600) + }) + + indexDir := filepath.Join(it.Root(), "index") + if err := os.Mkdir(indexDir, 0777); err != nil { + if os.IsExist(err) { + it.t.Log("initializeIndex should only be called once") + return + } + it.t.Fatal(err) + } + + cmd := exec.Command("tar", "xzf", "-", "-C", indexDir) + cmd.Stdin = bytes.NewReader(indexTar) + if err := cmd.Run(); err != nil { + it.t.Fatalf("cannot restore index from cache: %s", err) + } +} + +func initFromGitClone() ([]byte, error) { + const tarName = "index.tar" + indexRoot, err := ioutil.TempDir("", "krew-index-cache") + if err != nil { + return nil, err + } + defer func() { + err := os.RemoveAll(indexRoot) + glog.V(1).Infoln("cannot remove temporary directory:", err) + }() + + cmd := exec.Command("git", "clone", "--depth=1", "--single-branch", "--no-tags", constants.IndexURI) + cmd.Dir = indexRoot + if err = cmd.Run(); err != nil { + return nil, err + } + + cmd = exec.Command("tar", "czf", tarName, "-C", "krew-index", ".") + cmd.Dir = indexRoot + if err = cmd.Run(); err != nil { + return nil, err + } + + return ioutil.ReadFile(filepath.Join(indexRoot, tarName)) +} diff --git a/test/krew/krew.go b/test/krew/krew.go new file mode 100644 index 00000000..afe55241 --- /dev/null +++ b/test/krew/krew.go @@ -0,0 +1,164 @@ +// 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. + +package krew + +import ( + "context" + "fmt" + "os" + "os/exec" + "testing" + "time" + + "github.com/golang/glog" + "github.com/pkg/errors" + "sigs.k8s.io/krew/pkg/testutil" +) + +const krewBinaryEnv = "KREW_BINARY" + +// ITest is used to set up `krew` integration tests. +type ITest struct { + t *testing.T + plugin string + args []string + env []string + tempDir *testutil.TempDir +} + +// NewTest creates a fluent krew ITest. +func NewTest(t *testing.T) (*ITest, func()) { + tempDir, cleanup := testutil.NewTempDir(t) + pathEnv := setupPathEnv(t, tempDir) + return &ITest{ + t: t, + env: []string{pathEnv, fmt.Sprintf("KREW_ROOT=%s", tempDir.Root())}, + tempDir: tempDir, + }, cleanup +} + +func setupPathEnv(t *testing.T, tempDir *testutil.TempDir) string { + krewBinPath := tempDir.Path("bin") + if err := os.MkdirAll(krewBinPath, os.ModePerm); err != nil { + t.Fatal(err) + } + + if krewBinary, found := os.LookupEnv(krewBinaryEnv); found { + if err := os.Symlink(krewBinary, tempDir.Path("bin/kubectl-krew")); err != nil { + t.Fatalf("Cannot link to krew: %s", err) + } + } else { + t.Logf("Environment variable %q was not found, using krew installation from host", krewBinaryEnv) + } + + path, found := os.LookupEnv("PATH") + if !found { + t.Fatalf("PATH variable is not set up") + } + + return fmt.Sprintf("PATH=%s:%s", krewBinPath, path) +} + +// Call configures the runner to call plugin with arguments args. +func (it *ITest) Call(plugin string, args ...string) *ITest { + it.plugin = plugin + it.args = args + return it +} + +// Krew configures the runner to call krew with arguments args. +func (it *ITest) Krew(args ...string) *ITest { + it.plugin = "krew" + it.args = args + return it +} + +// Root returns the krew root directory for this test. +func (it *ITest) Root() string { + return it.tempDir.Root() +} + +// WithIndex initializes the index with the actual krew-index from github/kubernetes-sigs/krew-index. +func (it *ITest) WithIndex() *ITest { + it.initializeIndex() + return it +} + +// WithEnv sets an environment variable for the krew run. +func (it *ITest) WithEnv(key string, value interface{}) *ITest { + if key == "KREW_ROOT" { + glog.V(1).Infoln("Overriding KREW_ROOT in tests is forbidden") + return it + } + it.env = append(it.env, fmt.Sprintf("%s=%v", key, value)) + return it +} + +// RunOrFail runs the krew command and fails the test if the command returns an error. +func (it *ITest) RunOrFail() { + it.t.Helper() + if err := it.Run(); err != nil { + it.t.Fatal(err) + } +} + +// Run runs the krew command. +func (it *ITest) Run() error { + it.t.Helper() + + cmd := it.cmd(context.Background()) + glog.V(1).Infoln(cmd.Args) + + start := time.Now() + if err := cmd.Run(); err != nil { + return errors.Wrapf(err, "krew %v", it.args) + } + + glog.V(1).Infoln("Ran in", time.Since(start)) + return nil +} + +// RunOrFailOutput runs the krew command and fails the test if the command +// returns an error. It only returns the standard output. +func (it *ITest) RunOrFailOutput() []byte { + it.t.Helper() + + cmd := it.cmd(context.Background()) + glog.V(1).Infoln(cmd.Args) + + start := time.Now() + out, err := cmd.CombinedOutput() + if err != nil { + it.t.Fatalf("krew %v: %v, %s", it.args, err, out) + } + + glog.V(1).Infoln("Ran in", time.Since(start)) + return out +} + +func (it *ITest) cmd(ctx context.Context) *exec.Cmd { + args := make([]string, 0, len(it.args)+1) + args = append(args, it.plugin) + args = append(args, it.args...) + + cmd := exec.CommandContext(ctx, "kubectl", args...) + cmd.Env = append(os.Environ(), it.env...) + + return cmd +} + +func (it *ITest) TempDir() *testutil.TempDir { + return it.tempDir +} diff --git a/test/krew_test.go b/test/krew_test.go new file mode 100644 index 00000000..8f95ba93 --- /dev/null +++ b/test/krew_test.go @@ -0,0 +1,53 @@ +// 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. + +// Package test contains integration tests for krew. +package test + +import ( + "testing" + + "sigs.k8s.io/krew/test/krew" +) + +const ( + // validPlugin is a valid plugin with a small download size + validPlugin = "konfig" +) + +func TestKrewInstall(t *testing.T) { + skipShort(t) + + test, cleanup := krew.NewTest(t) + defer cleanup() + + test.WithIndex().Krew("install", validPlugin).RunOrFailOutput() + test.Call(validPlugin, "--help").RunOrFail() +} + +func TestKrewHelp(t *testing.T) { + skipShort(t) + + test, cleanup := krew.NewTest(t) + defer cleanup() + + test.Krew("help").RunOrFail() +} + +func skipShort(t *testing.T) { + t.Helper() + if testing.Short() { + t.Skip("skipping integration test") + } +}