Manual changes in /third_party/forked/celopenapi to use Kubernetes types and remove unused code.

This commit is contained in:
cici37 2021-11-03 15:23:22 -07:00 committed by Joe Betz
parent a37dfa7f0e
commit 89d0623b65
18 changed files with 663 additions and 1853 deletions

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,4 @@
This directory contains a port of https://github.com/google/cel-policy-templates-go/tree/master/policy/model modified in a few ways:
- Uses the Structural schema types
- All template related code has been removed

View File

@ -1,303 +0,0 @@
// 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)
}

View File

@ -1,121 +0,0 @@
// 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)
}
})
}
}

View File

@ -1,209 +0,0 @@
// 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:
// <receiver_type>_<func>_<arg_type0>_<arg_typeN>
//
// 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:
// <func>_<arg_type0>_<arg_typeN>
//
// When the function name is global, <func> 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)
}

View File

@ -1,73 +0,0 @@
// 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)
}
}

View File

@ -0,0 +1,80 @@
/*
Copyright 2021 The Kubernetes Authors.
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
)
// celReservedSymbols is a list of RESERVED symbols defined in the CEL lexer.
// No identifiers are allowed to collide with these symbols.
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax
var celReservedSymbols = sets.NewString(
"true", "false", "null", "in",
"as", "break", "const", "continue", "else",
"for", "function", "if", "import", "let",
"loop", "package", "namespace", "return", // !! 'namespace' is used heavily in Kubernetes
"var", "void", "while",
)
// celLanguageIdentifiers is a list of identifiers that are part of the CEL language.
// This does NOT include builtin macro or function identifiers.
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#values
var celLanguageIdentifiers = sets.NewString(
"int", "uint", "double", "bool", "string", "bytes", "list", "map", "null_type", "type",
)
// IsRootReserved returns true if an identifier is reserved by CEL. Declaring root variables in CEL with
// these identifiers is not allowed and would result in an "overlapping identifier for name '<identifier>'"
// CEL compilation error.
func IsRootReserved(prop string) bool {
return celLanguageIdentifiers.Has(prop)
}
// Escape escapes identifiers in the AlwaysReservedIdentifiers set by prefixing ident with "_" and by prefixing
// any ident already prefixed with N '_' with N+1 '_'.
// For an identifier that does not require escaping, the identifier is returned as-is.
func Escape(ident string) string {
if strings.HasPrefix(ident, "_") || celReservedSymbols.Has(ident) {
return "_" + ident
}
return ident
}
// EscapeSlice returns identifiers with Escape applied to each.
func EscapeSlice(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
result[i] = Escape(prop)
}
return result
}
// Unescape unescapes an identifier escaped by Escape.
func Unescape(escaped string) string {
if strings.HasPrefix(escaped, "_") {
trimmed := strings.TrimPrefix(escaped, "_")
if strings.HasPrefix(trimmed, "_") || celReservedSymbols.Has(trimmed) {
return trimmed
}
panic(fmt.Sprintf("failed to unescape improperly escaped string: %v", escaped))
}
return escaped
}

View File

@ -0,0 +1,115 @@
/*
Copyright 2021 The Kubernetes Authors.
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"testing"
)
// TestEscaping tests that property names are escaped as expected.
func TestEscaping(t *testing.T) {
cases := []struct{
unescaped string
escaped string
reservedAtRoot bool
} {
// CEL lexer RESERVED keywords must be escaped
{ unescaped: "true", escaped: "_true" },
{ unescaped: "false", escaped: "_false" },
{ unescaped: "null", escaped: "_null" },
{ unescaped: "in", escaped: "_in" },
{ unescaped: "as", escaped: "_as" },
{ unescaped: "break", escaped: "_break" },
{ unescaped: "const", escaped: "_const" },
{ unescaped: "continue", escaped: "_continue" },
{ unescaped: "else", escaped: "_else" },
{ unescaped: "for", escaped: "_for" },
{ unescaped: "function", escaped: "_function" },
{ unescaped: "if", escaped: "_if" },
{ unescaped: "import", escaped: "_import" },
{ unescaped: "let", escaped: "_let" },
{ unescaped: "loop", escaped: "_loop" },
{ unescaped: "package", escaped: "_package" },
{ unescaped: "namespace", escaped: "_namespace" },
{ unescaped: "return", escaped: "_return" },
{ unescaped: "var", escaped: "_var" },
{ unescaped: "void", escaped: "_void" },
{ unescaped: "while", escaped: "_while" },
// CEL language identifiers do not need to be escaped, but collide with builtin language identifier if bound as
// root variable names.
// i.e. "self.int == 1" is legal, but "int == 1" is not.
{ unescaped: "int", escaped: "int", reservedAtRoot: true },
{ unescaped: "uint", escaped: "uint", reservedAtRoot: true },
{ unescaped: "double", escaped: "double", reservedAtRoot: true },
{ unescaped: "bool", escaped: "bool", reservedAtRoot: true },
{ unescaped: "string", escaped: "string", reservedAtRoot: true },
{ unescaped: "bytes", escaped: "bytes", reservedAtRoot: true },
{ unescaped: "list", escaped: "list", reservedAtRoot: true },
{ unescaped: "map", escaped: "map", reservedAtRoot: true },
{ unescaped: "null_type", escaped: "null_type", reservedAtRoot: true },
{ unescaped: "type", escaped: "type", reservedAtRoot: true },
// To prevent escaping from colliding with other identifiers, all identifiers prefixed by _s are escaped by
// prefixing them with N+1 _s.
{ unescaped: "_if", escaped: "__if" },
{ unescaped: "__if", escaped: "___if" },
{ unescaped: "___if", escaped: "____if" },
{ unescaped: "_has", escaped: "__has" },
{ unescaped: "_int", escaped: "__int" },
{ unescaped: "_anything", escaped: "__anything" },
// CEL macro and function names do not need to be escaped because the parser can disambiguate them from the function and
// macro identifiers.
{ unescaped: "has", escaped: "has" },
{ unescaped: "all", escaped: "all" },
{ unescaped: "exists", escaped: "exists" },
{ unescaped: "exists_one", escaped: "exists_one" },
{ unescaped: "filter", escaped: "filter" },
{ unescaped: "size", escaped: "size" },
{ unescaped: "contains", escaped: "contains" },
{ unescaped: "startsWith", escaped: "startsWith" },
{ unescaped: "endsWith", escaped: "endsWith" },
{ unescaped: "matches", escaped: "matches" },
{ unescaped: "duration", escaped: "duration" },
{ unescaped: "timestamp", escaped: "timestamp" },
{ unescaped: "getDate", escaped: "getDate" },
{ unescaped: "getDayOfMonth", escaped: "getDayOfMonth" },
{ unescaped: "getDayOfWeek", escaped: "getDayOfWeek" },
{ unescaped: "getFullYear", escaped: "getFullYear" },
{ unescaped: "getHours", escaped: "getHours" },
{ unescaped: "getMilliseconds", escaped: "getMilliseconds" },
{ unescaped: "getMinutes", escaped: "getMinutes" },
{ unescaped: "getMonth", escaped: "getMonth" },
{ unescaped: "getSeconds", escaped: "getSeconds" },
}
for _, tc := range cases {
t.Run(tc.unescaped, func(t *testing.T) {
e := Escape(tc.unescaped)
if tc.escaped != e {
t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
}
u := Unescape(tc.escaped)
if tc.unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, e)
}
isRootReserved := IsRootReserved(tc.unescaped)
if tc.reservedAtRoot != isRootReserved {
t.Errorf("Expected isRootReserved=%t for %s, but got %t", tc.reservedAtRoot, tc.unescaped, isRootReserved)
}
})
}
}

View File

@ -1,148 +0,0 @@
// 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
}

View File

@ -15,32 +15,14 @@
package model
import (
"fmt"
"sync"
"github.com/google/cel-go/cel"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// 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)
@ -50,25 +32,15 @@ type Resolver interface {
// 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{},
schemas: map[string]*schema.Structural{},
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,
@ -82,44 +54,10 @@ func NewRegistry(stdExprEnv *cel.Env) *Registry {
//
// 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
rwMux sync.RWMutex
exprEnvs map[string]*cel.Env
schemas map[string]*schema.Structural
types map[string]*DeclType
}
// FindType implements the Resolver interface method.
@ -133,53 +71,6 @@ func (r *Registry) FindType(name string) (*DeclType, bool) {
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: '#<simpleName>'.
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()

View File

@ -15,437 +15,151 @@
package model
import (
"strings"
"gopkg.in/yaml.v3"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// 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
// SchemaDeclTypes 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)
func SchemaDeclTypes(s *schema.Structural, maybeRootType string) (*DeclType, map[string]*DeclType) {
root := SchemaDeclType(s).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)
// SchemaDeclType returns the cel type name associated with the schema element.
func SchemaDeclType(s *schema.Structural) *DeclType {
if s == nil {
return nil
}
if s.XIntOrString {
// schemas using this extension are not required to have a type, so they must be handled before type lookup
return intOrStringType
}
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 nil
}
// We ignore XPreserveUnknownFields since we don't support validation rules on
// data that we don't have schema information for.
if s.XEmbeddedResource {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible
// to validation rules since this part of the schema is well known and validated when CRDs
// are created and updated.
s = WithTypeAndObjectMeta(s)
}
switch declType.TypeName() {
case ListType.TypeName():
return NewListType(SchemaDeclType(s.Items))
case MapType.TypeName():
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
return NewMapType(StringType, SchemaDeclType(s.AdditionalProperties.Structural))
}
fields := make(map[string]*DeclField, len(s.Properties))
required := map[string]bool{}
if s.ValueValidation != nil {
for _, f := range s.ValueValidation.Required {
required[f] = true
}
}
for name, prop := range s.Properties {
var enumValues []interface{}
if prop.ValueValidation != nil {
for _, e := range prop.ValueValidation.Enum {
enumValues = append(enumValues, e.Object)
}
}
if fieldType := SchemaDeclType(&prop); fieldType != nil {
fields[Escape(name)] = &DeclField{
Name: Escape(name),
Required: required[name],
Type: fieldType,
defaultValue: prop.Default.Object,
enumValues: enumValues, // Enum values are represented as strings in CEL
}
}
}
return NewObjectType("object", fields)
case StringType.TypeName():
if s.ValueValidation != nil {
switch s.ValueValidation.Format {
case "byte":
return StringType // OpenAPIv3 byte format represents base64 encoded string
case "binary":
return BytesType
case "duration":
return DurationType
case "date", "date-time":
return TimestampType
}
}
}
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{
openAPISchemaTypes = map[string]*DeclType{
"boolean": BoolType,
"number": DoubleType,
"integer": IntType,
"null": NullType,
"string": StringType,
"google-duration": DurationType,
"google-datetime": TimestampType,
"date": TimestampType,
"date-time": TimestampType,
"date": DateType,
"array": ListType,
"object": MapType,
"": AnyType,
}
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as an object where either the srtVal
// or intVal field is set. In CEL, this allows for typesafe expressions like:
//
// require that the string representation be a percentage:
// `has(intOrStringField.strVal) && intOrStringField.strVal.matches(r'(\d+(\.\d+)?%)')`
// validate requirements on both the int and string representation:
// `has(intOrStringField.intVal) ? intOrStringField.intVal < 5 : double(intOrStringField.strVal.replace('%', '')) < 0.5
//
intOrStringType = NewObjectType("intOrString", map[string]*DeclField{
"strVal": {Name: "strVal", Type: StringType},
"intVal": {Name: "intVal", Type: IntType},
})
)
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)
// WithTypeAndObjectMeta ensures the kind, apiVersion and
// metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural {
if s.Properties != nil &&
s.Properties["kind"].Type == "string" &&
s.Properties["apiVersion"].Type == "string" &&
s.Properties["metadata"].Type == "object" &&
s.Properties["metadata"].Properties != nil &&
s.Properties["metadata"].Properties["name"].Type == "string" &&
s.Properties["metadata"].Properties["generateName"].Type == "string" {
return s
}
envSchema = NewOpenAPISchema()
in = strings.ReplaceAll(envSchemaYaml, "\t", " ")
err = yaml.Unmarshal([]byte(in), envSchema)
if err != nil {
panic(err)
result := &schema.Structural{
Generic: s.Generic,
Extensions: s.Extensions,
ValueValidation: s.ValueValidation,
}
schemaDef = NewOpenAPISchema()
in = strings.ReplaceAll(schemaDefYaml, "\t", " ")
err = yaml.Unmarshal([]byte(in), schemaDef)
if err != nil {
panic(err)
props := make(map[string]schema.Structural, len(s.Properties))
for k, prop := range s.Properties {
props[k] = prop
}
templateSchema = NewOpenAPISchema()
in = strings.ReplaceAll(templateSchemaYaml, "\t", " ")
err = yaml.Unmarshal([]byte(in), templateSchema)
if err != nil {
panic(err)
stringType := schema.Structural{Generic: schema.Generic{Type: "string"}}
props["kind"] = stringType
props["apiVersion"] = stringType
props["metadata"] = schema.Structural{
Generic: schema.Generic{Type: "object"},
Properties: map[string]schema.Structural{
"name": stringType,
"generateName": stringType,
},
}
result.Properties = props
return result
}

View File

@ -18,43 +18,43 @@ import (
"reflect"
"testing"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"google.golang.org/protobuf/proto"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
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())
cust := SchemaDeclType(ts)
if cust.TypeName() != "object" {
t.Errorf("incorrect type name, got %v, wanted object", 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)
prop, found := ts.Properties[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 prop.Default.Object != nil {
pdv := types.DefaultTypeAdapter.NativeToValue(prop.Default.Object)
if !reflect.DeepEqual(fdv, pdv) {
t.Errorf("field and schema do not agree on default value, field: %s", f.Name)
t.Errorf("field and schema do not agree on default value for field: %s, field value: %v, schema default: %v", f.Name, fdv, pdv)
}
}
if prop.Enum == nil && len(f.EnumValues()) != 0 {
if (prop.ValueValidation == nil || len(prop.ValueValidation.Enum) == 0) && len(f.EnumValues()) != 0 {
t.Errorf("field had more enum values than the property. field: %s", f.Name)
}
if prop.Enum != nil {
if prop.ValueValidation != 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) {
for _, pev := range prop.ValueValidation.Enum {
celpev := types.DefaultTypeAdapter.NativeToValue(pev.Object)
if reflect.DeepEqual(fev, celpev) {
found = true
break
}
@ -67,27 +67,28 @@ func TestSchemaDeclType(t *testing.T) {
}
}
}
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)
if ts.ValueValidation != nil {
for _, name := range ts.ValueValidation.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")
cust, typeMap := SchemaDeclTypes(ts, "CustomObject")
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,
"CustomObject": cust,
"CustomObject.nested": nested.Type,
"CustomObject.metadata": metadata.Type,
}
objTypeMap := map[string]*DeclType{}
for name, t := range typeMap {
@ -96,7 +97,7 @@ func TestSchemaDeclTypes(t *testing.T) {
}
}
if len(objTypeMap) != len(expectedObjTypeMap) {
t.Errorf("got different type set. got=%v, wanted=%v", typeMap, expectedObjTypeMap)
t.Errorf("got different type set. got=%v, wanted=%v", objTypeMap, expectedObjTypeMap)
}
for exp, expType := range expectedObjTypeMap {
actType, found := objTypeMap[exp]
@ -108,17 +109,9 @@ func TestSchemaDeclTypes(t *testing.T) {
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 {
func testSchema() *schema.Structural {
// Manual construction of a schema with the following definition:
//
// schema:
@ -160,44 +153,86 @@ func testSchema() *OpenAPISchema {
// 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
ts := &schema.Structural{
Generic: schema.Generic{
Type: "object",
},
Properties: map[string]schema.Structural{
"name": {
Generic: schema.Generic{
Type: "string",
},
},
"value": {
Generic: schema.Generic{
Type: "integer",
Default: schema.JSON{Object: int64(1)},
},
ValueValidation: &schema.ValueValidation{
Format: "int64",
Enum: []schema.JSON{{Object: int64(1)}, {Object: int64(2)}, {Object: int64(3)}},
},
},
"nested": {
Generic: schema.Generic{
Type: "object",
},
Properties: map[string]schema.Structural{
"subname": {
Generic: schema.Generic{
Type: "string",
},
},
"flags": {
Generic: schema.Generic{
Type: "object",
AdditionalProperties: &schema.StructuralOrBool{
Structural: &schema.Structural{
Generic: schema.Generic{
Type: "boolean",
},
},
},
},
},
"dates": {
Generic: schema.Generic{
Type: "array",
},
Items: &schema.Structural{
Generic: schema.Generic{
Type: "string",
},
ValueValidation: &schema.ValueValidation{
Format: "date-time",
},
},
},
},
},
"metadata": {
Generic: schema.Generic{
Type: "object",
},
Properties: map[string]schema.Structural{
"name": {
Generic: schema.Generic{
Type: "string",
},
},
"value": {
Generic: schema.Generic{
Type: "array",
},
Items: &schema.Structural{
Generic: schema.Generic{
Type: "string",
},
},
},
},
},
},
}
return ts
}

View File

@ -1,190 +0,0 @@
// 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
}

View File

@ -1,159 +0,0 @@
// 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
}

View File

@ -27,6 +27,7 @@ import (
"google.golang.org/protobuf/proto"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// NewListType returns a parameterized list type with a specified element type.
@ -92,7 +93,7 @@ func newSimpleType(name string, exprType *exprpb.Type, zeroVal ref.Val) *DeclTyp
}
}
// DeclType represents the universal type descriptor for Policy Templates.
// DeclType represents the universal type descriptor for OpenAPIv3 types.
type DeclType struct {
fmt.Stringer
@ -304,7 +305,7 @@ func (f *DeclField) EnumValues() []ref.Val {
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
func NewRuleTypes(kind string,
schema *OpenAPISchema,
schema *schema.Structural,
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
@ -325,13 +326,13 @@ func NewRuleTypes(kind string,
// type-system.
type RuleTypes struct {
ref.TypeProvider
Schema *OpenAPISchema
Schema *schema.Structural
ruleSchemaDeclTypes *schemaTypeProvider
typeAdapter ref.TypeAdapter
resolver Resolver
}
// EnvOptions returns a set of cel.EnvOption values which includes the Template's declaration set
// EnvOptions returns a set of cel.EnvOption values which includes the 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
@ -358,7 +359,7 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
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)
"type %s definition differs between CEL environment and rule", name)
}
}
return []cel.EnvOption{
@ -370,7 +371,7 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
}, nil
}
// FindType attempts to resolve the typeName provided from the template's rule-schema, or if not
// FindType attempts to resolve the typeName provided from the rule's rule-schema, or if not
// from the embedded ref.TypeProvider.
//
// FindType overrides the default type-finding behavior of the embedded TypeProvider.
@ -425,25 +426,10 @@ func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType,
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.
// of rule 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)
}
return rt.typeAdapter.NativeToValue(val)
}
// TypeNames returns the list of type names declared within the RuleTypes object.
@ -499,8 +485,8 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
}
}
func newSchemaTypeProvider(kind string, schema *OpenAPISchema) (*schemaTypeProvider, error) {
root := schema.DeclType().MaybeAssignTypeName(kind)
func newSchemaTypeProvider(kind string, schema *schema.Structural) (*schemaTypeProvider, error) {
root := SchemaDeclType(schema).MaybeAssignTypeName(kind)
types := FieldTypeMap(kind, root)
return &schemaTypeProvider{
root: root,
@ -515,7 +501,7 @@ type schemaTypeProvider struct {
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.
// types supported.
AnyType = newSimpleType("any", decls.Any, nil)
// BoolType is equivalent to the CEL 'bool' type.
@ -530,6 +516,9 @@ var (
// DurationType is equivalent to the CEL 'duration' type.
DurationType = newSimpleType("duration", decls.Duration, types.Duration{Duration: time.Duration(0)})
// DateType is equivalent to the CEL 'date' type.
DateType = newSimpleType("date", decls.Timestamp, types.Timestamp{Time: time.Time{}})
// 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)
@ -544,10 +533,6 @@ var (
// 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{}})

View File

@ -67,10 +67,11 @@ func TestTypes_MapType(t *testing.T) {
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
stdEnv, _ := cel.NewEnv()
reg := NewRegistry(stdEnv)
rt, err := NewRuleTypes("mock_template", testSchema(), reg)
rt, err := NewRuleTypes("CustomObject", testSchema(), reg)
if err != nil {
t.Fatal(err)
}
rt.TypeProvider = stdEnv.TypeProvider()
nestedFieldType, found := rt.FindFieldType("CustomObject", "nested")
if !found {
t.Fatal("got field not found for 'CustomObject.nested', wanted found")
@ -119,17 +120,17 @@ func TestTypes_RuleTypesFieldMapping(t *testing.T) {
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")
}
//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 {

View File

@ -45,13 +45,6 @@ const (
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.
@ -158,10 +151,6 @@ func exprValue(value interface{}) (ref.Val, *DeclType, error) {
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:

View File

@ -35,8 +35,6 @@ func TestConvertToType(t *testing.T) {
{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},
@ -63,8 +61,7 @@ func TestConvertToType(t *testing.T) {
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),
true, []byte("bytes"), float64(1.2), int64(-42), uint64(63), time.Duration(300),
time.Now().UTC(), types.NullValue, NewListValue(), NewMapValue(),
NewObjectValue(NewObjectType("TestObject", map[string]*DeclField{})),
}
@ -327,7 +324,7 @@ 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: "<eom>"},
"message": &DeclField{Name: "message", Type: StringType, defaultValue: "<eom>"},
})
name := NewField(1, "name")
name.Ref = testValue(t, 2, "alert")