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