Skip to content

Commit a4e1d5f

Browse files
committed
HYPERFLEET-549 - feat: standard configuration
1 parent 797ed3f commit a4e1d5f

22 files changed

Lines changed: 2114 additions & 252 deletions

charts/templates/configmap.yaml

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,40 @@ metadata:
66
{{- include "sentinel.labels" . | nindent 4 }}
77
data:
88
config.yaml: |
9+
# Sentinel information
10+
sentinel:
11+
name: {{ tpl .Values.config.sentinel.name . }}
12+
13+
# Debug configuration
14+
debug_config: {{ .Values.config.debugConfig }}
15+
16+
# Logging configuration
17+
log:
18+
level: {{ .Values.config.log.level | quote }}
19+
format: {{ .Values.config.log.format | quote }}
20+
output: {{ .Values.config.log.output | quote }}
21+
22+
# Client configurations
23+
clients:
24+
# HyperFleet API client
25+
hyperfleet_api:
26+
base_url: {{ .Values.config.clients.hyperfleetApi.baseUrl }}
27+
version: {{ .Values.config.clients.hyperfleetApi.version | quote }}
28+
timeout: {{ .Values.config.clients.hyperfleetApi.timeout }}
29+
retry_attempts: {{ .Values.config.clients.hyperfleetApi.retryAttempts }}
30+
retry_backoff: {{ .Values.config.clients.hyperfleetApi.retryBackoff | quote }}
31+
base_delay: {{ .Values.config.clients.hyperfleetApi.baseDelay }}
32+
max_delay: {{ .Values.config.clients.hyperfleetApi.maxDelay }}
33+
{{- if .Values.config.clients.hyperfleetApi.defaultHeaders }}
34+
default_headers:
35+
{{- toYaml .Values.config.clients.hyperfleetApi.defaultHeaders | nindent 10 }}
36+
{{- end }}
37+
38+
# Broker client
39+
broker:
40+
subscription_id: {{ .Values.config.clients.broker.subscriptionId | quote }}
41+
topic: {{ tpl .Values.broker.topic . | quote }}
42+
943
# Sentinel configuration
1044
resource_type: {{ .Values.config.resourceType }}
1145
poll_interval: {{ .Values.config.pollInterval }}
@@ -21,11 +55,6 @@ data:
2155
{{- end }}
2256
{{- end }}
2357
24-
# HyperFleet API configuration
25-
hyperfleet_api:
26-
endpoint: {{ .Values.config.hyperfleetApi.baseUrl }}
27-
timeout: {{ .Values.config.hyperfleetApi.timeout }}
28-
2958
{{- if .Values.config.messageData }}
3059
# CloudEvents data payload configuration
3160
message_data:

charts/values.yaml

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,44 @@ podDisruptionBudget:
7575

7676
# Sentinel configuration
7777
config:
78+
# Sentinel information
79+
sentinel:
80+
# Sentinel component name - will be templated with shard value if resource selector is used
81+
# Example: hyperfleet-sentinel-clusters-shard-1
82+
name: hyperfleet-sentinel-{{ .Values.config.resourceType }}
83+
84+
# Debug configuration - log merged config on startup
85+
debugConfig: false
86+
87+
# Logging configuration
88+
log:
89+
level: info
90+
format: json
91+
output: stdout
92+
93+
# Client configurations
94+
clients:
95+
# HyperFleet API client configuration
96+
hyperfleetApi:
97+
# Use in-cluster service name for API endpoint
98+
baseUrl: http://hyperfleet-api:8000
99+
version: v1
100+
timeout: 10s
101+
retryAttempts: 3
102+
retryBackoff: exponential
103+
baseDelay: 1s
104+
maxDelay: 30s
105+
# Optional default headers
106+
# defaultHeaders:
107+
# X-Custom-Header: "value"
108+
109+
# Broker configuration
110+
# Note: broker implementation details (RabbitMQ URL, etc.) are in broker section below
111+
broker:
112+
subscriptionId: ""
113+
# Topic will be set from broker.topic template below
114+
topic: ""
115+
78116
# Resource type to watch (clusters, nodepools)
79117
resourceType: clusters
80118

@@ -93,12 +131,6 @@ config:
93131
- label: shard
94132
value: "1"
95133

96-
# HyperFleet API configuration
97-
hyperfleetApi:
98-
# Use in-cluster service name for API endpoint
99-
baseUrl: http://hyperfleet-api:8000
100-
timeout: 5s
101-
102134
# CloudEvents data payload configuration
103135
messageData:
104136
id: resource.id

cmd/sentinel/main.go

Lines changed: 131 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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
5558
func 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 variablesdefaults
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 varsCLI 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

Comments
 (0)