package controller import ( "context" "fmt" "net/http" "net/http/httptest" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1" ) const ( // unusedEndpoint is a syntactically valid URL no test here ever expects // to actually be dialed (the cases using it fail before reaching the // server-reachability check). unusedEndpoint = "http://unused.invalid" // tokenKey is the data key every test's credentials Secret uses. tokenKey = "token" ) // fakeTerdutServer is an httptest.Server standing in for terdut-server's // GET /api/version, per DESIGN.md ยง11 ("terdut-server's REST API is faked // with a small httptest.Server per controller test ... no real Postgres or // real terdut-server binary needed for controller unit tests"). func fakeTerdutServer(version string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/version" { w.WriteHeader(http.StatusNotFound) return } fmt.Fprintf(w, `{"version":%q}`, version) //nolint:errcheck })) } var _ = Describe("TerdutServer Controller", func() { const operatorNamespace = "default" var ( reconciler *TerdutServerReconciler name string objKey types.NamespacedName ) BeforeEach(func() { reconciler = &TerdutServerReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), OperatorNamespace: operatorNamespace, } name = fmt.Sprintf("test-server-%d-%d", GinkgoRandomSeed(), GinkgoParallelProcess()) objKey = types.NamespacedName{Name: name, Namespace: operatorNamespace} }) AfterEach(func(ctx SpecContext) { srv := &terdutv1alpha1.TerdutServer{} if err := k8sClient.Get(ctx, objKey, srv); err == nil { Expect(k8sClient.Delete(ctx, srv)).To(Succeed()) } }) createServer := func(ctx context.Context, spec terdutv1alpha1.TerdutServerSpec) { srv := &terdutv1alpha1.TerdutServer{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: operatorNamespace}, Spec: spec, } Expect(k8sClient.Create(ctx, srv)).To(Succeed()) } reconcileOnce := func(ctx context.Context) { _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objKey}) Expect(err).NotTo(HaveOccurred()) } readyCondition := func(ctx context.Context) metav1.Condition { srv := &terdutv1alpha1.TerdutServer{} Expect(k8sClient.Get(ctx, objKey, srv)).To(Succeed()) c := meta.FindStatusCondition(srv.Status.Conditions, terdutv1alpha1.ConditionReady) Expect(c).NotTo(BeNil(), "Ready condition should always be set after a reconcile") return *c } When("spec.credentialsSecretRef is unset", func() { It("reports Ready: False, reason CredentialsSecretRefRequired, and makes no API call", func(ctx SpecContext) { createServer(ctx, terdutv1alpha1.TerdutServerSpec{Endpoint: unusedEndpoint}) reconcileOnce(ctx) cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonCredentialsSecretRefRequired)) }) }) When("the referenced Secret does not exist", func() { It("reports Ready: False, reason CredentialsSecretNotFound", func(ctx SpecContext) { createServer(ctx, terdutv1alpha1.TerdutServerSpec{ Endpoint: unusedEndpoint, CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: "does-not-exist", Key: tokenKey}, }) reconcileOnce(ctx) cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonCredentialsSecretNotFound)) }) }) When("the referenced Secret exists but has no data under the given key", func() { It("reports Ready: False, reason CredentialsSecretInvalid", func(ctx SpecContext) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: name + "-creds", Namespace: operatorNamespace}, Data: map[string][]byte{"wrong-key": []byte("tdsa_something")}, } Expect(k8sClient.Create(ctx, secret)).To(Succeed()) defer func() { _ = k8sClient.Delete(ctx, secret) }() createServer(ctx, terdutv1alpha1.TerdutServerSpec{ Endpoint: unusedEndpoint, CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: secret.Name, Key: tokenKey}, }) reconcileOnce(ctx) cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonCredentialsSecretInvalid)) }) }) When("the Secret is valid but the server is unreachable", func() { It("reports Ready: False, reason ServerUnreachable", func(ctx SpecContext) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: name + "-creds", Namespace: operatorNamespace}, Data: map[string][]byte{tokenKey: []byte("tdsa_something")}, } Expect(k8sClient.Create(ctx, secret)).To(Succeed()) defer func() { _ = k8sClient.Delete(ctx, secret) }() createServer(ctx, terdutv1alpha1.TerdutServerSpec{ // Port 1 is never listening (closed cleanly and immediately, // unlike an unroutable address which would hang on a // connect timeout) -- keeps the test fast. Endpoint: "http://127.0.0.1:1", CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: secret.Name, Key: tokenKey}, }) reconcileOnce(ctx) cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonServerUnreachable)) }) }) When("the Secret is valid and the server answers /api/version", func() { It("reports Ready: True, Bootstrapped: True, and mirrors credentialsSecretRef into status", func(ctx SpecContext) { fake := fakeTerdutServer("v0.20.0") DeferCleanup(fake.Close) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: name + "-creds", Namespace: operatorNamespace}, Data: map[string][]byte{tokenKey: []byte("tdsa_something")}, } Expect(k8sClient.Create(ctx, secret)).To(Succeed()) defer func() { _ = k8sClient.Delete(ctx, secret) }() createServer(ctx, terdutv1alpha1.TerdutServerSpec{ Endpoint: fake.URL, CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: secret.Name, Key: tokenKey}, }) reconcileOnce(ctx) srv := &terdutv1alpha1.TerdutServer{} Expect(k8sClient.Get(ctx, objKey, srv)).To(Succeed()) ready := meta.FindStatusCondition(srv.Status.Conditions, terdutv1alpha1.ConditionReady) Expect(ready).NotTo(BeNil()) Expect(ready.Status).To(Equal(metav1.ConditionTrue)) Expect(ready.Reason).To(Equal(terdutv1alpha1.ReasonAdopted)) bootstrapped := meta.FindStatusCondition(srv.Status.Conditions, terdutv1alpha1.ConditionBootstrapped) Expect(bootstrapped).NotTo(BeNil()) Expect(bootstrapped.Status).To(Equal(metav1.ConditionTrue)) Expect(srv.Status.CredentialsSecretRef).NotTo(BeNil()) Expect(srv.Status.CredentialsSecretRef.Name).To(Equal(secret.Name)) Expect(srv.Status.CredentialsSecretRef.Key).To(Equal(tokenKey)) Expect(srv.Status.ObservedGeneration).To(Equal(srv.Generation)) }) }) When("the TerdutServer object no longer exists", func() { It("returns no error (deleted between enqueue and reconcile)", func(ctx SpecContext) { _, err := reconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: types.NamespacedName{Name: "never-created", Namespace: operatorNamespace}, }) Expect(err).NotTo(HaveOccurred()) }) }) }) var _ = Describe("TerdutServerReconciler sanity", func() { It("treats a real apierrors.IsNotFound the same as any other caller would", func() { // Guards against a refactor accidentally swapping in a different // not-found check that stops matching what client.Client actually // returns. Expect(apierrors.IsNotFound(apierrors.NewNotFound( terdutv1alpha1.GroupVersion.WithResource("terdutservers").GroupResource(), "x"))).To(BeTrue()) }) })