package controller import ( "context" "net/http/httptest" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "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("TerdutEscalationRule Controller", func() { const operatorNamespace = "default" var ( reconciler *TerdutEscalationRuleReconciler fake *fakeTerdutServer fakeSrv *httptest.Server srv *terdutv1alpha1.TerdutServer team *terdutv1alpha1.TerdutTeam ruleName string ruleKey types.NamespacedName ) BeforeEach(func(ctx SpecContext) { fake, fakeSrv = newFakeTerdutServer() DeferCleanup(fakeSrv.Close) srv = bootstrapReadyTerdutServer(ctx, uniqueName("erserver"), fakeSrv.URL) team = readyTerdutTeam(ctx, operatorNamespace, uniqueName("erteam"), srv, fakeSrv.URL) reconciler = &TerdutEscalationRuleReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), OperatorNamespace: operatorNamespace, NewClient: func(string) *tdclient.Client { return tdclient.New(fakeSrv.URL) }, } ruleName = uniqueName("escalation") ruleKey = types.NamespacedName{Name: ruleName, Namespace: operatorNamespace} }) AfterEach(func(ctx SpecContext) { rule := &terdutv1alpha1.TerdutEscalationRule{} if err := k8sClient.Get(ctx, ruleKey, rule); err == nil { rule.Finalizers = nil _ = k8sClient.Update(ctx, rule) _ = k8sClient.Delete(ctx, rule) } // team/srv carry their own finalizers from readyTerdutTeam/ // bootstrapReadyTerdutServer -- clear them directly the same way // terdutteam_controller_test.go's own AfterEach does, rather than // relying on either reconciler to ever run again here. teamKey := types.NamespacedName{Name: team.Name, Namespace: operatorNamespace} if err := k8sClient.Get(ctx, teamKey, team); err == nil { team.Finalizers = nil _ = k8sClient.Update(ctx, team) _ = k8sClient.Delete(ctx, team) } 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) } }) createRule := func(ctx context.Context, teamRef terdutv1alpha1.TerdutTeamRef, levels []terdutv1alpha1.EscalationLevel) { rule := &terdutv1alpha1.TerdutEscalationRule{ ObjectMeta: metav1.ObjectMeta{Name: ruleName, Namespace: operatorNamespace}, Spec: terdutv1alpha1.TerdutEscalationRuleSpec{ TeamRef: teamRef, Levels: levels, }, } Expect(k8sClient.Create(ctx, rule)).To(Succeed()) } reconcileOnce := func(ctx context.Context) { _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: ruleKey}) Expect(err).NotTo(HaveOccurred()) } readyCondition := func(ctx context.Context) metav1.Condition { rule := &terdutv1alpha1.TerdutEscalationRule{} Expect(k8sClient.Get(ctx, ruleKey, rule)).To(Succeed()) c := meta.FindStatusCondition(rule.Status.Conditions, terdutv1alpha1.ConditionReady) Expect(c).NotTo(BeNil()) return *c } sameTeamRef := func() terdutv1alpha1.TerdutTeamRef { return terdutv1alpha1.TerdutTeamRef{Name: team.Name} } Describe("the happy path", func() { It("resolves usernames and applies the escalation policy", func(ctx SpecContext) { fake.seedUser("alice") createRule(ctx, sameTeamRef(), []terdutv1alpha1.EscalationLevel{ {Timeout: "5m", Targets: []terdutv1alpha1.EscalationTarget{ {Kind: terdutv1alpha1.EscalationTargetUser, Username: "alice"}, }}, {Timeout: "10m", Targets: []terdutv1alpha1.EscalationTarget{ {Kind: terdutv1alpha1.EscalationTargetOncall}, }}, }) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) // resolve + apply cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonChildAdopted)) applied, ok := fake.escalation[team.Status.TeamID] Expect(ok).To(BeTrue()) Expect(applied.Levels).To(HaveLen(2)) Expect(applied.Levels[0].Targets[0].Kind).To(Equal("user")) Expect(applied.Levels[0].Targets[0].UserID).NotTo(BeNil()) Expect(*applied.Levels[0].Targets[0].UserID).To(Equal(fake.users["alice"])) Expect(applied.Levels[1].Targets[0].Kind).To(Equal("oncall")) Expect(applied.Levels[1].Targets[0].UserID).To(BeNil()) }) }) Describe("an unresolvable username", func() { It("reports UnknownUser and never calls PUT /api/teams/{id}/escalation", func(ctx SpecContext) { createRule(ctx, sameTeamRef(), []terdutv1alpha1.EscalationLevel{ {Timeout: "5m", Targets: []terdutv1alpha1.EscalationTarget{ {Kind: terdutv1alpha1.EscalationTargetUser, Username: "ghost"}, }}, }) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) cond := readyCondition(ctx) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonUnknownUser)) _, applied := fake.escalation[team.Status.TeamID] Expect(applied).To(BeFalse()) }) }) Describe("waiting on the referenced TerdutTeam", func() { It("reports TeamRefNotFound when the TerdutTeam doesn't exist", func(ctx SpecContext) { createRule(ctx, terdutv1alpha1.TerdutTeamRef{Name: testRefNotFoundName}, []terdutv1alpha1.EscalationLevel{ {Timeout: "5m", Targets: []terdutv1alpha1.EscalationTarget{{Kind: terdutv1alpha1.EscalationTargetOncall}}}, }) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) Expect(readyCondition(ctx).Reason).To(Equal(terdutv1alpha1.ReasonTeamRefNotFound)) }) It("reports WaitingForTeam when the TerdutTeam exists but isn't Ready yet", func(ctx SpecContext) { unreadyName := uniqueName("erteam-unready") unready := &terdutv1alpha1.TerdutTeam{ ObjectMeta: metav1.ObjectMeta{Name: unreadyName, Namespace: operatorNamespace}, Spec: terdutv1alpha1.TerdutTeamSpec{ ServerRef: terdutv1alpha1.TerdutServerRef{Name: srv.Name}, DisplayName: testUnreadyDisplayName, }, } Expect(k8sClient.Create(ctx, unready)).To(Succeed()) DeferCleanup(func() { _ = k8sClient.Delete(ctx, unready) }) createRule(ctx, terdutv1alpha1.TerdutTeamRef{Name: unreadyName}, []terdutv1alpha1.EscalationLevel{ {Timeout: "5m", Targets: []terdutv1alpha1.EscalationTarget{{Kind: terdutv1alpha1.EscalationTargetOncall}}}, }) reconcileOnce(ctx) // finalizer reconcileOnce(ctx) Expect(readyCondition(ctx).Reason).To(Equal(terdutv1alpha1.ReasonWaitingForTeam)) }) }) Describe("deletion", func() { It("PUTs an empty policy (this resource's only available undo) and removes the finalizer", func(ctx SpecContext) { fake.seedUser("alice") createRule(ctx, sameTeamRef(), []terdutv1alpha1.EscalationLevel{ {Timeout: "5m", Targets: []terdutv1alpha1.EscalationTarget{ {Kind: terdutv1alpha1.EscalationTargetUser, Username: "alice"}, }}, }) reconcileOnce(ctx) reconcileOnce(ctx) Expect(readyCondition(ctx).Status).To(Equal(metav1.ConditionTrue)) Expect(fake.escalation[team.Status.TeamID].Levels).To(HaveLen(1)) rule := &terdutv1alpha1.TerdutEscalationRule{} Expect(k8sClient.Get(ctx, ruleKey, rule)).To(Succeed()) Expect(k8sClient.Delete(ctx, rule)).To(Succeed()) reconcileOnce(ctx) // runs the finalizer Expect(fake.escalation[team.Status.TeamID].Levels).To(BeEmpty()) err := k8sClient.Get(ctx, ruleKey, rule) Expect(err).To(HaveOccurred(), "the TerdutEscalationRule itself should be gone once the finalizer clears") }) }) })