Skip to content

backend: oidc-callback: Stop logging unrelated outer-scope error on missing state#5724

Merged
illume merged 5 commits into
kubernetes-sigs:mainfrom
govindup63:fix/4839-oidc-callback-misleading-error-log
May 19, 2026
Merged

backend: oidc-callback: Stop logging unrelated outer-scope error on missing state#5724
illume merged 5 commits into
kubernetes-sigs:mainfrom
govindup63:fix/4839-oidc-callback-misleading-error-log

Conversation

@govindup63
Copy link
Copy Markdown
Contributor

Summary

The /oidc-callback handler in backend/cmd/headlamp.go logged a stray err variable when the OIDC state query parameter was missing. The variable was not declared in the handler closure: it resolved to the outer-scope err returned by the kubeconfig load that runs once at server startup inside createHeadlampHandler.

The practical effect is that every malformed callback request surfaced whatever happened at boot, attached to an unrelated 400 response. For example, a typical in-cluster pod without a local kubeconfig logs this on every empty-state callback:

error loading kubeconfig files: error reading kubeconfig file:
open /home/headlamp/.config/Headlamp/kubeconfigs/config: no such file or directory

That is the exact misleading line reported in the issue.

Related Issue

Fixes #4839

Changes

  • backend/cmd/headlamp.go: pass nil to logger.Log on the missing-state branch (instead of the captured outer-scope err). Added a short comment explaining the scoping trap so the capture is not reintroduced.
  • backend/cmd/headlamp_test.go: new regression test TestOidcCallbackEmptyStateDoesNotLogStaleError. Hooks the logger via logger.SetLogFunc, runs createHeadlampHandler with a deliberately broken KubeConfigPath so the kubeconfig load returns a known error into the captured outer-scope err, fires GET /oidc-callback with no state, then asserts:
    • The response is 400 Bad Request (unchanged behavior).
    • The captured log entry for invalid request state is empty carries a nil error value (not the stale kubeconfig load error).

Steps to Test

  1. cd backend && go test ./cmd/ -run TestOidcCallbackEmptyStateDoesNotLogStaleError -v. Passes.
  2. Reverting only headlamp.go (keeping the test) reproduces the bug: the test fails with Expected nil, but got: &errors.errorString{s:"error loading kubeconfig files: error reading kubeconfig file: open .../kubeconfigs/config: no such file or directory"}. That is exactly the misleading log the reporter described.
  3. cd backend && go test ./cmd/... -race. All existing cmd/ tests still pass.
  4. cd backend && go build ./... && go vet ./cmd/.... Clean.

Notes for the Reviewer

  • This is a minimal correctness fix. The behavior of the endpoint (400 Bad Request on missing state) is unchanged. Only the log line argument changes.
  • There is a sibling logger misuse at the type-assertion branch a few lines below (logger.Log(... , nil, err, ...) when the oauth2 token has no expected token field). At that point the closure's local err is always nil, so the call is misleading-but-harmless rather than a real outer-scope leak. I left it untouched to keep this PR scoped to the issue. Happy to clean it up in a follow-up.
  • The regression test uses logger.SetLogFunc, which the logger package already exposes for tests (see backend/pkg/logger/logger_test.go). No new test helpers were added.

@k8s-ci-robot k8s-ci-robot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label May 17, 2026
@k8s-ci-robot k8s-ci-robot requested a review from kahirokunn May 17, 2026 17:21
@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label May 17, 2026
@k8s-ci-robot k8s-ci-robot requested a review from yolossn May 17, 2026 17:21
@govindup63 govindup63 force-pushed the fix/4839-oidc-callback-misleading-error-log branch from 495ea88 to 2ada63a Compare May 17, 2026 17:24
@illume illume requested a review from Copilot May 17, 2026 18:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes misleading /oidc-callback logging by ensuring the “missing state” branch does not log a captured outer-scope kubeconfig-load error, and adds a regression test to prevent reintroduction.

Changes:

  • Update /oidc-callback missing-state logging to pass nil error (avoids outer-scope err capture) and document the scoping pitfall.
  • Add a regression test that hooks logger.SetLogFunc and asserts the missing-state log entry carries a nil error.

CI/check status and PR commit history (e.g., merge commits) aren’t available in this review context—please confirm CI is green and the PR history is linear.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
backend/cmd/headlamp.go Fixes the missing-state log call to avoid logging an unrelated outer-scope error; adds an explanatory comment.
backend/cmd/headlamp_test.go Adds regression test capturing logger calls to ensure no stale error is logged on empty state.
Comments suppressed due to low confidence (3)

backend/cmd/headlamp_test.go:1448

  • In this test, calling createHeadlampHandler(context.Background(), cfg) with an empty PluginDir triggers the background plugin watcher paths in createHeadlampHandler (because !UseInCluster), and plugins.Watch treats an empty watchPath as the current directory, which can walk/watch a large tree and consume many fsnotify resources during tests. Consider setting PluginDir to t.TempDir() (and optionally UserPluginDir/StaticPluginDir) and using a cancellable context (context.WithCancel + t.Cleanup(cancel)) so the watcher goroutines are stopped at the end of the test.
	cfg := &HeadlampConfig{
		HeadlampConfig: &headlampconfig.HeadlampConfig{
			HeadlampCFG: &headlampconfig.HeadlampCFG{
				UseInCluster:    false,
				KubeConfigPath:  "/tmp/" + staleErrMarker,
				KubeConfigStore: kubeconfig.NewContextStore(),
			},
			Cache:            cache.New[interface{}](),
			TelemetryConfig:  GetDefaultTestTelemetryConfig(),
			TelemetryHandler: &telemetry.RequestHandler{},
		},
	}

backend/cmd/headlamp_test.go:1463

  • logMu is locked and held via defer until the end of the test. Since the logger hook acquires the same mutex on every log call, any background goroutine that logs after this point will block until the test returns, which can create unnecessary contention or even hangs in the presence of cleanup that logs. Prefer copying logCalls under the mutex and unlocking immediately before searching/asserting.
	logMu.Lock()
	defer logMu.Unlock()

backend/cmd/headlamp_test.go:1417

  • Using a hard-coded "/tmp/" + marker for KubeConfigPath can collide if that path happens to exist on the test machine (making the startup kubeconfig load succeed and weakening the regression). Consider using filepath.Join(t.TempDir(), "missing") (or similar) to guarantee a non-existent kubeconfig path.
	// Bad path so the startup kubeconfig load returns a distinctive err.
	staleErrMarker := "no-such-kubeconfig-for-issue-4839"

Comment thread backend/cmd/headlamp_test.go Outdated
…issing state

The /oidc-callback handler logged a stray err variable when the state
query parameter was missing:

    logger.Log(logger.LevelError, nil, err, "invalid request state is empty")

That err was not declared inside the handler closure. It resolved to
the err returned by the kubeconfig load that runs once when
createHeadlampHandler is constructed at server startup. Every malformed
callback request therefore surfaced whatever happened at boot, for
example

    error loading kubeconfig files: error reading kubeconfig file:
    open /home/headlamp/.config/Headlamp/kubeconfigs/config: no such
    file or directory

attached to an unrelated 400 response, making the log misleading and
hard to interpret.

Pass nil to logger.Log on this branch and leave a short comment so the
next reader does not reintroduce the capture. Add a regression test
that drives the handler with a deliberately broken KubeConfigPath,
fires GET /oidc-callback with no state, captures logger output via
logger.SetLogFunc, and asserts the missing-state log carries no error
value.

Issue: kubernetes-sigs#4839
Signed-off-by: Govind Pandey <govindup63@gmail.com>
@govindup63 govindup63 force-pushed the fix/4839-oidc-callback-misleading-error-log branch from 2ada63a to b296550 Compare May 17, 2026 23:39
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for working on this.

Would you mind addressing the open Copilot review comments? Please mark each comment as resolved after addressing it.

@govindup63
Copy link
Copy Markdown
Contributor Author

Thanks @illume, marked them as resolved.

The regression test added in the previous commit tripped two lint rules
in CI:

  - funlen: the test body is 67 lines, over the 60-line cap. Other tests
    in the same file already use //nolint:funlen for the same reason
    (see TestHelmRouteReleaseHandlerTokenExtraction). Apply the same
    directive.
  - wsl_v5: missing blank line around the captured := append(...) line
    that copies logCalls out from under the mutex.

Signed-off-by: Govind Pandey <govindup63@gmail.com>
@govindup63
Copy link
Copy Markdown
Contributor Author

golangci-lint was failing on the new test (funlen + wsl_v5). Pushed a fix in b7d6729bb.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread backend/cmd/headlamp_test.go
createHeadlampHandler unconditionally reassigns config.StaticPluginDir
from the HEADLAMP_STATIC_PLUGINS_DIR environment variable, so the test
setting cfg.HeadlampCFG.StaticPluginDir = scratch was a no-op and the
plugin watcher could end up walking whatever directory the env var
named in CI. Set the env var via t.Setenv so the static-plugin dir is
deterministic for the duration of the test, and drop the dead field
assignment.

Signed-off-by: Govind Pandey <govindup63@gmail.com>
@govindup63
Copy link
Copy Markdown
Contributor Author

Thanks @copilot, addressed in b829fbefe (use t.Setenv to pin the static-plugin dir, dropped the no-op field assignment). Marked resolved.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread backend/cmd/headlamp.go
…eclaration

A comment was load-bearing in the previous commit: it warned future
editors that the early-branch logger.Log must not reference err,
because err would resolve to createHeadlampHandler's outer-scope
kubeconfig-load result. Hoist `var err error` to the top of the
/oidc-callback closure so the shadow is enforced by the language, not
by a comment. Any future log call inside the handler now references
the closure-local err that begins life as nil.

Signed-off-by: Govind Pandey <govindup63@gmail.com>
@govindup63
Copy link
Copy Markdown
Contributor Author

Thanks @copilot, applied in e0e34ef78 — hoisted var err error to the top of the closure so the shadow is enforced by the language. Marked resolved.

Comment thread backend/cmd/headlamp.go Outdated
…ogger.Log

The shadow declaration on the previous commit reliably leaves err nil
at the missing-state branch, so logger.Log was being passed nil
indirectly. Pass nil directly to make the call self-evident; the
shadow declaration above still protects against future log calls
elsewhere in the closure.

Signed-off-by: Govind Pandey <govindup63@gmail.com>
@govindup63
Copy link
Copy Markdown
Contributor Author

Right, fixed in 47bc03b6e — passing nil directly. Marked resolved.

Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

🎉 thanks!

@k8s-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: govindup63, illume

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label May 19, 2026
@illume illume merged commit 683943f into kubernetes-sigs:main May 19, 2026
13 of 14 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +1428 to +1434
originalLogFunc := logger.SetLogFunc(func(level uint, _ map[string]string, err interface{}, msg string) {
logMu.Lock()
defer logMu.Unlock()

logCalls = append(logCalls, logCall{level: level, msg: msg, err: err})
})
defer logger.SetLogFunc(originalLogFunc)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Insecure Error Logging

4 participants