From 1c3bddf8ca599452362dbbd902f6179b57936512 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?=
Date: Mon, 10 Mar 2025 14:28:44 +0100
Subject: [PATCH] feat: support mock in CLI for VPs (#12344)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: support mock in CLI for VPs
Signed-off-by: Charles-Edouard Brétéché
* implement get cm mock
Signed-off-by: Charles-Edouard Brétéché
* move into cel package
Signed-off-by: Charles-Edouard Brétéché
---------
Signed-off-by: Charles-Edouard Brétéché
---
.../kubectl-kyverno/apis/v1alpha1/context.go | 22 ++
.../kubectl-kyverno/commands/apply/command.go | 23 +-
.../commands/apply/command_test.go | 34 +++
.../config/crds/cli.kyverno.io_contexts.yaml | 50 +++++
cmd/cli/kubectl-kyverno/context/load.go | 34 +++
cmd/cli/kubectl-kyverno/context/load_test.go | 149 +++++++++++++
.../data/crds/cli.kyverno.io_contexts.yaml | 50 +++++
docs/user/cli/commands/kyverno_apply.md | 1 +
docs/user/cli/crd/index.html | 108 +++++++++
.../cli/crd/kyverno_kubectl.v1alpha1.html | 208 ++++++++++++++++++
pkg/cel/policy/fake_context.go | 84 +++++++
.../policy-with-cm/context.yaml | 14 ++
.../policy-with-cm/pod1.yaml | 14 ++
.../policy-with-cm/pod2.yaml | 15 ++
.../policy-with-cm/policy.yaml | 18 ++
15 files changed, 823 insertions(+), 1 deletion(-)
create mode 100644 cmd/cli/kubectl-kyverno/apis/v1alpha1/context.go
create mode 100644 cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_contexts.yaml
create mode 100644 cmd/cli/kubectl-kyverno/context/load.go
create mode 100644 cmd/cli/kubectl-kyverno/context/load_test.go
create mode 100644 cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_contexts.yaml
create mode 100644 pkg/cel/policy/fake_context.go
create mode 100644 test/cli/test-validating-policy/policy-with-cm/context.yaml
create mode 100644 test/cli/test-validating-policy/policy-with-cm/pod1.yaml
create mode 100644 test/cli/test-validating-policy/policy-with-cm/pod2.yaml
create mode 100644 test/cli/test-validating-policy/policy-with-cm/policy.yaml
diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/context.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/context.go
new file mode 100644
index 0000000000..59af21eb27
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/context.go
@@ -0,0 +1,22 @@
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+// +genclient
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:scope="Cluster"
+
+// Values declares values to be loaded by the Kyverno CLI
+type Context struct {
+ metav1.TypeMeta `json:",inline,omitempty"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ ContextSpec `json:"spec"`
+}
+
+type ContextSpec struct {
+ Resources []unstructured.Unstructured `json:"resources,omitempty"`
+}
diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command.go b/cmd/cli/kubectl-kyverno/commands/apply/command.go
index 4ede72c356..3590398c1b 100644
--- a/cmd/cli/kubectl-kyverno/commands/apply/command.go
+++ b/cmd/cli/kubectl-kyverno/commands/apply/command.go
@@ -16,6 +16,7 @@ import (
kyvernov2 "github.com/kyverno/kyverno/api/kyverno/v2"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command"
+ clicontext "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/context"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/data"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/deprecations"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/exception"
@@ -65,6 +66,7 @@ type ApplyCommandConfig struct {
Variables []string
ValuesFile string
UserInfoPath string
+ ContextPath string
Cluster bool
PolicyReport bool
OutputFormat string
@@ -162,6 +164,7 @@ func Command() *cobra.Command {
cmd.Flags().StringVarP(&applyCommandConfig.UserInfoPath, "userinfo", "u", "", "Admission Info including Roles, Cluster Roles and Subjects")
cmd.Flags().StringSliceVarP(&applyCommandConfig.Variables, "set", "s", nil, "Variables that are required")
cmd.Flags().StringVarP(&applyCommandConfig.ValuesFile, "values-file", "f", "", "File containing values for policy variables")
+ cmd.Flags().StringVarP(&applyCommandConfig.ContextPath, "context-file", "", "", "File containing context data for CEL policies")
cmd.Flags().BoolVarP(&applyCommandConfig.PolicyReport, "policy-report", "p", false, "Generates policy report when passed (default policyviolation)")
cmd.Flags().StringVarP(&applyCommandConfig.OutputFormat, "output-format", "", "yaml", "Specifies the policy report format (json or yaml). Default: yaml.")
cmd.Flags().StringVarP(&applyCommandConfig.Namespace, "namespace", "n", "", "Optional Policy parameter passed with cluster flag")
@@ -373,6 +376,24 @@ func (c *ApplyCommandConfig) applyValidatingPolicies(
return nil, err
}
restMapper = restmapper.NewDiscoveryRESTMapper(apiGroupResources)
+ fakeContextProvider := celpolicy.NewFakeContextProvider()
+ if c.ContextPath != "" {
+ ctx, err := clicontext.Load(nil, c.ContextPath)
+ if err != nil {
+ return nil, err
+ }
+ for _, resource := range ctx.ContextSpec.Resources {
+ gvk := resource.GroupVersionKind()
+ mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+ if err != nil {
+ return nil, err
+ }
+ if err := fakeContextProvider.AddResource(mapping.Resource, &resource); err != nil {
+ return nil, err
+ }
+ }
+ }
+ contextProvider = fakeContextProvider
}
responses := make([]engineapi.EngineResponse, 0)
responsesTemp := make([]engine.EngineResponse, 0)
@@ -398,7 +419,7 @@ func (c *ApplyCommandConfig) applyValidatingPolicies(
"",
resource.GetName(),
resource.GetNamespace(),
- // TODO
+ // TODO: how to manage other operations ?
admissionv1.Create,
resource,
nil,
diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command_test.go b/cmd/cli/kubectl-kyverno/commands/apply/command_test.go
index 9be483d858..a3adc1c4ed 100644
--- a/cmd/cli/kubectl-kyverno/commands/apply/command_test.go
+++ b/cmd/cli/kubectl-kyverno/commands/apply/command_test.go
@@ -586,6 +586,40 @@ func Test_Apply_ValidatingPolicies(t *testing.T) {
},
}},
},
+ {
+ config: ApplyCommandConfig{
+ PolicyPaths: []string{"../../../../../test/cli/test-validating-policy/policy-with-cm/policy.yaml"},
+ ResourcePaths: []string{"../../../../../test/cli/test-validating-policy/policy-with-cm/pod1.yaml"},
+ ContextPath: "../../../../../test/cli/test-validating-policy/policy-with-cm/context.yaml",
+ PolicyReport: true,
+ },
+ expectedPolicyReports: []policyreportv1alpha2.PolicyReport{{
+ Summary: policyreportv1alpha2.PolicyReportSummary{
+ Pass: 1,
+ Fail: 0,
+ Skip: 0,
+ Error: 0,
+ Warn: 0,
+ },
+ }},
+ },
+ {
+ config: ApplyCommandConfig{
+ PolicyPaths: []string{"../../../../../test/cli/test-validating-policy/policy-with-cm/policy.yaml"},
+ ResourcePaths: []string{"../../../../../test/cli/test-validating-policy/policy-with-cm/pod2.yaml"},
+ ContextPath: "../../../../../test/cli/test-validating-policy/policy-with-cm/context.yaml",
+ PolicyReport: true,
+ },
+ expectedPolicyReports: []policyreportv1alpha2.PolicyReport{{
+ Summary: policyreportv1alpha2.PolicyReportSummary{
+ Pass: 0,
+ Fail: 1,
+ Skip: 0,
+ Error: 0,
+ Warn: 0,
+ },
+ }},
+ },
}
compareSummary := func(expected policyreportv1alpha2.PolicyReportSummary, actual policyreportv1alpha2.PolicyReportSummary, desc string) {
diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_contexts.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_contexts.yaml
new file mode 100644
index 0000000000..3695c6d08f
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_contexts.yaml
@@ -0,0 +1,50 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: (devel)
+ name: contexts.cli.kyverno.io
+spec:
+ group: cli.kyverno.io
+ names:
+ kind: Context
+ listKind: ContextList
+ plural: contexts
+ singular: context
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: Values declares values to be loaded by the Kyverno CLI
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ resources:
+ items:
+ type: object
+ type: array
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
diff --git a/cmd/cli/kubectl-kyverno/context/load.go b/cmd/cli/kubectl-kyverno/context/load.go
new file mode 100644
index 0000000000..fbab0828a4
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/context/load.go
@@ -0,0 +1,34 @@
+package context
+
+import (
+ "io"
+ "os"
+
+ "github.com/go-git/go-billy/v5"
+ "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/v1alpha1"
+ "k8s.io/apimachinery/pkg/util/yaml"
+)
+
+func Load(f billy.Filesystem, filepath string) (*v1alpha1.Context, error) {
+ yamlBytes, err := readFile(f, filepath)
+ if err != nil {
+ return nil, err
+ }
+ vals := &v1alpha1.Context{}
+ if err := yaml.UnmarshalStrict(yamlBytes, vals); err != nil {
+ return nil, err
+ }
+ return vals, nil
+}
+
+func readFile(f billy.Filesystem, filepath string) ([]byte, error) {
+ if f != nil {
+ file, err := f.Open(filepath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ return io.ReadAll(file)
+ }
+ return os.ReadFile(filepath)
+}
diff --git a/cmd/cli/kubectl-kyverno/context/load_test.go b/cmd/cli/kubectl-kyverno/context/load_test.go
new file mode 100644
index 0000000000..4bd2fc39d2
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/context/load_test.go
@@ -0,0 +1,149 @@
+package context
+
+// import (
+// "os"
+// "reflect"
+// "testing"
+
+// "github.com/go-git/go-billy/v5"
+// "github.com/go-git/go-billy/v5/memfs"
+// "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/v1alpha1"
+// )
+
+// func Test_readFile(t *testing.T) {
+// mustReadFile := func(path string) []byte {
+// t.Helper()
+// data, err := os.ReadFile(path)
+// if err != nil {
+// t.Fatal(err)
+// }
+// return data
+// }
+// tests := []struct {
+// name string
+// f billy.Filesystem
+// filepath string
+// want []byte
+// wantErr bool
+// }{{
+// name: "empty",
+// filepath: "",
+// want: nil,
+// wantErr: true,
+// }, {
+// name: "does not exist",
+// filepath: "../_testdata/values/doesnotexist",
+// want: nil,
+// wantErr: true,
+// }, {
+// name: "bad format",
+// filepath: "../_testdata/values/bad-format.yaml",
+// want: mustReadFile("../_testdata/values/bad-format.yaml"),
+// wantErr: false,
+// }, {
+// name: "valid",
+// filepath: "../_testdata/values/limit-configmap-for-sa.yaml",
+// want: mustReadFile("../_testdata/values/limit-configmap-for-sa.yaml"),
+// wantErr: false,
+// }, {
+// name: "empty (billy)",
+// f: memfs.New(),
+// filepath: "",
+// want: nil,
+// wantErr: true,
+// }, {
+// name: "valid (billy)",
+// f: func() billy.Filesystem {
+// f := memfs.New()
+// file, err := f.Create("valid.yaml")
+// if err != nil {
+// t.Fatal(err)
+// }
+// defer file.Close()
+// if _, err := file.Write([]byte("foo: bar")); err != nil {
+// t.Fatal(err)
+// }
+// return f
+// }(),
+// filepath: "valid.yaml",
+// want: []byte("foo: bar"),
+// wantErr: false,
+// }}
+// for _, tt := range tests {
+// t.Run(tt.name, func(t *testing.T) {
+// got, err := readFile(tt.f, tt.filepath)
+// if (err != nil) != tt.wantErr {
+// t.Errorf("readFile() error = %v, wantErr %v", err, tt.wantErr)
+// return
+// }
+// if !reflect.DeepEqual(got, tt.want) {
+// t.Errorf("readFile() = %v, want %v", got, tt.want)
+// }
+// })
+// }
+// }
+
+// func TestLoad(t *testing.T) {
+// tests := []struct {
+// name string
+// f billy.Filesystem
+// filepath string
+// want *v1alpha1.Values
+// wantErr bool
+// }{{
+// name: "empty",
+// filepath: "",
+// want: nil,
+// wantErr: true,
+// }, {
+// name: "does not exist",
+// filepath: "../_testdata/values/doesnotexist",
+// want: nil,
+// wantErr: true,
+// }, {
+// name: "bad format",
+// filepath: "../_testdata/values/bad-format.yaml",
+// want: nil,
+// wantErr: true,
+// }, {
+// name: "valid",
+// filepath: "../_testdata/values/limit-configmap-for-sa.yaml",
+// want: &v1alpha1.Values{
+// ValuesSpec: v1alpha1.ValuesSpec{
+// NamespaceSelectors: []v1alpha1.NamespaceSelector{{
+// Name: "test1",
+// Labels: map[string]string{
+// "foo.com/managed-state": "managed",
+// },
+// }},
+// Policies: []v1alpha1.Policy{{
+// Name: "limit-configmap-for-sa",
+// Resources: []v1alpha1.Resource{{
+// Name: "any-configmap-name-good",
+// Values: map[string]interface{}{
+// "request.operation": "UPDATE",
+// },
+// }, {
+// Name: "any-configmap-name-bad",
+// Values: map[string]interface{}{
+// "request.operation": "UPDATE",
+// },
+// }},
+// }},
+// },
+// },
+// wantErr: false,
+// }}
+// for _, tt := range tests {
+// t.Run(tt.name, func(t *testing.T) {
+// got, err := Load(tt.f, tt.filepath)
+// if (err != nil) != tt.wantErr {
+// t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
+// return
+// }
+// if !reflect.DeepEqual(got, tt.want) {
+// t.Errorf("Load() = %v, want %v", got, tt.want)
+// }
+// })
+// }
+// }
diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_contexts.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_contexts.yaml
new file mode 100644
index 0000000000..3695c6d08f
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_contexts.yaml
@@ -0,0 +1,50 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: (devel)
+ name: contexts.cli.kyverno.io
+spec:
+ group: cli.kyverno.io
+ names:
+ kind: Context
+ listKind: ContextList
+ plural: contexts
+ singular: context
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: Values declares values to be loaded by the Kyverno CLI
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ resources:
+ items:
+ type: object
+ type: array
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
diff --git a/docs/user/cli/commands/kyverno_apply.md b/docs/user/cli/commands/kyverno_apply.md
index 0cca1507dc..48369fe910 100644
--- a/docs/user/cli/commands/kyverno_apply.md
+++ b/docs/user/cli/commands/kyverno_apply.md
@@ -40,6 +40,7 @@ kyverno apply [flags]
--audit-warn If set to true, will flag audit policies as warnings instead of failures
-c, --cluster Checks if policies should be applied to cluster in the current context
--context string The name of the kubeconfig context to use
+ --context-file string File containing context data for CEL policies
--continue-on-fail If set to true, will continue to apply policies on the next resource upon failure to apply to the current resource instead of exiting out
--detailed-results If set to true, display detailed results
-e, --exception strings Policy exception to be considered when evaluating policies against resources
diff --git a/docs/user/cli/crd/index.html b/docs/user/cli/crd/index.html
index bdf69d3a59..a8d60a307d 100644
--- a/docs/user/cli/crd/index.html
+++ b/docs/user/cli/crd/index.html
@@ -25,6 +25,8 @@ background-color: #1589dd;
cli.kyverno.io/v1alpha1
Resource Types:
+Context
+
+
+
Values declares values to be loaded by the Kyverno CLI
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+apiVersion
+string |
+
+
+cli.kyverno.io/v1alpha1
+
+ |
+
+
+
+kind
+string
+ |
+Context |
+
+
+
+metadata
+
+
+Kubernetes meta/v1.ObjectMeta
+
+
+ |
+
+Refer to the Kubernetes API documentation for the fields of the
+metadata field.
+ |
+
+
+
+spec
+
+
+ContextSpec
+
+
+ |
+
+
+
+
+ |
+
+
+
+
Test
@@ -426,6 +503,37 @@ github.com/kyverno/kyverno-json/pkg/apis/policy/v1alpha1.Any
+ContextSpec
+
+
+(Appears on:
+Context)
+
+
+
+
+
NamespaceSelector
diff --git a/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html b/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html
index da72cfd14a..71513918fa 100644
--- a/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html
+++ b/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html
@@ -25,6 +25,8 @@
Resource Types: