Scenario 1: Warning Eliminated (Create & Destroy)

ObjectiveVerify the [controller-runtime] log.SetLogger(...) was never called warning no longer appears on any CLI code path
Maps toPrimary acceptance criteria
MethodBefore/after binary comparison with forced timer reproduction

Verification Steps

StepCheckResultEvidence
0 Reproduce warning with unfixed binary (destroy path) FAIL (expected) Unfixed hcp binary prints warning + goroutine stack trace to stderr
1 Verify unfixed binary has no ctrl.SetLogger() call PASS grep of main branch product-cli/main.go returns no matches
2 Verify fixed binary adds ctrl.SetLogger() before cmd.ExecuteContext() PASS ctrl.SetLogger(newLogger()) at line 45, before cobra command at line 47
3 Run fixed binary on same destroy path — no warning PASS No log.SetLogger warning; clean JSON log output
4 Run fixed binary on create path (flag validation) — no warning PASS grep count = 0
5 Confirm warning mechanism requires 30s runtime PASS controller-runtime pkg/log/log.go:58-69 fires warning after 30s via timer

Evidence

Step 0: Reproducing the warning (unfixed binary, destroy path)

Built from main branch without ctrl.SetLogger(). The controller-runtime deferred logger fires when SetLogger() is never called, printing a warning and goroutine stack trace to stderr.

$ /tmp/hcp-unfixed destroy cluster azure --name nonexistent-cluster-78310 \
    --azure-creds /Users/brcox/.azure/self-managed-azure-credentials.json \
    --dns-zone-rg-name fake-rg

[controller-runtime] log.SetLogger(...) was never called; logs will not be displayed.
Detected at:
	>  goroutine 1 [running, locked to thread]:
	>  runtime/debug.Stack()
	>  	/opt/homebrew/Cellar/go/1.26.0/libexec/src/runtime/debug/stack.go:26 +0x64
	>  sigs.k8s.io/controller-runtime/pkg/log.eventuallyFulfillRoot()
	>  	vendor/sigs.k8s.io/controller-runtime/pkg/log/log.go:60 +0xe4
	>  sigs.k8s.io/controller-runtime/pkg/log.(*delegatingLogSink).Init(...)
	>  	vendor/sigs.k8s.io/controller-runtime/pkg/log/deleg.go:101 +0x2c
	>  github.com/go-logr/logr.New(...)
	>  	vendor/github.com/go-logr/logr/logr.go:217
	>  sigs.k8s.io/controller-runtime/pkg/log.init()
	>  	vendor/sigs.k8s.io/controller-runtime/pkg/log/log.go:87 +0xf8
{"level":"info","ts":"2026-07-13T09:02:03-04:00","msg":"Hosted cluster not found, destroying infrastructure from user input","namespace":"clusters","name":"nonexistent-cluster-78310","infraID":""}
Error: required inputs are missing: infrastructure ID is required

Step 3: Fixed binary on same destroy path — no warning

$ /tmp/hcp-after destroy cluster azure --name nonexistent-cluster-78310 \
    --azure-creds /Users/brcox/.azure/self-managed-azure-credentials.json \
    --dns-zone-rg-name fake-rg

{"level":"info","ts":"2026-07-13T09:02:14-04:00","msg":"Hosted cluster not found, destroying infrastructure from user input","namespace":"clusters","name":"nonexistent-cluster-78310","infraID":""}
Error: required inputs are missing: infrastructure ID is required

Step 1: Unfixed binary has no ctrl.SetLogger()

$ git stash   # stash PR #8955 changes to test main branch
$ grep -n "SetLogger\|ctrl\.\|zap\." product-cli/main.go
(no logger initialization found)
$ git stash pop

Step 2: Fixed binary adds ctrl.SetLogger()

$ grep -n "SetLogger\|ctrl\.\|zap\.\|newLogger" product-cli/main.go
36:func newLogger(extraOpts ...zap.Opts) logr.Logger {
37:	opts := []zap.Opts{zap.JSONEncoder(func(o *zapcore.EncoderConfig) {
41:	return zap.New(opts...)
45:	ctrl.SetLogger(newLogger())

Step 4: Fixed binary on create path — no warning

$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig /tmp/hcp-after create cluster azure \
    --name test-78310-warning \
    --azure-creds /Users/brcox/.azure/self-managed-azure-credentials.json \
    --pull-secret /Users/brcox/.openshift/pull-secret \
    --dns-zone-rg-name os4-common \
    --workload-identities-file /dev/null \
    2>&1 | grep -c "log.SetLogger"
0

Step 5: Warning mechanism fires after 30s

$ grep -A15 "func checkPromise" vendor/sigs.k8s.io/controller-runtime/pkg/log/log.go
func checkPromise() {
	if logFullfilled.Load() {
		return
	}
	if time.Since(rootLogCreated).Seconds() >= 30 {
		if logFullfilled.CompareAndSwap(false, true) {
			...
			fmt.Fprintf(os.Stderr,
				"[controller-runtime] log.SetLogger(...) was never called; logs will not be displayed.\n...")

« Dashboard Scenario 2: Structured JSON Logging »