Skip to content

Commit

Permalink
Initial work for supporting ValidatingAdmissionPolicy
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Conner <[email protected]>
  • Loading branch information
knrc committed Oct 3, 2023
1 parent cb05301 commit d1ebc8f
Show file tree
Hide file tree
Showing 21 changed files with 1,730 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ serve: ## Serve static files.
.PHONY: update-data
update-data: ## Update the web/assets/data.json file.
yq -ojson '.' examples.yaml > web/assets/data.json
yq -ojson '.' validating_examples.yaml > web/assets/validating_data.json
yq -ojson -i '.versions.cel-go = "$(CEL_GO_VERSION)"' web/assets/data.json
yq -ojson -i '.versions.cel-go = "$(CEL_GO_VERSION)"' web/assets/validating_data.json

.PHONY: addlicense
addlicense: ## Add copyright license headers in source code files.
Expand Down
29 changes: 26 additions & 3 deletions cmd/wasm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,22 @@ import (
"gopkg.in/yaml.v3"

"github.com/undistro/cel-playground/eval"
"github.com/undistro/cel-playground/k8s"
)

func main() {
evalFunc := js.FuncOf(evalWrapper)
js.Global().Set("eval", evalFunc)
defer evalFunc.Release()

defer addFunction("eval", evalWrapper).Release()
defer addFunction("vapEval", validatingAdmissionPolicyWrapper).Release()
<-make(chan bool)
}

func addFunction(name string, fn func(js.Value, []js.Value) any) js.Func {
function := js.FuncOf(fn)
js.Global().Set(name, function)
return function
}

// evalWrapper wraps the eval function with `syscall/js` parameters
func evalWrapper(_ js.Value, args []js.Value) any {
if len(args) < 2 {
Expand All @@ -52,6 +59,22 @@ func evalWrapper(_ js.Value, args []js.Value) any {
return response(output, nil)
}

// ValidatingAdmissionPolicy functionality
func validatingAdmissionPolicyWrapper(_ js.Value, args []js.Value) any {
if len(args) < 3 {
return response("", errors.New("invalid arguments"))
}
policy := []byte(args[0].String())
originalValue := []byte(args[1].String())
updatedValue := []byte(args[2].String())

output, err := k8s.EvalValidatingAdmissionPolicy(policy, originalValue, updatedValue)
if err != nil {
return response("", err)
}
return response(output, nil)
}

func response(out string, err error) any {
if err != nil {
out = err.Error()
Expand Down
97 changes: 97 additions & 0 deletions k8s/evals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2023 Undistro 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 k8s

import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter"
)

type lazyVariableEval struct {
name string
ast *cel.Ast
val *ref.Val
}

func (lve *lazyVariableEval) eval(env *cel.Env, activation interpreter.Activation) ref.Val {
val := lve.evalExpression(env, activation)
lve.val = &val
return val
}

func (lve *lazyVariableEval) evalExpression(env *cel.Env, activation interpreter.Activation) ref.Val {
prog, err := env.Program(lve.ast, celProgramOptions...)
if err != nil {
return types.NewErr("Unexpected error parsing expression %s: %v", lve.name, err)
}
val, _, err := prog.Eval(activation)
if err != nil {
return types.NewErr("Unexpected error parsing expression %s: %v", lve.name, err)
}
return val
}

type EvalVariable struct {
Name string `json:"name"`
Value any `json:"value"`
Error *string `json:"error"`
}

type EvalValidation struct {
Result any `json:"result"`
Error *string `json:"error"`
}

type EvalResponse struct {
Variables []EvalVariable `json:"variables,omitempty"`
Validations []EvalValidation `json:"validations,omitempty"`
}

func getResults(val *ref.Val) (any, *string) {
value := (*val).Value()
if err, ok := value.(error); ok {
errResponse := err.Error()
return nil, &errResponse
}
return value, nil
}

func generateResponse(variableLazyEvals map[string]*lazyVariableEval, validationEvals []ref.Val) *EvalResponse {
variables := []EvalVariable{}
for _, varLazyEval := range variableLazyEvals {
if varLazyEval.val != nil {
value, err := getResults(varLazyEval.val)
variables = append(variables, EvalVariable{
Name: varLazyEval.name,
Value: value,
Error: err,
})
}
}
validations := []EvalValidation{}
for _, validationEval := range validationEvals {
value, err := getResults(&validationEval)
validations = append(validations, EvalValidation{
Result: value,
Error: err,
})
}
return &EvalResponse{
Variables: variables,
Validations: validations,
}
}
129 changes: 129 additions & 0 deletions k8s/extractcelinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2023 Undistro 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 k8s

import (
"fmt"
"reflect"

"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
)

type MatchCondition struct {
name string
expression string
}

type CelVariableInfo struct {
name string
expression string
}

type CelValidationInfo struct {
expression string
message string
messageExpression string
}

type CelInformation struct {
name string
namespace string
variables []CelVariableInfo
validations []CelValidationInfo
}

func deserialize(data []byte) (runtime.Object, error) {
decoder := scheme.Codecs.UniversalDeserializer()

runtimeObject, _, err := decoder.Decode(data, nil, nil)
if err != nil {
return nil, err
}

return runtimeObject, nil
}

func extractCelInformation(policyInput []byte) (*CelInformation, error) {
if deser, err := deserialize(policyInput); err != nil {
return nil, fmt.Errorf("failed to decode ValidatingAdmissionPolicy: %w", err)
} else {
switch policy := deser.(type) {
case *v1alpha1.ValidatingAdmissionPolicy:
return extractV1Alpha1CelInformation(policy), nil
case *v1beta1.ValidatingAdmissionPolicy:
return extractV1Beta1CelInformation(policy), nil
default:
policyType := reflect.TypeOf(deser)
return nil, fmt.Errorf("expected ValidatingAdmissionPolicy, received %s", policyType.Kind())
}
}
}

func extractV1Alpha1CelInformation(policy *v1alpha1.ValidatingAdmissionPolicy) *CelInformation {
namespace := policy.ObjectMeta.GetNamespace()
name := policy.ObjectMeta.GetName()

variables := []CelVariableInfo{}
for _, variable := range policy.Spec.Variables {
variables = append(variables, CelVariableInfo{
name: variable.Name,
expression: variable.Expression,
})
}

validations := []CelValidationInfo{}
for _, validation := range policy.Spec.Validations {

validations = append(validations, CelValidationInfo{
expression: validation.Expression,
message: validation.Message,
messageExpression: validation.MessageExpression,
})
}

return &CelInformation{
name, namespace, variables, validations,
}
}

func extractV1Beta1CelInformation(policy *v1beta1.ValidatingAdmissionPolicy) *CelInformation {
namespace := policy.ObjectMeta.GetNamespace()
name := policy.ObjectMeta.GetName()

variables := []CelVariableInfo{}
for _, variable := range policy.Spec.Variables {
variables = append(variables, CelVariableInfo{
name: variable.Name,
expression: variable.Expression,
})
}

validations := []CelValidationInfo{}
for _, validation := range policy.Spec.Validations {

validations = append(validations, CelValidationInfo{
expression: validation.Expression,
message: validation.Message,
messageExpression: validation.MessageExpression,
})
}

return &CelInformation{
name, namespace, variables, validations,
}
}
15 changes: 15 additions & 0 deletions k8s/testdata/policy1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "force-ha-in-prod"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas >= 3"
message: "All production deployments should be HA with at least three replicas"
14 changes: 14 additions & 0 deletions k8s/testdata/policy2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.example.com"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 5"
43 changes: 43 additions & 0 deletions k8s/testdata/updated1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: "2023-10-02T15:26:06Z"
generation: 1
labels:
app: kubernetes-bootcamp
name: kubernetes-bootcamp
namespace: default
resourceVersion: "246826"
uid: dcdda63b-1611-467d-8927-43e3c73bc963
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: kubernetes-bootcamp
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: kubernetes-bootcamp
spec:
containers:
- image: gcr.io/google-samples/kubernetes-bootcamp:v1
imagePullPolicy: IfNotPresent
name: kubernetes-bootcamp
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
Loading

0 comments on commit d1ebc8f

Please sign in to comment.