Testing Kubernetes CEL admission/valdiation

Why it’s needed

CEL expressions are normally only executed at runtime, so it’s hard to know what a new expression will actually do without deploying it. Even when test-deploying it it’s hard to know if all edge-cases are covered.

Why it’s hard

  • There is no simple CEL test package
  • The expressions themselves are not code, but yaml on kubernetes resources

How to do it

  • Read CEL expressions from manifests
  • Load resources from 1-All clusters (… or write up good example resources)
  • Run resources against expressions locally (code below)
  • Print the outcomes
func selectResourcesMatchingAllConditions(resources []unstructured.Unstructured, expressions []adregv1.MatchCondition) ([]unstructured.Unstructured, error) {
	// avoid setup overhead if there is nothing to do
	if len(resources) == 0 || len(expressions) == 0 {
		return resources, nil
	}

	env, err := createCELEnvironment()
	if err != nil {
		return nil, err
	}

	programs, err := compileToCELPrograms(expressions, env)
	if err != nil {
		return nil, err
	}

	return selectResourcesMatchingAllCELPrograms(resources, programs)
}

func selectResourcesMatchingAllCELPrograms(resources []unstructured.Unstructured, programs []cel.Program) (selectedResources []unstructured.Unstructured, err error) {
	for _, u := range resources {
		// convert resource to program input
		object, err := structpb.NewStruct(u.Object)
		if err != nil {
			return nil, fmt.Errorf("converting to struct: %w", err)
		}
		input := map[string]interface{}{"object": object}

		// select resource if it matches all programs
		selected := true
		for _, program := range programs {
			out, _, err := program.Eval(input)
			if err != nil {
				return nil, fmt.Errorf("evaluation: %w", err)
			}
			if !out.Value().(bool) {
				selected = false
				break
			}
		}
		if selected {
			selectedResources = append(selectedResources, u)
		}
	}
	return
}

func createCELEnvironment() (*cel.Env, error) {
	env, err := cel.NewEnv(
		cel.Declarations(decls.NewVar("object", decls.NewMapType(decls.String, decls.Dyn))),
	)
	if err != nil {
		return nil, fmt.Errorf("creating CEL environment: %w", err)
	}
	return env, nil
}

func compileToCELPrograms(expressions []adregv1.MatchCondition, env *cel.Env) (programs []cel.Program, err error) {
	for _, exp := range expressions {
		ast, issues := env.Compile(exp.Expression)
		if issues != nil && issues.Err() != nil {
			return nil, fmt.Errorf("compile for %s %s: %w", exp.Name, exp.Expression, issues.Err())
		}
		prg, err := env.Program(ast)
		if err != nil {
			return nil, fmt.Errorf("program construction: %w", err)
		}
		programs = append(programs, prg)
	}
	return
}

Producing Golang error backtraces / stacktraces

Why go does not have nice backtraces

Collecting stacktraces is expensive, so the core language will never implement them for everything.
If you need your code to be highly performant then consider making a error wrapper that can be turned on/off based on a environment variable.

Simple fix

  • import github.com/pkg/errors and use it instead of errors package (it is deprecated but works fine)
  • always errors.Wrap, errrors.New, or errors.Errorf from “github.com/pkg/errors” when passing from outside your code or creating errors
  • when subsequently passing errors along, a return err and DO NOT return fmt.Errorf("building foo: %w", err) (multiple wraps create multiple backtraces which is noisy but not a big problem, but using fmt.Errorf discards the backtrace)
  • in your main.go add this:
// go playground https://go.dev/play/p/n1gcwD-cv4K
import (
	"fmt"
	"github.com/pkg/errors"
)

type withStackTrace interface {
	StackTrace() errors.StackTrace
}

func main() {
	err := errors.New("example")
	err = errors.Wrap(err, "more context")
	if errWithStack, ok := err.(withStackTrace); ok {
		fmt.Fprintf(os.Stderr, "%+v\n", errWithStack)
	} else {
		fmt.Fprintln(os.Stderr, "Warning: unable to print stacktrace: use `errors.Wrap`")
	}
}

Nicer fix

Use a more complete solution like eris.