Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/fluxcd/source-controller/api v1.6.2
github.com/getsops/sops/v3 v3.10.2
github.com/goccy/go-yaml v1.18.0
github.com/google/cel-go v0.26.0
github.com/google/go-containerregistry v0.20.6
github.com/google/go-jsonnet v0.21.0
github.com/hashicorp/hcl/v2 v2.24.0
Expand Down Expand Up @@ -57,6 +58,7 @@ require (
github.com/ProtonMail/gopenpgp/v2 v2.8.3 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
Expand Down Expand Up @@ -172,6 +174,7 @@ require (
github.com/siderolabs/protoenc v0.2.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/urfave/cli v1.22.16 // indirect
Expand All @@ -194,6 +197,7 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
Expand Down
156 changes: 156 additions & 0 deletions pkg/blueprint/feature_evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package blueprint

import (
"fmt"
"reflect"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)

// The FeatureEvaluator is a CEL-based expression evaluator for blueprint feature conditions.
// It provides GitHub Actions-style conditional expression evaluation capabilities with support
// for nested object access, logical operators, and type-safe variable declarations.
// The FeatureEvaluator enables dynamic feature activation based on user configuration values.

// =============================================================================
// Types
// =============================================================================

// FeatureEvaluator provides CEL expression evaluation capabilities for feature conditions.
type FeatureEvaluator struct {
env *cel.Env
}

// =============================================================================
// Constructor
// =============================================================================

// NewFeatureEvaluator creates a new CEL-based feature evaluator configured for evaluating feature conditions.
// The evaluator is pre-configured with standard libraries and custom functions needed
// for blueprint feature evaluation.
func NewFeatureEvaluator() (*FeatureEvaluator, error) {
env, err := cel.NewEnv(
cel.HomogeneousAggregateLiterals(),
cel.EagerlyValidateDeclarations(true),
)
if err != nil {
return nil, fmt.Errorf("failed to create CEL environment: %w", err)
}

return &FeatureEvaluator{
env: env,
}, nil
Copy link

Choose a reason for hiding this comment

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

Bug: Redundant Initialization in FeatureEvaluator

The FeatureEvaluator initializes a cel.Env in its constructor, but this env field is never used. Instead, the CompileExpression method creates a new cel.Env from scratch on each call, making the constructor's initialization redundant.

Additional Locations (1)

Fix in Cursor Fix in Web

}

// =============================================================================
// Public Methods
// =============================================================================

// CompileExpression compiles a CEL expression string with variable declarations derived from the config structure.
// The expression should follow GitHub Actions-style syntax with support for:
// - Equality/inequality: ==, !=
// - Logical operators: &&, ||
// - Parentheses for grouping: (expression)
// - Nested object access: provider, observability.enabled, vm.driver
// Returns a compiled program that can be evaluated against configuration data.
func (e *FeatureEvaluator) CompileExpression(expression string, config map[string]any) (cel.Program, error) {
if expression == "" {
return nil, fmt.Errorf("expression cannot be empty")
}

var envOptions []cel.EnvOption
envOptions = append(envOptions, cel.HomogeneousAggregateLiterals())
envOptions = append(envOptions, cel.EagerlyValidateDeclarations(true))

for key, value := range config {
envOptions = append(envOptions, cel.Variable(key, e.getCELType(value)))
}

env, err := cel.NewEnv(envOptions...)
if err != nil {
return nil, fmt.Errorf("failed to create CEL environment with config: %w", err)
}

ast, issues := env.Compile(expression)
if issues.Err() != nil {
return nil, fmt.Errorf("failed to compile expression '%s': %w", expression, issues.Err())
}

program, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("failed to create program for expression '%s': %w", expression, err)
}

return program, nil
}

// EvaluateProgram executes a compiled CEL program against the provided configuration data.
// The configuration data should be a map containing the user's configuration values
// that the expression will be evaluated against.
// Returns true if the expression evaluates to true, false otherwise.
func (e *FeatureEvaluator) EvaluateProgram(program cel.Program, config map[string]any) (bool, error) {
if config == nil {
config = make(map[string]any)
}

result, _, err := program.Eval(config)
if err != nil {
return false, fmt.Errorf("failed to evaluate expression: %w", err)
}

return e.convertToBool(result)
}

// EvaluateExpression is a convenience method that compiles and evaluates an expression in one call.
// This is useful for one-time evaluations where the compiled program won't be reused.
func (e *FeatureEvaluator) EvaluateExpression(expression string, config map[string]any) (bool, error) {
program, err := e.CompileExpression(expression, config)
if err != nil {
return false, err
}

return e.EvaluateProgram(program, config)
}

// =============================================================================
// Private Methods
// =============================================================================

// convertToBool converts a CEL result value to a boolean.
// CEL expressions should evaluate to boolean values for feature conditions.
func (e *FeatureEvaluator) convertToBool(result ref.Val) (bool, error) {
if result.Type() == types.BoolType {
return result.Value().(bool), nil
}

return false, fmt.Errorf("expression must evaluate to boolean, got %s", result.Type())
}

// getCELType determines the appropriate CEL type for a Go value.
// This is used to create variable declarations for the CEL environment.
func (e *FeatureEvaluator) getCELType(value any) *cel.Type {
if value == nil {
return cel.DynType
}

switch reflect.TypeOf(value).Kind() {
case reflect.String:
return cel.StringType
case reflect.Bool:
return cel.BoolType
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return cel.IntType
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return cel.UintType
case reflect.Float32, reflect.Float64:
return cel.DoubleType
case reflect.Map:
return cel.MapType(cel.StringType, cel.DynType)
case reflect.Slice, reflect.Array:
return cel.ListType(cel.DynType)
default:
return cel.DynType
}
}
Loading
Loading