1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-23 00:01:55 +00:00

feat: support mock in CLI for VPs (#12344)

* feat: support mock in CLI for VPs

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* implement get cm mock

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* move into cel package

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2025-03-10 14:28:44 +01:00 committed by GitHub
parent 0e76c0ed4b
commit 1c3bddf8ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 823 additions and 1 deletions

View file

@ -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"`
}

View file

@ -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,

View file

@ -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) {

View file

@ -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

View file

@ -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)
}

View file

@ -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)
// }
// })
// }
// }

View file

@ -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

View file

@ -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

View file

@ -25,6 +25,8 @@ background-color: #1589dd;
<h2 id="cli.kyverno.io/v1alpha1">cli.kyverno.io/v1alpha1</h2>
Resource Types:
<ul><li>
<a href="#cli.kyverno.io/v1alpha1.Context">Context</a>
</li><li>
<a href="#cli.kyverno.io/v1alpha1.Test">Test</a>
</li><li>
<a href="#cli.kyverno.io/v1alpha1.UserInfo">UserInfo</a>
@ -32,6 +34,81 @@ Resource Types:
<a href="#cli.kyverno.io/v1alpha1.Values">Values</a>
</li></ul>
<hr />
<h3 id="cli.kyverno.io/v1alpha1.Context">Context
</h3>
<p>
<p>Values declares values to be loaded by the Kyverno CLI</p>
</p>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiVersion</code><br/>
string</td>
<td>
<code>
cli.kyverno.io/v1alpha1
</code>
</td>
</tr>
<tr>
<td>
<code>kind</code><br/>
string
</td>
<td><code>Context</code></td>
</tr>
<tr>
<td>
<code>metadata</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#objectmeta-v1-meta">
Kubernetes meta/v1.ObjectMeta
</a>
</em>
</td>
<td>
Refer to the Kubernetes API documentation for the fields of the
<code>metadata</code> field.
</td>
</tr>
<tr>
<td>
<code>spec</code><br/>
<em>
<a href="#cli.kyverno.io/v1alpha1.ContextSpec">
ContextSpec
</a>
</em>
</td>
<td>
<br/>
<br/>
<table class="table table-striped">
<tr>
<td>
<code>resources</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#unstructured-unstructured-v1">
[]Kubernetes meta/v1/unstructured.Unstructured
</a>
</em>
</td>
<td>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<hr />
<h3 id="cli.kyverno.io/v1alpha1.Test">Test
</h3>
<p>
@ -426,6 +503,37 @@ github.com/kyverno/kyverno-json/pkg/apis/policy/v1alpha1.Any
</tbody>
</table>
<hr />
<h3 id="cli.kyverno.io/v1alpha1.ContextSpec">ContextSpec
</h3>
<p>
(<em>Appears on:</em>
<a href="#cli.kyverno.io/v1alpha1.Context">Context</a>)
</p>
<p>
</p>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>resources</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#unstructured-unstructured-v1">
[]Kubernetes meta/v1/unstructured.Unstructured
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<hr />
<h3 id="cli.kyverno.io/v1alpha1.NamespaceSelector">NamespaceSelector
</h3>
<p>

View file

@ -25,6 +25,8 @@
<h3>Resource Types:</h3>
<ul><li>
<a href="#cli-kyverno-io-v1alpha1-Context">Context</a>
</li><li>
<a href="#cli-kyverno-io-v1alpha1-Test">Test</a>
</li><li>
<a href="#cli-kyverno-io-v1alpha1-UserInfo">UserInfo</a>
@ -34,6 +36,149 @@
<H3 id="cli-kyverno-io-v1alpha1-Context">Context
</H3>
<p><p>Values declares values to be loaded by the Kyverno CLI</p>
</p>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>apiVersion</code></br>string</td>
<td><code>cli.kyverno.io/v1alpha1</code></td>
</tr>
<tr>
<td><code>kind</code></br>string</td>
<td><code>Context</code></td>
</tr>
<tr>
<td><code>metadata</code>
<span style="color:blue;"> *</span>
</br>
<span style="font-family: monospace">meta/v1.ObjectMeta</span>
</td>
<td>
Refer to the Kubernetes API documentation for the fields of the
<code>metadata</code> field.
</td>
</tr>
<tr>
<td><code>spec</code>
<span style="color:blue;"> *</span>
</br>
<a href="#cli-kyverno-io-v1alpha1-ContextSpec">
<span style="font-family: monospace">ContextSpec</span>
</a>
</td>
<td>
<br/>
<br/>
<table>
<tr>
<td><code>resources</code>
<span style="color:blue;"> *</span>
</br>
<span style="font-family: monospace">[]meta/v1/unstructured.Unstructured</span>
</td>
<td>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<H3 id="cli-kyverno-io-v1alpha1-Test">Test
</H3>
@ -856,6 +1001,69 @@ This field is deprecated, use <code>metadata.name</code> instead</p>
</td>
</tr>
</tbody>
</table>
<H3 id="cli-kyverno-io-v1alpha1-ContextSpec">ContextSpec
</H3>
<p>
(<em>Appears in:</em>
<a href="#cli-kyverno-io-v1alpha1-Context">Context</a>)
</p>
<p></p>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>resources</code>
<span style="color:blue;"> *</span>
</br>
<span style="font-family: monospace">[]meta/v1/unstructured.Unstructured</span>
</td>
<td>
</td>
</tr>

View file

@ -0,0 +1,84 @@
package policy
import (
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type FakeContextProvider struct {
resources map[string]map[string]map[string]*unstructured.Unstructured
}
func NewFakeContextProvider() *FakeContextProvider {
return &FakeContextProvider{
resources: map[string]map[string]map[string]*unstructured.Unstructured{},
}
}
func (cp *FakeContextProvider) AddResource(gvr schema.GroupVersionResource, obj runtime.Object) error {
object, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return err
}
resource := &unstructured.Unstructured{Object: object}
resources := cp.resources[gvr.String()]
if resources == nil {
resources = map[string]map[string]*unstructured.Unstructured{}
cp.resources[gvr.String()] = resources
}
namespace := resources[resource.GetNamespace()]
if namespace == nil {
namespace = map[string]*unstructured.Unstructured{}
resources[resource.GetNamespace()] = namespace
}
namespace[resource.GetName()] = resource
return nil
}
func (cp *FakeContextProvider) GetConfigMap(ns, n string) (unstructured.Unstructured, error) {
cm, err := cp.GetResource("v1", "configmaps", ns, n)
if err != nil {
return unstructured.Unstructured{}, err
}
return *cm, nil
}
func (cp *FakeContextProvider) GetGlobalReference(string, string) (any, error) {
panic("not implemented")
}
func (cp *FakeContextProvider) GetImageData(string) (*imagedataloader.ImageData, error) {
panic("not implemented")
}
func (cp *FakeContextProvider) ParseImageReference(string) (imagedataloader.ImageReference, error) {
panic("not implemented")
}
func (cp *FakeContextProvider) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
panic("not implemented")
}
func (cp *FakeContextProvider) GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) {
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return nil, err
}
gvr := gv.WithResource(resource)
resources := cp.resources[gvr.String()]
if resources == nil {
return nil, kerrors.NewNotFound(gvr.GroupResource(), name)
}
namespaced := resources[namespace]
if namespaced == nil {
return nil, kerrors.NewNotFound(gvr.GroupResource(), name)
}
resourced := namespaced[name]
if resourced == nil {
return nil, kerrors.NewNotFound(gvr.GroupResource(), name)
}
return resourced, nil
}

View file

@ -0,0 +1,14 @@
apiVersion: cli.kyverno.io/v1alpha1
kind: Context
metadata:
name: kyverno-test.yaml
spec:
resources:
- apiVersion: v1
kind: ConfigMap
metadata:
namespace: default
name: policy-cm
data:
name: good-pod

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: Pod
metadata:
name: good-pod
spec:
containers:
- name:
image: nginx
volumeMounts:
- name: udev
mountPath: /data
volumes:
- name: udev
emptyDir: {}

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
spec:
containers:
- name:
image: nginx
volumeMounts:
- name: udev
mountPath: /data
volumes:
- name: udev
hostPath:
path: /etc/udev

View file

@ -0,0 +1,18 @@
apiVersion: policies.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: disallow-host-path
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
variables:
- name: cm
expression: >-
context.GetConfigMap(object.metadata.namespace, "policy-cm")
validations:
- expression: >-
object.metadata.name == variables.cm.data.name