Add tests for flux trace command

Add tests for flux trace command that fake out the kubernetes client,
load objects from a yaml file and create them in the client, and
assert on the output of the trace command to an expected golden file.

This is a follow up from the suggestions in PR https://github.com/fluxcd/flux2/pull/1626 which suggested that additional
testing would be helpful. This test approach is modeled after the helm command tests.

This required some changes to the kubernetes client setup to make it
possible to use a fake. If we agree this pattern makes sense, it can be
applied to other commands.

Signed-off-by: Allen Porter <allen@thebends.org>
pull/1671/head
Allen Porter 4 years ago
parent eba6706f15
commit 351d287d88

@ -25,6 +25,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/pkg/manifestgen/install"
) )
@ -126,6 +127,18 @@ func NewRootFlags() rootFlags {
return rf return rf
} }
type rootContext struct {
kubeManager utils.KubeManager
}
var rootCtx = NewRootContext()
func NewRootContext() rootContext {
var rc rootContext
rc.kubeManager = utils.DefaultKubeManager()
return rc
}
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
configureKubeconfig() configureKubeconfig()

@ -0,0 +1,26 @@
Object: deployment/podinfo
Namespace: podinfo
Status: Managed by Flux
---
HelmRelease: podinfo
Namespace: podinfo
Revision: 6.0.0
Status: Last reconciled at 2021-07-16 15:42:20 +0000 UTC
Message: Release reconciliation succeeded
---
HelmChart: podinfo-podinfo
Namespace: flux-system
Chart: podinfo
Version: 6.0.0
Revision: 6.0.0
Status: Last reconciled at 2021-07-16 15:32:09 +0000 UTC
Message: Fetched revision: 6.0.0
---
HelmRepository: podinfo
Namespace: flux-system
URL: https://stefanprodan.github.io/podinfo
Revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56
Status: Last reconciled at 2021-07-11 00:25:46 +0000 UTC
Message: Fetched revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56

@ -0,0 +1,129 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: podinfo
helm.toolkit.fluxcd.io/name: podinfo
helm.toolkit.fluxcd.io/namespace: podinfo
name: podinfo
namespace: podinfo
spec:
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: podinfo
spec:
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: podinfo
namespace: podinfo
spec:
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
status:
conditions:
- lastTransitionTime: "2021-07-16T15:42:20Z"
message: Release reconciliation succeeded
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmChart
metadata:
name: podinfo-podinfo
namespace: flux-system
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
version: 6.0.0
status:
artifact:
checksum: cf13ba96773d9a879cd052c86e73199b3f96c854
lastUpdateTime: "2021-08-01T04:42:55Z"
revision: 6.0.0
conditions:
- lastTransitionTime: "2021-07-16T15:32:09Z"
message: 'Fetched revision: 6.0.0'
reason: ChartPullSucceeded
status: "True"
type: Ready
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: podinfo
namespace: flux-system
spec:
interval: 5m
timeout: 1m0s
url: https://stefanprodan.github.io/podinfo
status:
artifact:
checksum: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56
lastUpdateTime: "2021-07-11T00:25:46Z"
revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56
conditions:
- lastTransitionTime: "2021-07-11T00:25:46Z"
message: 'Fetched revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56'
reason: IndexationSucceed
status: "True"
type: Ready
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
spec:
path: ./infrastructure/
sourceRef:
kind: GitRepository
name: flux-system
validation: client
status:
conditions:
- lastTransitionTime: "2021-08-01T04:52:56Z"
message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
reason: ReconciliationSucceeded
status: "True"
type: Ready
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: flux-system
namespace: flux-system
spec:
gitImplementation: go-git
ref:
branch: main
secretRef:
name: flux-system

@ -0,0 +1,19 @@
Object: HelmRelease/podinfo
Namespace: podinfo
Status: Managed by Flux
---
Kustomization: infrastructure
Namespace: flux-system
Path: ./infrastructure
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f
Status: Last reconciled at 2021-08-01 04:52:56 +0000 UTC
Message: Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f
---
GitRepository: flux-system
Namespace: flux-system
URL: ssh://git@github.com/example/repo
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f
Status: Last reconciled at 2021-07-20 00:48:16 +0000 UTC
Message: Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f

@ -0,0 +1,73 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: podinfo
namespace: podinfo
spec:
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
status:
conditions:
- lastTransitionTime: "2021-07-16T15:42:20Z"
message: Release reconciliation succeeded
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
spec:
path: ./infrastructure
sourceRef:
kind: GitRepository
name: flux-system
validation: client
status:
conditions:
- lastTransitionTime: "2021-08-01T04:52:56Z"
message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
reason: ReconciliationSucceeded
status: "True"
type: Ready
lastAppliedRevision: main/696f056df216eea4f9401adbee0ff744d4df390f
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: flux-system
namespace: flux-system
spec:
gitImplementation: go-git
secretRef:
name: flux-system
url: ssh://git@github.com/example/repo
status:
artifact:
lastUpdateTime: "2021-08-01T04:28:42Z"
revision: main/696f056df216eea4f9401adbee0ff744d4df390f
conditions:
- lastTransitionTime: "2021-07-20T00:48:16Z"
message: 'Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
reason: GitOperationSucceed
status: "True"
type: Ready

@ -0,0 +1,20 @@
Object: HelmRelease/podinfo
Namespace: podinfo
Status: Managed by Flux
---
Kustomization: infrastructure
Namespace: flux-system
Path: ./infrastructure
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f
Status: Last reconciled at 2021-08-01 04:52:56 +0000 UTC
Message: Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f
---
GitRepository: flux-system
Namespace: flux-system
URL: ssh://git@github.com/example/repo
Branch: main
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f
Status: Last reconciled at 2021-07-20 00:48:16 +0000 UTC
Message: Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f

@ -0,0 +1,75 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: podinfo
namespace: podinfo
spec:
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
status:
conditions:
- lastTransitionTime: "2021-07-16T15:42:20Z"
message: Release reconciliation succeeded
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
spec:
path: ./infrastructure
sourceRef:
kind: GitRepository
name: flux-system
validation: client
status:
conditions:
- lastTransitionTime: "2021-08-01T04:52:56Z"
message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
reason: ReconciliationSucceeded
status: "True"
type: Ready
lastAppliedRevision: main/696f056df216eea4f9401adbee0ff744d4df390f
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: flux-system
namespace: flux-system
spec:
gitImplementation: go-git
ref:
branch: main
secretRef:
name: flux-system
url: ssh://git@github.com/example/repo
status:
artifact:
lastUpdateTime: "2021-08-01T04:28:42Z"
revision: main/696f056df216eea4f9401adbee0ff744d4df390f
conditions:
- lastTransitionTime: "2021-07-20T00:48:16Z"
message: 'Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
reason: GitOperationSucceed
status: "True"
type: Ready

@ -0,0 +1 @@
object name is required

@ -89,7 +89,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) kubeClient, err := rootCtx.kubeManager.NewClient(rootArgs.kubeconfig, rootArgs.kubecontext)
if err != nil { if err != nil {
return err return err
} }
@ -121,7 +121,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(report) rootCmd.Print(report)
return nil return nil
} }
@ -130,7 +130,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Println(report) rootCmd.Print(report)
return nil return nil
} }

@ -0,0 +1,180 @@
package main
import (
"bufio"
"bytes"
"context"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/fluxcd/flux2/internal/utils"
"github.com/google/go-cmp/cmp"
shellwords "github.com/mattn/go-shellwords"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func init() {
// Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC")
}
func readYamlObjects(objectFile string) ([]client.Object, error) {
obj, err := ioutil.ReadFile(objectFile)
if err != nil {
return nil, err
}
objects := []client.Object{}
reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(obj)))
for {
doc, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
}
unstructuredObj := &unstructured.Unstructured{}
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(doc), len(doc))
err = decoder.Decode(unstructuredObj)
if err != nil {
return nil, err
}
objects = append(objects, unstructuredObj)
}
return objects, nil
}
// A KubeManager that can create objects that are subject to a test.
type fakeKubeManager struct {
fakeClient client.Client
}
func (m *fakeKubeManager) NewClient(kubeconfig string, kubecontext string) (client.Client, error) {
return m.fakeClient, nil
}
func (m *fakeKubeManager) CreateObjects(clientObjects []client.Object) error {
for _, obj := range clientObjects {
err := m.fakeClient.Create(context.Background(), obj)
if err != nil {
return err
}
}
return nil
}
func NewFakeKubeManager() *fakeKubeManager {
c := fakeclient.NewClientBuilder().WithScheme(utils.NewScheme()).Build()
return &fakeKubeManager{
fakeClient: c,
}
}
// Run the command and return the captured output.
func executeCommand(cmd string) (string, error) {
args, err := shellwords.Parse(cmd)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
rootCmd.SetArgs(args)
_, err = rootCmd.ExecuteC()
result := buf.String()
return result, err
}
// Structure used for each test to load objects into kubernetes, run
// commands and assert on the expected output.
type cmdTestCase struct {
// The command line arguments to test.
args string
// When true, the test expects the command to fail.
wantError bool
// Filename that contains the expected test output.
goldenFile string
// Filename that contains yaml objects to load into Kubernetes
objectFile string
}
func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
km := NewFakeKubeManager()
rootCtx.kubeManager = km
if cmd.objectFile != "" {
clientObjects, err := readYamlObjects(cmd.objectFile)
if err != nil {
t.Fatalf("Error loading yaml: '%v'", err)
}
err = km.CreateObjects(clientObjects)
if err != nil {
t.Fatalf("Error creating test objects: '%v'", err)
}
}
actual, err := executeCommand(cmd.args)
if (err != nil) != cmd.wantError {
t.Fatalf("Expected error='%v', Got: %v", cmd.wantError, err)
}
if err != nil {
actual = err.Error()
}
contents, err := ioutil.ReadFile(cmd.goldenFile)
if err != nil {
t.Fatalf("Error reading golden file: '%s'", err)
}
expected := strings.TrimSuffix(string(contents), "\n")
diff := cmp.Diff(expected, actual)
if diff != "" {
t.Errorf("Mismatch from '%s' (-want +got):\n%s", cmd.goldenFile, diff)
}
}
func TestTraceNoArgs(t *testing.T) {
cmd := cmdTestCase{
args: "trace",
wantError: true,
goldenFile: "testdata/trace/no-args.txt",
}
cmd.runTestCmd(t)
}
func TestTraceDeployment(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind deployment --api-version=apps/v1",
wantError: false,
goldenFile: "testdata/trace/deployment.txt",
objectFile: "testdata/trace/deployment.yaml",
}
cmd.runTestCmd(t)
}
func TestTraceHelmRelease(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
wantError: false,
goldenFile: "testdata/trace/helmrelease.txt",
objectFile: "testdata/trace/helmrelease.yaml",
}
cmd.runTestCmd(t)
}
func TestTraceHelmReleaseMissingGitRef(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
wantError: false,
goldenFile: "testdata/trace/helmrelease-missing-git-ref.txt",
objectFile: "testdata/trace/helmrelease-missing-git-ref.yaml",
}
cmd.runTestCmd(t)
}

@ -18,8 +18,10 @@ require (
github.com/fluxcd/pkg/version v0.0.1 github.com/fluxcd/pkg/version v0.0.1
github.com/fluxcd/source-controller/api v0.15.3 github.com/fluxcd/source-controller/api v0.15.3
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/google/go-cmp v0.5.5
github.com/google/go-containerregistry v0.2.0 github.com/google/go-containerregistry v0.2.0
github.com/manifoldco/promptui v0.7.0 github.com/manifoldco/promptui v0.7.0
github.com/mattn/go-shellwords v1.0.12
github.com/olekukonko/tablewriter v0.0.4 github.com/olekukonko/tablewriter v0.0.4
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5

@ -537,6 +537,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=

@ -131,12 +131,39 @@ func KubeConfig(kubeConfigPath string, kubeContext string) (*rest.Config, error)
return cfg, nil return cfg, nil
} }
func KubeClient(kubeConfigPath string, kubeContext string) (client.Client, error) { // KubeManger creates a Kubernetes client.Client. This interface exists to
// facilitate unit testing and provide a fake client.
type KubeManager interface {
NewClient(string, string) (client.Client, error)
}
type defaultKubeManager struct{}
func DefaultKubeManager() KubeManager {
var manager defaultKubeManager
return manager
}
func (m defaultKubeManager) NewClient(kubeConfigPath string, kubeContext string) (client.Client, error) {
cfg, err := KubeConfig(kubeConfigPath, kubeContext) cfg, err := KubeConfig(kubeConfigPath, kubeContext)
if err != nil { if err != nil {
return nil, fmt.Errorf("kubernetes client initialization failed: %w", err) return nil, fmt.Errorf("kubernetes client initialization failed: %w", err)
} }
scheme := NewScheme()
kubeClient, err := client.New(cfg, client.Options{
Scheme: scheme,
})
if err != nil {
return nil, fmt.Errorf("kubernetes client initialization failed: %w", err)
}
return kubeClient, nil
}
// Create the Scheme, methods for serializing and deserializing API objects
// which can be shared by tests.
func NewScheme() *apiruntime.Scheme {
scheme := apiruntime.NewScheme() scheme := apiruntime.NewScheme()
_ = apiextensionsv1.AddToScheme(scheme) _ = apiextensionsv1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme)
@ -149,15 +176,13 @@ func KubeClient(kubeConfigPath string, kubeContext string) (client.Client, error
_ = notificationv1.AddToScheme(scheme) _ = notificationv1.AddToScheme(scheme)
_ = imagereflectv1.AddToScheme(scheme) _ = imagereflectv1.AddToScheme(scheme)
_ = imageautov1.AddToScheme(scheme) _ = imageautov1.AddToScheme(scheme)
return scheme
kubeClient, err := client.New(cfg, client.Options{
Scheme: scheme,
})
if err != nil {
return nil, fmt.Errorf("kubernetes client initialization failed: %w", err)
} }
return kubeClient, nil func KubeClient(kubeConfigPath string, kubeContext string) (client.Client, error) {
m := DefaultKubeManager()
kubeClient, err := m.NewClient(kubeConfigPath, kubeContext)
return kubeClient, err
} }
// SplitKubeConfigPath splits the given KUBECONFIG path based on the runtime OS // SplitKubeConfigPath splits the given KUBECONFIG path based on the runtime OS

Loading…
Cancel
Save