33package hawtiotest
44
55import (
6+ "bytes"
67 "context"
78 "fmt"
9+ "io"
10+ "net/http"
811 "os"
912 "path/filepath"
1013 "regexp"
@@ -40,6 +43,8 @@ import (
4043
4144 "github.com/go-logr/logr"
4245 "sigs.k8s.io/yaml"
46+
47+ "github.com/hawtio/hawtio-operator/pkg/resources"
4348)
4449
4550const (
@@ -68,6 +73,62 @@ type ManagerState struct {
6873 Cancel context.CancelFunc
6974}
7075
76+ // MockRegistryTransport intercepts HTTP calls and returns fake image digests.
77+ type MockRegistryTransport struct {
78+ mu sync.Mutex
79+ // Maps a registry URL path to the sha256 digest we want to return
80+ DigestMap map [string ][]string
81+ ShouldFail bool
82+ }
83+
84+ func (m * MockRegistryTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
85+ if m .ShouldFail {
86+ return nil , http .ErrServerClosed // Simulate a hard network/air-gap failure
87+ }
88+
89+ // Intercept the mandatory Docker API version ping
90+ if req .URL .Path == "/v2/" {
91+ return & http.Response {
92+ StatusCode : http .StatusOK ,
93+ Body : io .NopCloser (bytes .NewBufferString ("{}" )),
94+ Header : make (http.Header ),
95+ }, nil
96+ }
97+
98+ m .mu .Lock ()
99+ defer m .mu .Unlock ()
100+
101+ digests , ok := m .DigestMap [req .URL .Path ]
102+ if ! ok || len (digests ) == 0 {
103+ errMsg := "mock missing path: " + req .URL .Path
104+ return & http.Response {
105+ StatusCode : http .StatusNotFound ,
106+ Body : io .NopCloser (bytes .NewBufferString (errMsg )),
107+ Header : make (http.Header ),
108+ }, nil
109+ }
110+
111+ // Get the current digest for this request
112+ digest := digests [0 ]
113+
114+ // If there's another digest in the sequence, pop it so the next
115+ // request gets the new one
116+ if len (digests ) > 1 {
117+ m .DigestMap [req .URL .Path ] = digests [1 :]
118+ }
119+
120+ // go-containerregistry strictly requires this header to extract the digest
121+ header := make (http.Header )
122+ header .Set ("Docker-Content-Digest" , digest )
123+ header .Set ("Content-Type" , "application/vnd.docker.distribution.manifest.v2+json" )
124+
125+ return & http.Response {
126+ StatusCode : http .StatusOK ,
127+ Body : io .NopCloser (bytes .NewBufferString ("{}" )),
128+ Header : header ,
129+ }, nil
130+ }
131+
71132var buildVariables = util.BuildVariables {
72133 ImageRepository : "quay.io/hawtio/online" ,
73134 ImageVersion : "2.3.0" ,
@@ -199,7 +260,7 @@ func lookupKey(hawtio *hawtiov2.Hawtio) types.NamespacedName {
199260 }
200261}
201262
202- func StartManager (testTools * TestTools ) * ManagerState {
263+ func StartManager (testTools * TestTools , extraOpts ... hawtiomgr. MgrOption ) * ManagerState {
203264 ctx , cancel := context .WithCancel (context .Background ())
204265 wg := & sync.WaitGroup {}
205266
@@ -209,14 +270,17 @@ func StartManager(testTools *TestTools) *ManagerState {
209270 BindAddress : "0" , // Disable metrics for tests
210271 }
211272
212- mgr , err := hawtiomgr .New (
273+ opts := [] hawtiomgr.MgrOption {
213274 hawtiomgr .WithRestConfig (testTools .Cfg ),
214275 hawtiomgr .WithWatchNamespace (HawtioNamespace ),
215276 hawtiomgr .WithPodNamespace (HawtioNamespace ),
216277 hawtiomgr .WithBuildVariables (buildVariables ),
217278 hawtiomgr .WithScheme (testTools .Scheme ),
218279 hawtiomgr .WithClientTools (testTools .ClientTools ),
219- hawtiomgr .WithMetrics (metricsOptions ))
280+ hawtiomgr .WithMetrics (metricsOptions ),
281+ }
282+ opts = append (opts , extraOpts ... )
283+ mgr , err := hawtiomgr .New (opts ... )
220284
221285 Expect (err ).NotTo (HaveOccurred ())
222286
@@ -397,3 +461,197 @@ func PerformCommonResourceTest(testTools *TestTools, ctx context.Context, platfo
397461 Expect (service .Labels ).To (HaveKeyWithValue ("app" , "hawtio" ), "Should have expected app label" )
398462 compareResource ("Service" , service , expSvc )
399463}
464+
465+ //
466+ // PerformCommonUpdaterTest tests the updater conducting
467+ // queries on a mocked registry. The second time it makes
468+ // a query, the digest is updated and the new update made
469+ // to the deployment resource
470+ func PerformCommonUpdaterTest (testTools * TestTools , mgrState * ManagerState , platform string ) {
471+ By ("Stopping the Manager to Configure Mocked Registry" )
472+ // Stop the default manager started by BeforeEach
473+ mgrState .Cancel ()
474+ mgrState .Wg .Wait ()
475+
476+ By ("Configuring the mock registry" )
477+ onlineHash := "sha256:1111111111111111111111111111111111111111111111111111111111111111"
478+ updatedOnlineHash := "sha256:2222222222222222222222222222222222222222222222222222222222222222"
479+ gatewayHash := "sha256:3333333333333333333333333333333333333333333333333333333333333333"
480+ updatedGatewayHash := "sha256:4444444444444444444444444444444444444444444444444444444444444444"
481+
482+ // Build the sequenced mock
483+ mockTransport := & MockRegistryTransport {
484+ DigestMap : map [string ][]string {
485+ fmt .Sprintf ("/v2/hawtio/online/manifests/%s" , buildVariables .ImageVersion ): {onlineHash , updatedOnlineHash },
486+ fmt .Sprintf ("/v2/hawtio/online-gateway/manifests/%s" , buildVariables .GatewayImageVersion ): {gatewayHash , updatedGatewayHash },
487+ },
488+ }
489+
490+ By ("Restarting the Manager with the Sequenced Mock Transport" )
491+ // Restart the manager with the mocked internet by create a new temporary state
492+ newMgrState := StartManager (testTools ,
493+ hawtiomgr .WithRegistryTransport (mockTransport ),
494+ // Must be > 2 seconds so the Reconciler has time to build the baseline
495+ hawtiomgr .WithUpdatePollingInterval (3 * time .Second ))
496+
497+ // Dereference BOTH pointers to overwrite the original manager
498+ * mgrState = * newMgrState
499+
500+ By ("Creating the Hawtio CR" )
501+ hawtioName := "hawtio-poller-test"
502+ hawtio := & hawtiov2.Hawtio {
503+ ObjectMeta : metav1.ObjectMeta {Name : hawtioName , Namespace : HawtioNamespace },
504+ Spec : hawtiov2.HawtioSpec {
505+ Type : hawtiov2 .NamespaceHawtioDeploymentType ,
506+ },
507+ }
508+ Expect (testTools .K8sClient .Create (mgrState .Ctx , hawtio )).To (Succeed ())
509+
510+ DeferCleanup (func () {
511+ PerformDeleteHawtioCR (testTools , hawtioName , HawtioNamespace )
512+ })
513+
514+ deploymentKey := types.NamespacedName {Name : hawtioName , Namespace : HawtioNamespace }
515+ createdDeployment := & appsv1.Deployment {}
516+
517+ By ("Waiting for the Reconciler to build the Deployment using the baseline digests" )
518+ Eventually (func (g Gomega ) {
519+ err := testTools .K8sClient .Get (mgrState .Ctx , deploymentKey , createdDeployment )
520+ g .Expect (err ).NotTo (HaveOccurred ())
521+
522+ annotations := createdDeployment .Spec .Template .Annotations
523+ g .Expect (annotations ).NotTo (BeNil ())
524+ // The Reconciler should have successfully applied the first digest
525+ g .Expect (annotations [resources .OnlineDigestAnnotation ]).To (Equal (onlineHash ))
526+ g .Expect (annotations [resources .GatewayDigestAnnotation ]).To (Equal (gatewayHash ))
527+ }, Timeout , Interval ).Should (Succeed ())
528+
529+ By ("Waiting for the background poller to tick, fire an event, and trigger a dynamic update" )
530+ Eventually (func (g Gomega ) {
531+ err := testTools .K8sClient .Get (mgrState .Ctx , deploymentKey , createdDeployment )
532+ g .Expect (err ).NotTo (HaveOccurred ())
533+
534+ annotations := createdDeployment .Spec .Template .Annotations
535+ g .Expect (annotations ).NotTo (BeNil ())
536+ // The Reconciler should have seamlessly swapped to the SECOND digest!
537+ g .Expect (annotations [resources .OnlineDigestAnnotation ]).To (Equal (updatedOnlineHash ))
538+ g .Expect (annotations [resources .GatewayDigestAnnotation ]).To (Equal (updatedGatewayHash ))
539+ }, Timeout , Interval ).Should (Succeed ())
540+ }
541+
542+ //
543+ // PerformCommonUpdaterNetworkFailureTest tests the updater failing to
544+ // query the mocked registry simulating a network failure or air-gapped
545+ // installation. The updating should retreat from any update and allow
546+ // the deployment to continue with the original image:tags.
547+ func PerformCommonUpdaterNetworkFailureTest (testTools * TestTools , mgrState * ManagerState , platform string ) {
548+ By ("Stopping the Manager to Configure Failing Mock Registry" )
549+ mgrState .Cancel ()
550+ mgrState .Wg .Wait ()
551+
552+ By ("Configuring the mock registry to simulate a complete network outage" )
553+ mockTransport := & MockRegistryTransport {
554+ ShouldFail : true , // Triggers simulated http.ErrServerClosed
555+ }
556+
557+ By ("Restarting the Manager with the Failing Transport" )
558+ newMgrState := StartManager (testTools ,
559+ hawtiomgr .WithRegistryTransport (mockTransport ),
560+ // Must be > 2 seconds so the Reconciler has time to build the baseline
561+ hawtiomgr .WithUpdatePollingInterval (3 * time .Second ))
562+
563+ * mgrState = * newMgrState
564+
565+ By ("Creating the Hawtio CR" )
566+ hawtioName := "hawtio-network-fail-test"
567+ hawtio := & hawtiov2.Hawtio {
568+ ObjectMeta : metav1.ObjectMeta {Name : hawtioName , Namespace : HawtioNamespace },
569+ Spec : hawtiov2.HawtioSpec {Type : hawtiov2 .NamespaceHawtioDeploymentType },
570+ }
571+ Expect (testTools .K8sClient .Create (mgrState .Ctx , hawtio )).To (Succeed ())
572+
573+ DeferCleanup (func () {
574+ PerformDeleteHawtioCR (testTools , hawtioName , HawtioNamespace )
575+ })
576+
577+ deploymentKey := types.NamespacedName {Name : hawtioName , Namespace : HawtioNamespace }
578+ createdDeployment := & appsv1.Deployment {}
579+
580+ By ("Waiting for the Reconciler to safely fallback to default tags" )
581+ Eventually (func (g Gomega ) {
582+ err := testTools .K8sClient .Get (mgrState .Ctx , deploymentKey , createdDeployment )
583+ g .Expect (err ).NotTo (HaveOccurred ())
584+
585+ // Ensure the operator didn't inject empty annotations
586+ annotations := createdDeployment .Spec .Template .Annotations
587+ if annotations != nil {
588+ g .Expect (annotations ).NotTo (HaveKey (resources .OnlineDigestAnnotation ))
589+ }
590+
591+ // Ensure the containers are using the standard tags, NOT digests (@sha256:...)
592+ spec := createdDeployment .Spec .Template .Spec
593+ g .Expect (spec .Containers ).To (HaveLen (2 ))
594+ for _ , c := range spec .Containers {
595+ g .Expect (c .Image ).NotTo (ContainSubstring ("@sha256:" ))
596+ g .Expect (c .Image ).To (ContainSubstring (":" + buildVariables .ImageVersion ))
597+ }
598+ }, Timeout , Interval ).Should (Succeed ())
599+ }
600+
601+ //
602+ // PerformCommonUpdaterPartialFailureTest tests the use-case that one
603+ // image has been updated in the mocked registy but not the other. Both
604+ // images are required for a successful update so the updater backs off
605+ // and continues with the original images.
606+ func PerformCommonUpdaterPartialFailureTest (testTools * TestTools , mgrState * ManagerState , platform string ) {
607+ By ("Stopping the Manager to Configure Partial Mock Registry" )
608+ mgrState .Cancel ()
609+ mgrState .Wg .Wait ()
610+
611+ By ("Configuring the mock registry to simulate a missing gateway image" )
612+ validOnlineHash := "sha256:1111111111111111111111111111111111111111111111111111111111111111"
613+
614+ mockTransport := & MockRegistryTransport {
615+ DigestMap : map [string ][]string {
616+ // The Online image succeeds...
617+ fmt .Sprintf ("/v2/hawtio/online/manifests/%s" , buildVariables .ImageVersion ): {validOnlineHash },
618+ // ...but the Gateway image is missing from the map! (Will return 404)
619+ },
620+ }
621+
622+ By ("Restarting the Manager with the Partial Transport" )
623+ newMgrState := StartManager (testTools ,
624+ hawtiomgr .WithRegistryTransport (mockTransport ),
625+ // Must be > 2 seconds so the Reconciler has time to build the baseline
626+ hawtiomgr .WithUpdatePollingInterval (3 * time .Second ))
627+
628+ * mgrState = * newMgrState
629+
630+ By ("Creating the Hawtio CR" )
631+ hawtioName := "hawtio-partial-fail-test"
632+ hawtio := & hawtiov2.Hawtio {
633+ ObjectMeta : metav1.ObjectMeta {Name : hawtioName , Namespace : HawtioNamespace },
634+ Spec : hawtiov2.HawtioSpec {Type : hawtiov2 .NamespaceHawtioDeploymentType },
635+ }
636+ Expect (testTools .K8sClient .Create (mgrState .Ctx , hawtio )).To (Succeed ())
637+
638+ DeferCleanup (func () {
639+ PerformDeleteHawtioCR (testTools , hawtioName , HawtioNamespace )
640+ })
641+
642+ deploymentKey := types.NamespacedName {Name : hawtioName , Namespace : HawtioNamespace }
643+ createdDeployment := & appsv1.Deployment {}
644+
645+ By ("Waiting for the Reconciler to reject the split-brain state and fallback to defaults" )
646+ Eventually (func (g Gomega ) {
647+ err := testTools .K8sClient .Get (mgrState .Ctx , deploymentKey , createdDeployment )
648+ g .Expect (err ).NotTo (HaveOccurred ())
649+
650+ spec := createdDeployment .Spec .Template .Spec
651+ g .Expect (spec .Containers ).To (HaveLen (2 ))
652+ for _ , c := range spec .Containers {
653+ // Neither container should get a digest because the batch fetch failed
654+ g .Expect (c .Image ).NotTo (ContainSubstring ("@sha256:" ))
655+ }
656+ }, Timeout , Interval ).Should (Succeed ())
657+ }
0 commit comments