Repository: kudobuilder/kuttl
Pull request #17 (2020-02-25 19:44:42)

CLI for kuttl
I'm not sure how this was lost... there was a commit in history that indicates adding the CLI but it is missing.  This is that code.

supports: `help`, `test` and `version`

Signed-off-by: Ken Sipe <kensipe@gmail.com>


Patch:
diff --git a/cmd/kubectl-kuttl/main.go b/cmd/kubectl-kuttl/main.go
new file mode 100644
index 0000000..a990e2f
--- /dev/null
+++ b/cmd/kubectl-kuttl/main.go
@@ -0,0 +1,27 @@
+// Copyright 2020 KUDO 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 main
+
+import (
+	"os"
+
+	"github.com/kudobuilder/kuttl/pkg/kuttlctl/cmd"
+)
+
+func main() {
+	if err := cmd.NewKuttlCmd().Execute(); err != nil {
+		os.Exit(-1)
+	}
+}
diff --git a/pkg/kuttlctl/cmd/root.go b/pkg/kuttlctl/cmd/root.go
new file mode 100644
index 0000000..a3abb7f
--- /dev/null
+++ b/pkg/kuttlctl/cmd/root.go
@@ -0,0 +1,31 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/kudobuilder/kuttl/pkg/version"
+)
+
+// NewKuttlCmd creates a new root command for kuttlctl
+func NewKuttlCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "kubectl-kuttl",
+		Short: "CLI to Test Kubernetes",
+		Long: `KUTTL CLI and future sub-commands can be used to manipulate, inspect and troubleshoot CRDs
+and serves as an API aggregation layer.
+`,
+		SilenceUsage: true,
+		Example: `  # Run integration tests against a Kubernetes cluster or mocked control plane.
+  kubectl kuttl test
+
+  # View kuttl version
+  kubectl kuttl version
+`,
+		Version: version.Get().GitVersion,
+	}
+
+	cmd.AddCommand(newTestCmd())
+	cmd.AddCommand(newVersionCmd())
+
+	return cmd
+}
diff --git a/pkg/kuttlctl/cmd/version.go b/pkg/kuttlctl/cmd/version.go
new file mode 100644
index 0000000..5051a3e
--- /dev/null
+++ b/pkg/kuttlctl/cmd/version.go
@@ -0,0 +1,34 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/kudobuilder/kuttl/pkg/version"
+)
+
+var (
+	versionExample = `  # Print the current installed KUTTL package version
+  kubectl kuttl version`
+)
+
+// newVersionCmd returns a new initialized instance of the version sub command
+func newVersionCmd() *cobra.Command {
+	versionCmd := &cobra.Command{
+		Use:     "version",
+		Short:   "Print the current KUTTL package version.",
+		Long:    `Print the current installed KUTTL package version.`,
+		Example: versionExample,
+		RunE:    VersionCmd,
+	}
+
+	return versionCmd
+}
+
+// VersionCmd performs the version sub command
+func VersionCmd(cmd *cobra.Command, args []string) error {
+	kuttlVersion := version.Get()
+	fmt.Printf("KUTTL Version: %s\n", fmt.Sprintf("%#v", kuttlVersion))
+	return nil
+}
diff --git a/pkg/version/base.go b/pkg/version/base.go
new file mode 100644
index 0000000..70d5cb0
--- /dev/null
+++ b/pkg/version/base.go
@@ -0,0 +1,24 @@
+package version
+
+// Base version information.
+//
+// This is the fallback data used when version information from git is not
+// provided via go ldflags. It provides an approximation of the Kubernetes
+// version for ad-hoc builds (e.g. `go build`) that cannot get the version
+// information from git.
+//
+// If you are looking at these fields in the git tree, they look
+// strange. They are modified on the fly by the build process. The
+// in-tree values are dummy values used for "git archive", which also
+// works for GitHub tar downloads.
+
+var (
+	// semantic version, derived by build scripts (see
+	// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md
+	// for a detailed discussion of this field)
+
+	gitVersion = "v0.0.0-master+$Format:%h$"
+	gitCommit  = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD)
+
+	buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
+)
diff --git a/pkg/version/version.go b/pkg/version/version.go
new file mode 100644
index 0000000..5dbc664
--- /dev/null
+++ b/pkg/version/version.go
@@ -0,0 +1,116 @@
+package version
+
+import (
+	"fmt"
+	"os"
+	"runtime"
+	"strings"
+
+	"github.com/Masterminds/semver"
+)
+
+// Info contains versioning information.
+type Info struct {
+	GitVersion string `json:"gitVersion"`
+	GitCommit  string `json:"gitCommit"`
+	BuildDate  string `json:"buildDate"`
+	GoVersion  string `json:"goVersion"`
+	Compiler   string `json:"compiler"`
+	Platform   string `json:"platform"`
+}
+
+// String returns info as a human-friendly version string.
+func (info Info) String() string {
+	return info.GitVersion
+}
+
+// Get returns the overall codebase version. It's for detecting
+// what code a binary was built from.
+func Get() Info {
+	// These variables typically come from -ldflags settings and in
+	// their absence fallback to the settings in pkg/version/base.go
+	// developer fallback for version
+
+	// this only happens when running from a build.  Release runs ARE correct.
+	if strings.Contains(gitVersion, "$Format") {
+		// on dev box, lets use a env var for version
+		gitVersion = os.Getenv("KUTTL_DEV_VERSION")
+		if gitVersion == "" {
+			gitVersion = "dev"
+		}
+		gitCommit = "dev"
+		//TODO (kensipe): add debug message!
+	}
+
+	return Info{
+		GitVersion: gitVersion,
+		GitCommit:  gitCommit,
+		BuildDate:  buildDate,
+		GoVersion:  runtime.Version(),
+		Compiler:   runtime.Compiler,
+		Platform:   fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
+	}
+}
+
+// Version is an extension of semver.Version
+type Version struct {
+	*semver.Version
+}
+
+// CompareMajorMinor provides Compare results -1, 0, 1 for only the major and minor element
+// of the semver, ignoring the patch or prerelease elements.   This is useful if you are looking
+// for minVersion for example 1.15.6 is version 1.15 or higher.
+func (v *Version) CompareMajorMinor(o *Version) int {
+	if d := compareSegment(v.Major(), o.Major()); d != 0 {
+		return d
+	}
+	if d := compareSegment(v.Minor(), o.Minor()); d != 0 {
+		return d
+	}
+	return 0
+}
+
+// compares v1 against v2 resulting in -1, 0, 1 for less than, equal, greater than
+func compareSegment(v1, v2 int64) int {
+	if v1 < v2 {
+		return -1
+	}
+	if v1 > v2 {
+		return 1
+	}
+
+	return 0
+}
+
+// New provides an instance of Version from a semver string
+func New(v string) (*Version, error) {
+	ver, err := semver.NewVersion(v)
+	if err != nil {
+		return nil, err
+	}
+	return FromSemVer(ver), nil
+}
+
+// FromGithubVersion provides a version parsed from github semver which starts with "v".
+// v1.5.2 provides a sem version of 1.5.2
+func FromGithubVersion(v string) (*Version, error) {
+	return New(Clean(v))
+}
+
+// FromSemVer converts a semver.Version to our Version
+func FromSemVer(v *semver.Version) *Version {
+	return &Version{v}
+}
+
+// MustParse parses a given version and panics on error.
+func MustParse(v string) *Version {
+	return FromSemVer(semver.MustParse(v))
+}
+
+// Clean returns version without a prefixed v if it exists
+func Clean(ver string) string {
+	if strings.HasPrefix(ver, "v") {
+		return ver[1:]
+	}
+	return ver
+}


Test patch:
diff --git a/kuttl-test.yaml b/kuttl-test.yaml
new file mode 100644
index 0000000..e5ad5bf
--- /dev/null
+++ b/kuttl-test.yaml
@@ -0,0 +1,6 @@
+apiVersion: kudo.dev/v1beta1
+kind: TestSuite
+testDirs:
+- ./test/integration
+startControlPlane: true
+parallel: 4
diff --git a/pkg/kuttlctl/cmd/root_test.go b/pkg/kuttlctl/cmd/root_test.go
new file mode 100644
index 0000000..2777cb8
--- /dev/null
+++ b/pkg/kuttlctl/cmd/root_test.go
@@ -0,0 +1,24 @@
+package cmd
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestNormalizationFuncGlobalExistence(t *testing.T) {
+	root := NewKuttlCmd()
+
+	if root.Parent() != nil {
+		t.Fatal("We expect the root command to be returned")
+	}
+
+	sub := root
+	for sub.HasSubCommands() {
+		sub = sub.Commands()[0]
+	}
+
+	// In case of failure of this test check this PR: spf13/cobra#110
+	if reflect.ValueOf(sub.Flags().GetNormalizeFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() {
+		t.Fatal("child and root commands should have the same normalization functions")
+	}
+}
diff --git a/pkg/kuttlctl/cmd/test.go b/pkg/kuttlctl/cmd/test.go
new file mode 100644
index 0000000..da7b921
--- /dev/null
+++ b/pkg/kuttlctl/cmd/test.go
@@ -0,0 +1,197 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"os"
+	"testing"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+
+	harness "github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1"
+	"github.com/kudobuilder/kuttl/pkg/test"
+	testutils "github.com/kudobuilder/kuttl/pkg/test/utils"
+)
+
+var (
+	testExample = `  Run tests configured by kuttl-test.yaml:
+    kubectl kuttl test
+
+  Load a specific test configuration:
+    kubectl kuttl test --config test.yaml
+
+  Run tests against an existing Kubernetes cluster:
+    kubectl kuttl test ./test/integration/
+
+  Run tests against an existing Kubernetes cluster, and install manifests, and CRDs for the tests:
+    kubectl kuttl test --crd-dir ./config/crds/ --manifests-dir ./test/manifests/ ./test/integration/
+
+  Run a Kubernetes control plane and install manifests and CRDs for the running tests:
+    kubectl kuttl test --start-control-plane  --crd-dir ./config/crds/ --manifests-dir ./test/manifests/ ./test/integration/
+`
+)
+
+// newTestCmd creates the test command for the CLI
+func newTestCmd() *cobra.Command {
+	configPath := ""
+	crdDir := ""
+	manifestDirs := []string{}
+	testToRun := ""
+	startControlPlane := false
+	startKIND := false
+	kindConfig := ""
+	kindContext := ""
+	skipDelete := false
+	skipClusterDelete := false
+	parallel := 0
+	artifactsDir := ""
+
+	options := harness.TestSuite{}
+
+	testCmd := &cobra.Command{
+		Use:   "test [flags]... [test directories]...",
+		Short: "Test KUTTL and Operators.",
+		Long: `Runs integration tests against a Kubernetes cluster.
+
+The test operator supports connecting to an existing Kubernetes cluster or it can start a Kubernetes API server during the test run.
+It can also apply manifests before running the tests. If no arguments are provided, the test harness will attempt to
+load the test configuration from kuttl-test.yaml.
+
+For more detailed documentation, visit: https://kudo.dev/docs/testing`,
+		Example: testExample,
+		PreRunE: func(cmd *cobra.Command, args []string) error {
+			flags := cmd.Flags()
+
+			options.TestDirs = args
+
+			// If a config is not set and kuttl-test.yaml exists, set configPath to kuttl-test.yaml.
+			if configPath == "" {
+				if _, err := os.Stat("kuttl-test.yaml"); err == nil {
+					configPath = "kuttl-test.yaml"
+				} else {
+					return errors.New("kuttl-test.yaml not found, provide either --config or arguments indicating the tests to load")
+				}
+			}
+
+			// Load the configuration YAML into options.
+			if configPath != "" {
+				objects, err := testutils.LoadYAML(configPath)
+				if err != nil {
+					return err
+				}
+
+				for _, obj := range objects {
+					kind := obj.GetObjectKind().GroupVersionKind().Kind
+
+					if kind == "TestSuite" {
+						options = *obj.(*harness.TestSuite)
+					} else {
+						log.Println(fmt.Errorf("unknown object type: %s", kind))
+					}
+				}
+			}
+
+			// Override configuration file options with any command line flags if they are set.
+
+			if isSet(flags, "crd-dir") {
+				options.CRDDir = crdDir
+			}
+
+			if isSet(flags, "manifest-dir") {
+				options.ManifestDirs = manifestDirs
+			}
+
+			if isSet(flags, "start-control-plane") {
+				options.StartControlPlane = startControlPlane
+			}
+
+			if isSet(flags, "start-kind") {
+				options.StartKIND = startKIND
+			}
+
+			if isSet(flags, "kind-config") {
+				options.StartKIND = true
+				options.KINDConfig = kindConfig
+			}
+
+			if isSet(flags, "kind-context") {
+				options.KINDContext = kindContext
+			}
+
+			if options.KINDContext == "" {
+				options.KINDContext = harness.DefaultKINDContext
+			}
+
+			if options.StartControlPlane && options.StartKIND {
+				return errors.New("only one of --start-control-plane and --start-kind can be set")
+			}
+
+			if isSet(flags, "skip-delete") {
+				options.SkipDelete = skipDelete
+			}
+
+			if isSet(flags, "skip-cluster-delete") {
+				options.SkipClusterDelete = skipClusterDelete
+			}
+
+			if isSet(flags, "parallel") {
+				options.Parallel = parallel
+			}
+
+			if isSet(flags, "artifacts-dir") {
+				options.ArtifactsDir = artifactsDir
+			}
+
+			if len(args) != 0 {
+				options.TestDirs = args
+			}
+
+			if len(options.TestDirs) == 0 {
+				return errors.New("no test directories provided, please provide either --config or test directories on the command line")
+			}
+
+			return nil
+		},
+		Run: func(cmd *cobra.Command, args []string) {
+			testutils.RunTests("kuttl", testToRun, options.Parallel, func(t *testing.T) {
+				harness := test.Harness{
+					TestSuite: options,
+					T:         t,
+				}
+
+				harness.Run()
+			})
+		},
+	}
+
+	testCmd.Flags().StringVar(&configPath, "config", "", "Path to file to load test settings from (must not be set with any other arguments).")
+	testCmd.Flags().StringVar(&crdDir, "crd-dir", "", "Directory to load CustomResourceDefinitions from prior to running the tests.")
+	testCmd.Flags().StringSliceVar(&manifestDirs, "manifest-dir", []string{}, "One or more directories containing manifests to apply before running the tests.")
+	testCmd.Flags().StringVar(&testToRun, "test", "", "If set, the specific test case to run.")
+	testCmd.Flags().BoolVar(&startControlPlane, "start-control-plane", false, "Start a local Kubernetes control plane for the tests (requires etcd and kube-apiserver binaries, cannot be used with --start-kind).")
+	testCmd.Flags().BoolVar(&startKIND, "start-kind", false, "Start a KIND cluster for the tests (cannot be used with --start-control-plane).")
+	testCmd.Flags().StringVar(&kindConfig, "kind-config", "", "Specify the KIND configuration file path (implies --start-kind, cannot be used with --start-control-plane).")
+	testCmd.Flags().StringVar(&kindContext, "kind-context", "", "Specify the KIND context name to use (default: kind).")
+	testCmd.Flags().StringVar(&artifactsDir, "artifacts-dir", "", "Directory to output kind logs to (if not specified, the current working directory).")
+	testCmd.Flags().BoolVar(&skipDelete, "skip-delete", false, "If set, do not delete resources created during tests (helpful for debugging test failures, implies --skip-cluster-delete).")
+	testCmd.Flags().BoolVar(&skipClusterDelete, "skip-cluster-delete", false, "If set, do not delete the mocked control plane or kind cluster.")
+	// The default value here is only used for the help message. The default is actually enforced in RunTests.
+	testCmd.Flags().IntVar(&parallel, "parallel", 8, "The maximum number of tests to run at once.")
+
+	return testCmd
+}
+
+// isSet returns true if a flag is set on the command line.
+func isSet(flagSet *pflag.FlagSet, name string) bool {
+	found := false
+
+	flagSet.Visit(func(flag *pflag.Flag) {
+		if flag.Name == name {
+			found = true
+		}
+	})
+
+	return found
+}
diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go
new file mode 100644
index 0000000..325bd4d
--- /dev/null
+++ b/pkg/version/version_test.go
@@ -0,0 +1,50 @@
+package version
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validVersion(t *testing.T) {
+
+	tests := []struct {
+		name     string
+		actual   *Version
+		expected *Version
+		val      int
+	}{
+		{"expect early version", MustParse("1.5"), MustParse("1.4"), -1},
+		{"expect same version", MustParse("1.5"), MustParse("1.5"), 0},
+		{"expect newer version", MustParse("1.5"), MustParse("1.6"), 1},
+		{"full semver is not a factor", MustParse("1.5.8"), MustParse("1.5.0"), 0},
+	}
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			val := tt.expected.CompareMajorMinor(tt.actual)
+			assert.Equal(t, val, tt.val)
+		})
+	}
+}
+
+func TestClean(t *testing.T) {
+	tests := []struct {
+		name     string
+		actual   string
+		expected string
+	}{
+		{"clean ver", "1.0.0", "1.0.0"},
+		{"clean ver", "v1.0.0", "1.0.0"},
+		{"short ver", "v1.0", "1.0"},
+	}
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			result := Clean(tt.actual)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}