Skip to content

Commit 981e874

Browse files
committed
fix: improve error handling and provider registry consistency
- Add include depth limit to config includes - Improve error handling for flag parsing and TLS/HTTP providers - Make provider registry internal and update references - Update Vault provider docs for lowercase health fields - Return error when all container components fail to resolve - Add default HTTP status check if no CEL checks configured - Fix time CEL function panics on wrong type - Update example config for Kubernetes kind field - Miscellaneous doc and comment improvements
1 parent 7d3c737 commit 981e874

File tree

18 files changed

+153
-92
lines changed

18 files changed

+153
-92
lines changed

pkg/checks/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ph context my-app -o yaml
1919

2020
## CEL Expression Syntax
2121

22-
CEL expressions must evaluate to a boolean (`true` for healthy, `false` for unhealthy). Expressions have access to provider-specific context variables (e.g., `response` for REST, `resource` for Kubernetes).
22+
CEL expressions must evaluate to a boolean (`true` for healthy, `false` for unhealthy). Expressions have access to provider-specific context variables (e.g., `response` for HTTP, `resource` for Kubernetes).
2323

2424
## Common CEL Patterns
2525

pkg/checks/cel.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,11 @@ func ParseConfig(raw any) ([]Expression, error) {
249249
// ValidateExpression validates CEL expression syntax at configuration time.
250250
// Variables should be declared using cel.Variable() options.
251251
func ValidateExpression(expression string, variables ...cel.EnvOption) error {
252-
env, err := cel.NewEnv(append(variables, standardExtensions...)...)
252+
allOpts := make([]cel.EnvOption, 0, len(standardFunctions)+len(variables)+len(standardExtensions))
253+
allOpts = append(allOpts, standardFunctions...)
254+
allOpts = append(allOpts, variables...)
255+
allOpts = append(allOpts, standardExtensions...)
256+
env, err := cel.NewEnv(allOpts...)
253257
if err != nil {
254258
return fmt.Errorf("failed to create CEL environment: %w", err)
255259
}
@@ -262,7 +266,7 @@ func ValidateExpression(expression string, variables ...cel.EnvOption) error {
262266
}
263267

264268
// EvaluateAny compiles and evaluates a CEL expression returning its result.
265-
// Unlike Evaluate(), this does not require boolean output - any type is allowed.
269+
// Unlike Check.Evaluate(), this does not require boolean output - any type is allowed.
266270
// The result is converted to native Go types for serialization.
267271
// Note: Uses the cached environment but compiles the AST directly (no caching)
268272
// since the boolean output validation in getOrCompileAST() doesn't apply here.

pkg/checks/functions/time.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ func Time() []cel.EnvOption {
2727
[]*cel.Type{cel.TimestampType},
2828
cel.DurationType,
2929
cel.UnaryBinding(func(arg ref.Val) ref.Val {
30-
ts := arg.(types.Timestamp)
30+
ts, ok := arg.(types.Timestamp)
31+
if !ok {
32+
return types.NewErr("time.Since: expected timestamp, got %T", arg)
33+
}
3134
return types.Duration{Duration: time.Since(ts.Time)}
3235
}),
3336
),
@@ -38,7 +41,10 @@ func Time() []cel.EnvOption {
3841
[]*cel.Type{cel.TimestampType},
3942
cel.DurationType,
4043
cel.UnaryBinding(func(arg ref.Val) ref.Val {
41-
ts := arg.(types.Timestamp)
44+
ts, ok := arg.(types.Timestamp)
45+
if !ok {
46+
return types.NewErr("time.Until: expected timestamp, got %T", arg)
47+
}
4248
return types.Duration{Duration: time.Until(ts.Time)}
4349
}),
4450
),

pkg/commands/check/check.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,8 @@ func setup(cmd *cobra.Command, _ []string) (err error) {
115115
return err
116116
}
117117

118-
// In strict mode, fail if any configuration errors were found
119-
if strict {
120-
if err := result.EnforceStrict(log); err != nil {
121-
return err
122-
}
118+
if err := result.EnforceStrict(log); err != nil {
119+
return err
123120
}
124121

125122
conf = result

pkg/commands/context/context.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,14 @@ func displayContext(cmd *cobra.Command, checkProvider provider.InstanceWithCheck
138138
v := phctx.Viper(cmd.Context())
139139
output := v.GetString("output-format")
140140

141-
defaultExprs, _ := cmd.Flags().GetStringArray("expr")
142-
eachExprs, _ := cmd.Flags().GetStringArray("expr-each")
141+
defaultExprs, err := cmd.Flags().GetStringArray("expr")
142+
if err != nil {
143+
return fmt.Errorf("failed to get expr flag: %w", err)
144+
}
145+
eachExprs, err := cmd.Flags().GetStringArray("expr-each")
146+
if err != nil {
147+
return fmt.Errorf("failed to get expr-each flag: %w", err)
148+
}
143149

144150
if len(defaultExprs) > 0 || len(eachExprs) > 0 {
145151
celConfig := checkProvider.GetCheckConfig()

pkg/commands/server/server.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,8 @@ func setup(cmd *cobra.Command, args []string) (err error) {
5959
return err
6060
}
6161

62-
// In strict mode, fail if any configuration errors were found
63-
if strict {
64-
if err := result.EnforceStrict(log); err != nil {
65-
return err
66-
}
62+
if err := result.EnforceStrict(log); err != nil {
63+
return err
6764
}
6865

6966
conf = result

pkg/config/includes.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ func (s IncludeStack) CycleString(cyclePath string) string {
5050
return strings.Join(parts, " -> ")
5151
}
5252

53+
const maxIncludeDepth = 10
54+
5355
// ProcessIncludes recursively processes includes in a config map.
5456
// basePath is the directory containing the current config file.
5557
// stack is used for loop detection via content hashing.
@@ -58,6 +60,10 @@ func (s IncludeStack) CycleString(cyclePath string) string {
5860
// (not replaced), and scalar values are overridden by later includes.
5961
// The local config (non-included content) takes highest priority.
6062
func ProcessIncludes(configMap map[string]any, basePath string, stack IncludeStack) (map[string]any, error) {
63+
if len(stack) > maxIncludeDepth {
64+
return nil, fmt.Errorf("include depth exceeds maximum of %d", maxIncludeDepth)
65+
}
66+
6167
// Extract includes list
6268
includesRaw, hasIncludes := configMap["includes"]
6369
if !hasIncludes {

pkg/provider/README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,24 @@ type BaseWithChecks struct {
7575
}
7676
```
7777

78-
Embed this in your provider to get default implementations of `GetChecks()` and `SetChecks()`. In your provider's `SetChecks()` method, call `SetChecksAndCompile(exprs, celConfig)` to compile the CEL expressions against your provider's CEL configuration.
78+
Embed this in your provider to get default implementations of `GetChecks()`, `HasChecks()`, `EvaluateChecks()`, and `EvaluateChecksByMode()`. In your provider's `SetChecks()` method, call `SetChecksAndCompile(exprs, celConfig)` to compile the CEL expressions against your provider's CEL configuration.
79+
80+
### Container
81+
82+
Providers that group related child health checks implement the `Container` interface:
83+
84+
```go
85+
type Container interface {
86+
SetComponents(config map[string]any)
87+
GetComponents() []Instance
88+
ComponentErrors() []error
89+
}
90+
```
91+
92+
Methods:
93+
94+
- `SetComponents()`: Stores raw component configuration (called by factory before Setup)
95+
- `GetComponents()`: Returns resolved child instances (available after Setup)
96+
- `ComponentErrors()`: Returns validation errors from component resolution
97+
98+
The `BaseContainer` struct provides a default implementation. Call `ResolveComponents()` from your provider's `Setup()` method to resolve child instances from stored config.

pkg/provider/container.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,9 @@ func (b *BaseContainer) ResolveComponents() error {
6161
b.resolved = append(b.resolved, instance)
6262
}
6363

64-
return nil // errors collected in resolutionErrors, not returned
64+
if len(b.resolved) == 0 && len(b.resolutionErrors) > 0 {
65+
return errors.Join(b.resolutionErrors...)
66+
}
67+
68+
return nil
6569
}

pkg/provider/factory.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func WithTimeout(timeout time.Duration) Option {
9595
// For configured instances, use NewInstance instead.
9696
func New(providerType string) (Instance, error) {
9797
mu.RLock()
98-
registeredType, ok := Providers[providerType]
98+
registeredType, ok := providers[providerType]
9999
mu.RUnlock()
100100
if !ok {
101101
return nil, fmt.Errorf("unknown provider type: %q", providerType)
@@ -110,7 +110,7 @@ func New(providerType string) (Instance, error) {
110110
// If no name is provided via WithName, defaults to the provider type.
111111
func NewInstance(providerType string, opts ...Option) (Instance, error) {
112112
mu.RLock()
113-
registeredType, ok := Providers[providerType]
113+
registeredType, ok := providers[providerType]
114114
mu.RUnlock()
115115
if !ok {
116116
return nil, fmt.Errorf("unknown provider type: %q", providerType)

0 commit comments

Comments
 (0)