github.com/zntrio/harp/v2@v2.0.9/pkg/bundle/selector/match_cel.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package selector
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  
    24  	"github.com/google/cel-go/cel"
    25  	celext "github.com/google/cel-go/ext"
    26  	"go.uber.org/zap"
    27  
    28  	bundlev1 "github.com/zntrio/harp/v2/api/gen/go/harp/bundle/v1"
    29  	"github.com/zntrio/harp/v2/pkg/bundle/ruleset/engine/cel/ext"
    30  	"github.com/zntrio/harp/v2/pkg/sdk/log"
    31  )
    32  
    33  // MatchCEL returns a CEL package matcher specification.
    34  func MatchCEL(expressions []string) (Specification, error) {
    35  	// Check arguments
    36  	if len(expressions) == 0 {
    37  		return nil, errors.New("CEL expressions could not be empty for matcher")
    38  	}
    39  
    40  	// Prepare CEL Environment
    41  	env, err := cel.NewEnv(
    42  		cel.Types(&bundlev1.Bundle{}, &bundlev1.Package{}, &bundlev1.SecretChain{}, &bundlev1.KV{}),
    43  		ext.Packages(),
    44  		ext.Secrets(),
    45  		celext.Strings(),
    46  	)
    47  	if err != nil {
    48  		return nil, fmt.Errorf("unable to prepare CEL engine environment: %w", err)
    49  	}
    50  
    51  	// Assemble the complete ruleset
    52  	ruleset := make([]cel.Program, 0, len(expressions))
    53  	for _, exp := range expressions {
    54  		// Parse expression
    55  		parsed, issues := env.Parse(exp)
    56  		if issues != nil && issues.Err() != nil {
    57  			return nil, fmt.Errorf("unable to parse %q, go error: %w", exp, issues.Err())
    58  		}
    59  
    60  		// Extract AST
    61  		ast, cerr := env.Check(parsed)
    62  		if cerr != nil && cerr.Err() != nil {
    63  			return nil, fmt.Errorf("invalid CEL expression: %w", cerr.Err())
    64  		}
    65  
    66  		// request matching is a boolean operation, so we don't really know
    67  		// what to do if the expression returns a non-boolean type
    68  		if ast.OutputType() != cel.BoolType {
    69  			return nil, fmt.Errorf("CEL rule engine expects return type of bool, not %s", ast.OutputType())
    70  		}
    71  
    72  		// Compile the program
    73  		p, err := env.Program(ast)
    74  		if err != nil {
    75  			return nil, fmt.Errorf("error while creating CEL program: %w", err)
    76  		}
    77  
    78  		// Add to context
    79  		ruleset = append(ruleset, p)
    80  	}
    81  
    82  	// Wrap as a builder
    83  	return &celMatcher{
    84  		cel:     env,
    85  		ruleset: ruleset,
    86  	}, nil
    87  }
    88  
    89  type celMatcher struct {
    90  	cel     *cel.Env
    91  	ruleset []cel.Program
    92  }
    93  
    94  // IsSatisfiedBy returns specification satisfaction status.
    95  func (s *celMatcher) IsSatisfiedBy(object interface{}) bool {
    96  	// If object is a package
    97  	if p, ok := object.(*bundlev1.Package); ok {
    98  		// Evaluate filter compliance
    99  		matched, err := s.celEvaluate(p)
   100  		if err != nil {
   101  			log.Bg().Debug("cel evaluation failed", zap.Error(err))
   102  			return false
   103  		}
   104  
   105  		return matched
   106  	}
   107  
   108  	return false
   109  }
   110  
   111  // -----------------------------------------------------------------------------
   112  
   113  func (s *celMatcher) celEvaluate(input *bundlev1.Package) (bool, error) {
   114  	// Check arguments
   115  	if input == nil {
   116  		return false, errors.New("unable to evaluate nil package")
   117  	}
   118  
   119  	// Apply evaluation (implicit AND between rules)
   120  	for _, exp := range s.ruleset {
   121  		// Evaluate using the bundle context
   122  		out, _, err := exp.Eval(map[string]interface{}{
   123  			"p": input,
   124  		})
   125  		if err != nil {
   126  			return false, fmt.Errorf("an error occurred during the rule evaluation: %w", err)
   127  		}
   128  
   129  		// Boolean rule returned false
   130  		if out.Value() == false {
   131  			return false, nil
   132  		}
   133  	}
   134  
   135  	// No error
   136  	return true, nil
   137  }