@@ -26,71 +26,219 @@ package controller
2626
2727import (
2828 "context"
29+ "time"
2930
3031 . "github.com/onsi/ginkgo/v2"
3132 . "github.com/onsi/gomega"
3233 v1 "k8s.io/api/core/v1"
3334 "k8s.io/apimachinery/pkg/api/errors"
35+ "k8s.io/apimachinery/pkg/api/meta"
36+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3437 "k8s.io/apimachinery/pkg/types"
38+ "k8s.io/client-go/tools/record"
39+ "sigs.k8s.io/controller-runtime/pkg/client"
40+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3541 "sigs.k8s.io/controller-runtime/pkg/reconcile"
3642
37- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38-
3943 aliecsv1alpha1 "github.com/AliceO2Group/Control/control-operator/api/v1alpha1"
4044)
4145
4246var _ = Describe ("Task Controller" , func () {
43- Context ("When reconciling a resource" , func () {
44- const resourceName = "test-resource"
47+ // Reconcile takes the Task one step further on every pass and then returns to wait
48+ // for the next event: first the finalizer, then the Pod, then the gRPC connection,
49+ // then the state of the container, and finally the transition. These specs run in
50+ // order and each one drives the pass responsible for the next step.
51+ Describe ("When reconciling a Task step by step" , Ordered , func () {
52+ const (
53+ resourceName = "test-resource"
54+ namespace = "default"
55+ containerName = "placeholder-name"
56+ containerImg = "placeholder-image:latest"
57+ // The OCC stub listens on loopback, so the Pod IP the controller dials has
58+ // to point back at the test process.
59+ podIP = "127.0.0.1"
60+ )
4561
46- ctx := context .Background ()
62+ var (
63+ ctx context.Context
64+ taskName types.NamespacedName
65+ podName types.NamespacedName
66+ occServer * occServerStub
67+ reconciler * TaskReconciler
68+ )
4769
48- typeNamespacedName := types.NamespacedName {
49- Name : resourceName ,
50- Namespace : "default" , // TODO(user):Modify as needed
70+ reconcileOnce := func () reconcile.Result {
71+ GinkgoHelper ()
72+ res , err := reconciler .Reconcile (ctx , reconcile.Request {NamespacedName : taskName })
73+ Expect (err ).NotTo (HaveOccurred ())
74+ return res
5175 }
52- task := & aliecsv1alpha1.Task {}
5376
54- BeforeEach (func () {
55- By ("creating the custom resource for the Kind Task" )
56- err := k8sClient .Get (ctx , typeNamespacedName , task )
57- if err != nil && errors .IsNotFound (err ) {
58- resource := & aliecsv1alpha1.Task {
59- ObjectMeta : metav1.ObjectMeta {
60- Name : resourceName ,
61- Namespace : "default" ,
62- },
63- Spec : aliecsv1alpha1.TaskSpec {
64- State : "standby" ,
65- Pod : v1.PodSpec {Containers : []v1.Container {}},
66- },
67- }
68- Expect (k8sClient .Create (ctx , resource )).To (Succeed ())
69- }
70- })
71-
72- AfterEach (func () {
73- // TODO(user): Cleanup logic after each test, like removing the task instance.
77+ getTask := func () * aliecsv1alpha1.Task {
78+ GinkgoHelper ()
7479 task := & aliecsv1alpha1.Task {}
75- err := k8sClient .Get (ctx , typeNamespacedName , task )
80+ Expect (k8sClient .Get (ctx , taskName , task )).To (Succeed ())
81+ return task
82+ }
83+
84+ BeforeAll (func () {
85+ ctx = context .Background ()
86+ taskName = types.NamespacedName {Name : resourceName , Namespace : namespace }
87+ podName = types.NamespacedName {Name : podNameFromTask (resourceName ), Namespace : namespace }
88+
89+ By ("starting the OCC server stub that stands in for the task container" )
90+ stub , port , stopOccServer , err := startOccServerStub ()
7691 Expect (err ).NotTo (HaveOccurred ())
92+ DeferCleanup (stopOccServer )
7793
78- By ("Cleanup the specific resource instance Task" )
79- Expect (k8sClient .Delete (ctx , task )).To (Succeed ())
80- })
81- It ("should successfully reconcile the resource" , func () {
82- By ("Reconciling the created resource" )
83- controllerReconciler := & TaskReconciler {
94+ occServer = stub
95+ occServer .setState ("standby" )
96+
97+ reconciler = & TaskReconciler {
8498 Client : k8sClient ,
8599 Scheme : k8sClient .Scheme (),
100+ // A zero-value FakeRecorder discards events; NewFakeRecorder would block
101+ // once its buffer filled up.
102+ Recorder : & record.FakeRecorder {},
86103 }
87104
88- _ , err := controllerReconciler .Reconcile (ctx , reconcile.Request {
89- NamespacedName : typeNamespacedName ,
105+ By ("creating the custom resource for the Kind Task" )
106+ task := & aliecsv1alpha1.Task {
107+ ObjectMeta : metav1.ObjectMeta {
108+ Name : resourceName ,
109+ Namespace : namespace ,
110+ },
111+ Spec : aliecsv1alpha1.TaskSpec {
112+ Control : aliecsv1alpha1.TaskSpecControl {Mode : "direct" , Port : port },
113+ State : "standby" ,
114+ Pod : v1.PodSpec {
115+ Containers : []v1.Container {{Name : containerName , Image : containerImg }},
116+ },
117+ },
118+ }
119+ Expect (k8sClient .Create (ctx , task )).To (Succeed ())
120+
121+ // The last spec deletes the Task and its Pod through the controller, so this
122+ // only clears what an earlier failure left behind.
123+ DeferCleanup (func () {
124+ if c , loaded := clientsForContainers .LoadAndDelete (resourceName ); loaded {
125+ Expect (c .(* OccClient ).Close ()).To (Succeed ())
126+ }
127+
128+ pod := & v1.Pod {}
129+ if err := k8sClient .Get (ctx , podName , pod ); err == nil {
130+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , pod , client .GracePeriodSeconds (0 )))).To (Succeed ())
131+ }
132+
133+ leftover := & aliecsv1alpha1.Task {}
134+ if err := k8sClient .Get (ctx , taskName , leftover ); err == nil {
135+ controllerutil .RemoveFinalizer (leftover , taskFinalizer )
136+ Expect (k8sClient .Update (ctx , leftover )).To (Succeed ())
137+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , leftover ))).To (Succeed ())
138+ }
90139 })
91- Expect (err ).NotTo (HaveOccurred ())
92- // TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
93- // Example: If you expect a certain status condition after reconciliation, verify it here.
140+ })
141+
142+ It ("sets the finalizer before touching anything else" , func () {
143+ reconcileOnce ()
144+
145+ Expect (controllerutil .ContainsFinalizer (getTask (), taskFinalizer )).To (BeTrue ())
146+ Expect (k8sClient .Get (ctx , podName , & v1.Pod {})).To (Satisfy (errors .IsNotFound ),
147+ "the Pod should wait until the finalizer is persisted" )
148+ })
149+
150+ It ("creates the Pod for the Task" , func () {
151+ reconcileOnce ()
152+
153+ pod := & v1.Pod {}
154+ Expect (k8sClient .Get (ctx , podName , pod )).To (Succeed ())
155+
156+ Expect (pod .Spec .Containers ).To (HaveLen (1 ))
157+ Expect (pod .Spec .Containers [0 ].Image ).To (Equal (containerImg ))
158+ Expect (pod .Spec .RestartPolicy ).To (Equal (v1 .RestartPolicyNever ))
159+ Expect (pod .Labels ).To (HaveKeyWithValue ("task_name" , resourceName ))
160+
161+ Expect (pod .OwnerReferences ).To (HaveLen (1 ))
162+ Expect (pod .OwnerReferences [0 ].Name ).To (Equal (resourceName ))
163+ Expect (pod .OwnerReferences [0 ].Controller ).To (HaveValue (BeTrue ()))
164+ })
165+
166+ It ("waits for the Pod to report an IP before connecting" , func () {
167+ reconcileOnce ()
168+
169+ _ , connected := clientsForContainers .Load (resourceName )
170+ Expect (connected ).To (BeFalse ())
171+ Expect (meta .FindStatusCondition (getTask ().Status .Conditions , aliecsv1alpha1 .ConditionPodReady )).To (BeNil ())
172+ })
173+
174+ It ("connects to the container once the Pod is running" , func () {
175+ By ("reporting the Pod status that a kubelet would report" )
176+ // envtest runs no kubelet, so nothing else moves the Pod to Running.
177+ pod := & v1.Pod {}
178+ Expect (k8sClient .Get (ctx , podName , pod )).To (Succeed ())
179+ pod .Status .Phase = v1 .PodRunning
180+ pod .Status .PodIP = podIP
181+ Expect (k8sClient .Status ().Update (ctx , pod )).To (Succeed ())
182+
183+ reconcileOnce ()
184+
185+ occClient , connected := clientsForContainers .Load (resourceName )
186+ Expect (connected ).To (BeTrue ())
187+
188+ task := getTask ()
189+ Expect (meta .IsStatusConditionTrue (task .Status .Conditions , aliecsv1alpha1 .ConditionPodReady )).To (BeTrue ())
190+ Expect (meta .IsStatusConditionTrue (task .Status .Conditions , aliecsv1alpha1 .ConditionGRPCConnected )).To (BeTrue ())
191+
192+ // This pass asked to be requeued until the connection is Ready, which is what
193+ // the next step needs.
194+ connectCtx , cancel := context .WithTimeout (ctx , 5 * time .Second )
195+ defer cancel ()
196+ Expect (occClient .(* OccClient ).WaitUntilConnected (connectCtx )).To (Succeed ())
197+ })
198+
199+ It ("reads the state from the container" , func () {
200+ reconcileOnce ()
201+
202+ task := getTask ()
203+ Expect (task .Status .State ).To (Equal (occServer .getState ()))
204+ Expect (task .Status .Error ).To (BeEmpty ())
205+ Expect (meta .IsStatusConditionTrue (task .Status .Conditions , aliecsv1alpha1 .ConditionStateAccessible )).To (BeTrue ())
206+ })
207+
208+ It ("transitions the container to a newly requested state" , func () {
209+ By ("asking the Task for a state the container is not in" )
210+ task := getTask ()
211+ task .Spec .State = "configured"
212+ Expect (k8sClient .Update (ctx , task )).To (Succeed ())
213+
214+ reconcileOnce ()
215+
216+ task = getTask ()
217+ Expect (task .Status .State ).To (Equal ("configured" ))
218+ Expect (task .Status .Error ).To (BeEmpty ())
219+ Expect (meta .IsStatusConditionTrue (task .Status .Conditions , aliecsv1alpha1 .ConditionStateTransitionResult )).To (BeTrue ())
220+
221+ By ("leaving the container in the requested state" )
222+ Expect (occServer .getState ()).To (Equal ("configured" ))
223+ })
224+
225+ It ("clears the connection and the Pod when the Task is deleted" , func () {
226+ Expect (k8sClient .Delete (ctx , getTask ())).To (Succeed ())
227+
228+ reconcileOnce ()
229+
230+ _ , connected := clientsForContainers .Load (resourceName )
231+ Expect (connected ).To (BeFalse ())
232+
233+ // Nothing schedules the Pod in envtest, so the API server drops it right away
234+ // instead of waiting for a kubelet to confirm termination.
235+ Expect (k8sClient .Get (ctx , podName , & v1.Pod {})).To (Satisfy (errors .IsNotFound ))
236+ Expect (controllerutil .ContainsFinalizer (getTask (), taskFinalizer )).To (BeTrue (),
237+ "this pass only deletes the Pod, the finalizer goes on the next one" )
238+
239+ reconcileOnce ()
240+
241+ Expect (k8sClient .Get (ctx , taskName , & aliecsv1alpha1.Task {})).To (Satisfy (errors .IsNotFound ))
94242 })
95243 })
96244})
0 commit comments