Skip to content

Commit f1a72c6

Browse files
IshwarKanseclaude
andcommitted
fix: filter by container name in restart detection loop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8bcce30 commit f1a72c6

1 file changed

Lines changed: 94 additions & 62 deletions

File tree

test/e2e/zz_tls_profile_test.go

Lines changed: 94 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@ func TestTLSProfileWatcher(t *testing.T) {
6161
// running and healthy when the cluster uses the default TLS profile (no
6262
// tlsSecurityProfile set on the APIServer CR, which defaults to Intermediate).
6363
func assertOperatorRunningWithDefaultTLSProfile(t *testing.T) {
64-
// Verify the APIServer CR exists and is readable.
6564
apiServer := &configv1.APIServer{}
6665
err := f.K8sClient.Get(context.Background(), types.NamespacedName{Name: "cluster"}, apiServer)
6766
assert.NilError(t, err, "failed to get APIServer CR")
6867

69-
// Verify the operator deployment is ready.
7068
f.AssertDeploymentReady(operatorDeploymentName, f.OperatorNamespace,
7169
framework.WithTimeout(2*time.Minute))(t)
7270

@@ -78,36 +76,29 @@ func assertOperatorRunningWithDefaultTLSProfile(t *testing.T) {
7876
func assertOperatorRestartsOnTLSProfileChange(t *testing.T) {
7977
ctx := context.Background()
8078

81-
// Save original APIServer spec so we can restore it.
8279
apiServer := &configv1.APIServer{}
8380
err := f.K8sClient.Get(ctx, types.NamespacedName{Name: "cluster"}, apiServer)
8481
assert.NilError(t, err, "failed to get APIServer CR")
8582
originalTLSProfile := apiServer.Spec.TLSSecurityProfile
8683

87-
// Ensure we restore the original profile even if the test fails.
8884
f.CleanUp(t, func() {
8985
restoreTLSProfile(t, originalTLSProfile)
9086
})
9187

92-
// Wait for the operator to be stable before we start.
9388
f.AssertDeploymentReady(operatorDeploymentName, f.OperatorNamespace,
9489
framework.WithTimeout(2*time.Minute))(t)
9590

96-
// Record the current container restart count.
9791
initialRestarts := getOperatorContainerRestartCount(t)
9892
t.Logf("container restart count before TLS change: %d", initialRestarts)
9993

100-
// Change TLS profile to Old.
10194
t.Log("setting TLS profile to Old")
10295
setTLSProfile(t, &configv1.TLSSecurityProfile{
10396
Type: configv1.TLSProfileOldType,
10497
Old: &configv1.OldTLSProfile{},
10598
})
10699

107-
// The operator should restart: wait for the restart count to increase.
108100
waitForOperatorContainerRestart(t, initialRestarts)
109101

110-
// After restart, the operator should become ready again.
111102
f.AssertDeploymentReady(operatorDeploymentName, f.OperatorNamespace,
112103
framework.WithTimeout(5*time.Minute))(t)
113104
t.Log("operator restarted and is ready after TLS profile change to Old")
@@ -131,12 +122,8 @@ func assertOperatorRestartsOnCustomTLSProfile(t *testing.T) {
131122
f.AssertDeploymentReady(operatorDeploymentName, f.OperatorNamespace,
132123
framework.WithTimeout(5*time.Minute))(t)
133124

134-
// Wait for the restart count to stabilize — the previous test's cleanup
135-
// may have triggered a restart that hasn't completed yet.
136125
initialRestarts := waitForStableRestartCount(t)
137126

138-
// Set a Custom TLS profile that differs from the default Intermediate
139-
// profile by using a smaller cipher list (a subset of Intermediate's ciphers).
140127
t.Log("setting TLS profile to Custom")
141128
setTLSProfile(t, &configv1.TLSSecurityProfile{
142129
Type: configv1.TLSProfileCustomType,
@@ -173,7 +160,6 @@ func assertOperatorStableOnNonTLSChange(t *testing.T) {
173160
framework.WithTimeout(5*time.Minute))(t)
174161
initialRestarts := waitForStableRestartCount(t)
175162

176-
// Update a non-TLS field: add a test annotation.
177163
const testAnnotation = "observability-operator.rhobs/e2e-tls-test"
178164
apiServer := &configv1.APIServer{}
179165
err := f.K8sClient.Get(ctx, types.NamespacedName{Name: "cluster"}, apiServer)
@@ -201,22 +187,36 @@ func assertOperatorStableOnNonTLSChange(t *testing.T) {
201187
assert.NilError(t, err, "failed to patch APIServer with test annotation")
202188
t.Log("added test annotation to APIServer CR")
203189

204-
// Wait a reasonable period and verify the restart count has not increased.
190+
originalPod := getRunningOperatorPod(t)
191+
originalPodUID := originalPod.UID
192+
205193
t.Log("waiting 60s to confirm operator does not restart")
194+
var lastErr error
206195
err = wait.PollUntilContextTimeout(ctx, 10*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) {
207-
currentRestarts := getOperatorContainerRestartCount(t)
208-
if currentRestarts > initialRestarts {
209-
return true, fmt.Errorf("operator restarted unexpectedly: restart count changed from %d to %d", initialRestarts, currentRestarts)
196+
pod, podErr := runningOperatorPod()
197+
if podErr != nil {
198+
lastErr = podErr
199+
return false, nil
200+
}
201+
lastErr = nil
202+
if pod.UID != originalPodUID {
203+
return true, fmt.Errorf("operator pod replaced unexpectedly: old UID=%s, new UID=%s", originalPodUID, pod.UID)
210204
}
211-
return false, nil // keep polling — we want this to NOT match (timeout = success)
205+
for _, cs := range pod.Status.ContainerStatuses {
206+
if cs.Name == operatorContainerName && cs.RestartCount > initialRestarts {
207+
return true, fmt.Errorf("operator restarted unexpectedly: restart count changed from %d to %d", initialRestarts, cs.RestartCount)
208+
}
209+
}
210+
return false, nil
212211
})
213212

214213
if wait.Interrupted(err) {
215-
// Timeout means the restart count never changed — that's what we want.
214+
if lastErr != nil {
215+
t.Fatalf("operator pod was not observable during stability window: %v", lastErr)
216+
}
216217
t.Log("operator remained stable after non-TLS APIServer change")
217218
return
218219
}
219-
// If we get here, it means the restart count increased — fail.
220220
assert.NilError(t, err, "operator should not restart on non-TLS APIServer changes")
221221
}
222222

@@ -254,8 +254,7 @@ func restoreTLSProfile(t *testing.T, profile *configv1.TLSSecurityProfile) {
254254
return
255255
}
256256

257-
// Wait for operator to recover after the restore.
258-
_ = wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
257+
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
259258
dep := &appsv1.Deployment{}
260259
if err := f.K8sClient.Get(ctx, types.NamespacedName{
261260
Name: operatorDeploymentName,
@@ -264,55 +263,82 @@ func restoreTLSProfile(t *testing.T, profile *configv1.TLSSecurityProfile) {
264263
return false, nil
265264
}
266265
return dep.Status.ReadyReplicas == *dep.Spec.Replicas, nil
267-
})
266+
}); err != nil {
267+
t.Logf("cleanup: operator deployment did not become ready: %v", err)
268+
}
268269
}
269270

270-
// getOperatorContainerRestartCount returns the total restart count of the
271-
// operator container in the running operator pod.
272-
func getOperatorContainerRestartCount(t *testing.T) int32 {
273-
t.Helper()
274-
275-
pod := getRunningOperatorPod(t)
271+
// operatorContainerRestartCount returns the restart count for the operator
272+
// container, or an error if the pod or container cannot be found. This is
273+
// safe to call inside poll loops since it does not call t.Fatal.
274+
func operatorContainerRestartCount() (int32, error) {
275+
pod, err := runningOperatorPod()
276+
if err != nil {
277+
return 0, err
278+
}
276279
for _, cs := range pod.Status.ContainerStatuses {
277280
if cs.Name == operatorContainerName {
278-
return cs.RestartCount
281+
return cs.RestartCount, nil
279282
}
280283
}
281-
282-
t.Fatalf("container %q not found in operator pod", operatorContainerName)
283-
return 0
284+
return 0, fmt.Errorf("container %q not found in operator pod", operatorContainerName)
284285
}
285286

286-
// getRunningOperatorPod returns the running operator pod.
287-
func getRunningOperatorPod(t *testing.T) *corev1.Pod {
288-
t.Helper()
287+
// runningOperatorPod returns the running operator pod, or an error if it
288+
// cannot be found. This is safe to call inside poll loops since it does
289+
// not call t.Fatal.
290+
func runningOperatorPod() (*corev1.Pod, error) {
291+
ctx := context.Background()
289292

290293
dep := &appsv1.Deployment{}
291-
err := f.K8sClient.Get(context.Background(), types.NamespacedName{
294+
if err := f.K8sClient.Get(ctx, types.NamespacedName{
292295
Name: operatorDeploymentName,
293296
Namespace: f.OperatorNamespace,
294-
}, dep)
295-
assert.NilError(t, err, "failed to get operator deployment")
297+
}, dep); err != nil {
298+
return nil, fmt.Errorf("failed to get operator deployment: %w", err)
299+
}
296300

297301
selector, err := metav1.LabelSelectorAsSelector(dep.Spec.Selector)
298-
assert.NilError(t, err, "failed to parse deployment selector")
302+
if err != nil {
303+
return nil, fmt.Errorf("failed to parse deployment selector: %w", err)
304+
}
299305

300306
var pods corev1.PodList
301-
err = f.K8sClient.List(context.Background(), &pods,
307+
if err := f.K8sClient.List(ctx, &pods,
302308
client.InNamespace(f.OperatorNamespace),
303309
client.MatchingLabelsSelector{Selector: selector},
304-
)
305-
assert.NilError(t, err, "failed to list operator pods")
310+
); err != nil {
311+
return nil, fmt.Errorf("failed to list operator pods: %w", err)
312+
}
306313

307314
for i := range pods.Items {
308315
p := &pods.Items[i]
309316
if p.Status.Phase == corev1.PodRunning && p.DeletionTimestamp == nil {
310-
return p
317+
return p, nil
311318
}
312319
}
313320

314-
t.Fatal("no running operator pod found")
315-
return nil
321+
return nil, fmt.Errorf("no running operator pod found")
322+
}
323+
324+
// getOperatorContainerRestartCount returns the restart count for the operator
325+
// container. It calls t.Fatal if the pod or container cannot be found; use
326+
// operatorContainerRestartCount() instead when calling from inside poll loops.
327+
func getOperatorContainerRestartCount(t *testing.T) int32 {
328+
t.Helper()
329+
count, err := operatorContainerRestartCount()
330+
assert.NilError(t, err)
331+
return count
332+
}
333+
334+
// getRunningOperatorPod returns the running operator pod. It calls t.Fatal if
335+
// the pod cannot be found; use runningOperatorPod() instead when calling from
336+
// inside poll loops.
337+
func getRunningOperatorPod(t *testing.T) *corev1.Pod {
338+
t.Helper()
339+
pod, err := runningOperatorPod()
340+
assert.NilError(t, err)
341+
return pod
316342
}
317343

318344
// waitForOperatorContainerRestart polls until the operator process has
@@ -322,7 +348,6 @@ func getRunningOperatorPod(t *testing.T) *corev1.Pod {
322348
func waitForOperatorContainerRestart(t *testing.T, baselineRestarts int32) {
323349
t.Helper()
324350

325-
// Record the current pod UID so we can detect pod replacement.
326351
originalPod := getRunningOperatorPod(t)
327352
originalPodUID := originalPod.UID
328353

@@ -352,14 +377,12 @@ func waitForOperatorContainerRestart(t *testing.T, baselineRestarts int32) {
352377
if p.DeletionTimestamp != nil {
353378
continue
354379
}
355-
// Detect pod replacement (new pod UID).
356380
if p.UID != originalPodUID && p.Status.Phase == corev1.PodRunning {
357381
t.Logf("operator pod replaced: old UID=%s, new UID=%s", originalPodUID, p.UID)
358382
return true, nil
359383
}
360-
// Detect container restart within the same pod.
361384
for _, cs := range p.Status.ContainerStatuses {
362-
if cs.RestartCount > baselineRestarts {
385+
if cs.Name == operatorContainerName && cs.RestartCount > baselineRestarts {
363386
t.Logf("operator container restarted: restart count %d -> %d", baselineRestarts, cs.RestartCount)
364387
return true, nil
365388
}
@@ -381,7 +404,10 @@ func waitForStableRestartCount(t *testing.T) int32 {
381404
var stableSince time.Time
382405

383406
err := wait.PollUntilContextTimeout(context.Background(), 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
384-
current := getOperatorContainerRestartCount(t)
407+
current, restartErr := operatorContainerRestartCount()
408+
if restartErr != nil {
409+
return false, nil
410+
}
385411
if current != lastCount {
386412
lastCount = current
387413
stableSince = time.Now()
@@ -391,20 +417,26 @@ func waitForStableRestartCount(t *testing.T) int32 {
391417
return false, nil
392418
}
393419

394-
// Also verify the container has been running long enough for
395-
// the watcher to process its initial reconcile event.
396-
pod := getRunningOperatorPod(t)
420+
pod, podErr := runningOperatorPod()
421+
if podErr != nil {
422+
return false, nil
423+
}
397424
for _, cs := range pod.Status.ContainerStatuses {
398-
if cs.State.Running != nil {
399-
uptime := time.Since(cs.State.Running.StartedAt.Time)
400-
if uptime < 15*time.Second {
401-
t.Logf("operator container uptime %s < 15s, waiting longer", uptime.Round(time.Second))
402-
return false, nil
403-
}
425+
if cs.Name != operatorContainerName {
426+
continue
427+
}
428+
if cs.State.Running == nil {
429+
return false, nil
404430
}
431+
uptime := time.Since(cs.State.Running.StartedAt.Time)
432+
if uptime < 15*time.Second {
433+
t.Logf("operator container uptime %s < 15s, waiting longer", uptime.Round(time.Second))
434+
return false, nil
435+
}
436+
return true, nil
405437
}
406438

407-
return true, nil
439+
return false, nil
408440
})
409441
assert.NilError(t, err, "operator restart count did not stabilize within timeout")
410442
t.Logf("operator restart count stabilized at %d", lastCount)

0 commit comments

Comments
 (0)