Ensure log level is applied before loading configuration - #29258
Conversation
| func setEarlyLogLevel() { | ||
| index := parseIndex() | ||
| if index > 1 { | ||
| return | ||
| } | ||
|
|
||
| var logLevel string | ||
| var debug bool | ||
| fs := pflag.NewFlagSet("log level", pflag.ContinueOnError) | ||
| fs.ParseErrorsAllowlist.UnknownFlags = true | ||
| fs.Usage = func() {} | ||
| fs.SetInterspersed(false) | ||
| fs.StringVar(&logLevel, "log-level", "", "") | ||
| fs.BoolVarP(&debug, "debug", "D", false, "") | ||
| fs.BoolP("help", "h", false, "") // Need a fake help flag to avoid the `pflag: help requested` error | ||
| if err := fs.Parse(os.Args[index:]); err != nil { | ||
| return | ||
| } | ||
|
|
||
| if debug && logLevel == "" { | ||
| logrus.SetLevel(logrus.DebugLevel) | ||
| } else if !debug && logLevel != "" { | ||
| if level, err := logrus.ParseLevel(logLevel); err == nil { | ||
| logrus.SetLevel(level) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
so this adds the fifth argument parser here that has to walk argv. I Do not think this is sustainable/
This, modules, and the remote parsing should be refactored so they only use one flag set once and parse once.
There was a problem hiding this comment.
@Luap99 I refactored this as you suggested:
- Consolidated the early parsers for logging, modules, and remote into a single entry point (getEarlyCLIOptions() in config.go).
- There's now a single pflag.FlagSet, parsed exactly once via sync.Once (earlySync). remote.go no longer has its own parser, it reuses the cached result.
- Added tests covering shell completion, environment variables (CONTAINER_HOST/CONTAINER_CONNECTION), podmansh, invalid flags, and the local/remote cases (--connection, --context, --host, --url).
Could you take another look when you have a chance?
c7dfab3 to
3c95926
Compare
|
[NON-BLOCKING] Packit jobs failed. @containers/packit-build please check. Everyone else, feel free to ignore. |
|
|
||
| // getEarlyCLIOptions parses flags needed during command initialization. | ||
| // Cobra parses and validates the complete command line later. | ||
| func getEarlyCLIOptions() *earlyCLIOptions { |
There was a problem hiding this comment.
Nit: feels like this would be better as a sync.OnceValue()
There was a problem hiding this comment.
there is not point at all in this if we call it in newPodmanConfig which is already part of a sync.once so it feels like we should go further and just make this structu part of PodmanConfig and parse this one in newPodmanConfig where we have the sync.Once already. No point in using several nested sync.once
There was a problem hiding this comment.
I’ll remove the nested synchronization. The early options can be parsed directly in newPodmanConfig(), which is already protected by podmanSync. The parsed state will be incorporated into the Podman configuration initialization.
| if options.parseErr != nil { | ||
| var flagErr interface{ GetFlag() *pflag.Flag } | ||
| if !errors.As(options.parseErr, &flagErr) { | ||
| return options.modules, options.parseErr |
There was a problem hiding this comment.
(It seems like just bailing if parseErr is not nil is all that's necessary?)
There was a problem hiding this comment.
yes this should just return parseErr
There was a problem hiding this comment.
I’ll simplify this to return parseErr directly and remove the flag-specific error classification. The related tests for malformed non-module flags will be adjusted accordingly.
|
|
||
| // getEarlyCLIOptions parses flags needed during command initialization. | ||
| // Cobra parses and validates the complete command line later. | ||
| func getEarlyCLIOptions() *earlyCLIOptions { |
There was a problem hiding this comment.
there is not point at all in this if we call it in newPodmanConfig which is already part of a sync.once so it feels like we should go further and just make this structu part of PodmanConfig and parse this one in newPodmanConfig where we have the sync.Once already. No point in using several nested sync.once
| if options.parseErr != nil { | ||
| var flagErr interface{ GetFlag() *pflag.Flag } | ||
| if !errors.As(options.parseErr, &flagErr) { | ||
| return options.modules, options.parseErr |
There was a problem hiding this comment.
yes this should just return parseErr
| func resetEarlyCLIState(t *testing.T, args ...string) { | ||
| t.Helper() | ||
| oldArgs := os.Args | ||
| os.Args = args | ||
| earlyOptions = earlyCLIOptions{} | ||
| earlySync = sync.Once{} | ||
| t.Cleanup(func() { | ||
| os.Args = oldArgs | ||
| earlyOptions = earlyCLIOptions{} | ||
| earlySync = sync.Once{} | ||
| }) | ||
| } |
There was a problem hiding this comment.
from a unit test POV this is really ugly.
write the function so that you test it without a sync.once and make the os.ARgs an argument to the function then you have a function that can be unit tested which makes a lot more sense that this global state modification.
There was a problem hiding this comment.
I’ll separate the parsing logic from the global initialization. The parser will receive the argument slice explicitly, while newPodmanConfig() will pass the relevant portion of os.Args. Unit tests will then invoke the parser directly without resetting os.Args, sync.Once, or shared option state.
| EOF | ||
|
|
||
| for log_arg in --log-level=debug --log-level=trace --debug; do | ||
| CONTAINERS_CONF="$conf_tmp" run_podman "$log_arg" version |
There was a problem hiding this comment.
this should use CONTAINERS_CONF_OVERRIDE instead
There was a problem hiding this comment.
I’ll update the regression test to use CONTAINERS_CONF_OVERRIDE for the temporary configuration file.
| } | ||
|
|
||
| // Return the containers.conf modules to load. | ||
| func containersConfModules(options *earlyCLIOptions) ([]string, error) { |
There was a problem hiding this comment.
Is this function necessary? Maybe the call to this function can be rewritten to this:
options := parseEarlyCLIOptions(os.Args)
setEarlyLogLevel(options)
if options.parseErr != nil && !options.completion {
fmt.Fprintf(os.Stderr, "Error parsing command-line flags: %v\n", options.parseErr)
os.Exit(1)
}
modules := options.modules
if options.completion {
modules = nil
}There was a problem hiding this comment.
Fixed in 22f49ad. Removed containersConfModules and inlined the completion, error, and module handling in newPodmanConfig. The system test now covers incomplete flags and verifies that modules are not loaded during completion.
| } | ||
|
|
||
| podmanOptions = entities.PodmanConfig{ContainersConf: &config.Config{}, ContainersConfDefaultsRO: defaultConfig, EngineMode: mode} | ||
| podmanOptions = entities.PodmanConfig{ContainersConf: &config.Config{}, ContainersConfDefaultsRO: defaultConfig, EngineMode: mode, Remote: remote} |
There was a problem hiding this comment.
Removed the initialization in 22f49ad. PodmanConfig.Remote is used as the --remote flag target in rootFlags, but BoolVarP initializes it from registry.IsRemote(), so assigning remote in newPodmanConfig was redundant.
| setEarlyLogLevel(options) | ||
| modules, err := containersConfModules(options) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error parsing containers.conf modules: %v\n", err) |
There was a problem hiding this comment.
I think this is a better error message.
| fmt.Fprintf(os.Stderr, "Error parsing containers.conf modules: %v\n", err) | |
| fmt.Fprintf(os.Stderr, "Error parsing command-line flags: %v\n", err) |
There was a problem hiding this comment.
Fixed in 22f49ad. The message now says "Error parsing command-line flags", which covers every flag handled by the early parser.
Honny1
left a comment
There was a problem hiding this comment.
Can you please fix linters:
+ /home/ubuntu/golangci-lint-2.12.2-linux-amd64/golangci-lint run --build-tags=apparmor,seccomp,selinux
Error: cmd/podman/registry/config_test.go:94:1: File is not properly formatted (gofumpt)
name string
^
Honny1
left a comment
There was a problem hiding this comment.
CI failure seems to be related. Please resolve that and also squash all your commits. Thanks.
2b40290 to
569b49e
Compare
|
Fixed and squashed in 569b49e. |
Honny1
left a comment
There was a problem hiding this comment.
LGTM, Thanks!
Non-blocking question: What bout --debug --log-level=<same-as-default-value> edge case? What will happen when both flags are used?
Luap99
left a comment
There was a problem hiding this comment.
First commit looks good to me but I think the second commit is just wrong and should be dropped. I see no point in special casing --debug --log-level=warn, in fact to me it is wrong because that alters the behavior for just that one level? If both options are given explicitly that should always be treated as conflict
79a220a to
3334d95
Compare
| "go.podman.io/storage" | ||
| ) | ||
|
|
||
| func TestLoggingHookRejectsExplicitDefaultLogLevelWithDebug(t *testing.T) { |
There was a problem hiding this comment.
I don't think this test is necessary, so I'd drop it. The integration test is enough.
Honny1
left a comment
There was a problem hiding this comment.
Code LGTM. Just one last thing: please squash your commits.
Podman loads containers.conf before Cobra processes global flags, hiding debug and trace diagnostics emitted during configuration parsing. Parse early CLI options once so logging is configured first, then reuse the result for modules and remote-mode selection. Fixes: podman-container-tools#25362 Signed-off-by: Marcos Paulo Diniz <marcosdinizpaulo@gmail.com>
9dab354 to
18e1154
Compare
|
squashed in 18e1154. |
Honny1
left a comment
There was a problem hiding this comment.
LGTM
@podman-container-tools/podman-reviewers @podman-container-tools/podman-maintainers Please review and merge.
|
I do not have time to check tests right now, but the code looks good otherwise I think. LGTM. |
Podman loads its configuration during global initialization, before the
--log-leveloption is processed. As a result, debug and trace messages emitted while reading configuration files are suppressed, making invalid or unknown configuration keys difficult to diagnose.This change applies the requested log level before configuration initialization, ensuring that configuration-loading diagnostics are visible when commands are executed with
--log-level=debugor--log-level=trace.Fixes: #25362
Checklist
git commit -s).Fixes: #00000in commit message.make validatepr(format/lint checks).Does this PR introduce a user-facing change?