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;
+
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 + + + |
+
+ + +
|
+
@@ -426,6 +503,37 @@ github.com/kyverno/kyverno-json/pkg/apis/policy/v1alpha1.Any
+(Appears on: +Context) +
++
+Field | +Description | +
---|---|
+resources + + +[]Kubernetes meta/v1/unstructured.Unstructured + + + |
++ | +
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 @@
Values declares values to be loaded by the Kyverno CLI
+ + + +Field | +Description | +||
---|---|---|---|
apiVersion string |
+ cli.kyverno.io/v1alpha1 |
+ ||
kind string |
+ Context |
+ ||
metadata
+
+ *
+
+
+
+
+
+
+ meta/v1.ObjectMeta
+
+
+ |
+
+
+
+
+
+
+ Refer to the Kubernetes API documentation for the fields of the
+ metadata field.
+
+
+
+ |
+ ||
spec
+
+ *
+
+
+
+
+
+
+
+ ContextSpec
+
+
+
+ |
+
+
+
+
+
+
+
+
+ + +
|
+
metadata.name
instead
+
+
+
+
+
+
+
+
+
+
+ + (Appears in: + Context) +
+ + + + + +Field | +Description | +
---|---|
resources
+
+ *
+
+
+
+
+
+
+ []meta/v1/unstructured.Unstructured
+
+
+ |
+ + + + + + + + |