@@ -13,6 +13,8 @@ import (
1313 "github.com/prometheus/client_golang/prometheus"
1414 "github.com/prometheus/client_golang/prometheus/promhttp"
1515 "github.com/spf13/cobra"
16+ "github.com/spf13/pflag"
17+ "gopkg.in/yaml.v3"
1618
1719 "github.com/openshift-hyperfleet/hyperfleet-broker/broker"
1820 "github.com/openshift-hyperfleet/hyperfleet-sentinel/internal/client"
@@ -40,6 +42,7 @@ reconciliation events to a message broker based on configurable max age interval
4042 }
4143
4244 rootCmd .AddCommand (newServeCommand ())
45+ rootCmd .AddCommand (newConfigDumpCommand ())
4346
4447 if err := rootCmd .Execute (); err != nil {
4548 // Print error to stderr since SilenceErrors is true and logging may not be initialized
@@ -55,9 +58,6 @@ reconciliation events to a message broker based on configurable max age interval
5558func newServeCommand () * cobra.Command {
5659 var (
5760 configFile string
58- logLevel string
59- logFormat string
60- logOutput string
6161 healthBindAddress string
6262 metricsBindAddress string
6363 )
@@ -69,78 +69,145 @@ func newServeCommand() *cobra.Command {
6969 SilenceUsage : true , // Don't print usage on error
7070 SilenceErrors : true , // Don't print errors - we handle logging ourselves
7171 RunE : func (cmd * cobra.Command , args []string ) error {
72- // Initialize logging configuration
73- // Precedence: flags → environment variables → defaults
74- logCfg , err := initLogging ( logLevel , logFormat , logOutput )
72+ // Load configuration with CLI flags, env vars, and file
73+ // Precedence: flags → environment variables → config file → defaults
74+ cfg , err := config . LoadConfig ( configFile , cmd . Flags () )
7575 if err != nil {
76- return fmt . Errorf ( "failed to initialize logging: %w" , err )
76+ return err
7777 }
7878
79- // Load and validate configuration from YAML and env vars
80- cfg , err := config . LoadConfig ( configFile )
79+ // Initialize logging with merged configuration
80+ logCfg , err := initLogging ( & cfg . Log , cmd . Flags () )
8181 if err != nil {
82- return err
82+ return fmt . Errorf ( "failed to initialize logging: %w" , err )
8383 }
84+
8485 return runServe (cfg , logCfg , healthBindAddress , metricsBindAddress )
8586 },
8687 }
8788
88- // Add --config flag for YAML file path
89+ // Config file path
8990 cmd .Flags ().StringVarP (& configFile , "config" , "c" , "" , "Path to configuration file (YAML)" )
9091
91- // Add logging flags per HyperFleet logging specification
92- cmd .Flags ().StringVar (& logLevel , "log-level" , "" , "Log level: debug, info, warn, error (default: info)" )
93- cmd .Flags ().StringVar (& logFormat , "log-format" , "" , "Log format: text, json (default: text)" )
94- cmd .Flags ().StringVar (& logOutput , "log-output" , "" , "Log output: stdout, stderr (default: stdout)" )
95-
96- // Server bind address flags (consistent with hyperfleet-api)
92+ // Server bind address flags
9793 cmd .Flags ().StringVar (& healthBindAddress , "health-server-bindaddress" , ":8080" , "Health server bind address" )
9894 cmd .Flags ().StringVar (& metricsBindAddress , "metrics-server-bindaddress" , ":9090" , "Metrics server bind address" )
9995
96+ // Add config override flags
97+ addConfigOverrideFlags (cmd )
98+
10099 return cmd
101100}
102101
103- // getConfigValue returns the flag value if set, otherwise falls back to the environment variable.
104- // This implements the precedence: flags → environment variables → defaults
105- func getConfigValue (flag , envVar string ) string {
106- if flag != "" {
107- return flag
102+ func newConfigDumpCommand () * cobra.Command {
103+ var configFile string
104+
105+ cmd := & cobra.Command {
106+ Use : "config-dump" ,
107+ Short : "Load and print the merged sentinel configuration as YAML" ,
108+ Long : `Load the sentinel configuration from config file, environment variables,
109+ and CLI flags, then print the merged result as YAML to stdout.
110+ Exits with code 0 on success, non-zero on error.
111+
112+ Priority order (lowest to highest): config file < env vars < CLI flags` ,
113+ SilenceUsage : true ,
114+ SilenceErrors : true ,
115+ RunE : func (cmd * cobra.Command , args []string ) error {
116+ return runConfigDump (configFile , cmd .Flags ())
117+ },
108118 }
109- return os .Getenv (envVar )
119+
120+ cmd .Flags ().StringVarP (& configFile , "config" , "c" , "" , "Path to configuration file (YAML)" )
121+ addConfigOverrideFlags (cmd )
122+
123+ return cmd
124+ }
125+
126+ // addConfigOverrideFlags adds CLI flags for overriding configuration values
127+ func addConfigOverrideFlags (cmd * cobra.Command ) {
128+ // General
129+ cmd .Flags ().Bool ("debug-config" , false , "Log the full merged configuration after load. Env: HYPERFLEET_DEBUG_CONFIG" )
130+
131+ // Sentinel
132+ cmd .Flags ().String ("sentinel-name" , "" , "Sentinel component name. Env: HYPERFLEET_SENTINEL_NAME" )
133+
134+ cmd .Flags ().String ("log-level" , "" , "Log level: debug, info, warn, error. Env: HYPERFLEET_LOG_LEVEL" )
135+ cmd .Flags ().String ("log-format" , "" , "Log format: text, json. Env: HYPERFLEET_LOG_FORMAT" )
136+ cmd .Flags ().String ("log-output" , "" , "Log output: stdout, stderr. Env: HYPERFLEET_LOG_OUTPUT" )
137+
138+ // HyperFleet API
139+ cmd .Flags ().String ("hyperfleet-api-base-url" , "" , "HyperFleet API base URL. Env: HYPERFLEET_API_BASE_URL" )
140+ cmd .Flags ().String ("hyperfleet-api-version" , "" , "HyperFleet API version. Env: HYPERFLEET_API_VERSION" )
141+ cmd .Flags ().String ("hyperfleet-api-timeout" , "" , "HyperFleet API timeout (e.g., 10s). Env: HYPERFLEET_API_TIMEOUT" )
142+ cmd .Flags ().Int ("hyperfleet-api-retry-attempts" , 0 , "HyperFleet API retry attempts. Env: HYPERFLEET_API_RETRY_ATTEMPTS" )
143+ cmd .Flags ().String ("hyperfleet-api-retry-backoff" , "" , "HyperFleet API retry backoff strategy. Env: HYPERFLEET_API_RETRY_BACKOFF" )
144+ cmd .Flags ().String ("hyperfleet-api-base-delay" , "" , "HyperFleet API base retry delay. Env: HYPERFLEET_API_BASE_DELAY" )
145+ cmd .Flags ().String ("hyperfleet-api-max-delay" , "" , "HyperFleet API max retry delay. Env: HYPERFLEET_API_MAX_DELAY" )
146+
147+ // Broker
148+ cmd .Flags ().String ("broker-subscription-id" , "" , "Broker subscription ID. Env: HYPERFLEET_BROKER_SUBSCRIPTION_ID or BROKER_SUBSCRIPTION_ID" )
149+ cmd .Flags ().String ("broker-topic" , "" , "Broker topic. Env: HYPERFLEET_BROKER_TOPIC or BROKER_TOPIC" )
150+
151+ // Sentinel-specific
152+ cmd .Flags ().String ("resource-type" , "" , "Resource type to watch (clusters, nodepools). Env: HYPERFLEET_RESOURCE_TYPE" )
153+ cmd .Flags ().String ("poll-interval" , "" , "Poll interval (e.g., 5s). Env: HYPERFLEET_POLL_INTERVAL" )
154+ cmd .Flags ().String ("max-age-not-ready" , "" , "Max age for not-ready resources. Env: HYPERFLEET_MAX_AGE_NOT_READY" )
155+ cmd .Flags ().String ("max-age-ready" , "" , "Max age for ready resources. Env: HYPERFLEET_MAX_AGE_READY" )
110156}
111157
112- // initLogging initializes the logging configuration following the precedence:
113- // flags → environment variables → defaults
114- func initLogging (flagLevel , flagFormat , flagOutput string ) (* logger.LogConfig , error ) {
158+ // initLogging initializes the logging configuration.
159+ // Priority (lowest to highest): config file defaults → env vars → CLI flags.
160+ func initLogging (logCfg * config. LogConfig , flags * pflag. FlagSet ) (* logger.LogConfig , error ) {
115161 cfg := logger .DefaultConfig ()
116162 cfg .Version = version
117163 cfg .Component = "sentinel"
118164
119- // Apply log level
120- if levelStr := getConfigValue (flagLevel , "LOG_LEVEL" ); levelStr != "" {
121- level , err := logger .ParseLogLevel (levelStr )
165+ // Apply log level: config file → env var → CLI flag
166+ level := logCfg .Level
167+ if val := os .Getenv ("HYPERFLEET_LOG_LEVEL" ); val != "" {
168+ level = val
169+ }
170+ if f := flags .Lookup ("log-level" ); f != nil && f .Changed {
171+ level = f .Value .String ()
172+ }
173+ if level != "" {
174+ parsed , err := logger .ParseLogLevel (level )
122175 if err != nil {
123176 return nil , err
124177 }
125- cfg .Level = level
178+ cfg .Level = parsed
126179 }
127180
128- // Apply log format
129- if formatStr := getConfigValue (flagFormat , "LOG_FORMAT" ); formatStr != "" {
130- format , err := logger .ParseLogFormat (formatStr )
181+ // Apply log format: config file → env var → CLI flag
182+ format := logCfg .Format
183+ if val := os .Getenv ("HYPERFLEET_LOG_FORMAT" ); val != "" {
184+ format = val
185+ }
186+ if f := flags .Lookup ("log-format" ); f != nil && f .Changed {
187+ format = f .Value .String ()
188+ }
189+ if format != "" {
190+ parsed , err := logger .ParseLogFormat (format )
131191 if err != nil {
132192 return nil , err
133193 }
134- cfg .Format = format
194+ cfg .Format = parsed
135195 }
136196
137- // Apply log output
138- if outputStr := getConfigValue (flagOutput , "LOG_OUTPUT" ); outputStr != "" {
139- output , err := logger .ParseLogOutput (outputStr )
197+ // Apply log output: config file → env var → CLI flag
198+ output := logCfg .Output
199+ if val := os .Getenv ("HYPERFLEET_LOG_OUTPUT" ); val != "" {
200+ output = val
201+ }
202+ if f := flags .Lookup ("log-output" ); f != nil && f .Changed {
203+ output = f .Value .String ()
204+ }
205+ if output != "" {
206+ parsed , err := logger .ParseLogOutput (output )
140207 if err != nil {
141208 return nil , err
142209 }
143- cfg .Output = output
210+ cfg .Output = parsed
144211 }
145212
146213 // Set global config so all loggers use the same configuration
@@ -159,13 +226,23 @@ func runServe(cfg *config.SentinelConfig, logCfg *logger.LogConfig, healthBindAd
159226 Extra ("log_format" , logCfg .Format .String ()).
160227 Info (ctx , "Starting HyperFleet Sentinel" )
161228
229+ // Log full merged configuration if debug_config is enabled; sensitive values are redacted
230+ if cfg .DebugConfig {
231+ data , err := yaml .Marshal (cfg .RedactedCopy ())
232+ if err != nil {
233+ log .Warnf (ctx , "Failed to marshal config for debug logging: %v" , err )
234+ } else {
235+ log .Infof (ctx , "Debug config enabled - merged configuration:\n %s" , string (data ))
236+ }
237+ }
238+
162239 // Initialize Prometheus metrics registry
163240 registry := prometheus .NewRegistry ()
164241 // Register metrics once (uses sync.Once internally)
165242 metrics .NewSentinelMetrics (registry , version )
166243
167244 // Initialize components
168- hyperfleetClient , err := client .NewHyperFleetClient (cfg .HyperFleetAPI . Endpoint , cfg .HyperFleetAPI . Timeout )
245+ hyperfleetClient , err := client .NewHyperFleetClient (cfg .Clients . HyperfleetAPI . BaseURL , cfg .Clients . HyperfleetAPI . Timeout , cfg . Sentinel . Name , version )
169246 if err != nil {
170247 log .Errorf (ctx , "Failed to initialize OpenAPI client: %v" , err )
171248 return fmt .Errorf ("failed to initialize OpenAPI client: %w" , err )
@@ -292,3 +369,18 @@ func runServe(cfg *config.SentinelConfig, logCfg *logger.LogConfig, healthBindAd
292369 log .Info (ctx , "Sentinel stopped gracefully" )
293370 return nil
294371}
372+
373+ // runConfigDump loads the full sentinel configuration and prints it as YAML to stdout.
374+ func runConfigDump (configFile string , flags * pflag.FlagSet ) error {
375+ cfg , err := config .LoadConfig (configFile , flags )
376+ if err != nil {
377+ return err
378+ }
379+
380+ data , err := yaml .Marshal (cfg )
381+ if err != nil {
382+ return fmt .Errorf ("failed to marshal config: %w" , err )
383+ }
384+ fmt .Print (string (data ))
385+ return nil
386+ }
0 commit comments