From 351d287d88417e4ee690e9684fb3d2c86f5d1377 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 31 Jul 2021 14:05:35 -0700 Subject: [PATCH] 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 --- cmd/flux/main.go | 13 ++ cmd/flux/testdata/trace/deployment.txt | 26 +++ cmd/flux/testdata/trace/deployment.yaml | 129 +++++++++++++ .../trace/helmrelease-missing-git-ref.txt | 19 ++ .../trace/helmrelease-missing-git-ref.yaml | 73 +++++++ cmd/flux/testdata/trace/helmrelease.txt | 20 ++ cmd/flux/testdata/trace/helmrelease.yaml | 75 ++++++++ cmd/flux/testdata/trace/no-args.txt | 1 + cmd/flux/trace.go | 6 +- cmd/flux/trace_test.go | 180 ++++++++++++++++++ go.mod | 2 + go.sum | 2 + internal/utils/utils.go | 43 ++++- 13 files changed, 577 insertions(+), 12 deletions(-) create mode 100644 cmd/flux/testdata/trace/deployment.txt create mode 100644 cmd/flux/testdata/trace/deployment.yaml create mode 100644 cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt create mode 100644 cmd/flux/testdata/trace/helmrelease-missing-git-ref.yaml create mode 100644 cmd/flux/testdata/trace/helmrelease.txt create mode 100644 cmd/flux/testdata/trace/helmrelease.yaml create mode 100644 cmd/flux/testdata/trace/no-args.txt create mode 100644 cmd/flux/trace_test.go diff --git a/cmd/flux/main.go b/cmd/flux/main.go index 9295e83e..3ef4abac 100644 --- a/cmd/flux/main.go +++ b/cmd/flux/main.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/cobra" _ "k8s.io/client-go/plugin/pkg/client/auth" + "github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/pkg/manifestgen/install" ) @@ -126,6 +127,18 @@ func NewRootFlags() rootFlags { 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() { log.SetFlags(0) configureKubeconfig() diff --git a/cmd/flux/testdata/trace/deployment.txt b/cmd/flux/testdata/trace/deployment.txt new file mode 100644 index 00000000..04ad9399 --- /dev/null +++ b/cmd/flux/testdata/trace/deployment.txt @@ -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 + diff --git a/cmd/flux/testdata/trace/deployment.yaml b/cmd/flux/testdata/trace/deployment.yaml new file mode 100644 index 00000000..74fc5e46 --- /dev/null +++ b/cmd/flux/testdata/trace/deployment.yaml @@ -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 + diff --git a/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt b/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt new file mode 100644 index 00000000..f43f28cc --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt @@ -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 + diff --git a/cmd/flux/testdata/trace/helmrelease-missing-git-ref.yaml b/cmd/flux/testdata/trace/helmrelease-missing-git-ref.yaml new file mode 100644 index 00000000..dee892ae --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease-missing-git-ref.yaml @@ -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 + diff --git a/cmd/flux/testdata/trace/helmrelease.txt b/cmd/flux/testdata/trace/helmrelease.txt new file mode 100644 index 00000000..ac075ae8 --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease.txt @@ -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 + diff --git a/cmd/flux/testdata/trace/helmrelease.yaml b/cmd/flux/testdata/trace/helmrelease.yaml new file mode 100644 index 00000000..4f0db83d --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease.yaml @@ -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 + diff --git a/cmd/flux/testdata/trace/no-args.txt b/cmd/flux/testdata/trace/no-args.txt new file mode 100644 index 00000000..4f71b995 --- /dev/null +++ b/cmd/flux/testdata/trace/no-args.txt @@ -0,0 +1 @@ +object name is required diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go index d7ee022e..c18449f1 100644 --- a/cmd/flux/trace.go +++ b/cmd/flux/trace.go @@ -89,7 +89,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := rootCtx.kubeManager.NewClient(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { return err } @@ -121,7 +121,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println(report) + rootCmd.Print(report) return nil } @@ -130,7 +130,7 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println(report) + rootCmd.Print(report) return nil } diff --git a/cmd/flux/trace_test.go b/cmd/flux/trace_test.go new file mode 100644 index 00000000..e194be2a --- /dev/null +++ b/cmd/flux/trace_test.go @@ -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) +} diff --git a/go.mod b/go.mod index ed4d8972..e89ce2e3 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,10 @@ require ( github.com/fluxcd/pkg/version v0.0.1 github.com/fluxcd/source-controller/api v0.15.3 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/manifoldco/promptui v0.7.0 + github.com/mattn/go-shellwords v1.0.12 github.com/olekukonko/tablewriter v0.0.4 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index ba5342d4..a8695f81 100644 --- a/go.sum +++ b/go.sum @@ -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.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 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.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 50b903fa..b6e1ec90 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -131,12 +131,39 @@ func KubeConfig(kubeConfigPath string, kubeContext string) (*rest.Config, error) 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) if err != nil { 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() _ = apiextensionsv1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme) @@ -149,15 +176,13 @@ func KubeClient(kubeConfigPath string, kubeContext string) (client.Client, error _ = notificationv1.AddToScheme(scheme) _ = imagereflectv1.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