pkg/bundle/selector/match_cel.go (82 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package selector import ( "errors" "fmt" "github.com/google/cel-go/cel" "github.com/google/cel-go/checker/decls" celext "github.com/google/cel-go/ext" "go.uber.org/zap" "google.golang.org/protobuf/proto" bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1" "github.com/elastic/harp/pkg/bundle/ruleset/engine/cel/ext" "github.com/elastic/harp/pkg/sdk/log" ) // MatchCEL returns a CEL package matcher specification. func MatchCEL(expressions []string) (Specification, error) { // Check arguments if len(expressions) == 0 { return nil, errors.New("CEL expressions could not be empty for matcher") } // Prepare CEL Environment env, err := cel.NewEnv( cel.Types(&bundlev1.Bundle{}, &bundlev1.Package{}, &bundlev1.SecretChain{}, &bundlev1.KV{}), ext.Packages(), ext.Secrets(), celext.Strings(), ) if err != nil { return nil, fmt.Errorf("unable to prepare CEL engine environment: %w", err) } // Assemble the complete ruleset ruleset := make([]cel.Program, 0, len(expressions)) for _, exp := range expressions { // Parse expression parsed, issues := env.Parse(exp) if issues != nil && issues.Err() != nil { return nil, fmt.Errorf("unable to parse '%s', go error: %w", exp, issues.Err()) } // Extract AST ast, cerr := env.Check(parsed) if cerr != nil && cerr.Err() != nil { return nil, fmt.Errorf("invalid CEL expression: %w", cerr.Err()) } // request matching is a boolean operation, so we don't really know // what to do if the expression returns a non-boolean type //nolint:staticcheck // TODO: refactor for deprecations if !proto.Equal(ast.ResultType(), decls.Bool) { return nil, fmt.Errorf("CEL rule engine expects return type of bool, not %s", ast.ResultType()) } // Compile the program p, err := env.Program(ast) if err != nil { return nil, fmt.Errorf("error while creating CEL program: %w", err) } // Add to context ruleset = append(ruleset, p) } // Wrap as a builder return &celMatcher{ cel: env, ruleset: ruleset, }, nil } type celMatcher struct { cel *cel.Env ruleset []cel.Program } // IsSatisfiedBy returns specification satisfaction status func (s *celMatcher) IsSatisfiedBy(object interface{}) bool { // If object is a package if p, ok := object.(*bundlev1.Package); ok { // Evaluate filter compliance matched, err := s.celEvaluate(p) if err != nil { log.Bg().Debug("cel evaluation failed", zap.Error(err)) return false } return matched } return false } // ----------------------------------------------------------------------------- func (s *celMatcher) celEvaluate(input *bundlev1.Package) (bool, error) { // Check arguments if input == nil { return false, errors.New("unable to evaluate nil package") } // Apply evaluation (implicit AND between rules) for _, exp := range s.ruleset { // Evaluate using the bundle context out, _, err := exp.Eval(map[string]interface{}{ "p": input, }) if err != nil { return false, fmt.Errorf("an error occurred during the rule evaluation: %w", err) } // Boolean rule returned false if out.Value() == false { return false, nil } } // No error return true, nil }