package controller import ( "context" "fmt" "net/http/httptest" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "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" "git.ryuvia.com/niklas/terdut-operator/internal/tdclient" ) var _ = Describe("TerdutTeam Controller", func() { const operatorNamespace = "default" var ( reconciler *TerdutTeamReconciler fake *fakeTerdutServer fakeSrv *httptest.Server srv *terdutv1alpha1.TerdutServer teamName string teamKey types.NamespacedName ) BeforeEach(func(ctx SpecContext) { fake, fakeSrv = newFakeTerdutServer() DeferCleanup(fakeSrv.Close) srv = bootstrapReadyTerdutServer(ctx, uniqueName("ttserver"), fakeSrv.URL) reconciler = &TerdutTeamReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), OperatorNamespace: operatorNamespace, NewClient: func(string) *tdclient.Client { return tdclient.New(fakeSrv.URL) }, } teamName = uniqueName("team") teamKey = types.NamespacedName{Name: teamName, Namespace: operatorNamespace} }) AfterEach(func(ctx SpecContext) { team := &terdutv1alpha1.TerdutTeam{} if err := k8sClient.Get(ctx, teamKey, team); err == nil { team.Finalizers = nil _ = k8sClient.Update(ctx, team) _ = k8sClient.Delete(ctx, team) } _ = k8sClient.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s.%s-team-credentials", operatorNamespace, teamName), Namespace: operatorNamespace, }}) // The TerdutServer bootstrapReadyTerdutServer created in BeforeEach // carries its own finalizer; clear it directly the same way, rather // than relying on TerdutServerReconciler to ever run again here. srvKey := types.NamespacedName{Name: srv.Name, Namespace: operatorNamespace} if err := k8sClient.Get(ctx, srvKey, srv); err == nil { srv.Finalizers = nil _ = k8sClient.Update(ctx, srv) _ = k8sClient.Delete(ctx, srv) } _ = k8sClient.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s.%s-instance-credentials", operatorNamespace, srv.Name), Namespace: operatorNamespace, }}) }) createTeam := func(ctx context.Context, displayName string, serverRef terdutv1alpha1.TerdutServerRef) { team := &terdutv1alpha1.TerdutTeam{ ObjectMeta: metav1.ObjectMeta{Name: teamName, Namespace: operatorNamespace}, Spec: terdutv1alpha1.TerdutTeamSpec{ServerRef: serverRef, DisplayName: displayName}, } Expect(k8sClient.Create(ctx, team)).To(Succeed()) } reconcileOnce := func(ctx context.Context) { _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: teamKey}) Expect(err).NotTo(HaveOccurred()) } readyCondition := func(ctx context.Context) metav1.Condition { team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) c := meta.FindStatusCondition(team.Status.Conditions, terdutv1alpha1.ConditionReady) Expect(c).NotTo(BeNil()) return *c } sameNSRef := func() terdutv1alpha1.TerdutServerRef { return terdutv1alpha1.TerdutServerRef{Name: srv.Name} } Describe("the happy path", func() { It("creates the team, mints its credential, and applies rename/oidc-groups", func(ctx SpecContext) { createTeam(ctx, "platform", sameNSRef()) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) // full create+mint+apply cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonTeamAdopted)) team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) Expect(team.Status.TeamID).To(Equal(fake.teams["platform"])) Expect(team.Status.CredentialsSecretRef).NotTo(BeNil()) var credsSecret corev1.Secret Expect(k8sClient.Get(ctx, types.NamespacedName{ Name: team.Status.CredentialsSecretRef.Name, Namespace: operatorNamespace, }, &credsSecret)).To(Succeed()) Expect(credsSecret.Data[credentialsSecretDataKey]).NotTo(BeEmpty()) }) }) Describe("waiting on the referenced TerdutServer", func() { It("reports ServerRefNotFound when the TerdutServer doesn't exist", func(ctx SpecContext) { createTeam(ctx, "orphan", terdutv1alpha1.TerdutServerRef{Name: testRefNotFoundName}) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) Expect(readyCondition(ctx).Reason).To(Equal(terdutv1alpha1.ReasonServerRefNotFound)) }) It("reports WaitingForServer when the TerdutServer exists but isn't Bootstrapped yet", func(ctx SpecContext) { unreadyName := uniqueName("ttserver-unready") unready := &terdutv1alpha1.TerdutServer{ ObjectMeta: metav1.ObjectMeta{Name: unreadyName, Namespace: operatorNamespace}, Spec: terdutv1alpha1.TerdutServerSpec{ Image: terdutv1alpha1.ImageSpec{Repository: testImageRepo, Tag: testImageTag}, Networking: terdutv1alpha1.NetworkingSpec{Hostname: "unready.example.invalid", ServicePort: 8080}, Database: terdutv1alpha1.DatabaseSpec{DSN: testDSN}, }, } Expect(k8sClient.Create(ctx, unready)).To(Succeed()) DeferCleanup(func() { _ = k8sClient.Delete(ctx, unready) }) createTeam(ctx, "waiting", terdutv1alpha1.TerdutServerRef{Name: unreadyName}) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) Expect(readyCondition(ctx).Reason).To(Equal(terdutv1alpha1.ReasonWaitingForServer)) }) }) Describe("cross-namespace serverRef", func() { var otherNS string BeforeEach(func(ctx SpecContext) { otherNS = uniqueName("ns") Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherNS}})).To(Succeed()) DeferCleanup(func() { _ = k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherNS}}) }) }) It("denies by default (From: None)", func(ctx SpecContext) { team := &terdutv1alpha1.TerdutTeam{ ObjectMeta: metav1.ObjectMeta{Name: teamName, Namespace: otherNS}, Spec: terdutv1alpha1.TerdutTeamSpec{ ServerRef: terdutv1alpha1.TerdutServerRef{Name: srv.Name, Namespace: operatorNamespace}, DisplayName: "cross-ns-denied", }, } Expect(k8sClient.Create(ctx, team)).To(Succeed()) DeferCleanup(func() { _ = k8sClient.Delete(ctx, team) }) crossKey := types.NamespacedName{Name: teamName, Namespace: otherNS} _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: crossKey}) // finalizer Expect(err).NotTo(HaveOccurred()) _, err = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: crossKey}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient.Get(ctx, crossKey, team)).To(Succeed()) cond := meta.FindStatusCondition(team.Status.Conditions, terdutv1alpha1.ConditionReady) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonRefNotPermitted)) }) It("permits when the TerdutServer's allowedTeams.namespaces.from is All", func(ctx SpecContext) { Expect(k8sClient.Get(ctx, types.NamespacedName{Name: srv.Name, Namespace: operatorNamespace}, srv)).To(Succeed()) srv.Spec.AllowedTeams.Namespaces.From = "All" Expect(k8sClient.Update(ctx, srv)).To(Succeed()) team := &terdutv1alpha1.TerdutTeam{ ObjectMeta: metav1.ObjectMeta{Name: teamName, Namespace: otherNS}, Spec: terdutv1alpha1.TerdutTeamSpec{ ServerRef: terdutv1alpha1.TerdutServerRef{Name: srv.Name, Namespace: operatorNamespace}, DisplayName: "cross-ns-allowed", }, } Expect(k8sClient.Create(ctx, team)).To(Succeed()) DeferCleanup(func() { _ = k8sClient.Delete(ctx, team) }) crossKey := types.NamespacedName{Name: teamName, Namespace: otherNS} _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: crossKey}) // finalizer Expect(err).NotTo(HaveOccurred()) _, err = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: crossKey}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient.Get(ctx, crossKey, team)).To(Succeed()) cond := meta.FindStatusCondition(team.Status.Conditions, terdutv1alpha1.ConditionReady) // Past the gate: Adopted (the fake server has no reason to // reject this), definitely not RefNotPermitted. Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonTeamAdopted)) }) }) Describe("adopt-on-409 recovery", func() { It("adopts an already-created team instead of erroring", func(ctx SpecContext) { fake.nextTeamID = 1 fake.teams["platform"] = 1 fake.teamNames[1] = "platform" createTeam(ctx, "platform", sameNSRef()) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) Expect(team.Status.TeamID).To(Equal(int64(1))) Expect(readyCondition(ctx).Status).To(Equal(metav1.ConditionTrue)) }) It("adopts an already-created team-scoped service account instead of erroring", func(ctx SpecContext) { createTeam(ctx, "platform", sameNSRef()) reconcileOnce(ctx) // finalizer // Pre-seed the service account the mint step is about to try to // create, simulating an attempt that got this far before being // interrupted. fake.nextID = 1 saName := fmt.Sprintf("terdut-team.%s.%s", operatorNamespace, teamName) fake.accounts[saName] = 1 fake.keyMints[1] = 1 reconcileOnce(ctx) Expect(readyCondition(ctx).Status).To(Equal(metav1.ConditionTrue)) team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) var credsSecret corev1.Secret Expect(k8sClient.Get(ctx, types.NamespacedName{ Name: team.Status.CredentialsSecretRef.Name, Namespace: operatorNamespace, }, &credsSecret)).To(Succeed()) // Minted fresh (mint2), not the pre-seeded account's original // (never-issued-to-this-reconcile) key. Expect(string(credsSecret.Data[credentialsSecretDataKey])).To(Equal("instance-key-1-mint2")) }) }) Describe("spec.invite", func() { It("mints a link into the TerdutTeam's own namespace, not the operator's", func(ctx SpecContext) { createTeam(ctx, "platform", sameNSRef()) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) // create+mint+apply team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) team.Spec.Invite.Enabled = true Expect(k8sClient.Update(ctx, team)).To(Succeed()) reconcileOnce(ctx) Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) Expect(team.Status.InviteSecretRef).NotTo(BeNil()) var secret corev1.Secret Expect(k8sClient.Get(ctx, types.NamespacedName{ Name: team.Status.InviteSecretRef.Name, Namespace: team.Namespace, }, &secret)).To(Succeed()) Expect(string(secret.Data[inviteSecretURLKey])).To(ContainSubstring("invite=")) Expect(string(secret.Data[inviteSecretInviteIDKey])).To(Equal("1")) inv := fake.invites[team.Status.TeamID][1] Expect(inv.Role).To(Equal("member"), "default role") Expect(inv.MaxUses).To(Equal(int64(1)), "default max uses") }) It("refreshes a link that's within a day of terdut-server's 7-day TTL", func(ctx SpecContext) { createTeam(ctx, "platform", sameNSRef()) reconcileOnce(ctx) reconcileOnce(ctx) team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) team.Spec.Invite.Enabled = true Expect(k8sClient.Update(ctx, team)).To(Succeed()) reconcileOnce(ctx) Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) firstSecretName := team.Status.InviteSecretRef.Name var secret corev1.Secret Expect(k8sClient.Get(ctx, types.NamespacedName{Name: firstSecretName, Namespace: team.Namespace}, &secret)).To(Succeed()) // Simulate the stored link being within the refresh window of // expiry, the way it genuinely would be six days from now, // without the test waiting six days. secret.Data[inviteSecretExpiresAtKey] = []byte(time.Now().Add(12 * time.Hour).Format(time.RFC3339)) // inside inviteRefreshWindow Expect(k8sClient.Update(ctx, &secret)).To(Succeed()) reconcileOnce(ctx) Expect(fake.inviteDelete[1]).To(BeTrue(), "the stale invite should have been revoked") Expect(fake.invites[team.Status.TeamID]).To(HaveKey(int64(2)), "a replacement should have been minted") }) It("revokes the invite when spec.invite.enabled flips back to false", func(ctx SpecContext) { createTeam(ctx, "platform", sameNSRef()) reconcileOnce(ctx) reconcileOnce(ctx) team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) team.Spec.Invite.Enabled = true Expect(k8sClient.Update(ctx, team)).To(Succeed()) reconcileOnce(ctx) Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) secretName := team.Status.InviteSecretRef.Name team.Spec.Invite.Enabled = false Expect(k8sClient.Update(ctx, team)).To(Succeed()) reconcileOnce(ctx) Expect(fake.inviteDelete[1]).To(BeTrue()) Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) Expect(team.Status.InviteSecretRef).To(BeNil()) var secret corev1.Secret err := k8sClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: team.Namespace}, &secret) Expect(err).To(HaveOccurred(), "the invite Secret should have been deleted") }) }) Describe("deletion", func() { It("deletes the team server-side and removes the credentials Secret", func(ctx SpecContext) { createTeam(ctx, "to-delete", sameNSRef()) reconcileOnce(ctx) reconcileOnce(ctx) Expect(readyCondition(ctx).Status).To(Equal(metav1.ConditionTrue)) team := &terdutv1alpha1.TerdutTeam{} Expect(k8sClient.Get(ctx, teamKey, team)).To(Succeed()) teamID := team.Status.TeamID credsName := team.Status.CredentialsSecretRef.Name Expect(k8sClient.Delete(ctx, team)).To(Succeed()) reconcileOnce(ctx) // runs the finalizer Expect(fake.teamDelete[teamID]).To(BeTrue()) err := k8sClient.Get(ctx, teamKey, team) Expect(err).To(HaveOccurred(), "the TerdutTeam itself should be gone once the finalizer clears") var leftover corev1.Secret err = k8sClient.Get(ctx, types.NamespacedName{Name: credsName, Namespace: operatorNamespace}, &leftover) Expect(err).To(HaveOccurred(), "the team-credentials Secret should have been cleaned up") }) }) })