Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

major refactor #519

Merged
merged 17 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
project_name: status
version: 2
release:
github:
owner: bergerx
name: kubectl-status
before:
hooks:
- go generate ./...
- go mod tidy
builds:
- id: status
goos:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ vet:

.PHONY: staticcheck
staticcheck:
go run honnef.co/go/tools/cmd/staticcheck@v0.4.7 ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 ./...

.PHONY: clean
clean:
Expand Down
20 changes: 13 additions & 7 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,17 @@ func RootCmd() *cobra.Command {
Long: longCmdMessage,
Example: examplesMessage,
PreRun: func(cmd *cobra.Command, args []string) {
_ = viper.BindPFlags(cmd.Flags())
viper.AutomaticEnv()
err := viper.BindPFlags(cmd.Flags())
if err != nil {
cmd.PrintErr("error binding flags", err)
}
},
SilenceUsage: true,
Version: version,
}
initColorCobra(cmd)
configFlags := initFlags(cmd)
cobra.OnInitialize(viper.AutomaticEnv)
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
f := cmdutil.NewFactory(configFlags)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
Expand All @@ -99,7 +102,8 @@ func RootCmd() *cobra.Command {
})
cmdutil.CheckErr(complete(f))
cmdutil.CheckErr(validate())
if b, _ := cmd.Flags().GetBool("time-hack-ago"); b {
if b, _ := cmd.Flags().GetBool("test-hack"); b {
viper.Set("test-hack", true)
plugin.SetDurationRound(func(_ interface{}) string { return "1m" })
}
ioStreams := genericiooptions.IOStreams{In: cmd.InOrStdin(), Out: cmd.OutOrStdout(), ErrOut: cmd.ErrOrStderr()}
Expand Down Expand Up @@ -147,11 +151,13 @@ func initColorCobra(cmd *cobra.Command) {
}

func hideNoisyFlags(flags *pflag.FlagSet) {
flagsToHide := []string{"add_dir_header", "as-uid", "alsologtostderr", "as", "as-group", "cache-dir",
flagsToHide := []string{
"add_dir_header", "as-uid", "alsologtostderr", "as", "as-group", "cache-dir",
"certificate-authority", "client-certificate", "client-key", "cluster", "context", "insecure-skip-tls-verify",
"kubeconfig", "log_backtrace_at", "log_dir", "log_file", "log_file_max_size", "logtostderr", "one_output",
"password", "request-timeout", "server", "skip_headers", "skip_log_headers", "stderrthreshold",
"tls-server-name", "token", "user", "username", "vmodule", "time-hack-ago"}
"tls-server-name", "token", "user", "username", "vmodule", "test-hack",
}
for _, flagName := range flagsToHide {
flags.Lookup(flagName).Hidden = true
}
Expand Down Expand Up @@ -201,8 +207,8 @@ func addRenderFlags(flags *pflag.FlagSet) {
"Show all available flags.")
flags.String("color", "auto",
"One of 'auto', 'never' or 'always'.")
flags.Bool("time-hack-ago", false,
"always report 1m for any time duration")
flags.Bool("test-hack", false,
"helper flag for tests, e.g. always report 1m for any time duration, 1.1.1.1 for IPs, etc.")
}

func isBoolConfigExplicitlySetToTrue(key string) bool {
Expand Down
230 changes: 192 additions & 38 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,68 @@ package main

import (
"bytes"
"context"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"

"github.com/bergerx/kubectl-status/pkg/plugin"
)

type cmdTest struct {
name string
args []string
stdoutRegex string // Regex
stdoutEqual string // Regex
stderrRegex string // Regex
stderrEqual string // Regex
wantErr string // Contains
name string
args []string
stdoutRegex string // Regex
stdoutRegexPath string // Regex match against file contents under test folder
stdoutEqual string // Exact
stdoutEqualPath string // Exact match with file contents under test folder
stderrRegex string // Regex
stderrEqual string // Exact
wantErr string // Contains
}

func (c cmdTest) assert(t *testing.T) {
func nodeNameModifier(stdout string) string {
return string(regexp.MustCompile(`Node/[a-z0-9-]+`).ReplaceAll([]byte(stdout), []byte(`Node/minikube`)))
}

func (c cmdTest) assert(t *testing.T, stdoutModifier func(string) string) {
t.Helper()
t.Logf("running cmdTest assert: %s", c)
stdout, stderr, err := executeCMD(t, c.args)
if stdoutModifier != nil {
stdout = nodeNameModifier(stdout)
}
switch {
case c.stdoutRegex == "" && c.stdoutEqual == "":
case c.stdoutRegex == "" && c.stdoutEqual == "" && c.stdoutRegexPath == "" && c.stdoutEqualPath == "":
assert.Empty(t, stdout)
case c.stdoutRegex != "":
assert.Regexp(t, c.stdoutRegex, stdout)
case c.stdoutEqual != "":
assert.Equal(t, c.stdoutEqual, stdout)
case c.stdoutEqualPath != "":
outFile := path.Join("..", "tests", c.stdoutEqualPath)
out, err := os.ReadFile(outFile)
assert.NoErrorf(t, err, "failed to read test artifact file: %s", outFile)
assert.Equal(t, string(out), stdout)
case c.stdoutRegexPath != "":
outFile := path.Join("..", "tests", c.stdoutRegexPath)
regexBytes, err := os.ReadFile(outFile)
assert.NoErrorf(t, err, "failed to read test artifact file: %s", outFile)
regex := `(?ms)` + string(regexBytes)
assert.Regexp(t, regex, stdout)
}
switch {
case c.stderrRegex == "" && c.stderrEqual == "":
Expand All @@ -51,7 +79,7 @@ func (c cmdTest) assert(t *testing.T) {
}

func TestRootCmdWithoutACluster(t *testing.T) {
_ = os.Setenv("KUBECONFIG", "/dev/null")
t.Setenv("KUBECONFIG", "/dev/null")
defer plugin.SetDurationRound(func(_ interface{}) string { return "1m" })()
tests := []cmdTest{
{
Expand Down Expand Up @@ -102,19 +130,14 @@ $`,
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.assert(t)
tt.assert(t, nil)
})
}
}

func TestE2EAgainstVanillaMinikube(t *testing.T) {
if os.Getenv("RUN_E2E_TESTS") != "true" {
t.Skip("Skipping e2e test")
}
if os.Getenv("ASSUME_MINIKUBE_IS_CONFIGURED") != "true" {
defer startMinikube(t, "kubectl-status-e2e")()
}
defer plugin.SetDurationRound(func(_ interface{}) string { return "1m" })()
e2eMinikubeTest(t)
testHack(t)
klog.InitFlags(nil)
t.Log("starting tests...")
tests := []cmdTest{
Expand Down Expand Up @@ -145,27 +168,44 @@ func TestE2EAgainstVanillaMinikube(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.assert(t)
viperTestHack(t)
tt.assert(t, nil)
})
}
}

func TestAllArtifacts(t *testing.T) {
defer plugin.SetDurationRound(func(_ interface{}) string { return "1m" })()
_ = os.Setenv("KUBECONFIG", "/dev/null")
viper.Set("test", true)
func testHack(t *testing.T) {
t.Helper()
durationRevert := plugin.SetDurationRound(func(_ interface{}) string { return "1m" })
t.Cleanup(func() {
durationRevert()
})
}

func viperTestHack(t *testing.T) {
t.Helper()
viper.Reset()
viper.Set("test-hack", true)
t.Cleanup(func() {
viper.Reset()
})
}

func TestAllArtifactsLocal(t *testing.T) {
t.Setenv("KUBECONFIG", "/dev/null")
testHack(t)
viperTestHack(t)
artifacts, err := filepath.Glob("../tests/artifacts/*.yaml")
assert.NoError(t, err)
for _, artifact := range artifacts {
t.Run(strings.Replace(artifact, "../", "", -1), func(t *testing.T) {
outFile := strings.Replace(artifact, ".yaml", ".out", -1)
out, err := os.ReadFile(outFile)
assert.NoError(t, err)
name := strings.Replace(artifact, "../tests/", "", 1)
name = strings.Replace(name, ".yaml", "", 1)
t.Run(name, func(t *testing.T) {
test := cmdTest{
args: []string{"-f", artifact, "--local", "--shallow"},
stdoutEqual: string(out),
args: []string{"-f", artifact, "--local", "--shallow", "--v", "255"},
stdoutEqualPath: name + ".out",
}
test.assert(t) // to update the out files check /tests/artifacts/README.md
test.assert(t, nil) // to update the out files check /tests/artifacts/README.md
})
}
}
Expand All @@ -183,25 +223,139 @@ func executeCMD(t *testing.T, args []string) (string, string, error) {
return stdout.String(), stderr.String(), err
}

func startMinikube(t *testing.T, clusterName string) (deleteMinikube func()) {
func startMinikube(t *testing.T) {
t.Helper()
t.Log("Creating temp folder for minikube.kubeconfig...")
clusterName := t.Name()
t.Logf("Creating temp folder for minikube.kubeconfig for minikube %s ...", clusterName)
dir, err := os.MkdirTemp("", clusterName)
assert.NoError(t, err)
require.NoError(t, err)
kubeconfig := path.Join(dir, "minikube.kubeconfig")
t.Setenv("KUBECONFIG", kubeconfig)
t.Log("Starting Minikube cluster...")
t.Logf("Starting Minikube cluster %s with %s ...", clusterName, kubeconfig)
startMinikube := exec.Command("minikube", "start", "-p", clusterName)
assert.NoError(t, startMinikube.Run())
return func() {
require.NoError(t, startMinikube.Run())
t.Cleanup(func() {
cmd := exec.Command("minikube", "delete", "-p", clusterName)
t.Log("Deleting Minikube cluster...")
t.Logf("Deleting Minikube cluster %s...", clusterName)
if err := cmd.Run(); err != nil {
t.Log("Error deleting Minikube cluster:", err)
}
t.Log("Deleting temp folder of minikube.kubeconfig...")
t.Logf("Deleting temp folder for minikube %s: %s ...", clusterName, dir)
if err := os.RemoveAll(dir); err != nil {
t.Log("Error deleting temp folder of minikube.kubeconfig:", err)
}
})
}

func e2eMinikubeTest(t *testing.T) {
t.Helper()
if os.Getenv("RUN_E2E_TESTS") != "true" {
t.Skip("Skipping e2e test as RUN_E2E_TESTS is not set to true")
}
if os.Getenv("ASSUME_MINIKUBE_IS_CONFIGURED") == "true" {
t.Logf("assuming current kubeconfig context is pointng a minikube to run e2e tests")
} else {
startMinikube(t)
}
}

func TestE2EDynamicManifests(t *testing.T) {
e2eMinikubeTest(t)
testHack(t)
kubeconfigPath := os.Getenv("KUBECONFIG")
if kubeconfigPath == "" {
homeDir := os.Getenv("HOME")
kubeconfigPath = filepath.Join(homeDir, ".kube", "config")
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
t.Fatal(err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
t.Run("owners should be included with deep", func(t *testing.T) {
viperTestHack(t)
owner := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "owner",
Namespace: "default",
},
}
owner, err := clientset.CoreV1().Secrets("default").Create(context.TODO(), owner, metav1.CreateOptions{})
defer clientset.CoreV1().Secrets("default").Delete(context.TODO(), "owner", metav1.DeleteOptions{})
require.NoError(t, err)
uid := owner.GetUID()
t.Logf("owner secret is created, uid is %s", uid)
// Create the child secret with owner reference
child := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "child",
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "Secret",
Name: "owner",
UID: uid,
},
},
},
}
_, err = clientset.CoreV1().Secrets("default").Create(context.TODO(), child, metav1.CreateOptions{})
t.Log("child secret is created")
defer clientset.CoreV1().Secrets("default").Delete(context.TODO(), "child", metav1.DeleteOptions{})
require.NoError(t, err)

test := cmdTest{
args: []string{"secret/child", "--deep", "--v", "7"},
stdoutRegex: `(?ms)
Secret\/child -n default, created 1m ago by Secret/owner
Current: Resource is always ready
Known\/recorded manage events:
1m ago Updated by [^ ]+ \(metadata, type\)
Owners:
Secret\/owner -n default, created 1m ago
Current: Resource is always ready
Known\/recorded manage events:
1m ago Updated by [^ ]+ \(type\)
`,
}
test.assert(t, nil) // to update the out files check /tests/artifacts/README.md
})
t.Run("sts-with-ingress", func(t *testing.T) {
viperTestHack(t)
// using sts here as the pod name is predictable in that case, not true for deployments and ds
applyManifest(t, "e2e-artifacts/sts-with-ingress.yaml")
waitFor(t, "sts/sts-with-ingress", "jsonpath={.status.readyReplicas}=1")
cmdTest{
args: []string{"pod/sts-with-ingress-0", "--include-events=false", "--v", "5"},
stdoutEqualPath: "e2e-artifacts/sts-with-ingress.pod.out",
}.assert(t, nodeNameModifier)
})
}

func applyManifest(t *testing.T, filepath string) {
t.Helper()
filepath = path.Join("..", "tests", filepath)
cmd := exec.Command("kubectl", "apply", "-f", filepath)
output, err := cmd.CombinedOutput()
t.Cleanup(func() {
t.Logf("deleting manifest %s", filepath)
cmd := exec.Command("kubectl", "delete", "-f", filepath)
output, err := cmd.CombinedOutput()
assert.NoError(t, err)
t.Logf("manifest deleted %s: %s", filepath, string(output))
})
require.NoError(t, err)
t.Logf("applied manifest %s: %s", filepath, string(output))
}

func waitFor(t *testing.T, resource, forParam string) {
t.Helper()
cmd := exec.Command("kubectl", "wait", "--for", forParam, resource)
output, err := cmd.CombinedOutput()
t.Logf("wait result for %s: %s", resource, string(output))
require.NoError(t, err)
}
Loading
Loading