Skip to content

Commit 0958bd9

Browse files
committed
HAWNG-1855: Adds integration tests for the updater polling
* Tests the updater integrated with the hawtio controller and its effect on reconciling the deployment.
1 parent 755637f commit 0958bd9

6 files changed

Lines changed: 311 additions & 13 deletions

File tree

pkg/controller/hawtiotest/kubernetes/controller_integration_kubernetes_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,17 @@ var _ = Describe("Testing the Hawtio Controller", Ordered, func() {
6767
g.Expect(testTools.K8sClient.Get(mgrState.Ctx, ingressKey, ingress)).To(Succeed())
6868
}, hawtiotest.Timeout, hawtiotest.Interval).Should(Succeed())
6969
})
70+
71+
It("Dynamically updating Deployment images when the background poller detects new digests", func() {
72+
hawtiotest.PerformCommonUpdaterTest(testTools, mgrState, "Kubernetes")
73+
})
74+
75+
It("Updater poller tries to update images but encounters a network failure", func() {
76+
hawtiotest.PerformCommonUpdaterNetworkFailureTest(testTools, mgrState, "OpenShift")
77+
})
78+
79+
It("Updater poller tries to update images but encounters only a single updated image", func() {
80+
hawtiotest.PerformCommonUpdaterPartialFailureTest(testTools, mgrState, "OpenShift")
81+
})
7082
})
7183
})

pkg/controller/hawtiotest/openshift/controller_integration_openshift_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,5 +182,17 @@ var _ = Describe("Testing the Hawtio Controller", Ordered, func() {
182182
g.Expect(testTools.K8sClient.Get(mgrState.Ctx, hawtioKey, &routev1.Route{})).To(Succeed())
183183
}, "5s", "1s").Should(Succeed())
184184
})
185+
186+
It("Dynamically updating Deployment images when the background poller detects new digests", func() {
187+
hawtiotest.PerformCommonUpdaterTest(testTools, mgrState, "OpenShift")
188+
})
189+
190+
It("Updater poller tries to update images but encounters a network failure", func() {
191+
hawtiotest.PerformCommonUpdaterNetworkFailureTest(testTools, mgrState, "OpenShift")
192+
})
193+
194+
It("Updater poller tries to update images but encounters only a single updated image", func() {
195+
hawtiotest.PerformCommonUpdaterPartialFailureTest(testTools, mgrState, "OpenShift")
196+
})
185197
})
186198
})

pkg/controller/hawtiotest/test_functions.go

Lines changed: 261 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
package hawtiotest
44

55
import (
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

4550
const (
@@ -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+
71132
var 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

Comments
 (0)