From a37dfa7f0ed471a99e24da9536d0d69a3a1735df Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 9 Nov 2021 16:09:52 -0500 Subject: [PATCH] Add models from cel-policy-templates --- .../forked/celopenapi/model/decisions.go | 303 +++++++ .../forked/celopenapi/model/decisions_test.go | 121 +++ .../forked/celopenapi/model/env.go | 209 +++++ .../forked/celopenapi/model/env_test.go | 73 ++ .../forked/celopenapi/model/instance.go | 148 ++++ .../forked/celopenapi/model/registry.go | 189 +++++ .../forked/celopenapi/model/schemas.go | 451 ++++++++++ .../forked/celopenapi/model/schemas_test.go | 203 +++++ .../forked/celopenapi/model/source.go | 190 +++++ .../forked/celopenapi/model/template.go | 159 ++++ .../forked/celopenapi/model/types.go | 562 +++++++++++++ .../forked/celopenapi/model/types_test.go | 155 ++++ .../forked/celopenapi/model/value.go | 782 ++++++++++++++++++ .../forked/celopenapi/model/value_test.go | 363 ++++++++ 14 files changed, 3908 insertions(+) create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions_test.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env_test.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/instance.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/registry.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/source.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/template.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value.go create mode 100644 staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value_test.go diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions.go new file mode 100644 index 00000000000..aa99f7efd80 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions.go @@ -0,0 +1,303 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "fmt" + "strings" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// NewDecision returns an empty Decision instance. +func NewDecision() *Decision { + return &Decision{} +} + +// Decision contains a decision name, or reference to a decision name, and an output expression. +type Decision struct { + Name string + Reference *cel.Ast + Output *cel.Ast +} + +// DecisionValue represents a named decision and value. +type DecisionValue interface { + fmt.Stringer + + // Name returns the decision name. + Name() string + + // IsFinal returns whether the decision value will change with additional rule evaluations. + // + // When a decision is final, additional productions and rules which may also trigger the same + // decision may be skipped. + IsFinal() bool +} + +// SingleDecisionValue extends the DecisionValue which contains a single decision value as well +// as some metadata about the evaluation details and the rule that spawned the value. +type SingleDecisionValue interface { + DecisionValue + + // Value returns the single value for the decision. + Value() ref.Val + + // Details returns the evaluation details, if present, that produced the value. + Details() *cel.EvalDetails + + // RuleID indicate which policy rule id within an instance that produced the decision. + RuleID() int64 +} + +// MultiDecisionValue extends the DecisionValue which contains a set of decision values as well as +// the corresponding metadata about how each value was produced. +type MultiDecisionValue interface { + DecisionValue + + // Values returns the collection of values produced for the decision. + Values() []ref.Val + + // Details returns the evaluation details for each value in the decision. + // The value index correponds to the details index. The details may be nil. + Details() []*cel.EvalDetails + + // RulesIDs returns the rule id within an instance which produce the decision values. + // The value index corresponds to the rule id index. + RuleIDs() []int64 +} + +// DecisionSelector determines whether the given decision is the decision set requested by the +// caller. +type DecisionSelector func(decision string) bool + +// NewBoolDecisionValue returns a boolean decision with an initial value. +func NewBoolDecisionValue(name string, value types.Bool) *BoolDecisionValue { + return &BoolDecisionValue{ + name: name, + value: value, + } +} + +// BoolDecisionValue represents the decision value type associated with a decision. +type BoolDecisionValue struct { + name string + value ref.Val + isFinal bool + details *cel.EvalDetails + ruleID int64 +} + +// And logically ANDs the current decision value with the incoming CEL value. +// +// And follows CEL semantics with respect to errors and unknown values where errors may be +// absorbed or short-circuited away by subsequent 'false' values. When unkonwns are encountered +// the unknown values combine and aggregate within the decision. Unknowns may also be absorbed +// per CEL semantics. +func (dv *BoolDecisionValue) And(other ref.Val) *BoolDecisionValue { + v, vBool := dv.value.(types.Bool) + if vBool && v == types.False { + return dv + } + o, oBool := other.(types.Bool) + if oBool && o == types.False { + dv.value = types.False + return dv + } + if vBool && oBool { + return dv + } + dv.value = logicallyMergeUnkErr(dv.value, other) + return dv +} + +// Details implements the SingleDecisionValue interface method. +func (dv *BoolDecisionValue) Details() *cel.EvalDetails { + return dv.details +} + +// Finalize marks the decision as immutable with additional input and indicates the rule and +// evaluation details which triggered the finalization. +func (dv *BoolDecisionValue) Finalize(details *cel.EvalDetails, rule Rule) DecisionValue { + dv.details = details + if rule != nil { + dv.ruleID = rule.GetID() + } + dv.isFinal = true + return dv +} + +// IsFinal returns whether the decision is final. +func (dv *BoolDecisionValue) IsFinal() bool { + return dv.isFinal +} + +// Or logically ORs the decision value with the incoming CEL value. +// +// The ORing logic follows CEL semantics with respect to errors and unknown values. +// Errors may be absorbed or short-circuited away by subsequent 'true' values. When unkonwns are +// encountered the unknown values combine and aggregate within the decision. Unknowns may also be +// absorbed per CEL semantics. +func (dv *BoolDecisionValue) Or(other ref.Val) *BoolDecisionValue { + v, vBool := dv.value.(types.Bool) + if vBool && v == types.True { + return dv + } + o, oBool := other.(types.Bool) + if oBool && o == types.True { + dv.value = types.True + return dv + } + if vBool && oBool { + return dv + } + dv.value = logicallyMergeUnkErr(dv.value, other) + return dv +} + +// Name implements the DecisionValue interface method. +func (dv *BoolDecisionValue) Name() string { + return dv.name +} + +// RuleID implements the SingleDecisionValue interface method. +func (dv *BoolDecisionValue) RuleID() int64 { + return dv.ruleID +} + +// String renders the decision value to a string for debug purposes. +func (dv *BoolDecisionValue) String() string { + var buf strings.Builder + buf.WriteString(dv.name) + buf.WriteString(": ") + buf.WriteString(fmt.Sprintf("rule[%d] -> ", dv.ruleID)) + buf.WriteString(fmt.Sprintf("%v", dv.value)) + return buf.String() +} + +// Value implements the SingleDecisionValue interface method. +func (dv *BoolDecisionValue) Value() ref.Val { + return dv.value +} + +// NewListDecisionValue returns a named decision value which contains a list of CEL values produced +// by one or more policy instances and / or production rules. +func NewListDecisionValue(name string) *ListDecisionValue { + return &ListDecisionValue{ + name: name, + values: []ref.Val{}, + details: []*cel.EvalDetails{}, + ruleIDs: []int64{}, + } +} + +// ListDecisionValue represents a named decision which collects into a list of values. +type ListDecisionValue struct { + name string + values []ref.Val + isFinal bool + details []*cel.EvalDetails + ruleIDs []int64 +} + +// Append accumulates the incoming CEL value into the decision's value list. +func (dv *ListDecisionValue) Append(val ref.Val, det *cel.EvalDetails, rule Rule) { + dv.values = append(dv.values, val) + dv.details = append(dv.details, det) + // Rule ids may be null if the policy is a singleton. + ruleID := int64(0) + if rule != nil { + ruleID = rule.GetID() + } + dv.ruleIDs = append(dv.ruleIDs, ruleID) +} + +// Details returns the list of evaluation details observed in computing the values in the decision. +// The details indices correlate 1:1 with the value indices. +func (dv *ListDecisionValue) Details() []*cel.EvalDetails { + return dv.details +} + +// Finalize marks the list decision complete. +func (dv *ListDecisionValue) Finalize() DecisionValue { + dv.isFinal = true + return dv +} + +// IsFinal implements the DecisionValue interface method. +func (dv *ListDecisionValue) IsFinal() bool { + return dv.isFinal +} + +// Name implements the DecisionValue interface method. +func (dv *ListDecisionValue) Name() string { + return dv.name +} + +// RuleIDs returns the list of rule ids which produced the evaluation results. +// The indices of the ruleIDs correlate 1:1 with the value indices. +func (dv *ListDecisionValue) RuleIDs() []int64 { + return dv.ruleIDs +} + +func (dv *ListDecisionValue) String() string { + var buf strings.Builder + buf.WriteString(dv.name) + buf.WriteString(": ") + for i, v := range dv.values { + if len(dv.ruleIDs) == len(dv.values) { + buf.WriteString(fmt.Sprintf("rule[%d] -> ", dv.ruleIDs[i])) + } + buf.WriteString(fmt.Sprintf("%v", v)) + buf.WriteString("\n") + if i < len(dv.values)-1 { + buf.WriteString("\t") + } + } + return buf.String() +} + +// Values implements the MultiDecisionValue interface method. +func (dv *ListDecisionValue) Values() []ref.Val { + return dv.values +} + +func logicallyMergeUnkErr(value, other ref.Val) ref.Val { + vUnk := types.IsUnknown(value) + oUnk := types.IsUnknown(other) + if vUnk && oUnk { + merged := types.Unknown{} + merged = append(merged, value.(types.Unknown)...) + merged = append(merged, other.(types.Unknown)...) + return merged + } + if vUnk { + return value + } + if oUnk { + return other + } + if types.IsError(value) { + return value + } + if types.IsError(other) { + return other + } + return types.NewErr( + "got values (%v, %v), wanted boolean values", + value, other) +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions_test.go new file mode 100644 index 00000000000..0623fcfcbc8 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/decisions_test.go @@ -0,0 +1,121 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "reflect" + "testing" + + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +func TestBoolDecisionValue_And(t *testing.T) { + tests := []struct { + name string + value types.Bool + ands []ref.Val + result ref.Val + }{ + { + name: "init_false_end_false", + value: types.False, + ands: []ref.Val{types.NewErr("err"), types.True}, + result: types.False, + }, + { + name: "init_true_end_false", + value: types.True, + ands: []ref.Val{types.NewErr("err"), types.False}, + result: types.False, + }, + { + name: "init_true_end_err", + value: types.True, + ands: []ref.Val{types.True, types.NewErr("err")}, + result: types.NewErr("err"), + }, + { + name: "init_true_end_unk", + value: types.True, + ands: []ref.Val{types.True, types.Unknown{1}, types.NewErr("err"), types.Unknown{2}}, + result: types.Unknown{1, 2}, + }, + } + for _, tst := range tests { + tc := tst + t.Run(tc.name, func(tt *testing.T) { + v := NewBoolDecisionValue(tc.name, tc.value) + for _, av := range tc.ands { + v = v.And(av) + } + v.Finalize(nil, nil) + if !reflect.DeepEqual(v.Value(), tc.result) { + tt.Errorf("decision AND failed. got %v, wanted %v", v.Value(), tc.result) + } + }) + } +} + +func TestBoolDecisionValue_Or(t *testing.T) { + tests := []struct { + name string + value types.Bool + ors []ref.Val + result ref.Val + }{ + { + name: "init_false_end_true", + value: types.False, + ors: []ref.Val{types.NewErr("err"), types.Unknown{1}, types.True}, + result: types.True, + }, + { + name: "init_true_end_true", + value: types.True, + ors: []ref.Val{types.NewErr("err"), types.False}, + result: types.True, + }, + { + name: "init_false_end_err", + value: types.False, + ors: []ref.Val{types.False, types.NewErr("err1"), types.NewErr("err2")}, + result: types.NewErr("err1"), + }, + { + name: "init_false_end_unk", + value: types.False, + ors: []ref.Val{types.False, types.Unknown{1}, types.NewErr("err"), types.Unknown{2}}, + result: types.Unknown{1, 2}, + }, + } + for _, tst := range tests { + tc := tst + t.Run(tc.name, func(tt *testing.T) { + v := NewBoolDecisionValue(tc.name, tc.value) + for _, av := range tc.ors { + v = v.Or(av) + } + // Test finalization + v.Finalize(nil, nil) + // Ensure that calling string on the value doesn't error. + _ = v.String() + // Compare the output result + if !reflect.DeepEqual(v.Value(), tc.result) { + tt.Errorf("decision OR failed. got %v, wanted %v", v.Value(), tc.result) + } + }) + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env.go new file mode 100644 index 00000000000..ddc1d803259 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env.go @@ -0,0 +1,209 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// NewEnv creates an empty Env instance with a fully qualified name that may be referenced +// within templates. +func NewEnv(name string) *Env { + return &Env{ + Name: name, + Functions: []*Function{}, + Vars: []*Var{}, + Types: map[string]*DeclType{}, + } +} + +// Env declares a set of variables, functions, and types available to a given set of CEL +// expressions. +// +// The Env name must be fully qualified as it will be referenced within template evaluators, +// validators, and possibly within the metadata of the instance rule schema. +// +// Note, the Types values currently only holds type definitions associated with a variable +// declaration. Any type mentioned in the environment which does not have a definition is +// treated as a reference to a type which must be supplied in the base CEL environment provided +// by the policy engine. +type Env struct { + Name string + Container string + Functions []*Function + Vars []*Var + Types map[string]*DeclType +} + +// ExprEnvOptions returns a set of CEL environment options to be used when extending the base +// policy engine CEL environment. +func (e *Env) ExprEnvOptions() []cel.EnvOption { + opts := []cel.EnvOption{} + if e.Container != "" { + opts = append(opts, cel.Container(e.Container)) + } + if len(e.Vars) > 0 { + vars := make([]*exprpb.Decl, len(e.Vars)) + for i, v := range e.Vars { + vars[i] = v.ExprDecl() + } + opts = append(opts, cel.Declarations(vars...)) + } + if len(e.Functions) > 0 { + funcs := make([]*exprpb.Decl, len(e.Functions)) + for i, f := range e.Functions { + funcs[i] = f.ExprDecl() + } + opts = append(opts, cel.Declarations(funcs...)) + } + return opts +} + +// NewVar creates a new variable with a name and a type. +func NewVar(name string, dt *DeclType) *Var { + return &Var{ + Name: name, + Type: dt, + } +} + +// Var represents a named instanced of a type. +type Var struct { + Name string + Type *DeclType +} + +// ExprDecl produces a CEL proto declaration for the variable. +func (v *Var) ExprDecl() *exprpb.Decl { + return decls.NewVar(v.Name, v.Type.ExprType()) +} + +// NewFunction creates a Function instance with a simple function name and a set of overload +// signatures. +func NewFunction(name string, overloads ...*Overload) *Function { + return &Function{ + Name: name, + Overloads: overloads, + } +} + +// Function represents a simple name and a set of overload signatures. +type Function struct { + Name string + Overloads []*Overload +} + +// ExprDecl produces a CEL proto declaration for the function and its overloads. +func (f *Function) ExprDecl() *exprpb.Decl { + overloadDecls := make([]*exprpb.Decl_FunctionDecl_Overload, len(f.Overloads)) + for i, o := range f.Overloads { + overloadDecls[i] = o.overloadDecl() + } + return decls.NewFunction(f.Name, overloadDecls...) +} + +// NewOverload returns a receiver-style overload declaration for a given function. +// +// The overload name must follow the conventions laid out within the CEL overloads.go file. +// +// // Receiver-style overload name: +// ___ +// +// Within this function, the first type supplied is the receiver type, and the last type supplied +// is used as the return type. At least two types must be specified for a zero-arity receiver +// function. +func NewOverload(name string, first *DeclType, rest ...*DeclType) *Overload { + argTypes := make([]*DeclType, 1+len(rest)) + argTypes[0] = first + for i := 1; i < len(rest)+1; i++ { + argTypes[i] = rest[i-1] + } + returnType := argTypes[len(argTypes)-1] + argTypes = argTypes[0 : len(argTypes)-1] + return newOverload(name, false, argTypes, returnType) +} + +// NewFreeFunctionOverload returns a free function overload for a given function name. +// +// The overload name must follow the conventions laid out within the CEL overloads.go file: +// +// // Free function style overload name: +// __ +// +// When the function name is global, will refer to the simple function name. When the +// function has a qualified name, replace the '.' characters in the fully-qualified name with +// underscores. +// +// Within this function, the last type supplied is used as the return type. At least one type must +// be specified for a zero-arity free function. +func NewFreeFunctionOverload(name string, first *DeclType, rest ...*DeclType) *Overload { + argTypes := make([]*DeclType, 1+len(rest)) + argTypes[0] = first + for i := 1; i < len(rest)+1; i++ { + argTypes[i] = rest[i-1] + } + returnType := argTypes[len(argTypes)-1] + argTypes = argTypes[0 : len(argTypes)-1] + return newOverload(name, true, argTypes, returnType) +} + +func newOverload(name string, + freeFunction bool, + argTypes []*DeclType, + returnType *DeclType) *Overload { + return &Overload{ + Name: name, + FreeFunction: freeFunction, + Args: argTypes, + ReturnType: returnType, + } +} + +// Overload represents a single function overload signature. +type Overload struct { + Name string + FreeFunction bool + Args []*DeclType + ReturnType *DeclType +} + +func (o *Overload) overloadDecl() *exprpb.Decl_FunctionDecl_Overload { + typeParams := map[string]struct{}{} + argExprTypes := make([]*exprpb.Type, len(o.Args)) + for i, a := range o.Args { + if a.TypeParam { + typeParams[a.TypeName()] = struct{}{} + } + argExprTypes[i] = a.ExprType() + } + returnType := o.ReturnType.ExprType() + if len(typeParams) == 0 { + if o.FreeFunction { + return decls.NewOverload(o.Name, argExprTypes, returnType) + } + return decls.NewInstanceOverload(o.Name, argExprTypes, returnType) + } + typeParamNames := make([]string, 0, len(typeParams)) + for param := range typeParams { + typeParamNames = append(typeParamNames, param) + } + if o.FreeFunction { + return decls.NewParameterizedOverload(o.Name, argExprTypes, returnType, typeParamNames) + } + return decls.NewParameterizedInstanceOverload(o.Name, argExprTypes, returnType, typeParamNames) +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env_test.go new file mode 100644 index 00000000000..d36016f6ff6 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/env_test.go @@ -0,0 +1,73 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "testing" + + "github.com/google/cel-go/cel" +) + +func TestEnv_Vars(t *testing.T) { + env := NewEnv("test.v1.Environment") + env.Container = "test.v1" + env.Vars = []*Var{ + NewVar("greeting", StringType), + NewVar("replies", NewListType(StringType)), + } + expr := `greeting == 'hello' && replies.size() > 0` + stdEnv, _ := cel.NewEnv() + ast, iss := stdEnv.Compile(expr) + if iss.Err() == nil { + t.Errorf("got ast %v, expected error", ast) + } + custEnv, err := stdEnv.Extend(env.ExprEnvOptions()...) + if err != nil { + t.Fatal(err) + } + _, iss = custEnv.Compile(expr) + if iss.Err() != nil { + t.Errorf("got error %v, wanted ast", iss) + } +} + +func TestEnv_Funcs(t *testing.T) { + env := NewEnv("test.v1.Environment") + env.Container = "test.v1" + env.Functions = []*Function{ + NewFunction("greeting", + NewOverload("string_greeting_string", StringType, StringType, BoolType), + NewFreeFunctionOverload("greeting_string", StringType, BoolType)), + NewFunction("getOrDefault", + NewOverload("map_get_or_default_param", + NewMapType(NewTypeParam("K"), NewTypeParam("V")), + NewTypeParam("K"), NewTypeParam("V"), + NewTypeParam("V"))), + } + expr := `greeting('hello') && 'jim'.greeting('hello') && {'a': 0}.getOrDefault('b', 1) == 1` + stdEnv, _ := cel.NewEnv() + ast, iss := stdEnv.Compile(expr) + if iss.Err() == nil { + t.Errorf("got ast %v, expected error", ast) + } + custEnv, err := stdEnv.Extend(env.ExprEnvOptions()...) + if err != nil { + t.Fatal(err) + } + _, iss = custEnv.Compile(expr) + if iss.Err() != nil { + t.Errorf("got error %v, wanted ast", iss) + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/instance.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/instance.go new file mode 100644 index 00000000000..20c70b6a28a --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/instance.go @@ -0,0 +1,148 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model contains abstract representations of policy template and instance config objects. +package model + +import ( + "strings" +) + +// NewInstance returns an empty policy instance. +func NewInstance(info SourceMetadata) *Instance { + return &Instance{ + Metadata: &InstanceMetadata{}, + Selectors: []Selector{}, + Rules: []Rule{}, + Meta: info, + } +} + +// Instance represents the compiled, type-checked, and validated policy instance. +type Instance struct { + APIVersion string + Kind string + Metadata *InstanceMetadata + Description string + + // Selectors determine whether the instance applies to the current evaluation context. + // All Selector values must return true for the policy instance to be included in policy + // evaluation step. + Selectors []Selector + + // Rules represent reference data to be used in evaluation policy decisions. + // Depending on the nature of the decisions being emitted, some or all Rules may be evaluated + // and the results aggregated according to the decision types being emitted. + Rules []Rule + + // Meta represents the source metadata from the input instance. + Meta SourceMetadata +} + +// MetadataMap returns the metadata name to value map, which can be used in evaluation. +// Only "name" field is supported for now. +func (i *Instance) MetadataMap() map[string]interface{} { + return map[string]interface{}{ + "name": i.Metadata.Name, + } +} + +// InstanceMetadata contains standard metadata which may be associated with an instance. +type InstanceMetadata struct { + UID string + Name string + Namespace string +} + +// Selector interface indicates a pre-formatted instance selection condition. +// +// The implementations of such conditions are expected to be platform specific. +// +// Note, if there is a clear need to tailor selection more heavily, then the schema definition +// for a selector should be moved into the Template schema. +type Selector interface { + isSelector() +} + +// LabelSelector matches key, value pairs of labels associated with the evaluation context. +// +// In Kubernetes, the such labels are provided as 'resource.labels'. +type LabelSelector struct { + // LabelValues provides a map of the string keys and values expected. + LabelValues map[string]string +} + +func (*LabelSelector) isSelector() {} + +// ExpressionSelector matches a label against an existence condition. +type ExpressionSelector struct { + // Label name being matched. + Label string + + // Operator determines the evaluation behavior. Must be one of Exists, NotExists, In, or NotIn. + Operator string + + // Values set, optional, to be used in the NotIn, In set membership tests. + Values []interface{} +} + +func (*ExpressionSelector) isSelector() {} + +// Rule interface indicates the value types that may be used as Rule instances. +// +// Note, the code within the main repo deals exclusively with custom, yaml-based rules, but it +// is entirely possible to use a protobuf message as the rule container. +type Rule interface { + isRule() + GetID() int64 + GetFieldID(field string) int64 +} + +// CustomRule embeds the DynValue and represents rules whose type definition is provided in the +// policy template. +type CustomRule struct { + *DynValue +} + +func (*CustomRule) isRule() {} + +// GetID returns the parse-time generated ID of the rule node. +func (c *CustomRule) GetID() int64 { + return c.ID +} + +// GetFieldID returns the parse-time generated ID pointing to the rule field. If field is not +// specified or is not found, falls back to the ID of the rule node. +func (c *CustomRule) GetFieldID(field string) int64 { + if field == "" { + return c.GetID() + } + paths := strings.Split(field, ".") + val := c.DynValue + for _, path := range paths { + var f *Field + var ok bool + switch v := val.Value().(type) { + case *ObjectValue: + f, ok = v.GetField(path) + case *MapValue: + f, ok = v.GetField(path) + } + if !ok { + return c.GetID() + } + val = f.Ref + } + return val.ID +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/registry.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/registry.go new file mode 100644 index 00000000000..99b316ac127 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/registry.go @@ -0,0 +1,189 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://www.apache.org/licenses/LICENSE2.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 model + +import ( + "fmt" + "sync" + + "github.com/google/cel-go/cel" +) + +// Resolver declares methods to find policy templates and related configuration objects. +type Resolver interface { + // FindEnv returns an Env object by its fully-qualified name, if present. + FindEnv(name string) (*Env, bool) + + // FindExprEnv returns a CEL expression environment by its fully-qualified name, if present. + // + // Note, the CEL expression environment name corresponds with the model Environment name; + // however, the expression environment may inherit configuration via the CEL env.Extend method. + FindExprEnv(name string) (*cel.Env, bool) + + // FindSchema returns an Open API Schema instance by name, if present. + // + // Schema names start with a `#` sign as this method is only used to resolve references to + // relative schema elements within `$ref` schema nodes. + FindSchema(name string) (*OpenAPISchema, bool) + + // FindTemplate returns a Template by its fully-qualified name, if present. + FindTemplate(name string) (*Template, bool) + + // FindType returns a DeclType instance corresponding to the given fully-qualified name, if + // present. + FindType(name string) (*DeclType, bool) +} + +// NewRegistry create a registry for keeping track of environments, schemas, templates, and more +// from a base cel.Env expression environment. +func NewRegistry(stdExprEnv *cel.Env) *Registry { + return &Registry{ + envs: map[string]*Env{}, + exprEnvs: map[string]*cel.Env{"": stdExprEnv}, + schemas: map[string]*OpenAPISchema{ + "#anySchema": AnySchema, + "#envSchema": envSchema, + "#instanceSchema": instanceSchema, + "#openAPISchema": schemaDef, + "#templateSchema": templateSchema, + }, + templates: map[string]*Template{}, + types: map[string]*DeclType{ + AnyType.TypeName(): AnyType, + BoolType.TypeName(): BoolType, + BytesType.TypeName(): BytesType, + DoubleType.TypeName(): DoubleType, + DurationType.TypeName(): DurationType, + IntType.TypeName(): IntType, + NullType.TypeName(): NullType, + PlainTextType.TypeName(): PlainTextType, + StringType.TypeName(): StringType, + TimestampType.TypeName(): TimestampType, + UintType.TypeName(): UintType, + ListType.TypeName(): ListType, + MapType.TypeName(): MapType, + }, + } +} + +// Registry defines a repository of environment, schema, template, and type definitions. +// +// Registry instances are concurrency-safe. +type Registry struct { + rwMux sync.RWMutex + envs map[string]*Env + exprEnvs map[string]*cel.Env + schemas map[string]*OpenAPISchema + templates map[string]*Template + types map[string]*DeclType +} + +// FindEnv implements the Resolver interface method. +func (r *Registry) FindEnv(name string) (*Env, bool) { + r.rwMux.RLock() + defer r.rwMux.RUnlock() + env, found := r.envs[name] + return env, found +} + +// FindExprEnv implements the Resolver interface method. +func (r *Registry) FindExprEnv(name string) (*cel.Env, bool) { + r.rwMux.RLock() + defer r.rwMux.RUnlock() + exprEnv, found := r.exprEnvs[name] + return exprEnv, found +} + +// FindSchema implements the Resolver interface method. +func (r *Registry) FindSchema(name string) (*OpenAPISchema, bool) { + r.rwMux.RLock() + defer r.rwMux.RUnlock() + schema, found := r.schemas[name] + return schema, found +} + +// FindTemplate implements the Resolver interface method. +func (r *Registry) FindTemplate(name string) (*Template, bool) { + r.rwMux.RLock() + defer r.rwMux.RUnlock() + tmpl, found := r.templates[name] + return tmpl, found +} + +// FindType implements the Resolver interface method. +func (r *Registry) FindType(name string) (*DeclType, bool) { + r.rwMux.RLock() + defer r.rwMux.RUnlock() + typ, found := r.types[name] + if found { + return typ, true + } + return typ, found +} + +// SetEnv registers an environment description by fully qualified name. +func (r *Registry) SetEnv(name string, env *Env) error { + r.rwMux.Lock() + defer r.rwMux.Unlock() + // Cleanup environment related artifacts when the env is reset. + priorEnv, found := r.envs[name] + if found { + for typeName := range priorEnv.Types { + delete(r.types, typeName) + } + } + // Configure the new environment. + baseExprEnv, found := r.exprEnvs[""] + if !found { + return fmt.Errorf("missing default expression environment") + } + exprEnv, err := baseExprEnv.Extend(env.ExprEnvOptions()...) + if err != nil { + return err + } + r.exprEnvs[name] = exprEnv + r.envs[name] = env + for typeName, typ := range env.Types { + r.types[typeName] = typ + } + return nil +} + +// SetSchema registers an OpenAPISchema fragment by its relative name so that it may be referenced +// as a reusable schema unit within other OpenAPISchema instances. +// +// Name format: '#'. +func (r *Registry) SetSchema(name string, schema *OpenAPISchema) error { + r.rwMux.Lock() + defer r.rwMux.Unlock() + r.schemas[name] = schema + return nil +} + +// SetTemplate registers a template by its fully qualified name. +func (r *Registry) SetTemplate(name string, tmpl *Template) error { + r.rwMux.Lock() + defer r.rwMux.Unlock() + r.templates[name] = tmpl + return nil +} + +// SetType registers a DeclType descriptor by its fully qualified name. +func (r *Registry) SetType(name string, declType *DeclType) error { + r.rwMux.Lock() + defer r.rwMux.Unlock() + r.types[name] = declType + return nil +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go new file mode 100644 index 00000000000..440f19ac450 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go @@ -0,0 +1,451 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://www.apache.org/licenses/LICENSE2.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 model + +import ( + "strings" + + "gopkg.in/yaml.v3" +) + +// NewOpenAPISchema returns an empty instance of an OpenAPISchema object. +func NewOpenAPISchema() *OpenAPISchema { + return &OpenAPISchema{ + Enum: []interface{}{}, + Metadata: map[string]string{}, + Properties: map[string]*OpenAPISchema{}, + Required: []string{}, + } +} + +// OpenAPISchema declares a struct capable of representing a subset of Open API Schemas +// supported by Kubernetes which can also be specified within Protocol Buffers. +// +// There are a handful of notable differences: +// - The validating constructs `allOf`, `anyOf`, `oneOf`, `not`, and type-related restrictsion are +// not supported as they can be better validated in the template 'validator' block. +// - The $ref field supports references to other schema definitions, but such aliases +// should be removed before being serialized. +// - The `additionalProperties` and `properties` fields are not currently mutually exclusive as is +// the case for Kubernetes. +// +// See: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation +type OpenAPISchema struct { + Title string `yaml:"title,omitempty"` + Description string `yaml:"description,omitempty"` + Type string `yaml:"type,omitempty"` + TypeParam string `yaml:"type_param,omitempty"` + TypeRef string `yaml:"$ref,omitempty"` + DefaultValue interface{} `yaml:"default,omitempty"` + Enum []interface{} `yaml:"enum,omitempty"` + Format string `yaml:"format,omitempty"` + Items *OpenAPISchema `yaml:"items,omitempty"` + Metadata map[string]string `yaml:"metadata,omitempty"` + Required []string `yaml:"required,omitempty"` + Properties map[string]*OpenAPISchema `yaml:"properties,omitempty"` + AdditionalProperties *OpenAPISchema `yaml:"additionalProperties,omitempty"` +} + +// DeclTypes constructs a top-down set of DeclType instances whose name is derived from the root +// type name provided on the call, if not set to a custom type. +func (s *OpenAPISchema) DeclTypes(maybeRootType string) (*DeclType, map[string]*DeclType) { + root := s.DeclType().MaybeAssignTypeName(maybeRootType) + types := FieldTypeMap(maybeRootType, root) + return root, types +} + +// DeclType returns the CEL Policy Templates type name associated with the schema element. +func (s *OpenAPISchema) DeclType() *DeclType { + if s.TypeParam != "" { + return NewTypeParam(s.TypeParam) + } + declType, found := openAPISchemaTypes[s.Type] + if !found { + return NewObjectTypeRef("*error*") + } + switch declType.TypeName() { + case ListType.TypeName(): + return NewListType(s.Items.DeclType()) + case MapType.TypeName(): + if s.AdditionalProperties != nil { + return NewMapType(StringType, s.AdditionalProperties.DeclType()) + } + fields := make(map[string]*DeclField, len(s.Properties)) + required := make(map[string]struct{}, len(s.Required)) + for _, name := range s.Required { + required[name] = struct{}{} + } + for name, prop := range s.Properties { + _, isReq := required[name] + fields[name] = &DeclField{ + Name: name, + Required: isReq, + Type: prop.DeclType(), + defaultValue: prop.DefaultValue, + enumValues: prop.Enum, + } + } + customType, hasCustomType := s.Metadata["custom_type"] + if !hasCustomType { + return NewObjectType("object", fields) + } + return NewObjectType(customType, fields) + case StringType.TypeName(): + switch s.Format { + case "byte", "binary": + return BytesType + case "google-duration": + return DurationType + case "date", "date-time", "google-datetime": + return TimestampType + case "int64": + return IntType + case "uint64": + return UintType + } + } + + return declType +} + +// FindProperty returns the Open API Schema type for the given property name. +// +// A property may either be explicitly defined in a `properties` map or implicitly defined in an +// `additionalProperties` block. +func (s *OpenAPISchema) FindProperty(name string) (*OpenAPISchema, bool) { + if s.DeclType() == AnyType { + return s, true + } + if s.Properties != nil { + prop, found := s.Properties[name] + if found { + return prop, true + } + } + if s.AdditionalProperties != nil { + return s.AdditionalProperties, true + } + return nil, false +} + +var ( + // SchemaDef defines an Open API Schema definition in terms of an Open API Schema. + schemaDef *OpenAPISchema + + // AnySchema indicates that the value may be of any type. + AnySchema *OpenAPISchema + + // EnvSchema defines the schema for CEL environments referenced within Policy Templates. + envSchema *OpenAPISchema + + // InstanceSchema defines a basic schema for defining Policy Instances where the instance rule + // references a TemplateSchema derived from the Instance's template kind. + instanceSchema *OpenAPISchema + + // TemplateSchema defines a schema for defining Policy Templates. + templateSchema *OpenAPISchema + + openAPISchemaTypes map[string]*DeclType = map[string]*DeclType{ + "boolean": BoolType, + "number": DoubleType, + "integer": IntType, + "null": NullType, + "string": StringType, + "google-duration": DurationType, + "google-datetime": TimestampType, + "date": TimestampType, + "date-time": TimestampType, + "array": ListType, + "object": MapType, + "": AnyType, + } +) + +const ( + schemaDefYaml = ` +type: object +properties: + $ref: + type: string + type: + type: string + type_param: # prohibited unless used within an environment. + type: string + format: + type: string + description: + type: string + required: + type: array + items: + type: string + enum: + type: array + items: + type: string + enumDescriptions: + type: array + items: + type: string + default: {} + items: + $ref: "#openAPISchema" + properties: + type: object + additionalProperties: + $ref: "#openAPISchema" + additionalProperties: + $ref: "#openAPISchema" + metadata: + type: object + additionalProperties: + type: string +` + + templateSchemaYaml = ` +type: object +required: + - apiVersion + - kind + - metadata + - evaluator +properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + required: + - name + properties: + uid: + type: string + name: + type: string + namespace: + type: string + default: "default" + etag: + type: string + labels: + type: object + additionalProperties: + type: string + pluralName: + type: string + description: + type: string + schema: + $ref: "#openAPISchema" + validator: + type: object + required: + - productions + properties: + description: + type: string + environment: + type: string + terms: + type: object + additionalProperties: {} + productions: + type: array + items: + type: object + required: + - message + properties: + match: + type: string + default: true + field: + type: string + message: + type: string + details: {} + evaluator: + type: object + required: + - productions + properties: + description: + type: string + environment: + type: string + ranges: + type: array + items: + type: object + required: + - in + properties: + in: + type: string + key: + type: string + index: + type: string + value: + type: string + terms: + type: object + additionalProperties: + type: string + productions: + type: array + items: + type: object + properties: + match: + type: string + default: "true" + decision: + type: string + decisionRef: + type: string + output: {} + decisions: + type: array + items: + type: object + required: + - output + properties: + decision: + type: string + decisionRef: + type: string + output: {} +` + + instanceSchemaYaml = ` +type: object +required: + - apiVersion + - kind + - metadata +properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + additionalProperties: + type: string + description: + type: string + selector: + type: object + properties: + matchLabels: + type: object + additionalProperties: + type: string + matchExpressions: + type: array + items: + type: object + required: + - key + - operator + properties: + key: + type: string + operator: + type: string + enum: ["DoesNotExist", "Exists", "In", "NotIn"] + values: + type: array + items: {} + default: [] + rule: + $ref: "#templateRuleSchema" + rules: + type: array + items: + $ref: "#templateRuleSchema" +` + + // TODO: support subsetting of built-in functions and macros + // TODO: support naming anonymous types within rule schema and making them accessible to + // declarations. + // TODO: consider supporting custom macros + envSchemaYaml = ` +type: object +required: + - name +properties: + name: + type: string + container: + type: string + variables: + type: object + additionalProperties: + $ref: "#openAPISchema" + functions: + type: object + properties: + extensions: + type: object + additionalProperties: + type: object # function name + additionalProperties: + type: object # overload name + required: + - return + properties: + free_function: + type: boolean + args: + type: array + items: + $ref: "#openAPISchema" + return: + $ref: "#openAPISchema" +` +) + +func init() { + AnySchema = NewOpenAPISchema() + + instanceSchema = NewOpenAPISchema() + in := strings.ReplaceAll(instanceSchemaYaml, "\t", " ") + err := yaml.Unmarshal([]byte(in), instanceSchema) + if err != nil { + panic(err) + } + envSchema = NewOpenAPISchema() + in = strings.ReplaceAll(envSchemaYaml, "\t", " ") + err = yaml.Unmarshal([]byte(in), envSchema) + if err != nil { + panic(err) + } + schemaDef = NewOpenAPISchema() + in = strings.ReplaceAll(schemaDefYaml, "\t", " ") + err = yaml.Unmarshal([]byte(in), schemaDef) + if err != nil { + panic(err) + } + templateSchema = NewOpenAPISchema() + in = strings.ReplaceAll(templateSchemaYaml, "\t", " ") + err = yaml.Unmarshal([]byte(in), templateSchema) + if err != nil { + panic(err) + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go new file mode 100644 index 00000000000..eb149767a37 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go @@ -0,0 +1,203 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "reflect" + "testing" + + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" + + "google.golang.org/protobuf/proto" +) + +func TestSchemaDeclType(t *testing.T) { + ts := testSchema() + cust := ts.DeclType() + if cust.TypeName() != "CustomObject" { + t.Errorf("incorrect type name, got %v, wanted CustomObject", cust.TypeName()) + } + if len(cust.Fields) != 4 { + t.Errorf("incorrect number of fields, got %d, wanted 4", len(cust.Fields)) + } + for _, f := range cust.Fields { + prop, found := ts.FindProperty(f.Name) + if !found { + t.Errorf("type field not found in schema, field: %s", f.Name) + } + fdv := f.DefaultValue() + if prop.DefaultValue != nil { + pdv := types.DefaultTypeAdapter.NativeToValue(prop.DefaultValue) + if !reflect.DeepEqual(fdv, pdv) { + t.Errorf("field and schema do not agree on default value, field: %s", f.Name) + } + } + if prop.Enum == nil && len(f.EnumValues()) != 0 { + t.Errorf("field had more enum values than the property. field: %s", f.Name) + } + if prop.Enum != nil { + fevs := f.EnumValues() + for _, fev := range fevs { + found := false + for _, pev := range prop.Enum { + pev = types.DefaultTypeAdapter.NativeToValue(pev) + if reflect.DeepEqual(fev, pev) { + found = true + break + } + } + if !found { + t.Errorf( + "could not find field enum value in property definition. field: %s, enum: %v", + f.Name, fev) + } + } + } + } + for _, name := range ts.Required { + df, found := cust.FindField(name) + if !found { + t.Errorf("custom type missing required field. field=%s", name) + } + if !df.Required { + t.Errorf("field marked as required in schema, but optional in type. field=%s", df.Name) + } + } +} + +func TestSchemaDeclTypes(t *testing.T) { + ts := testSchema() + cust, typeMap := ts.DeclTypes("mock_template") + nested, _ := cust.FindField("nested") + metadata, _ := cust.FindField("metadata") + metadataElem := metadata.Type.ElemType + expectedObjTypeMap := map[string]*DeclType{ + "CustomObject": cust, + "CustomObject.nested": nested.Type, + "CustomObject.metadata.@elem": metadataElem, + } + objTypeMap := map[string]*DeclType{} + for name, t := range typeMap { + if t.IsObject() { + objTypeMap[name] = t + } + } + if len(objTypeMap) != len(expectedObjTypeMap) { + t.Errorf("got different type set. got=%v, wanted=%v", typeMap, expectedObjTypeMap) + } + for exp, expType := range expectedObjTypeMap { + actType, found := objTypeMap[exp] + if !found { + t.Errorf("missing type in rule types: %s", exp) + continue + } + if !proto.Equal(expType.ExprType(), actType.ExprType()) { + t.Errorf("incompatible CEL types. got=%v, wanted=%v", actType.ExprType(), expType.ExprType()) + } + } + + metaExprType := metadata.Type.ExprType() + expectedMetaExprType := decls.NewMapType( + decls.String, + decls.NewObjectType("CustomObject.metadata.@elem")) + if !proto.Equal(expectedMetaExprType, metaExprType) { + t.Errorf("got metadata CEL type %v, wanted %v", metaExprType, expectedMetaExprType) + } +} + +func testSchema() *OpenAPISchema { + // Manual construction of a schema with the following definition: + // + // schema: + // type: object + // metadata: + // custom_type: "CustomObject" + // required: + // - name + // - value + // properties: + // name: + // type: string + // nested: + // type: object + // properties: + // subname: + // type: string + // flags: + // type: object + // additionalProperties: + // type: boolean + // dates: + // type: array + // items: + // type: string + // format: date-time + // metadata: + // type: object + // additionalProperties: + // type: object + // properties: + // key: + // type: string + // values: + // type: array + // items: string + // value: + // type: integer + // format: int64 + // default: 1 + // enum: [1,2,3] + nameField := NewOpenAPISchema() + nameField.Type = "string" + valueField := NewOpenAPISchema() + valueField.Type = "integer" + valueField.Format = "int64" + valueField.DefaultValue = int64(1) + valueField.Enum = []interface{}{int64(1), int64(2), int64(3)} + nestedObjField := NewOpenAPISchema() + nestedObjField.Type = "object" + nestedObjField.Properties["subname"] = NewOpenAPISchema() + nestedObjField.Properties["subname"].Type = "string" + nestedObjField.Properties["flags"] = NewOpenAPISchema() + nestedObjField.Properties["flags"].Type = "object" + nestedObjField.Properties["flags"].AdditionalProperties = NewOpenAPISchema() + nestedObjField.Properties["flags"].AdditionalProperties.Type = "boolean" + nestedObjField.Properties["dates"] = NewOpenAPISchema() + nestedObjField.Properties["dates"].Type = "array" + nestedObjField.Properties["dates"].Items = NewOpenAPISchema() + nestedObjField.Properties["dates"].Items.Type = "string" + nestedObjField.Properties["dates"].Items.Format = "date-time" + metadataKeyValue := NewOpenAPISchema() + metadataKeyValue.Type = "object" + metadataKeyValue.Properties["key"] = NewOpenAPISchema() + metadataKeyValue.Properties["key"].Type = "string" + metadataKeyValue.Properties["values"] = NewOpenAPISchema() + metadataKeyValue.Properties["values"].Type = "array" + metadataKeyValue.Properties["values"].Items = NewOpenAPISchema() + metadataKeyValue.Properties["values"].Items.Type = "string" + metadataObjField := NewOpenAPISchema() + metadataObjField.Type = "object" + metadataObjField.AdditionalProperties = metadataKeyValue + ts := NewOpenAPISchema() + ts.Type = "object" + ts.Metadata["custom_type"] = "CustomObject" + ts.Required = []string{"name", "value"} + ts.Properties["name"] = nameField + ts.Properties["value"] = valueField + ts.Properties["nested"] = nestedObjField + ts.Properties["metadata"] = metadataObjField + return ts +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/source.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/source.go new file mode 100644 index 00000000000..d49e0a0081d --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/source.go @@ -0,0 +1,190 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "github.com/google/cel-go/common" +) + +// ByteSource converts a byte sequence and location description to a model.Source. +func ByteSource(contents []byte, location string) *Source { + return StringSource(string(contents), location) +} + +// StringSource converts a string and location description to a model.Source. +func StringSource(contents, location string) *Source { + return &Source{ + Source: common.NewStringSource(contents, location), + } +} + +// Source represents the contents of a single source file. +type Source struct { + common.Source +} + +// Relative produces a RelativeSource object for the content provided at the absolute location +// within the parent Source as indicated by the line and column. +func (src *Source) Relative(content string, line, col int) *RelativeSource { + return &RelativeSource{ + Source: src.Source, + localSrc: common.NewStringSource(content, src.Description()), + absLoc: common.NewLocation(line, col), + } +} + +// RelativeSource represents an embedded source element within a larger source. +type RelativeSource struct { + common.Source + localSrc common.Source + absLoc common.Location +} + +// AbsoluteLocation returns the location within the parent Source where the RelativeSource starts. +func (rel *RelativeSource) AbsoluteLocation() common.Location { + return rel.absLoc +} + +// Content returns the embedded source snippet. +func (rel *RelativeSource) Content() string { + return rel.localSrc.Content() +} + +// OffsetLocation returns the absolute location given the relative offset, if found. +func (rel *RelativeSource) OffsetLocation(offset int32) (common.Location, bool) { + absOffset, found := rel.Source.LocationOffset(rel.absLoc) + if !found { + return common.NoLocation, false + } + return rel.Source.OffsetLocation(absOffset + offset) +} + +// NewLocation creates an absolute common.Location based on a local line, column +// position from a relative source. +func (rel *RelativeSource) NewLocation(line, col int) common.Location { + localLoc := common.NewLocation(line, col) + relOffset, found := rel.localSrc.LocationOffset(localLoc) + if !found { + return common.NoLocation + } + offset, _ := rel.Source.LocationOffset(rel.absLoc) + absLoc, _ := rel.Source.OffsetLocation(offset + relOffset) + return absLoc +} + +// NewSourceInfo creates SourceInfo metadata from a Source object. +func NewSourceInfo(src common.Source) *SourceInfo { + return &SourceInfo{ + Comments: make(map[int64][]*Comment), + LineOffsets: src.LineOffsets(), + Description: src.Description(), + Offsets: make(map[int64]int32), + } +} + +// SourceInfo contains metadata about the Source such as comments, line positions, and source +// element offsets. +type SourceInfo struct { + // Comments mapped by source element id to a comment set. + Comments map[int64][]*Comment + + // LineOffsets contains the list of character offsets where newlines occur in the source. + LineOffsets []int32 + + // Description indicates something about the source, such as its file name. + Description string + + // Offsets map from source element id to the character offset where the source element starts. + Offsets map[int64]int32 +} + +// SourceMetadata enables the lookup for expression source metadata by expression id. +type SourceMetadata interface { + // CommentsByID returns the set of comments associated with the expression id, if present. + CommentsByID(int64) ([]*Comment, bool) + + // LocationByID returns the CEL common.Location of the expression id, if present. + LocationByID(int64) (common.Location, bool) +} + +// CommentsByID returns the set of comments by expression id, if present. +func (info *SourceInfo) CommentsByID(id int64) ([]*Comment, bool) { + comments, found := info.Comments[id] + return comments, found +} + +// LocationByID returns the line and column location of source node by its id. +func (info *SourceInfo) LocationByID(id int64) (common.Location, bool) { + charOff, found := info.Offsets[id] + if !found { + return common.NoLocation, false + } + ln, lnOff := info.findLine(charOff) + return common.NewLocation(int(ln), int(charOff-lnOff)), true +} + +func (info *SourceInfo) findLine(characterOffset int32) (int32, int32) { + var line int32 = 1 + for _, lineOffset := range info.LineOffsets { + if lineOffset > characterOffset { + break + } else { + line++ + } + } + if line == 1 { + return line, 0 + } + return line, info.LineOffsets[line-2] +} + +// CommentStyle type used to indicate where a comment occurs. +type CommentStyle int + +const ( + // HeadComment indicates that the comment is defined in the lines preceding the source element. + HeadComment CommentStyle = iota + 1 + + // LineComment indicates that the comment occurs on the same line after the source element. + LineComment + + // FootComment indicates that the comment occurs after the source element with at least one + // blank line before the next source element. + FootComment +) + +// NewHeadComment creates a new HeadComment from the text. +func NewHeadComment(txt string) *Comment { + return &Comment{Text: txt, Style: HeadComment} +} + +// NewLineComment creates a new LineComment from the text. +func NewLineComment(txt string) *Comment { + return &Comment{Text: txt, Style: LineComment} +} + +// NewFootComment creates a new FootComment from the text. +func NewFootComment(txt string) *Comment { + return &Comment{Text: txt, Style: FootComment} +} + +// Comment represents a comment within source. +type Comment struct { + // Text contains the comment text. + Text string + + // Style indicates where the comment appears relative to a source element. + Style CommentStyle +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/template.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/template.go new file mode 100644 index 00000000000..3137aabcedb --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/template.go @@ -0,0 +1,159 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "github.com/google/cel-go/cel" + + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// NewTemplate produces an empty policy Template instance. +func NewTemplate(info SourceMetadata) *Template { + return &Template{ + Metadata: NewTemplateMetadata(), + Evaluator: NewEvaluator(), + Meta: info, + } +} + +// Template represents the compiled and type-checked policy template. +type Template struct { + APIVersion string + Kind string + Metadata *TemplateMetadata + Description string + RuleTypes *RuleTypes + Validator *Evaluator + Evaluator *Evaluator + Meta SourceMetadata +} + +// EvaluatorDecisionCount returns the number of decisions which can be produced by the template +// evaluator production rules. +func (t *Template) EvaluatorDecisionCount() int { + return t.Evaluator.DecisionCount() +} + +// MetadataMap returns the metadata name to value map, which can be used in evaluation. +// Only "name" field is supported for now. +func (t *Template) MetadataMap() map[string]interface{} { + return map[string]interface{}{ + "name": t.Metadata.Name, + } +} + +// NewTemplateMetadata returns an empty *TemplateMetadata instance. +func NewTemplateMetadata() *TemplateMetadata { + return &TemplateMetadata{ + Properties: make(map[string]string), + } +} + +// TemplateMetadata contains the top-level information about the Template, including its name and +// namespace. +type TemplateMetadata struct { + UID string + Name string + Namespace string + + // PluralMame is the plural form of the template name to use when managing a collection of + // template instances. + PluralName string + + // Properties contains an optional set of key-value information which external applications + // might find useful. + Properties map[string]string +} + +// NewEvaluator returns an empty instance of a Template Evaluator. +func NewEvaluator() *Evaluator { + return &Evaluator{ + Terms: []*Term{}, + Productions: []*Production{}, + } +} + +// Evaluator contains a set of production rules used to validate policy templates or +// evaluate template instances. +// +// The evaluator may optionally specify a named and versioned Environment as the basis for the +// variables and functions exposed to the CEL expressions within the Evaluator, and an optional +// set of terms. +// +// Terms are like template-local variables. Terms may rely on other terms which precede them. +// Term order matters, and no cycles are permitted among terms by design and convention. +type Evaluator struct { + Environment string + Ranges []*Range + Terms []*Term + Productions []*Production +} + +// DecisionCount returns the number of possible decisions which could be emitted by this evaluator. +func (e *Evaluator) DecisionCount() int { + decMap := map[string]struct{}{} + for _, p := range e.Productions { + for _, d := range p.Decisions { + decMap[d.Name] = struct{}{} + } + } + return len(decMap) +} + +// Range expresses a looping condition where the key (or index) and value can be extracted from the +// range CEL expression. +type Range struct { + ID int64 + Key *exprpb.Decl + Value *exprpb.Decl + Expr *cel.Ast +} + +// NewTerm produces a named Term instance associated with a CEL Ast and a list of the input +// terms needed to evaluate the Ast successfully. +func NewTerm(id int64, name string, expr *cel.Ast) *Term { + return &Term{ + ID: id, + Name: name, + Expr: expr, + } +} + +// Term is a template-local variable whose name may shadow names in the Template environment and +// which may depend on preceding terms as input. +type Term struct { + ID int64 + Name string + Expr *cel.Ast +} + +// NewProduction returns an empty instance of a Production rule which minimally contains a single +// Decision. +func NewProduction(id int64, match *cel.Ast) *Production { + return &Production{ + ID: id, + Match: match, + Decisions: []*Decision{}, + } +} + +// Production describes an match-decision pair where the match, if set, indicates whether the +// Decision is applicable, and the decision indicates its name and output value. +type Production struct { + ID int64 + Match *cel.Ast + Decisions []*Decision +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go new file mode 100644 index 00000000000..61cb4af98f4 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go @@ -0,0 +1,562 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "fmt" + "time" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" + + "google.golang.org/protobuf/proto" + + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// NewListType returns a parameterized list type with a specified element type. +func NewListType(elem *DeclType) *DeclType { + return &DeclType{ + name: "list", + ElemType: elem, + exprType: decls.NewListType(elem.ExprType()), + defaultValue: NewListValue(), + } +} + +// NewMapType returns a parameterized map type with the given key and element types. +func NewMapType(key, elem *DeclType) *DeclType { + return &DeclType{ + name: "map", + KeyType: key, + ElemType: elem, + exprType: decls.NewMapType(key.ExprType(), elem.ExprType()), + defaultValue: NewMapValue(), + } +} + +// NewObjectType creates an object type with a qualified name and a set of field declarations. +func NewObjectType(name string, fields map[string]*DeclField) *DeclType { + t := &DeclType{ + name: name, + Fields: fields, + exprType: decls.NewObjectType(name), + traitMask: traits.FieldTesterType | traits.IndexerType, + } + t.defaultValue = NewObjectValue(t) + return t +} + +// NewObjectTypeRef returns a reference to an object type by name +func NewObjectTypeRef(name string) *DeclType { + t := &DeclType{ + name: name, + exprType: decls.NewObjectType(name), + traitMask: traits.FieldTesterType | traits.IndexerType, + } + return t +} + +// NewTypeParam creates a type parameter type with a simple name. +// +// Type parameters are resolved at compilation time to concrete types, or CEL 'dyn' type if no +// type assignment can be inferred. +func NewTypeParam(name string) *DeclType { + return &DeclType{ + name: name, + TypeParam: true, + exprType: decls.NewTypeParamType(name), + } +} + +func newSimpleType(name string, exprType *exprpb.Type, zeroVal ref.Val) *DeclType { + return &DeclType{ + name: name, + exprType: exprType, + defaultValue: zeroVal, + } +} + +// DeclType represents the universal type descriptor for Policy Templates. +type DeclType struct { + fmt.Stringer + + name string + Fields map[string]*DeclField + KeyType *DeclType + ElemType *DeclType + TypeParam bool + Metadata map[string]string + + exprType *exprpb.Type + traitMask int + defaultValue ref.Val +} + +// MaybeAssignTypeName attempts to set the DeclType name to a fully qualified name, if the type +// is of `object` type. +// +// The DeclType must return true for `IsObject` or this assignment will error. +func (t *DeclType) MaybeAssignTypeName(name string) *DeclType { + if t.IsObject() { + objUpdated := false + if t.name != "object" { + name = t.name + } else { + objUpdated = true + } + fieldMap := make(map[string]*DeclField, len(t.Fields)) + for fieldName, field := range t.Fields { + fieldType := field.Type + fieldTypeName := fmt.Sprintf("%s.%s", name, fieldName) + updated := fieldType.MaybeAssignTypeName(fieldTypeName) + if updated == fieldType { + fieldMap[fieldName] = field + continue + } + objUpdated = true + fieldMap[fieldName] = &DeclField{ + Name: fieldName, + Type: updated, + Required: field.Required, + enumValues: field.enumValues, + defaultValue: field.defaultValue, + } + } + if !objUpdated { + return t + } + return &DeclType{ + name: name, + Fields: fieldMap, + KeyType: t.KeyType, + ElemType: t.ElemType, + TypeParam: t.TypeParam, + Metadata: t.Metadata, + exprType: decls.NewObjectType(name), + traitMask: t.traitMask, + defaultValue: t.defaultValue, + } + } + if t.IsMap() { + elemTypeName := fmt.Sprintf("%s.@elem", name) + updated := t.ElemType.MaybeAssignTypeName(elemTypeName) + if updated == t.ElemType { + return t + } + return NewMapType(t.KeyType, updated) + } + if t.IsList() { + elemTypeName := fmt.Sprintf("%s.@idx", name) + updated := t.ElemType.MaybeAssignTypeName(elemTypeName) + if updated == t.ElemType { + return t + } + return NewListType(updated) + } + return t +} + +// ExprType returns the CEL expression type of this declaration. +func (t *DeclType) ExprType() *exprpb.Type { + return t.exprType +} + +// FindField returns the DeclField with the given name if present. +func (t *DeclType) FindField(name string) (*DeclField, bool) { + f, found := t.Fields[name] + return f, found +} + +// HasTrait implements the CEL ref.Type interface making this type declaration suitable for use +// within the CEL evaluator. +func (t *DeclType) HasTrait(trait int) bool { + if t.traitMask&trait == trait { + return true + } + if t.defaultValue == nil { + return false + } + _, isDecl := t.defaultValue.Type().(*DeclType) + if isDecl { + return false + } + return t.defaultValue.Type().HasTrait(trait) +} + +// IsList returns whether the declaration is a `list` type which defines a parameterized element +// type, but not a parameterized key type or fields. +func (t *DeclType) IsList() bool { + return t.KeyType == nil && t.ElemType != nil && t.Fields == nil +} + +// IsMap returns whether the declaration is a 'map' type which defines parameterized key and +// element types, but not fields. +func (t *DeclType) IsMap() bool { + return t.KeyType != nil && t.ElemType != nil && t.Fields == nil +} + +// IsObject returns whether the declartion is an 'object' type which defined a set of typed fields. +func (t *DeclType) IsObject() bool { + return t.KeyType == nil && t.ElemType == nil && t.Fields != nil +} + +// String implements the fmt.Stringer interface method. +func (t *DeclType) String() string { + return t.name +} + +// TypeName returns the fully qualified type name for the DeclType. +func (t *DeclType) TypeName() string { + return t.name +} + +// DefaultValue returns the CEL ref.Val representing the default value for this object type, +// if one exists. +func (t *DeclType) DefaultValue() ref.Val { + return t.defaultValue +} + +// FieldTypeMap constructs a map of the field and object types nested within a given type. +func FieldTypeMap(path string, t *DeclType) map[string]*DeclType { + if t.IsObject() && t.TypeName() != "object" { + path = t.TypeName() + } + types := make(map[string]*DeclType) + buildDeclTypes(path, t, types) + return types +} + +func buildDeclTypes(path string, t *DeclType, types map[string]*DeclType) { + // Ensure object types are properly named according to where they appear in the schema. + if t.IsObject() { + // Hack to ensure that names are uniquely qualified and work well with the type + // resolution steps which require fully qualified type names for field resolution + // to function properly. + types[t.TypeName()] = t + for name, field := range t.Fields { + fieldPath := fmt.Sprintf("%s.%s", path, name) + buildDeclTypes(fieldPath, field.Type, types) + } + } + // Map element properties to type names if needed. + if t.IsMap() { + mapElemPath := fmt.Sprintf("%s.@elem", path) + buildDeclTypes(mapElemPath, t.ElemType, types) + types[path] = t + } + // List element properties. + if t.IsList() { + listIdxPath := fmt.Sprintf("%s.@idx", path) + buildDeclTypes(listIdxPath, t.ElemType, types) + types[path] = t + } +} + +// DeclField describes the name, ordinal, and optionality of a field declaration within a type. +type DeclField struct { + Name string + Type *DeclType + Required bool + enumValues []interface{} + defaultValue interface{} +} + +// TypeName returns the string type name of the field. +func (f *DeclField) TypeName() string { + return f.Type.TypeName() +} + +// DefaultValue returns the zero value associated with the field. +func (f *DeclField) DefaultValue() ref.Val { + if f.defaultValue != nil { + return types.DefaultTypeAdapter.NativeToValue(f.defaultValue) + } + return f.Type.DefaultValue() +} + +// EnumValues returns the set of values that this field may take. +func (f *DeclField) EnumValues() []ref.Val { + if f.enumValues == nil || len(f.enumValues) == 0 { + return []ref.Val{} + } + ev := make([]ref.Val, len(f.enumValues)) + for i, e := range f.enumValues { + ev[i] = types.DefaultTypeAdapter.NativeToValue(e) + } + return ev +} + +// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible. +func NewRuleTypes(kind string, + schema *OpenAPISchema, + res Resolver) (*RuleTypes, error) { + // Note, if the schema indicates that it's actually based on another proto + // then prefer the proto definition. For expressions in the proto, a new field + // annotation will be needed to indicate the expected environment and type of + // the expression. + schemaTypes, err := newSchemaTypeProvider(kind, schema) + if err != nil { + return nil, err + } + return &RuleTypes{ + Schema: schema, + ruleSchemaDeclTypes: schemaTypes, + resolver: res, + }, nil +} + +// RuleTypes extends the CEL ref.TypeProvider interface and provides an Open API Schema-based +// type-system. +type RuleTypes struct { + ref.TypeProvider + Schema *OpenAPISchema + ruleSchemaDeclTypes *schemaTypeProvider + typeAdapter ref.TypeAdapter + resolver Resolver +} + +// EnvOptions returns a set of cel.EnvOption values which includes the Template's declaration set +// as well as a custom ref.TypeProvider. +// +// Note, the standard declaration set includes 'rule' which is defined as the top-level rule-schema +// type if one is configured. +// +// If the RuleTypes value is nil, an empty []cel.EnvOption set is returned. +func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) { + if rt == nil { + return []cel.EnvOption{}, nil + } + var ta ref.TypeAdapter = types.DefaultTypeAdapter + tpa, ok := tp.(ref.TypeAdapter) + if ok { + ta = tpa + } + rtWithTypes := &RuleTypes{ + TypeProvider: tp, + typeAdapter: ta, + Schema: rt.Schema, + ruleSchemaDeclTypes: rt.ruleSchemaDeclTypes, + resolver: rt.resolver, + } + for name, declType := range rt.ruleSchemaDeclTypes.types { + tpType, found := tp.FindType(name) + if found && !proto.Equal(tpType, declType.ExprType()) { + return nil, fmt.Errorf( + "type %s definition differs between CEL environment and template", name) + } + } + return []cel.EnvOption{ + cel.CustomTypeProvider(rtWithTypes), + cel.CustomTypeAdapter(rtWithTypes), + cel.Declarations( + decls.NewVar("rule", rt.ruleSchemaDeclTypes.root.ExprType()), + ), + }, nil +} + +// FindType attempts to resolve the typeName provided from the template's rule-schema, or if not +// from the embedded ref.TypeProvider. +// +// FindType overrides the default type-finding behavior of the embedded TypeProvider. +// +// Note, when the type name is based on the Open API Schema, the name will reflect the object path +// where the type definition appears. +func (rt *RuleTypes) FindType(typeName string) (*exprpb.Type, bool) { + if rt == nil { + return nil, false + } + declType, found := rt.findDeclType(typeName) + if found { + return declType.ExprType(), found + } + return rt.TypeProvider.FindType(typeName) +} + +// FindDeclType returns the CPT type description which can be mapped to a CEL type. +func (rt *RuleTypes) FindDeclType(typeName string) (*DeclType, bool) { + if rt == nil { + return nil, false + } + return rt.findDeclType(typeName) +} + +// FindFieldType returns a field type given a type name and field name, if found. +// +// Note, the type name for an Open API Schema type is likely to be its qualified object path. +// If, in the future an object instance rather than a type name were provided, the field +// resolution might more accurately reflect the expected type model. However, in this case +// concessions were made to align with the existing CEL interfaces. +func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) { + st, found := rt.findDeclType(typeName) + if !found { + return rt.TypeProvider.FindFieldType(typeName, fieldName) + } + + f, found := st.Fields[fieldName] + if found { + ft := f.Type + return &ref.FieldType{ + Type: ft.ExprType(), + }, true + } + // This could be a dynamic map. + if st.IsMap() { + et := st.ElemType + return &ref.FieldType{ + Type: et.ExprType(), + }, true + } + return nil, false +} + +// ConvertToRule transforms an untyped DynValue into a typed object. +// +// Conversion is done deeply and will traverse the object graph represented by the dyn value. +func (rt *RuleTypes) ConvertToRule(dyn *DynValue) Rule { + ruleSchemaType := rt.ruleSchemaDeclTypes.root + // TODO: handle conversions to protobuf types. + dyn = rt.convertToCustomType(dyn, ruleSchemaType) + return &CustomRule{DynValue: dyn} +} + +// NativeToValue is an implementation of the ref.TypeAdapater interface which supports conversion +// of policy template values to CEL ref.Val instances. +func (rt *RuleTypes) NativeToValue(val interface{}) ref.Val { + switch v := val.(type) { + case *CustomRule: + return v.ExprValue() + default: + return rt.typeAdapter.NativeToValue(val) + } +} + +// TypeNames returns the list of type names declared within the RuleTypes object. +func (rt *RuleTypes) TypeNames() []string { + typeNames := make([]string, len(rt.ruleSchemaDeclTypes.types)) + i := 0 + for name := range rt.ruleSchemaDeclTypes.types { + typeNames[i] = name + i++ + } + return typeNames +} + +func (rt *RuleTypes) findDeclType(typeName string) (*DeclType, bool) { + declType, found := rt.ruleSchemaDeclTypes.types[typeName] + if found { + return declType, true + } + declType, found = rt.resolver.FindType(typeName) + if found { + return declType, true + } + return nil, false +} + +func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *DynValue { + switch v := dyn.Value().(type) { + case *MapValue: + if declType.IsObject() { + obj := v.ConvertToObject(declType) + for name, f := range obj.fieldMap { + field := declType.Fields[name] + f.Ref = rt.convertToCustomType(f.Ref, field.Type) + } + dyn.SetValue(obj) + return dyn + } + // TODO: handle complex map types which have non-string keys. + fieldType := declType.ElemType + for _, f := range v.fieldMap { + f.Ref = rt.convertToCustomType(f.Ref, fieldType) + } + return dyn + case *ListValue: + for i := 0; i < len(v.Entries); i++ { + elem := v.Entries[i] + elem = rt.convertToCustomType(elem, declType.ElemType) + v.Entries[i] = elem + } + return dyn + default: + return dyn + } +} + +func newSchemaTypeProvider(kind string, schema *OpenAPISchema) (*schemaTypeProvider, error) { + root := schema.DeclType().MaybeAssignTypeName(kind) + types := FieldTypeMap(kind, root) + return &schemaTypeProvider{ + root: root, + types: types, + }, nil +} + +type schemaTypeProvider struct { + root *DeclType + types map[string]*DeclType +} + +var ( + // AnyType is equivalent to the CEL 'protobuf.Any' type in that the value may have any of the + // types supported by CEL Policy Templates. + AnyType = newSimpleType("any", decls.Any, nil) + + // BoolType is equivalent to the CEL 'bool' type. + BoolType = newSimpleType("bool", decls.Bool, types.False) + + // BytesType is equivalent to the CEL 'bytes' type. + BytesType = newSimpleType("bytes", decls.Bytes, types.Bytes([]byte{})) + + // DoubleType is equivalent to the CEL 'double' type which is a 64-bit floating point value. + DoubleType = newSimpleType("double", decls.Double, types.Double(0)) + + // DurationType is equivalent to the CEL 'duration' type. + DurationType = newSimpleType("duration", decls.Duration, types.Duration{Duration: time.Duration(0)}) + + // DynType is the equivalent of the CEL 'dyn' concept which indicates that the type will be + // determined at runtime rather than compile time. + DynType = newSimpleType("dyn", decls.Dyn, nil) + + // IntType is equivalent to the CEL 'int' type which is a 64-bit signed int. + IntType = newSimpleType("int", decls.Int, types.IntZero) + + // NullType is equivalent to the CEL 'null_type'. + NullType = newSimpleType("null_type", decls.Null, types.NullValue) + + // StringType is equivalent to the CEL 'string' type which is expected to be a UTF-8 string. + // StringType values may either be string literals or expression strings. + StringType = newSimpleType("string", decls.String, types.String("")) + + // PlainTextType is equivalent to the CEL 'string' type, but which has been specifically + // designated as a string literal. + PlainTextType = newSimpleType("string_lit", decls.String, types.String("")) + + // TimestampType corresponds to the well-known protobuf.Timestamp type supported within CEL. + TimestampType = newSimpleType("timestamp", decls.Timestamp, types.Timestamp{Time: time.Time{}}) + + // UintType is equivalent to the CEL 'uint' type. + UintType = newSimpleType("uint", decls.Uint, types.Uint(0)) + + // ListType is equivalent to the CEL 'list' type. + ListType = NewListType(AnyType) + + // MapType is equivalent to the CEL 'map' type. + MapType = NewMapType(AnyType, AnyType) +) diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go new file mode 100644 index 00000000000..bfcd1891bfe --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go @@ -0,0 +1,155 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "testing" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +func TestTypes_ListType(t *testing.T) { + list := NewListType(StringType) + if !list.IsList() { + t.Error("list type not identifiable as list") + } + if list.TypeName() != "list" { + t.Errorf("got %s, wanted list", list.TypeName()) + } + if list.DefaultValue() == nil { + t.Error("got nil zero value for list type") + } + if list.ElemType.TypeName() != "string" { + t.Errorf("got %s, wanted elem type of string", list.ElemType.TypeName()) + } + if list.ExprType().GetListType() == nil { + t.Errorf("got %v, wanted CEL list type", list.ExprType()) + } +} + +func TestTypes_MapType(t *testing.T) { + mp := NewMapType(StringType, IntType) + if !mp.IsMap() { + t.Error("map type not identifiable as map") + } + if mp.TypeName() != "map" { + t.Errorf("got %s, wanted map", mp.TypeName()) + } + if mp.DefaultValue() == nil { + t.Error("got nil zero value for map type") + } + if mp.KeyType.TypeName() != "string" { + t.Errorf("got %s, wanted key type of string", mp.KeyType.TypeName()) + } + if mp.ElemType.TypeName() != "int" { + t.Errorf("got %s, wanted elem type of int", mp.ElemType.TypeName()) + } + if mp.ExprType().GetMapType() == nil { + t.Errorf("got %v, wanted CEL map type", mp.ExprType()) + } +} + +func TestTypes_RuleTypesFieldMapping(t *testing.T) { + stdEnv, _ := cel.NewEnv() + reg := NewRegistry(stdEnv) + rt, err := NewRuleTypes("mock_template", testSchema(), reg) + if err != nil { + t.Fatal(err) + } + nestedFieldType, found := rt.FindFieldType("CustomObject", "nested") + if !found { + t.Fatal("got field not found for 'CustomObject.nested', wanted found") + } + if nestedFieldType.Type.GetMessageType() != "CustomObject.nested" { + t.Errorf("got field type %v, wanted mock_template.nested", nestedFieldType.Type) + } + subnameFieldType, found := rt.FindFieldType("CustomObject.nested", "subname") + if !found { + t.Fatal("got field not found for 'CustomObject.nested.subname', wanted found") + } + if subnameFieldType.Type.GetPrimitive() != exprpb.Type_STRING { + t.Errorf("got field type %v, wanted string", subnameFieldType.Type) + } + flagsFieldType, found := rt.FindFieldType("CustomObject.nested", "flags") + if !found { + t.Fatal("got field not found for 'CustomObject.nested.flags', wanted found") + } + if flagsFieldType.Type.GetMapType() == nil { + t.Errorf("got field type %v, wanted map", flagsFieldType.Type) + } + flagFieldType, found := rt.FindFieldType("CustomObject.nested.flags", "my_flag") + if !found { + t.Fatal("got field not found for 'CustomObject.nested.flags.my_flag', wanted found") + } + if flagFieldType.Type.GetPrimitive() != exprpb.Type_BOOL { + t.Errorf("got field type %v, wanted bool", flagFieldType.Type) + } + + // Manually constructed instance of the schema. + name := NewField(1, "name") + name.Ref = testValue(t, 2, "test-instance") + nestedVal := NewMapValue() + flags := NewField(5, "flags") + flagsVal := NewMapValue() + myFlag := NewField(6, "my_flag") + myFlag.Ref = testValue(t, 7, true) + flagsVal.AddField(myFlag) + flags.Ref = testValue(t, 8, flagsVal) + dates := NewField(9, "dates") + dates.Ref = testValue(t, 10, NewListValue()) + nestedVal.AddField(flags) + nestedVal.AddField(dates) + nested := NewField(3, "nested") + nested.Ref = testValue(t, 4, nestedVal) + mapVal := NewMapValue() + mapVal.AddField(name) + mapVal.AddField(nested) + rule := rt.ConvertToRule(testValue(t, 11, mapVal)) + if rule == nil { + t.Error("map could not be converted to rule") + } + if rule.GetID() != 11 { + t.Errorf("got %d as the rule id, wanted 11", rule.GetID()) + } + ruleVal := rt.NativeToValue(rule) + if ruleVal == nil { + t.Error("got CEL rule value of nil, wanted non-nil") + } + + opts, err := rt.EnvOptions(stdEnv.TypeProvider()) + if err != nil { + t.Fatal(err) + } + ruleEnv, err := stdEnv.Extend(opts...) + if err != nil { + t.Fatal(err) + } + helloVal := ruleEnv.TypeAdapter().NativeToValue("hello") + if helloVal.Equal(types.String("hello")) != types.True { + t.Errorf("got %v, wanted types.String('hello')", helloVal) + } +} + +func testValue(t *testing.T, id int64, val interface{}) *DynValue { + t.Helper() + dv, err := NewDynValue(id, val) + if err != nil { + t.Fatalf("model.NewDynValue(%d, %v) failed: %v", id, val, err) + } + return dv +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value.go new file mode 100644 index 00000000000..8f3634409bb --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value.go @@ -0,0 +1,782 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "fmt" + "reflect" + "sync" + "time" + + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" +) + +// EncodeStyle is a hint for string encoding of parsed values. +type EncodeStyle int + +const ( + // BlockValueStyle is the default string encoding which preserves whitespace and newlines. + BlockValueStyle EncodeStyle = iota + + // FlowValueStyle indicates that the string is an inline representation of complex types. + FlowValueStyle + + // FoldedValueStyle is a multiline string with whitespace and newlines trimmed to a single + // a whitespace. Repeated newlines are replaced with a single newline rather than a single + // whitespace. + FoldedValueStyle + + // LiteralStyle is a multiline string that preserves newlines, but trims all other whitespace + // to a single character. + LiteralStyle +) + +// ParsedValue represents a top-level object representing either a template or instance value. +type ParsedValue struct { + ID int64 + Value *MapValue + Meta SourceMetadata +} + +// NewEmptyDynValue returns the zero-valued DynValue. +func NewEmptyDynValue() *DynValue { + // note: 0 is not a valid parse node identifier. + dv, _ := NewDynValue(0, nil) + return dv +} + +// NewDynValue returns a DynValue that corresponds to a parse node id and value. +func NewDynValue(id int64, val interface{}) (*DynValue, error) { + dv := &DynValue{ID: id} + err := dv.SetValue(val) + return dv, err +} + +// DynValue is a dynamically typed value used to describe unstructured content. +// Whether the value has the desired type is determined by where it is used within the Instance or +// Template, and whether there are schemas which might enforce a more rigid type definition. +type DynValue struct { + ID int64 + EncodeStyle EncodeStyle + value interface{} + exprValue ref.Val + declType *DeclType +} + +// DeclType returns the policy model type of the dyn value. +func (dv *DynValue) DeclType() *DeclType { + return dv.declType +} + +// ConvertToNative is an implementation of the CEL ref.Val method used to adapt between CEL types +// and Go-native types. +// +// The default behavior of this method is to first convert to a CEL type which has a well-defined +// set of conversion behaviors and proxy to the CEL ConvertToNative method for the type. +func (dv *DynValue) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { + ev := dv.ExprValue() + if types.IsError(ev) { + return nil, ev.(*types.Err) + } + return ev.ConvertToNative(typeDesc) +} + +// Equal returns whether the dyn value is equal to a given CEL value. +func (dv *DynValue) Equal(other ref.Val) ref.Val { + dvType := dv.Type() + otherType := other.Type() + // Preserve CEL's homogeneous equality constraint. + if dvType.TypeName() != otherType.TypeName() { + return types.MaybeNoSuchOverloadErr(other) + } + switch v := dv.value.(type) { + case ref.Val: + return v.Equal(other) + case PlainTextValue: + return celBool(string(v) == other.Value().(string)) + case *MultilineStringValue: + return celBool(v.Value == other.Value().(string)) + case time.Duration: + otherDuration := other.Value().(time.Duration) + return celBool(v == otherDuration) + case time.Time: + otherTimestamp := other.Value().(time.Time) + return celBool(v.Equal(otherTimestamp)) + default: + return celBool(reflect.DeepEqual(v, other.Value())) + } +} + +// ExprValue converts the DynValue into a CEL value. +func (dv *DynValue) ExprValue() ref.Val { + return dv.exprValue +} + +// Value returns the underlying value held by this reference. +func (dv *DynValue) Value() interface{} { + return dv.value +} + +// SetValue updates the underlying value held by this reference. +func (dv *DynValue) SetValue(value interface{}) error { + dv.value = value + var err error + dv.exprValue, dv.declType, err = exprValue(value) + return err +} + +// Type returns the CEL type for the given value. +func (dv *DynValue) Type() ref.Type { + return dv.ExprValue().Type() +} + +func exprValue(value interface{}) (ref.Val, *DeclType, error) { + switch v := value.(type) { + case bool: + return types.Bool(v), BoolType, nil + case []byte: + return types.Bytes(v), BytesType, nil + case float64: + return types.Double(v), DoubleType, nil + case int64: + return types.Int(v), IntType, nil + case string: + return types.String(v), StringType, nil + case uint64: + return types.Uint(v), UintType, nil + case PlainTextValue: + return types.String(string(v)), PlainTextType, nil + case *MultilineStringValue: + return types.String(v.Value), StringType, nil + case time.Duration: + return types.Duration{Duration: v}, DurationType, nil + case time.Time: + return types.Timestamp{Time: v}, TimestampType, nil + case types.Null: + return v, NullType, nil + case *ListValue: + return v, ListType, nil + case *MapValue: + return v, MapType, nil + case *ObjectValue: + return v, v.objectType, nil + default: + return nil, unknownType, fmt.Errorf("unsupported type: (%T)%v", v, v) + } +} + +// PlainTextValue is a text string literal which must not be treated as an expression. +type PlainTextValue string + +// MultilineStringValue is a multiline string value which has been parsed in a way which omits +// whitespace as well as a raw form which preserves whitespace. +type MultilineStringValue struct { + Value string + Raw string +} + +func newStructValue() *structValue { + return &structValue{ + Fields: []*Field{}, + fieldMap: map[string]*Field{}, + } +} + +type structValue struct { + Fields []*Field + fieldMap map[string]*Field +} + +// AddField appends a MapField to the MapValue and indexes the field by name. +func (sv *structValue) AddField(field *Field) { + sv.Fields = append(sv.Fields, field) + sv.fieldMap[field.Name] = field +} + +// ConvertToNative converts the MapValue type to a native go types. +func (sv *structValue) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { + if typeDesc.Kind() != reflect.Map && + typeDesc.Kind() != reflect.Struct && + typeDesc.Kind() != reflect.Ptr && + typeDesc.Kind() != reflect.Interface { + return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc) + } + + // TODO: Special case handling for protobuf Struct and Any if needed + + // Unwrap pointers, but track their use. + isPtr := false + if typeDesc.Kind() == reflect.Ptr { + tk := typeDesc + typeDesc = typeDesc.Elem() + if typeDesc.Kind() == reflect.Ptr { + return nil, fmt.Errorf("unsupported type conversion to '%v'", tk) + } + isPtr = true + } + + if typeDesc.Kind() == reflect.Map { + keyType := typeDesc.Key() + if keyType.Kind() != reflect.String && keyType.Kind() != reflect.Interface { + return nil, fmt.Errorf("object fields cannot be converted to type '%v'", keyType) + } + elemType := typeDesc.Elem() + sz := len(sv.fieldMap) + ntvMap := reflect.MakeMapWithSize(typeDesc, sz) + for name, val := range sv.fieldMap { + refVal, err := val.Ref.ConvertToNative(elemType) + if err != nil { + return nil, err + } + ntvMap.SetMapIndex(reflect.ValueOf(name), reflect.ValueOf(refVal)) + } + return ntvMap.Interface(), nil + } + + if typeDesc.Kind() == reflect.Struct { + ntvObjPtr := reflect.New(typeDesc) + ntvObj := ntvObjPtr.Elem() + for name, val := range sv.fieldMap { + f := ntvObj.FieldByName(name) + if !f.IsValid() { + return nil, fmt.Errorf("type conversion error, no such field %s in type %v", + name, typeDesc) + } + fv, err := val.Ref.ConvertToNative(f.Type()) + if err != nil { + return nil, err + } + f.Set(reflect.ValueOf(fv)) + } + if isPtr { + return ntvObjPtr.Interface(), nil + } + return ntvObj.Interface(), nil + } + return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc) +} + +// GetField returns a MapField by name if one exists. +func (sv *structValue) GetField(name string) (*Field, bool) { + field, found := sv.fieldMap[name] + return field, found +} + +// IsSet returns whether the given field, which is defined, has also been set. +func (sv *structValue) IsSet(key ref.Val) ref.Val { + k, ok := key.(types.String) + if !ok { + return types.MaybeNoSuchOverloadErr(key) + } + name := string(k) + _, found := sv.fieldMap[name] + return celBool(found) +} + +// NewObjectValue creates a struct value with a schema type and returns the empty ObjectValue. +func NewObjectValue(sType *DeclType) *ObjectValue { + return &ObjectValue{ + structValue: newStructValue(), + objectType: sType, + } +} + +// ObjectValue is a struct with a custom schema type which indicates the fields and types +// associated with the structure. +type ObjectValue struct { + *structValue + objectType *DeclType +} + +// ConvertToType is an implementation of the CEL ref.Val interface method. +func (o *ObjectValue) ConvertToType(t ref.Type) ref.Val { + if t == types.TypeType { + return types.NewObjectTypeValue(o.objectType.TypeName()) + } + if t.TypeName() == o.objectType.TypeName() { + return o + } + return types.NewErr("type conversion error from '%s' to '%s'", o.Type(), t) +} + +// Equal returns true if the two object types are equal and their field values are equal. +func (o *ObjectValue) Equal(other ref.Val) ref.Val { + // Preserve CEL's homogeneous equality semantics. + if o.objectType.TypeName() != other.Type().TypeName() { + return types.MaybeNoSuchOverloadErr(other) + } + o2 := other.(traits.Indexer) + for name := range o.objectType.Fields { + k := types.String(name) + v := o.Get(k) + ov := o2.Get(k) + vEq := v.Equal(ov) + if vEq != types.True { + return vEq + } + } + return types.True +} + +// Get returns the value of the specified field. +// +// If the field is set, its value is returned. If the field is not set, the default value for the +// field is returned thus allowing for safe-traversal and preserving proto-like field traversal +// semantics for Open API Schema backed types. +func (o *ObjectValue) Get(name ref.Val) ref.Val { + n, ok := name.(types.String) + if !ok { + return types.MaybeNoSuchOverloadErr(n) + } + nameStr := string(n) + field, found := o.fieldMap[nameStr] + if found { + return field.Ref.ExprValue() + } + fieldDef, found := o.objectType.Fields[nameStr] + if !found { + return types.NewErr("no such field: %s", nameStr) + } + defValue := fieldDef.DefaultValue() + if defValue != nil { + return defValue + } + return types.NewErr("no default for type: %s", fieldDef.TypeName()) +} + +// Type returns the CEL type value of the object. +func (o *ObjectValue) Type() ref.Type { + return o.objectType +} + +// Value returns the Go-native representation of the object. +func (o *ObjectValue) Value() interface{} { + return o +} + +// NewMapValue returns an empty MapValue. +func NewMapValue() *MapValue { + return &MapValue{ + structValue: newStructValue(), + } +} + +// MapValue declares an object with a set of named fields whose values are dynamically typed. +type MapValue struct { + *structValue +} + +// ConvertToObject produces an ObjectValue from the MapValue with the associated schema type. +// +// The conversion is shallow and the memory shared between the Object and Map as all references +// to the map are expected to be replaced with the Object reference. +func (m *MapValue) ConvertToObject(declType *DeclType) *ObjectValue { + return &ObjectValue{ + structValue: m.structValue, + objectType: declType, + } +} + +// Contains returns whether the given key is contained in the MapValue. +func (m *MapValue) Contains(key ref.Val) ref.Val { + v, found := m.Find(key) + if v != nil && types.IsUnknownOrError(v) { + return v + } + return celBool(found) +} + +// ConvertToType converts the MapValue to another CEL type, if possible. +func (m *MapValue) ConvertToType(t ref.Type) ref.Val { + switch t { + case types.MapType: + return m + case types.TypeType: + return types.MapType + } + return types.NewErr("type conversion error from '%s' to '%s'", m.Type(), t) +} + +// Equal returns true if the maps are of the same size, have the same keys, and the key-values +// from each map are equal. +func (m *MapValue) Equal(other ref.Val) ref.Val { + oMap, isMap := other.(traits.Mapper) + if !isMap { + return types.MaybeNoSuchOverloadErr(other) + } + if m.Size() != oMap.Size() { + return types.False + } + for name, field := range m.fieldMap { + k := types.String(name) + ov, found := oMap.Find(k) + if !found { + return types.False + } + v := field.Ref.ExprValue() + vEq := v.Equal(ov) + if vEq != types.True { + return vEq + } + } + return types.True +} + +// Find returns the value for the key in the map, if found. +func (m *MapValue) Find(name ref.Val) (ref.Val, bool) { + // Currently only maps with string keys are supported as this is best aligned with JSON, + // and also much simpler to support. + n, ok := name.(types.String) + if !ok { + return types.MaybeNoSuchOverloadErr(n), true + } + nameStr := string(n) + field, found := m.fieldMap[nameStr] + if found { + return field.Ref.ExprValue(), true + } + return nil, false +} + +// Get returns the value for the key in the map, or error if not found. +func (m *MapValue) Get(key ref.Val) ref.Val { + v, found := m.Find(key) + if found { + return v + } + return types.ValOrErr(key, "no such key: %v", key) +} + +// Iterator produces a traits.Iterator which walks over the map keys. +// +// The Iterator is frequently used within comprehensions. +func (m *MapValue) Iterator() traits.Iterator { + keys := make([]ref.Val, len(m.fieldMap)) + i := 0 + for k := range m.fieldMap { + keys[i] = types.String(k) + i++ + } + return &baseMapIterator{ + baseVal: &baseVal{}, + keys: keys, + } +} + +// Size returns the number of keys in the map. +func (m *MapValue) Size() ref.Val { + return types.Int(len(m.Fields)) +} + +// Type returns the CEL ref.Type for the map. +func (m *MapValue) Type() ref.Type { + return types.MapType +} + +// Value returns the Go-native representation of the MapValue. +func (m *MapValue) Value() interface{} { + return m +} + +type baseMapIterator struct { + *baseVal + keys []ref.Val + idx int +} + +// HasNext implements the traits.Iterator interface method. +func (it *baseMapIterator) HasNext() ref.Val { + if it.idx < len(it.keys) { + return types.True + } + return types.False +} + +// Next implements the traits.Iterator interface method. +func (it *baseMapIterator) Next() ref.Val { + key := it.keys[it.idx] + it.idx++ + return key +} + +// Type implements the CEL ref.Val interface metohd. +func (it *baseMapIterator) Type() ref.Type { + return types.IteratorType +} + +// NewField returns a MapField instance with an empty DynValue that refers to the +// specified parse node id and field name. +func NewField(id int64, name string) *Field { + return &Field{ + ID: id, + Name: name, + Ref: NewEmptyDynValue(), + } +} + +// Field specifies a field name and a reference to a dynamic value. +type Field struct { + ID int64 + Name string + Ref *DynValue +} + +// NewListValue returns an empty ListValue instance. +func NewListValue() *ListValue { + return &ListValue{ + Entries: []*DynValue{}, + } +} + +// ListValue contains a list of dynamically typed entries. +type ListValue struct { + Entries []*DynValue + initValueSet sync.Once + valueSet map[ref.Val]struct{} +} + +// Add concatenates two lists together to produce a new CEL list value. +func (lv *ListValue) Add(other ref.Val) ref.Val { + oArr, isArr := other.(traits.Lister) + if !isArr { + return types.MaybeNoSuchOverloadErr(other) + } + szRight := len(lv.Entries) + szLeft := int(oArr.Size().(types.Int)) + sz := szRight + szLeft + combo := make([]ref.Val, sz) + for i := 0; i < szRight; i++ { + combo[i] = lv.Entries[i].ExprValue() + } + for i := 0; i < szLeft; i++ { + combo[i+szRight] = oArr.Get(types.Int(i)) + } + return types.DefaultTypeAdapter.NativeToValue(combo) +} + +// Append adds another entry into the ListValue. +func (lv *ListValue) Append(entry *DynValue) { + lv.Entries = append(lv.Entries, entry) + // The append resets all previously built indices. + lv.initValueSet = sync.Once{} +} + +// Contains returns whether the input `val` is equal to an element in the list. +// +// If any pair-wise comparison between the input value and the list element is an error, the +// operation will return an error. +func (lv *ListValue) Contains(val ref.Val) ref.Val { + if types.IsUnknownOrError(val) { + return val + } + lv.initValueSet.Do(lv.finalizeValueSet) + if lv.valueSet != nil { + _, found := lv.valueSet[val] + if found { + return types.True + } + // Instead of returning false, ensure that CEL's heterogeneous equality constraint + // is satisfied by allowing pair-wise equality behavior to determine the outcome. + } + var err ref.Val + sz := len(lv.Entries) + for i := 0; i < sz; i++ { + elem := lv.Entries[i] + cmp := elem.Equal(val) + b, ok := cmp.(types.Bool) + if !ok && err == nil { + err = types.MaybeNoSuchOverloadErr(cmp) + } + if b == types.True { + return types.True + } + } + if err != nil { + return err + } + return types.False +} + +// ConvertToNative is an implementation of the CEL ref.Val method used to adapt between CEL types +// and Go-native array-like types. +func (lv *ListValue) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { + // Non-list conversion. + if typeDesc.Kind() != reflect.Slice && + typeDesc.Kind() != reflect.Array && + typeDesc.Kind() != reflect.Interface { + return nil, fmt.Errorf("type conversion error from list to '%v'", typeDesc) + } + + // If the list is already assignable to the desired type return it. + if reflect.TypeOf(lv).AssignableTo(typeDesc) { + return lv, nil + } + + // List conversion. + otherElem := typeDesc.Elem() + + // Allow the element ConvertToNative() function to determine whether conversion is possible. + sz := len(lv.Entries) + nativeList := reflect.MakeSlice(typeDesc, int(sz), int(sz)) + for i := 0; i < sz; i++ { + elem := lv.Entries[i] + nativeElemVal, err := elem.ConvertToNative(otherElem) + if err != nil { + return nil, err + } + nativeList.Index(int(i)).Set(reflect.ValueOf(nativeElemVal)) + } + return nativeList.Interface(), nil +} + +// ConvertToType converts the ListValue to another CEL type. +func (lv *ListValue) ConvertToType(t ref.Type) ref.Val { + switch t { + case types.ListType: + return lv + case types.TypeType: + return types.ListType + } + return types.NewErr("type conversion error from '%s' to '%s'", ListType, t) +} + +// Equal returns true if two lists are of the same size, and the values at each index are also +// equal. +func (lv *ListValue) Equal(other ref.Val) ref.Val { + oArr, isArr := other.(traits.Lister) + if !isArr { + return types.MaybeNoSuchOverloadErr(other) + } + sz := types.Int(len(lv.Entries)) + if sz != oArr.Size() { + return types.False + } + for i := types.Int(0); i < sz; i++ { + cmp := lv.Get(i).Equal(oArr.Get(i)) + if cmp != types.True { + return cmp + } + } + return types.True +} + +// Get returns the value at the given index. +// +// If the index is negative or greater than the size of the list, an error is returned. +func (lv *ListValue) Get(idx ref.Val) ref.Val { + iv, isInt := idx.(types.Int) + if !isInt { + return types.ValOrErr(idx, "unsupported index: %v", idx) + } + i := int(iv) + if i < 0 || i >= len(lv.Entries) { + return types.NewErr("index out of bounds: %v", idx) + } + return lv.Entries[i].ExprValue() +} + +// Iterator produces a traits.Iterator suitable for use in CEL comprehension macros. +func (lv *ListValue) Iterator() traits.Iterator { + return &baseListIterator{ + getter: lv.Get, + sz: len(lv.Entries), + } +} + +// Size returns the number of elements in the list. +func (lv *ListValue) Size() ref.Val { + return types.Int(len(lv.Entries)) +} + +// Type returns the CEL ref.Type for the list. +func (lv *ListValue) Type() ref.Type { + return types.ListType +} + +// Value returns the Go-native value. +func (lv *ListValue) Value() interface{} { + return lv +} + +// finalizeValueSet inspects the ListValue entries in order to make internal optimizations once all list +// entries are known. +func (lv *ListValue) finalizeValueSet() { + valueSet := make(map[ref.Val]struct{}) + for _, e := range lv.Entries { + switch e.value.(type) { + case bool, float64, int64, string, uint64, types.Null, PlainTextValue: + if valueSet != nil { + valueSet[e.ExprValue()] = struct{}{} + } + default: + lv.valueSet = nil + return + } + } + lv.valueSet = valueSet +} + +type baseVal struct{} + +func (*baseVal) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { + return nil, fmt.Errorf("unsupported native conversion to: %v", typeDesc) +} + +func (*baseVal) ConvertToType(t ref.Type) ref.Val { + return types.NewErr("unsupported type conversion to: %v", t) +} + +func (*baseVal) Equal(other ref.Val) ref.Val { + return types.NewErr("unsupported equality test between instances") +} + +func (v *baseVal) Value() interface{} { + return nil +} + +type baseListIterator struct { + *baseVal + getter func(idx ref.Val) ref.Val + sz int + idx int +} + +func (it *baseListIterator) HasNext() ref.Val { + if it.idx < it.sz { + return types.True + } + return types.False +} + +func (it *baseListIterator) Next() ref.Val { + v := it.getter(types.Int(it.idx)) + it.idx++ + return v +} + +func (it *baseListIterator) Type() ref.Type { + return types.IteratorType +} + +func celBool(pred bool) ref.Val { + if pred { + return types.True + } + return types.False +} + +var unknownType = &DeclType{name: "unknown"} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value_test.go new file mode 100644 index 00000000000..ca1f122fe6b --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/value_test.go @@ -0,0 +1,363 @@ +// Copyright 2020 Google LLC +// +// Licensed 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 +// +// https://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 model + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" +) + +func TestConvertToType(t *testing.T) { + objType := NewObjectType("TestObject", map[string]*DeclField{}) + tests := []struct { + val interface{} + typ ref.Type + }{ + {true, types.BoolType}, + {float64(1.2), types.DoubleType}, + {int64(-42), types.IntType}, + {uint64(63), types.UintType}, + {PlainTextValue("plain text"), types.StringType}, + {&MultilineStringValue{Value: "multiline", Raw: "multi\nline"}, types.StringType}, + {time.Duration(300), types.DurationType}, + {time.Now().UTC(), types.TimestampType}, + {types.NullValue, types.NullType}, + {NewListValue(), types.ListType}, + {NewMapValue(), types.MapType}, + {[]byte("bytes"), types.BytesType}, + {NewObjectValue(objType), objType}, + } + for i, tc := range tests { + idx := i + tst := tc + t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) { + dv := testValue(t, int64(idx), tst.val) + ev := dv.ExprValue() + if ev.ConvertToType(types.TypeType).(ref.Type).TypeName() != tst.typ.TypeName() { + t.Errorf("got %v, wanted %v type", ev.ConvertToType(types.TypeType), tst.typ) + } + if ev.ConvertToType(tst.typ).Equal(ev) != types.True { + t.Errorf("got %v, wanted input value %v", ev.ConvertToType(tst.typ), ev) + } + }) + } +} + +func TestEqual(t *testing.T) { + vals := []interface{}{ + true, []byte("bytes"), float64(1.2), int64(-42), uint64(63), PlainTextValue("plain text"), + &MultilineStringValue{Value: "multiline", Raw: "multi\nline"}, time.Duration(300), + time.Now().UTC(), types.NullValue, NewListValue(), NewMapValue(), + NewObjectValue(NewObjectType("TestObject", map[string]*DeclField{})), + } + for i, v := range vals { + dv := testValue(t, int64(i), v) + if dv.Equal(dv.ExprValue()) != types.True { + t.Errorf("got %v, wanted dyn value %v equal to itself", dv.Equal(dv.ExprValue()), dv.ExprValue()) + } + } +} + +func TestListValueAdd(t *testing.T) { + lv := NewListValue() + lv.Append(testValue(t, 1, "first")) + ov := NewListValue() + ov.Append(testValue(t, 2, "second")) + ov.Append(testValue(t, 3, "third")) + llv := NewListValue() + llv.Append(testValue(t, 4, lv)) + lov := NewListValue() + lov.Append(testValue(t, 5, ov)) + var v traits.Lister = llv.Add(lov).(traits.Lister) + if v.Size() != types.Int(2) { + t.Errorf("got list size %d, wanted 2", v.Size()) + } + complex, err := v.ConvertToNative(reflect.TypeOf([][]string{})) + complexList := complex.([][]string) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(complexList, [][]string{{"first"}, {"second", "third"}}) { + t.Errorf("got %v, wanted [['first'], ['second', 'third']]", complexList) + } +} + +func TestListValueContains(t *testing.T) { + lv := NewListValue() + lv.Append(testValue(t, 1, "first")) + lv.Append(testValue(t, 2, "second")) + lv.Append(testValue(t, 3, "third")) + for i := types.Int(0); i < lv.Size().(types.Int); i++ { + e := lv.Get(i) + contained := lv.Contains(e) + if contained != types.True { + t.Errorf("got %v, wanted list contains elem[%v] %v == true", contained, i, e) + } + } + if lv.Contains(types.String("fourth")) != types.False { + t.Errorf("got %v, wanted false 'fourth'", lv.Contains(types.String("fourth"))) + } + if !types.IsError(lv.Contains(types.Int(-1))) { + t.Errorf("got %v, wanted error for invalid type", lv.Contains(types.Int(-1))) + } +} + +func TestListValueContainsNestedList(t *testing.T) { + lvA := NewListValue() + lvA.Append(testValue(t, 1, int64(1))) + lvA.Append(testValue(t, 2, int64(2))) + + lvB := NewListValue() + lvB.Append(testValue(t, 3, int64(3))) + + elemA, elemB := testValue(t, 4, lvA), testValue(t, 5, lvB) + lv := NewListValue() + lv.Append(elemA) + lv.Append(elemB) + + contained := lv.Contains(elemA.ExprValue()) + if contained != types.True { + t.Errorf("got %v, wanted elemA contained in list value", contained) + } + contained = lv.Contains(elemB.ExprValue()) + if contained != types.True { + t.Errorf("got %v, wanted elemB contained in list value", contained) + } + contained = lv.Contains(types.DefaultTypeAdapter.NativeToValue([]int32{4})) + if contained != types.False { + t.Errorf("got %v, wanted empty list not contained", contained) + } +} + +func TestListValueConvertToNative(t *testing.T) { + lv := NewListValue() + none, err := lv.ConvertToNative(reflect.TypeOf([]interface{}{})) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(none, []interface{}{}) { + t.Errorf("got %v, wanted empty list", none) + } + lv.Append(testValue(t, 1, "first")) + one, err := lv.ConvertToNative(reflect.TypeOf([]string{})) + oneList := one.([]string) + if err != nil { + t.Fatal(err) + } + if len(oneList) != 1 { + t.Errorf("got len(one) == %d, wanted 1", len(oneList)) + } + if !reflect.DeepEqual(oneList, []string{"first"}) { + t.Errorf("got %v, wanted string list", oneList) + } + ov := NewListValue() + ov.Append(testValue(t, 2, "second")) + ov.Append(testValue(t, 3, "third")) + if ov.Size() != types.Int(2) { + t.Errorf("got list size %d, wanted 2", ov.Size()) + } + llv := NewListValue() + llv.Append(testValue(t, 4, lv)) + llv.Append(testValue(t, 5, ov)) + if llv.Size() != types.Int(2) { + t.Errorf("got list size %d, wanted 2", llv.Size()) + } + complex, err := llv.ConvertToNative(reflect.TypeOf([][]string{})) + complexList := complex.([][]string) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(complexList, [][]string{{"first"}, {"second", "third"}}) { + t.Errorf("got %v, wanted [['first'], ['second', 'third']]", complexList) + } +} + +func TestListValueIterator(t *testing.T) { + lv := NewListValue() + lv.Append(testValue(t, 1, "first")) + lv.Append(testValue(t, 2, "second")) + lv.Append(testValue(t, 3, "third")) + it := lv.Iterator() + if it.Type() != types.IteratorType { + t.Errorf("got type %v for iterator, wanted IteratorType", it.Type()) + } + i := types.Int(0) + for it.HasNext() == types.True { + v := it.Next() + if v.Equal(lv.Get(i)) != types.True { + t.Errorf("iterator value %v and value %v at index %d not equal", v, lv.Get(i), i) + } + i++ + } +} + +func TestMapValueConvertToNative(t *testing.T) { + mv := NewMapValue() + none, err := mv.ConvertToNative(reflect.TypeOf(map[string]interface{}{})) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(none, map[string]interface{}{}) { + t.Errorf("got %v, wanted empty map", none) + } + none, err = mv.ConvertToNative(reflect.TypeOf(map[interface{}]interface{}{})) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(none, map[interface{}]interface{}{}) { + t.Errorf("got %v, wanted empty map", none) + } + mv.AddField(NewField(1, "Test")) + tst, _ := mv.GetField("Test") + tst.Ref = testValue(t, 2, uint64(12)) + mv.AddField(NewField(3, "Check")) + chk, _ := mv.GetField("Check") + chk.Ref = testValue(t, 4, uint64(34)) + if mv.Size() != types.Int(2) { + t.Errorf("got size %d, wanted 2", mv.Size()) + } + if mv.Contains(types.String("Test")) != types.True { + t.Error("key 'Test' not found") + } + if mv.Contains(types.String("Check")) != types.True { + t.Error("key 'Check' not found") + } + if mv.Contains(types.String("Checked")) != types.False { + t.Error("key 'Checked' found, wanted not found") + } + it := mv.Iterator() + for it.HasNext() == types.True { + k := it.Next() + v := mv.Get(k) + if k == types.String("Test") && v != types.Uint(12) { + t.Errorf("key 'Test' not equal to 12u") + } + if k == types.String("Check") && v != types.Uint(34) { + t.Errorf("key 'Check' not equal to 34u") + } + } + mpStrUint, err := mv.ConvertToNative(reflect.TypeOf(map[string]uint64{})) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(mpStrUint, map[string]uint64{ + "Test": uint64(12), + "Check": uint64(34), + }) { + t.Errorf("got %v, wanted {'Test': 12u, 'Check': 34u}", mpStrUint) + } + tstStr, err := mv.ConvertToNative(reflect.TypeOf(&tstStruct{})) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(tstStr, &tstStruct{ + Test: uint64(12), + Check: uint64(34), + }) { + t.Errorf("got %v, wanted tstStruct{Test: 12u, Check: 34u}", tstStr) + } +} + +func TestMapValueEqual(t *testing.T) { + mv := NewMapValue() + name := NewField(1, "name") + name.Ref = testValue(t, 2, "alert") + priority := NewField(3, "priority") + priority.Ref = testValue(t, 4, int64(4)) + mv.AddField(name) + mv.AddField(priority) + if mv.Equal(mv) != types.True { + t.Fatalf("map.Equal(map) failed: %v", mv.Equal(mv)) + } +} + +func TestMapValueNotEqual(t *testing.T) { + mv := NewMapValue() + name := NewField(1, "name") + name.Ref = testValue(t, 2, "alert") + priority := NewField(3, "priority") + priority.Ref = testValue(t, 4, int64(4)) + mv.AddField(name) + mv.AddField(priority) + + mv2 := NewMapValue() + mv2.AddField(name) + if mv.Equal(mv2) != types.False { + t.Fatalf("mv.Equal(mv2) failed: %v", mv.Equal(mv2)) + } + + priority2 := NewField(5, "priority") + priority2.Ref = testValue(t, 6, int64(3)) + mv2.AddField(priority2) + if mv.Equal(mv2) != types.False { + t.Fatalf("mv.Equal(mv2) failed: %v", mv.Equal(mv2)) + } +} + +func TestMapValueIsSet(t *testing.T) { + mv := NewMapValue() + if mv.IsSet(types.String("name")) != types.False { + t.Error("map.IsSet('name') returned true for unset key") + } + mv.AddField(NewField(1, "name")) + if mv.IsSet(types.String("name")) != types.True { + t.Error("map.IsSet('name') returned false for a set key") + } +} + +func TestObjectValueEqual(t *testing.T) { + objType := NewObjectType("Notice", map[string]*DeclField{ + "name": &DeclField{Name: "name", Type: StringType}, + "priority": &DeclField{Name: "priority", Type: IntType}, + "message": &DeclField{Name: "message", Type: PlainTextType, defaultValue: ""}, + }) + name := NewField(1, "name") + name.Ref = testValue(t, 2, "alert") + priority := NewField(3, "priority") + priority.Ref = testValue(t, 4, int64(4)) + message := NewField(5, "message") + message.Ref = testValue(t, 6, "call immediately") + + mv1 := NewMapValue() + mv1.AddField(name) + mv1.AddField(priority) + obj1 := mv1.ConvertToObject(objType) + if obj1.Equal(obj1) != types.True { + t.Errorf("obj1.Equal(obj1) failed, got: %v", obj1.Equal(obj1)) + } + + mv2 := NewMapValue() + mv2.AddField(name) + mv2.AddField(priority) + mv2.AddField(message) + obj2 := mv2.ConvertToObject(objType) + if obj1.Equal(obj2) == types.True { + t.Error("obj1.Equal(obj2) returned true, wanted false") + } + if obj2.Equal(obj1) == types.True { + t.Error("obj2.Equal(obj1) returned true, wanted false") + } +} + +type tstStruct struct { + Test uint64 + Check uint64 +}