@@ -21,6 +21,7 @@ import (
2121 "fmt"
2222 "net/http"
2323 "os"
24+ "time"
2425
2526 "github.com/gophercloud/gophercloud/v2/testhelper"
2627 "github.com/gophercloud/gophercloud/v2/testhelper/client"
@@ -30,6 +31,8 @@ import (
3031 "k8s.io/apimachinery/pkg/api/meta"
3132 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3233 "k8s.io/apimachinery/pkg/types"
34+ "k8s.io/utils/clock"
35+ clocktesting "k8s.io/utils/clock/testing"
3336 ctrl "sigs.k8s.io/controller-runtime"
3437
3538 kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
@@ -221,6 +224,7 @@ var _ = Describe("Onboarding Controller", func() {
221224 Client : k8sClient ,
222225 Scheme : k8sClient .Scheme (),
223226 TestFlavorID : "1" ,
227+ Clock : clock.RealClock {},
224228 computeClient : client .ServiceClient (fakeServer ),
225229 testComputeClient : client .ServiceClient (fakeServer ),
226230 testImageClient : client .ServiceClient (fakeServer ),
@@ -340,6 +344,12 @@ var _ = Describe("Onboarding Controller", func() {
340344 })
341345
342346 Context ("running tests after initial setup" , func () {
347+ var (
348+ serverActionHandler func (http.ResponseWriter , * http.Request )
349+ serverDeleteHandler func (http.ResponseWriter , * http.Request )
350+ serverDetailHandler func (http.ResponseWriter , * http.Request )
351+ )
352+
343353 BeforeEach (func (ctx SpecContext ) {
344354 hv := & kvmv1.Hypervisor {}
345355 Expect (k8sClient .Get (ctx , namespacedName , hv )).To (Succeed ())
@@ -371,11 +381,14 @@ var _ = Describe("Onboarding Controller", func() {
371381 Expect (err ).NotTo (HaveOccurred ())
372382 })
373383
384+ serverDetailHandler = func (w http.ResponseWriter , _ * http.Request ) {
385+ _ , err := fmt .Fprint (w , emptyServersBody )
386+ Expect (err ).NotTo (HaveOccurred ())
387+ }
374388 fakeServer .Mux .HandleFunc ("GET /servers/detail" , func (w http.ResponseWriter , r * http.Request ) {
375389 w .Header ().Add ("Content-Type" , "application/json" )
376390 w .WriteHeader (http .StatusOK )
377- _ , err := fmt .Fprint (w , emptyServersBody )
378- Expect (err ).NotTo (HaveOccurred ())
391+ serverDetailHandler (w , r )
379392 })
380393
381394 fakeServer .Mux .HandleFunc ("POST /instance-ha" , func (w http.ResponseWriter , r * http.Request ) {
@@ -406,15 +419,21 @@ var _ = Describe("Onboarding Controller", func() {
406419 Expect (err ).NotTo (HaveOccurred ())
407420 })
408421
409- fakeServer . Mux . HandleFunc ( "POST /servers/server-id/action" , func (w http.ResponseWriter , r * http.Request ) {
422+ serverActionHandler = func (w http.ResponseWriter , _ * http.Request ) {
410423 w .Header ().Add ("Content-Type" , "application/json" )
411424 w .WriteHeader (http .StatusOK )
412425 _ , err := fmt .Fprintf (w , `{"output": "FAKE CONSOLE OUTPUT\nANOTHER\nLAST LINE\nohooc--%v-%v\n"}` , hv .Name , hv .UID )
413426 Expect (err ).NotTo (HaveOccurred ())
414-
427+ }
428+ fakeServer .Mux .HandleFunc ("POST /servers/server-id/action" , func (w http.ResponseWriter , r * http.Request ) {
429+ serverActionHandler (w , r )
415430 })
416- fakeServer .Mux .HandleFunc ("DELETE /servers/server-id" , func (w http.ResponseWriter , r * http.Request ) {
431+
432+ serverDeleteHandler = func (w http.ResponseWriter , _ * http.Request ) {
417433 w .WriteHeader (http .StatusAccepted )
434+ }
435+ fakeServer .Mux .HandleFunc ("DELETE /servers/server-id" , func (w http.ResponseWriter , r * http.Request ) {
436+ serverDeleteHandler (w , r )
418437 })
419438 })
420439
@@ -571,5 +590,80 @@ var _ = Describe("Onboarding Controller", func() {
571590 })
572591 })
573592
593+ When ("smoke test times out waiting for console output" , func () {
594+ var serverDeletedCalled bool
595+
596+ BeforeEach (func (ctx SpecContext ) {
597+ By ("Overriding HV status to Testing state" )
598+ hv := & kvmv1.Hypervisor {}
599+ Expect (k8sClient .Get (ctx , namespacedName , hv )).To (Succeed ())
600+ meta .SetStatusCondition (& hv .Status .Conditions , metav1.Condition {
601+ Type : kvmv1 .ConditionTypeOnboarding ,
602+ Status : metav1 .ConditionTrue ,
603+ Reason : kvmv1 .ConditionReasonTesting ,
604+ })
605+ Expect (k8sClient .Status ().Update (ctx , hv )).To (Succeed ())
606+
607+ // Construct the server name the controller will look for.
608+ serverName := fmt .Sprintf ("%s-%s-%s" , testPrefixName , hypervisorName , string (hv .UID ))
609+ detailCallCount := 0
610+
611+ // On the first reconcile GET /servers/detail returns empty so the controller
612+ // creates the server via POST (no launched_at → timeout cannot fire yet).
613+ // On the second reconcile GET /servers/detail returns the ACTIVE server with a
614+ // stale launched_at so createOrGetTestServer takes the "already-running" path and
615+ // smokeTest fires the timeout.
616+ serverDetailHandler = func (w http.ResponseWriter , _ * http.Request ) {
617+ if detailCallCount == 0 {
618+ detailCallCount ++
619+ _ , err := fmt .Fprint (w , emptyServersBody )
620+ Expect (err ).NotTo (HaveOccurred ())
621+ } else {
622+ _ , err := fmt .Fprintf (w ,
623+ `{"servers": [{"id": "server-id", "name": %q, "status": "ACTIVE", "OS-SRV-USG:launched_at": "2025-01-01T12:00:00.000000"}], "servers_links": []}` ,
624+ serverName )
625+ Expect (err ).NotTo (HaveOccurred ())
626+ }
627+ }
628+
629+ // Set the clock to 6 minutes after the launched_at above (past the 5-minute deadline).
630+ onboardingReconciler .Clock = clocktesting .NewFakeClock (time .Date (2025 , 1 , 1 , 12 , 6 , 0 , 0 , time .UTC ))
631+ serverDeletedCalled = false
632+
633+ // Console output that does NOT contain the server name, so the timeout path is exercised.
634+ serverActionHandler = func (w http.ResponseWriter , _ * http.Request ) {
635+ w .Header ().Add ("Content-Type" , "application/json" )
636+ w .WriteHeader (http .StatusOK )
637+ _ , err := fmt .Fprint (w , `{"output": "some unrelated console output\n"}` )
638+ Expect (err ).NotTo (HaveOccurred ())
639+ }
640+ serverDeleteHandler = func (w http.ResponseWriter , _ * http.Request ) {
641+ serverDeletedCalled = true
642+ w .WriteHeader (http .StatusAccepted )
643+ }
644+ })
645+
646+ It ("should delete the stalled server and record a timeout in the status" , func (ctx SpecContext ) {
647+ By ("First reconcile: controller creates the ACTIVE server; launched_at is absent so timeout does not fire yet" )
648+ _ , err := onboardingReconciler .Reconcile (ctx , reconcileReq )
649+ Expect (err ).NotTo (HaveOccurred ())
650+
651+ By ("Second reconcile: GET /servers/detail returns the stale server; timeout fires and the server is deleted" )
652+ _ , err = onboardingReconciler .Reconcile (ctx , reconcileReq )
653+ Expect (err ).NotTo (HaveOccurred ())
654+
655+ By ("Verifying the timed-out server was deleted" )
656+ Expect (serverDeletedCalled ).To (BeTrue ())
657+
658+ By ("Verifying the onboarding condition message indicates a timeout" )
659+ hv := & kvmv1.Hypervisor {}
660+ Expect (k8sClient .Get (ctx , namespacedName , hv )).To (Succeed ())
661+ onboardingCond := meta .FindStatusCondition (hv .Status .Conditions , kvmv1 .ConditionTypeOnboarding )
662+ Expect (onboardingCond ).NotTo (BeNil ())
663+ Expect (onboardingCond .Reason ).To (Equal (kvmv1 .ConditionReasonTesting ))
664+ Expect (onboardingCond .Message ).To (ContainSubstring ("timeout" ))
665+ })
666+ })
667+
574668 })
575669})
0 commit comments