Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dce0c27
init test running successfully for 5.5
mtulio Aug 6, 2026
14069d6
health: add per-poll TG snapshots and TG attribute introspection
mtulio Aug 6, 2026
9f6750a
e2e: add graceful shutdown, Scenario 5.2, consistent timing model
mtulio Aug 6, 2026
44f55af
e2e: master-node targeting, cross-zone, CAPA variant, t0-t4 timers
mtulio Aug 6, 2026
2e06ea6
e2e: 90s post-healthy observation, environment summary in report
mtulio Aug 6, 2026
c3068f1
e2e: fix TG healthy wait stuck, add PollOnce, use control-plane label
mtulio Aug 6, 2026
de7f1b1
e2e: log client and observer start times
mtulio Aug 6, 2026
013f9c2
health: add README documenting the NLB health transition test framework
mtulio Aug 6, 2026
e107f3e
e2e: add request statistics and per-phase breakdown to report
mtulio Aug 6, 2026
d5fa8dc
e2e: add duration column to per-phase request breakdown
mtulio Aug 6, 2026
bf37dd9
e2e: restore removed comments in phase classification
mtulio Aug 6, 2026
0b9194e
e2e: parallel client workers, fix unhealthy.draining state matching
mtulio Aug 6, 2026
21bee92
health: update README with parallel workers, request stats, CAPA finding
mtulio Aug 6, 2026
0e80b33
e2e: add per-server request distribution and server-side verdict
mtulio Aug 6, 2026
16bde17
health: update README with per-server distribution and verdict sections
mtulio Aug 6, 2026
990a4cb
e2e: tune workers to 8×100ms, add pod→node mapping, req/s metrics
mtulio Aug 12, 2026
199f306
e2e: hostNetwork mode, SIGTERM-based shutdown, privileged SCC
mtulio Aug 12, 2026
c962a18
e2e: unified binary with in-cluster client, aggregator, and metrics
mtulio Aug 13, 2026
db07955
e2e: add CLB baseline comparison, fix verdict phase boundaries, renam…
mtulio Aug 13, 2026
e914c77
e2e: add SDK-managed NLB tests with four-variant comparison matrix
mtulio Aug 13, 2026
def241a
e2e: add KAS-config SDK variants with real TG draining attributes
mtulio Aug 14, 2026
866f49d
e2e: add KAS TLS SDK variant and LateConnections shutdown metric
mtulio Aug 14, 2026
cfbea3d
e2e: add ctl in-place restart and KAS TLS+ctl SDK scenarios
mtulio Aug 14, 2026
e9f603e
e2e: use 30s ctl drain on KAS TLS scenario for reliable repro
mtulio Aug 14, 2026
f858305
e2e: add tls-ctl drain sweep variants and multi-cluster results
mtulio Aug 14, 2026
d0dacc7
update case 25 with 180 and 210 simulation
mtulio Aug 16, 2026
3c979b8
add report aggregator script
mtulio Aug 17, 2026
bde13c5
Add plan v27 KAS TG patch matrix for NLB health-transition e2e
mtulio Aug 17, 2026
a524105
ccm: added 240s and 300s cases on KAS patch variants
mtulio Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM golang:1.22-alpine AS builder

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu
file='openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile'
printf '%s\n' '--- Dockerfile ---'
cat -n "$file"
printf '%s\n' '--- image references ---'
rg -n '^\s*(FROM|USER|HEALTHCHECK)\b' "$file"

Repository: openshift/cluster-cloud-controller-manager-operator

Length of output: 643


Security Misconfiguration (CWE-494): Download of Code Without Integrity Check

Reachability: Internal

Pin the builder image by digest.

golang:1.22-alpine is a mutable non-Red-Hat tag. Use a reviewed, platform-appropriate digest.

🧰 Tools
🪛 Trivy (0.72.0)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile` at line 1, Update
the Dockerfile’s builder-stage FROM instruction to pin the golang:1.22-alpine
image by its reviewed, platform-appropriate immutable digest, retaining the
existing builder stage name.

Source: Path instructions

WORKDIR /build
COPY main.go .
RUN go mod init healthserver && \
go mod edit -go=1.22 && \
CGO_ENABLED=0 go build -ldflags="-s -w" -o healthserver .

FROM scratch
COPY --from=builder /build/healthserver /healthserver
ENTRYPOINT ["/healthserver"]
Comment on lines +8 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add a functional image health check.

The final image has no HEALTHCHECK. Because scratch has no shell or HTTP client, build and copy a small static probe binary, then use an exec-form health check against /readyz.

As per path instructions: “HEALTHCHECK defined.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile` around lines 8 -
10, Update the final scratch image definition near ENTRYPOINT to include a
HEALTHCHECK using exec form against /readyz. Build a small statically linked
probe binary in the builder stage, copy it into the final image alongside
/healthserver, and configure the health check to invoke that probe without
relying on shell or HTTP client binaries.

Source: Path instructions


🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

file='openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile'
printf '%s\n' '--- Dockerfile ---'
cat -n "$file"

printf '%s\n' '--- healthserver references ---'
rg -n --glob '!vendor' --glob '!node_modules' '8080|ListenAndServe|USER|healthserver' \
  openshift-tests/ccm-aws-tests/cmd/healthserver \
  openshift-tests/ccm-aws-tests 2>/dev/null | head -200

printf '%s\n' '--- final-stage instruction probe ---'
python3 - <<'PY'
from pathlib import Path

p = Path("openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile")
lines = p.read_text().splitlines()
from_idx = [i for i, line in enumerate(lines) if line.lstrip().upper().startswith("FROM ")]
last = from_idx[-1]
stage = lines[last:]
print("final_stage_start_line:", last + 1)
print("final_stage_has_user:", any(line.lstrip().upper().startswith("USER ") for line in stage))
print("final_stage_user_lines:", [
    (last + i + 1, line) for i, line in enumerate(stage)
    if line.lstrip().upper().startswith("USER ")
])
print("final_stage_instructions:")
for i, line in enumerate(stage, last + 1):
    stripped = line.strip()
    if stripped and not stripped.startswith("#"):
        print(f"{i}: {stripped}")
PY

Repository: openshift/cluster-cloud-controller-manager-operator

Length of output: 6360


Security Misconfiguration (CWE-250)

Reachability: Internal

Run /healthserver as a non-root user.

The final scratch image has no USER instruction and runs as UID 0 by default. Add USER 65532:65532 before ENTRYPOINT; port 8080 does not require root.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openshift-tests/ccm-aws-tests/cmd/healthserver/Dockerfile` around lines 8 -
10, Update the final scratch image in the Dockerfile to add USER 65532:65532
before ENTRYPOINT, so the /healthserver process runs as a non-root user while
preserving the existing port 8080 behavior.

Sources: Path instructions, Linters/SAST tools

222 changes: 222 additions & 0 deletions openshift-tests/ccm-aws-tests/cmd/healthserver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Record tcpUp after the listener binds.

Line 94 records t_tcp_up before ListenAndServe calls net.Listen. A bind failure or delay can make the lifecycle report claim that TCP was available when no listener existed. Create the listener explicitly, record tcpUp after net.Listen succeeds, then call srv.Serve(listener).

Proposed fix
+	"net"
...
-	s.mu.Lock()
-	s.tcpUp = time.Now()
-	s.mu.Unlock()
+	listener, err := net.Listen("tcp", srv.Addr)
+	if err != nil {
+		log.Fatalf("listen: %v", err)
+	}
+	s.mu.Lock()
+	s.tcpUp = time.Now()
+	s.mu.Unlock()

 	log.Printf("healthserver %s listening on :%d (startup-delay=%s)", id, *port, *startupDelay)
-	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+	if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
 		log.Fatalf("listen: %v", err)
 	}

Also applies to: 93-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openshift-tests/ccm-aws-tests/cmd/healthserver/main.go` at line 9, Update the
health server startup flow around the server creation and existing tcpUp
recording: explicitly create the listener with net.Listen, handle a bind error
before recording readiness, record tcpUp only after Listen succeeds, then serve
using srv.Serve(listener) instead of srv.ListenAndServe.

"os"
"os/signal"
"sync"
"syscall"
"time"
)

type serverState string

const (
statePreReadyz serverState = "pre-readyz"
stateReady serverState = "ready"
stateDraining serverState = "draining"
stateShutdown serverState = "shutdown"
)

type server struct {
mu sync.RWMutex
id string
state serverState
processStart time.Time
tcpUp time.Time
firstReadyz200 *time.Time
readyzFalseAt *time.Time
shutdownInitiated *time.Time
}

func main() {
port := flag.Int("port", 8080, "Service port")
startupDelay := flag.Duration("startup-delay", 30*time.Second, "Duration before /readyz returns 200")
flag.Parse()

id := os.Getenv("POD_NAME")
if id == "" {
id = fmt.Sprintf("healthserver-%d", os.Getpid())
}

s := &server{
id: id,
state: statePreReadyz,
processStart: time.Now(),
}

mux := http.NewServeMux()
mux.HandleFunc("GET /readyz", s.handleReadyz)
mux.HandleFunc("POST /admin/readyz", s.handleAdminReadyz)
mux.HandleFunc("POST /admin/shutdown", s.handleAdminShutdown)
mux.HandleFunc("GET /admin/lifecycle", s.handleAdminLifecycle)
mux.HandleFunc("/", s.handleMain)

srv := &http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: mux,
}
Comment on lines +58 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- healthserver source ---'
sed -n '1,180p' openshift-tests/ccm-aws-tests/cmd/healthserver/main.go

printf '%s\n' '--- healthserver references and deployment/service definitions ---'
rg -n -C 4 'healthserver|ReadHeaderTimeout|ReadTimeout|WriteTimeout|IdleTimeout|ListenAndServe|targetPort|port:|LoadBalancer|healthCheck' \
  openshift-tests/ccm-aws-tests/cmd openshift-tests/ccm-aws-tests/e2e/aws

Repository: openshift/cluster-cloud-controller-manager-operator

Length of output: 50409


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- remaining handlers ---'
sed -n '160,280p' openshift-tests/ccm-aws-tests/cmd/healthserver/main.go

printf '%s\n' '--- deployment and service builders ---'
rg -n -C 35 'func buildHealthserverDeployment|func buildHealthTransitionService' \
  openshift-tests/ccm-aws-tests/e2e/aws

printf '%s\n' '--- exact health-transition annotations and ports ---'
python3 - <<'PY'
from pathlib import Path

p = Path("openshift-tests/ccm-aws-tests/e2e/aws/lb_health_transition.go")
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
    if "func buildHealthserverDeployment" in line or "func buildHealthTransitionService" in line:
        lo = max(0, i - 2)
        hi = min(len(lines), i + 130)
        print(f"--- lines {lo+1}-{hi} ---")
        for n in range(lo, hi):
            print(f"{n+1}:{lines[n]}")
PY

Repository: openshift/cluster-cloud-controller-manager-operator

Length of output: 22100


Denial of Service (CWE-400): Uncontrolled Resource Consumption

Reachability: External · Exploitability: Moderate

Set bounded HTTP server timeouts. The health server is exposed through an external NLB. Without read deadlines, a client can keep connections open by sending request data slowly. Set ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout to bounded values for these short requests.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 59-62: This http.Server is constructed without a ReadTimeout. Without a read timeout, a slow or malicious client can hold connections open indefinitely (e.g. a Slowloris attack), exhausting server resources and causing a denial of service. Set ReadTimeout (and ideally ReadHeaderTimeout, WriteTimeout, and IdleTimeout) on the http.Server to bound how long the server waits while reading a request.
Context: http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: mux,
}
Note: [CWE-400] Uncontrolled Resource Consumption.

(http-server-missing-read-timeout-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openshift-tests/ccm-aws-tests/cmd/healthserver/main.go` around lines 59 - 63,
Update the http.Server initialization in the health server to set bounded
ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout values appropriate
for short health requests, while preserving the existing Addr and Handler
configuration.

Source: Linters/SAST tools


go func() {
log.Printf("startup delay: %s (readyz returns 503 until then)", *startupDelay)
time.Sleep(*startupDelay)
s.mu.Lock()
if s.state == statePreReadyz {
now := time.Now()
s.firstReadyz200 = &now
s.state = stateReady
log.Printf("startup delay elapsed, readyz now returns 200")
}
s.mu.Unlock()
}()

// On SIGTERM (sent by kubelet during pod deletion), transition to
// "draining" state: /readyz returns 503 but the server keeps serving
// on / with X-Server-State: draining. The pod continues to serve
// for terminationGracePeriodSeconds (set by the Deployment) before
// kubelet force-kills it. This matches KAS behavior during rollouts:
// SIGTERM → readyz→503 → keep serving for shutdown-delay-duration.
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
sig := <-ch
log.Printf("received %s, setting readyz→503 (draining), server continues serving", sig)
s.mu.Lock()
now := time.Now()
s.shutdownInitiated = &now
s.readyzFalseAt = &now
s.state = stateDraining
s.mu.Unlock()
// Do NOT call srv.Shutdown() — keep serving until kubelet kills us
// at the end of terminationGracePeriodSeconds. This simulates KAS
// keeping its TCP port open during shutdown-delay-duration.
}()

s.mu.Lock()
s.tcpUp = time.Now()
s.mu.Unlock()

log.Printf("healthserver %s listening on :%d (startup-delay=%s)", id, *port, *startupDelay)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}

func (s *server) handleMain(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
state := s.state
start := s.processStart
var firstReadyz string
if s.firstReadyz200 != nil {
firstReadyz = s.firstReadyz200.Format(time.RFC3339Nano)
} else {
firstReadyz = "never"
}
s.mu.RUnlock()

w.Header().Set("X-Server-State", string(state))
w.Header().Set("X-Server-ID", s.id)
w.Header().Set("X-Server-Start-Time", start.Format(time.RFC3339Nano))
w.Header().Set("X-First-Readyz-Time", firstReadyz)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "server_id=%s state=%s\n", s.id, state)
}

func (s *server) handleReadyz(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
state := s.state
s.mu.RUnlock()

if state == stateReady {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
} else {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, "not ready: %s", state)
}
}

func (s *server) handleAdminReadyz(w http.ResponseWriter, r *http.Request) {
ready := r.URL.Query().Get("ready")
s.mu.Lock()
defer s.mu.Unlock()

switch ready {
case "true":
if s.firstReadyz200 == nil {
now := time.Now()
s.firstReadyz200 = &now
}
s.state = stateReady
fmt.Fprint(w, "readyz=true")
case "false":
now := time.Now()
s.readyzFalseAt = &now
s.state = stateDraining
fmt.Fprint(w, "readyz=false")
default:
http.Error(w, "ready must be 'true' or 'false'", http.StatusBadRequest)
}
}

func (s *server) handleAdminShutdown(w http.ResponseWriter, r *http.Request) {
delayStr := r.URL.Query().Get("delay")
delay := time.Duration(0)
if delayStr != "" {
d, err := time.ParseDuration(delayStr)
if err != nil {
http.Error(w, fmt.Sprintf("invalid delay: %v", err), http.StatusBadRequest)
return
}
delay = d
}

s.mu.Lock()
now := time.Now()
s.shutdownInitiated = &now
s.state = stateShutdown
s.mu.Unlock()

fmt.Fprintf(w, "shutdown initiated, delay=%s", delay)

go func() {
time.Sleep(delay)
os.Exit(0)
}()
}

func (s *server) handleAdminLifecycle(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
j := struct {
ServerID string `json:"server_id"`
ProcessStart string `json:"t_process_start"`
TCPUp string `json:"t_tcp_up"`
FirstReadyz200 *string `json:"t_first_readyz_200"`
ReadyzFalseAt *string `json:"t_readyz_false_at"`
ShutdownInitiated *string `json:"t_shutdown_initiated"`
}{
ServerID: s.id,
ProcessStart: s.processStart.Format(time.RFC3339Nano),
TCPUp: s.tcpUp.Format(time.RFC3339Nano),
}
if s.firstReadyz200 != nil {
v := s.firstReadyz200.Format(time.RFC3339Nano)
j.FirstReadyz200 = &v
}
if s.readyzFalseAt != nil {
v := s.readyzFalseAt.Format(time.RFC3339Nano)
j.ReadyzFalseAt = &v
}
if s.shutdownInitiated != nil {
v := s.shutdownInitiated.Format(time.RFC3339Nano)
j.ShutdownInitiated = &v
}
s.mu.RUnlock()

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(j)
}
Loading