From 8cbd4e8172f97d72a56e51692c82fa592fdbcc72 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 4 Aug 2021 07:43:36 -0700 Subject: [PATCH] Add test for "flux create source git" The create source tests are more interesting than the existing tests as they create objects then wit for the flux source reconciler to complete. The tests simulate this with a background goroutine that waits for an object to be created then uses a test specific function to update it. The tests set a timeout so that if there is a failure they timeout somewhat quickly rather than hanging for a longer period of time. Signed-off-by: Allen Porter --- cmd/flux/create_source_git_test.go | 131 ++++++++++++++++++ cmd/flux/main_test.go | 9 +- cmd/flux/main_unit_test.go | 14 ++ .../testdata/create_source_git/success.golden | 6 + 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 cmd/flux/create_source_git_test.go create mode 100644 cmd/flux/testdata/create_source_git/success.golden diff --git a/cmd/flux/create_source_git_test.go b/cmd/flux/create_source_git_test.go new file mode 100644 index 00000000..5558ff1f --- /dev/null +++ b/cmd/flux/create_source_git_test.go @@ -0,0 +1,131 @@ +// +build unit + +package main + +import ( + "context" + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" + "time" +) + +var pollInterval = 50 * time.Millisecond +var testTimeout = 10 * time.Second + +// Update the GitRepository once created to exercise test specific behavior +type reconcileFunc func(repo *sourcev1.GitRepository) + +// reconciler waits for an object to be created, then invokes a test supplied +// function to mutate that object, simulating a controller. +// Test should invoke run() to run the background reconciler task which +// polls to wait for the object to exist before applying the update function. +// Any errors from the reconciler are asserted on test completion. +type reconciler struct { + client client.Client + name types.NamespacedName + reconcile reconcileFunc +} + +// Start the background task that waits for the object to exist then applies +// the update function. +func (r *reconciler) run(t *testing.T) { + result := make(chan error) + go func() { + defer close(result) + err := wait.PollImmediate( + pollInterval, + testTimeout, + r.conditionFunc) + result <- err + }() + t.Cleanup(func() { + if err := <-result; err != nil { + t.Errorf("Failure from test reconciler: '%v':", err.Error()) + } + }) +} + +// A ConditionFunction that waits for the named GitRepository to be created, +// then sets the ready condition to true. +func (r *reconciler) conditionFunc() (bool, error) { + var repo sourcev1.GitRepository + if err := r.client.Get(context.Background(), r.name, &repo); err != nil { + if errors.IsNotFound(err) { + return false, nil // Keep polling until object is created + } + return true, err + } + r.reconcile(&repo) + err := r.client.Status().Update(context.Background(), &repo) + return true, err +} + +func TestCreateSourceGit(t *testing.T) { + // Default command used for multiple tests + var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --timeout=" + testTimeout.String() + + cases := []struct { + name string + args string + assert assertFunc + reconcile reconcileFunc + }{ + { + "NoArgs", + "create source git", + assertError("GitRepository source name is required"), + nil, + }, { + "Succeeded", + command, + assertGoldenFile("testdata/create_source_git/success.golden"), + func(repo *sourcev1.GitRepository) { + meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionTrue, sourcev1.GitOperationSucceedReason, "succeeded message") + repo.Status.Artifact = &sourcev1.Artifact{ + Path: "some-path", + Revision: "v1", + } + }, + }, { + "Failed", + command, + assertError("failed message"), + func(repo *sourcev1.GitRepository) { + meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionFalse, sourcev1.URLInvalidReason, "failed message") + }, + }, { + "NoArtifact", + command, + assertError("GitRepository source reconciliation completed but no artifact was found"), + func(repo *sourcev1.GitRepository) { + // Updated with no artifact + meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionTrue, sourcev1.GitOperationSucceedReason, "succeeded message") + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ns := allocateNamespace("podinfo") + setupTestNamespace(ns, t) + if tc.reconcile != nil { + r := reconciler{ + client: testEnv.client, + name: types.NamespacedName{Namespace: ns, Name: "podinfo"}, + reconcile: tc.reconcile, + } + r.run(t) + } + cmd := cmdTestCase{ + args: tc.args + " -n=" + ns, + assert: tc.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 570e0302..f8e13165 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -14,6 +14,7 @@ import ( "text/template" "time" + "github.com/fluxcd/flux2/internal/utils" "github.com/google/go-cmp/cmp" "github.com/mattn/go-shellwords" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -132,7 +133,9 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String()) os.WriteFile(tmpFilename, kubeConfig, 0644) - k8sClient, err := client.NewWithWatch(cfg, client.Options{}) + k8sClient, err := client.NewWithWatch(cfg, client.Options{ + Scheme: utils.NewScheme(), + }) if err != nil { return nil, err } @@ -158,7 +161,9 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager if err != nil { return nil, err } - k8sClient, err := client.NewWithWatch(cfg, client.Options{}) + k8sClient, err := client.NewWithWatch(cfg, client.Options{ + Scheme: utils.NewScheme(), + }) if err != nil { return nil, err } diff --git a/cmd/flux/main_unit_test.go b/cmd/flux/main_unit_test.go index a26a928c..31de6b24 100644 --- a/cmd/flux/main_unit_test.go +++ b/cmd/flux/main_unit_test.go @@ -3,7 +3,10 @@ package main import ( + "context" "fmt" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "os" "testing" ) @@ -32,3 +35,14 @@ func TestMain(m *testing.M) { os.Exit(code) } + +func setupTestNamespace(namespace string, t *testing.T) { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + err := testEnv.client.Create(context.Background(), ns) + if err != nil { + t.Fatalf("Failed to create namespace: %v", err) + } + t.Cleanup(func() { + _ = testEnv.client.Delete(context.Background(), ns) + }) +} diff --git a/cmd/flux/testdata/create_source_git/success.golden b/cmd/flux/testdata/create_source_git/success.golden new file mode 100644 index 00000000..37492496 --- /dev/null +++ b/cmd/flux/testdata/create_source_git/success.golden @@ -0,0 +1,6 @@ +✚ generating GitRepository source +► applying GitRepository source +✔ GitRepository source created +◎ waiting for GitRepository source reconciliation +✔ GitRepository source reconciliation completed +✔ fetched revision: v1