github.com/zntrio/harp/v2@v2.0.9/pkg/bundle/ruleset/engine/cel/engine.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 cel
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  
    25  	"github.com/google/cel-go/cel"
    26  	celext "github.com/google/cel-go/ext"
    27  
    28  	bundlev1 "github.com/zntrio/harp/v2/api/gen/go/harp/bundle/v1"
    29  	"github.com/zntrio/harp/v2/pkg/bundle/ruleset/engine"
    30  	"github.com/zntrio/harp/v2/pkg/bundle/ruleset/engine/cel/ext"
    31  )
    32  
    33  // -----------------------------------------------------------------------------
    34  
    35  // New returns a Google CEL based linter engine.
    36  func New(expressions []string) (engine.PackageLinter, error) {
    37  	// Prepare CEL Environment
    38  	env, err := cel.NewEnv(
    39  		cel.Types(&bundlev1.Bundle{}, &bundlev1.Package{}, &bundlev1.SecretChain{}, &bundlev1.KV{}),
    40  		ext.Packages(),
    41  		ext.Secrets(),
    42  		celext.Strings(),
    43  	)
    44  	if err != nil {
    45  		return nil, fmt.Errorf("unable to prepare CEL engine environment: %w", err)
    46  	}
    47  
    48  	// Assemble the complete ruleset
    49  	ruleset := make([]cel.Program, 0, len(expressions))
    50  	for _, exp := range expressions {
    51  		// Parse expression
    52  		parsed, issues := env.Parse(exp)
    53  		if issues != nil && issues.Err() != nil {
    54  			return nil, fmt.Errorf("unable to parse %q, go error: %w", exp, issues.Err())
    55  		}
    56  
    57  		// Extract AST
    58  		ast, cerr := env.Check(parsed)
    59  		if cerr != nil && cerr.Err() != nil {
    60  			return nil, fmt.Errorf("invalid CEL expression: %w", cerr.Err())
    61  		}
    62  
    63  		// request matching is a boolean operation, so we don't really know
    64  		// what to do if the expression returns a non-boolean type
    65  		if ast.OutputType() != cel.BoolType {
    66  			return nil, fmt.Errorf("CEL rule engine expects return type of bool, not %s", ast.OutputType())
    67  		}
    68  
    69  		// Compile the program
    70  		p, err := env.Program(ast)
    71  		if err != nil {
    72  			return nil, fmt.Errorf("error while creating CEL program: %w", err)
    73  		}
    74  
    75  		// Add to context
    76  		ruleset = append(ruleset, p)
    77  	}
    78  
    79  	// Return rule engine
    80  	return &ruleEngine{
    81  		cel:     env,
    82  		ruleset: ruleset,
    83  	}, nil
    84  }
    85  
    86  // -----------------------------------------------------------------------------
    87  
    88  type ruleEngine struct {
    89  	cel     *cel.Env
    90  	ruleset []cel.Program
    91  }
    92  
    93  func (re *ruleEngine) EvaluatePackage(ctx context.Context, p *bundlev1.Package) error {
    94  	// Check arguments
    95  	if p == nil {
    96  		return errors.New("unable to evaluate nil package")
    97  	}
    98  
    99  	// Apply evaluation (implicit AND between rules)
   100  	for _, exp := range re.ruleset {
   101  		// Evaluate using the bundle context
   102  		out, _, err := exp.Eval(map[string]interface{}{
   103  			"p": p,
   104  		})
   105  		if err != nil {
   106  			return fmt.Errorf("an error occurred during the rule evaluation: %w", err)
   107  		}
   108  
   109  		// Boolean rule returned false
   110  		if out.Value() == false {
   111  			return engine.ErrRuleNotValid
   112  		}
   113  	}
   114  
   115  	// No error
   116  	return nil
   117  }