Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
node_modules


tools/k8s-test/internal/helmtestfs/manifests/
tools/k8s-test/bin/
24 changes: 24 additions & 0 deletions tools/k8s-test/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
APP := k8s-test
ROOT := $(shell git rev-parse --show-toplevel)
SRC_MANIFESTS := $(ROOT)/tools/helm-test/manifests
EMBED_DIR := internal/helmtestfs/manifests

.PHONY: embed-manifests
embed-manifests: ## Copy tools/helm-test/manifests into embedded tree
rm -rf $(EMBED_DIR)
mkdir -p $(EMBED_DIR)
cp -R $(SRC_MANIFESTS)/* $(EMBED_DIR)/

.PHONY: build
build: embed-manifests ## Build the CLI with embedded manifests
GO111MODULE=on CGO_ENABLED=0 go build -ldflags='-s -w' -o bin/$(APP) ./

.PHONY: clean
clean:
rm -rf bin $(EMBED_DIR)

.PHONY: help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-18s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)


53 changes: 53 additions & 0 deletions tools/k8s-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# k8s-test CLI

Self-contained CLI for running Helm chart tests using embedded manifests from `tools/helm-test/manifests`.

## Install

```bash
make build
install -m 0755 bin/k8s-test ~/bin/k8s-test
```

## Commands

```text
k8s-test # main command
k8s-test run # full workflow (cluster -> deps -> subject -> tests)

k8s-test cluster info
k8s-test cluster check
k8s-test cluster create [--workers N]
k8s-test cluster delete

k8s-test deps list
k8s-test deps deploy

k8s-test subject info
k8s-test subject deploy
k8s-test subject upgrade
k8s-test subject delete

k8s-test test list
k8s-test test run [--phase deploy|upgrade|delete]
```

## Global flags

- `-d, --test-dir`: directory containing `test-plan.yaml` (default: current directory)

## Run options

- `--tidy`: delete the cluster after tests complete
- `--workers`: (kind only) number of worker nodes when creating a cluster if no configFile is provided (default: 1)

## Requirements

- Tools on PATH: `kubectl`, `helm`, `flux`, and a cluster provider (`kind` or `minikube`)

## Notes

- Manifests are embedded from `tools/helm-test/manifests/` at build time; rebuild to pick up changes.
- If `subject.type` is omitted in `test-plan.yaml`, it defaults to `helm`.


131 changes: 131 additions & 0 deletions tools/k8s-test/cmd/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package cmd

import (
"fmt"
"os"
"os/exec"

"github.com/grafana/helm-chart-toolbox/tools/k8s-test/internal/plan"
"github.com/spf13/cobra"
)

func newCmdCluster() *cobra.Command {
cmd := &cobra.Command{Use: "cluster", Short: "Cluster commands"}
cmd.AddCommand(&cobra.Command{Use: "info", Short: "Show cluster info", RunE: clusterInfo})
cmd.AddCommand(&cobra.Command{Use: "check", Short: "Check if cluster exists", RunE: clusterCheck})
create := &cobra.Command{Use: "create", Short: "Create cluster", RunE: clusterCreate}
create.Flags().IntVar(&workers, "workers", 1, "Number of worker nodes for kind clusters")
cmd.AddCommand(create)
cmd.AddCommand(&cobra.Command{Use: "delete", Short: "Delete cluster", RunE: clusterDelete})
return cmd
}

func clusterInfo(cmd *cobra.Command, args []string) error {
tp, err := plan.Load(testDir)
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Name: %s\nType: %s\n", tp.ClusterName(), tp.Cluster.Type)
return nil
}

func clusterCheck(cmd *cobra.Command, args []string) error {
tp, err := plan.Load(testDir)
if err != nil {
return err
}
exists, err := clusterExists(tp)
if err != nil {
return err
}
if exists {
fmt.Fprintln(cmd.OutOrStdout(), "present")
} else {
fmt.Fprintln(cmd.OutOrStdout(), "absent")
}
return nil
}

func clusterCreate(cmd *cobra.Command, args []string) error {
tp, err := plan.Load(testDir)
if err != nil {
return err
}
switch tp.Cluster.Type {
case "kind":
args := []string{"create", "cluster", "--name", tp.ClusterName()}
if tp.Cluster.ConfigFile != "" {
args = append(args, "--config", tp.AbsPath(tp.Cluster.ConfigFile))
} else {
n := workers
if n < 1 {
n = 1
}
cfg := "kind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n- role: control-plane\n"
for i := 0; i < n; i++ {
cfg += "- role: worker\n"
}
f, err := os.CreateTemp("", "kind-config-*.yaml")
if err != nil {
return err
}
defer os.Remove(f.Name())
if _, err := f.WriteString(cfg); err != nil {
return err
}
_ = f.Close()
args = append(args, "--config", f.Name())
}
out, err := exec.Command("kind", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("kind create: %w\n%s", err, string(out))
}
case "minikube":
out, err := exec.Command("minikube", "start").CombinedOutput()
if err != nil {
return fmt.Errorf("minikube start: %w\n%s", err, string(out))
}
default:
return fmt.Errorf("cluster type not yet supported: %s", tp.Cluster.Type)
}
return nil
}

func clusterDelete(cmd *cobra.Command, args []string) error {
tp, err := plan.Load(testDir)
if err != nil {
return err
}
switch tp.Cluster.Type {
case "kind":
out, err := exec.Command("kind", "delete", "cluster", "--name", tp.ClusterName()).CombinedOutput()
if err != nil {
return fmt.Errorf("kind delete: %w\n%s", err, string(out))
}
case "minikube":
out, err := exec.Command("minikube", "delete").CombinedOutput()
if err != nil {
return fmt.Errorf("minikube delete: %w\n%s", err, string(out))
}
default:
return fmt.Errorf("cluster type not yet supported: %s", tp.Cluster.Type)
}
return nil
}

// clusterExists returns whether the referenced cluster exists
func clusterExists(tp *plan.TestPlan) (bool, error) {
switch tp.Cluster.Type {
case "kind":
out, err := exec.Command("kind", "get", "clusters").CombinedOutput()
if err != nil {
return false, fmt.Errorf("kind get clusters: %w\n%s", err, string(out))
}
return plan.ContainsLine(string(out), tp.ClusterName()), nil
case "minikube":
out, _ := exec.Command("minikube", "status").CombinedOutput()
return len(out) > 0, nil
default:
return false, nil
}
}
173 changes: 173 additions & 0 deletions tools/k8s-test/cmd/deps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cmd

import (
"fmt"
"io/fs"
"path/filepath"

"github.com/grafana/helm-chart-toolbox/tools/k8s-test/internal/helmtestfs"
"github.com/grafana/helm-chart-toolbox/tools/k8s-test/internal/plan"
"github.com/grafana/helm-chart-toolbox/tools/k8s-test/internal/sh"
"github.com/spf13/cobra"
)

func newCmdDeps() *cobra.Command {
cmd := &cobra.Command{Use: "deps", Short: "Dependency operations"}
cmd.AddCommand(&cobra.Command{Use: "list", Short: "List embedded presets", RunE: depsList})
cmd.AddCommand(&cobra.Command{Use: "deploy", Short: "Deploy dependencies (embedded)", RunE: depsDeploy})
return cmd
}

func depsList(cmd *cobra.Command, args []string) error {
f := helmtestfs.FS()
return fs.WalkDir(f, "presets", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if filepath.Ext(path) == ".yaml" {
fmt.Fprintln(cmd.OutOrStdout(), path)
}
return nil
})
}

func depsDeploy(cmd *cobra.Command, args []string) error {
f := helmtestfs.FS()
tp, err := plan.Load(testDir)
if err != nil {
return err
}

// Ensure Flux controllers are installed (idempotent)
if err := sh.Run(cmd, "flux", "install", "--components=source-controller,helm-controller"); err != nil {
return err
}

// Apply only the dependencies requested in the test plan
for i := range tp.Dependencies {
dep := tp.Dependencies[i]
switch dep.Preset {
case "grafana":
if err := apply(f, cmd, "presets/helm-repository-grafana.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/namespace-grafana.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/preset-grafana.yaml"); err != nil {
return err
}
case "prometheus":
if err := apply(f, cmd, "presets/helm-repository-prometheus.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/namespace-prometheus.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/preset-prometheus.yaml"); err != nil {
return err
}
case "loki":
if err := apply(f, cmd, "presets/helm-repository-grafana.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/namespace-loki.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/preset-loki.yaml"); err != nil {
return err
}
case "tempo":
if err := apply(f, cmd, "presets/helm-repository-grafana.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/namespace-tempo.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/preset-tempo.yaml"); err != nil {
return err
}
case "pyroscope":
if err := apply(f, cmd, "presets/helm-repository-grafana.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/namespace-pyroscope.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "presets/preset-pyroscope.yaml"); err != nil {
return err
}
}
if dep.Directory != "" {
if err := sh.Run(cmd, "kubectl", "apply", "-f", tp.AbsPath(dep.Directory)); err != nil {
return err
}
}
if dep.File != "" {
if err := sh.Run(cmd, "kubectl", "apply", "-f", tp.AbsPath(dep.File)); err != nil {
return err
}
}
if dep.URL != "" {
if err := sh.Run(cmd, "kubectl", "apply", "-f", dep.URL); err != nil {
return err
}
}
if dep.Manifest != "" {
if err := sh.KubectlApplyYAML(cmd, dep.Manifest); err != nil {
return err
}
}
}

// Install toolbox test packages only if referenced in tests
hasQuery := false
hasK8sObjs := false
hasDelay := false
for _, t := range tp.Tests {
switch t.Type {
case "query-test":
hasQuery = true
case "kubernetes-objects-test":
hasK8sObjs = true
case "delay":
hasDelay = true
}
}
if hasQuery || hasK8sObjs || hasDelay {
if err := apply(f, cmd, "tests/namespace-toolbox.yaml"); err != nil {
return err
}
if err := apply(f, cmd, "tests/helm-repository-toolbox.yaml"); err != nil {
return err
}
}
if hasQuery {
if err := apply(f, cmd, "tests/helmrelease-query-test.yaml"); err != nil {
return err
}
}
if hasK8sObjs {
if err := apply(f, cmd, "tests/helmrelease-kubernetes-objects-test.yaml"); err != nil {
return err
}
}
if hasDelay {
if err := apply(f, cmd, "tests/helmrelease-delay.yaml"); err != nil {
return err
}
}

return nil
}

func apply(f fs.FS, cmd *cobra.Command, path string) error {
b, err := fs.ReadFile(f, path)
if err != nil {
return err
}
return sh.KubectlApplyYAML(cmd, string(b))
}
Loading
Loading