1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-22 07:41:10 +00:00

feat: support json payload via CLI apply command (#12296)

* chore: remove unused code

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* feat: support json in CLI apply command

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: remove not used validation expressions

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: update codegen docs

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: add unit tests

Signed-off-by: ShutingZhao <shuting@nirmata.com>

---------

Signed-off-by: ShutingZhao <shuting@nirmata.com>
This commit is contained in:
shuting 2025-03-06 16:48:26 +08:00 committed by GitHub
parent 0bcc850d77
commit 637f756994
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 327 additions and 61 deletions

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/go-git/go-billy/v5/memfs"
"github.com/kyverno/kyverno-json/pkg/payload"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov2 "github.com/kyverno/kyverno/api/kyverno/v2"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
@ -79,6 +80,7 @@ type ApplyCommandConfig struct {
inlineExceptions bool
GenerateExceptions bool
GeneratedExceptionTTL time.Duration
JSONPaths []string
}
func Command() *cobra.Command {
@ -110,6 +112,9 @@ func Command() *cobra.Command {
for _, response := range responses {
var failedRules []engineapi.RuleResponse
resPath := fmt.Sprintf("%s/%s/%s", response.Resource.GetNamespace(), response.Resource.GetKind(), response.Resource.GetName())
if resPath == "//" {
resPath = "JSON payload"
}
for _, rule := range response.PolicyResponse.Rules {
if rule.Status() == engineapi.RuleStatusFail {
failedRules = append(failedRules, rule)
@ -143,6 +148,8 @@ func Command() *cobra.Command {
return exit(out, rc, applyCommandConfig.warnExitCode, applyCommandConfig.warnNoPassed)
},
}
cmd.Flags().StringSliceVarP(&applyCommandConfig.JSONPaths, "json", "", []string{}, "Path to JSON payload files")
cmd.Flags().StringSliceVarP(&applyCommandConfig.ResourcePaths, "resource", "r", []string{}, "Path to resource files")
cmd.Flags().StringSliceVarP(&applyCommandConfig.ResourcePaths, "resources", "", []string{}, "Path to resource files")
cmd.Flags().StringSliceVarP(&applyCommandConfig.TargetResourcePaths, "target-resource", "", []string{}, "Path to individual files containing target resources files for policies that have mutate existing")
@ -209,7 +216,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
}
var targetResources []*unstructured.Unstructured
if len(c.TargetResourcePaths) > 0 {
targetResources, err = c.loadResources(out, c.TargetResourcePaths, policies, vaps, nil)
targetResources, _, err = c.loadResources(out, c.TargetResourcePaths, policies, vaps, nil)
if err != nil {
return nil, nil, skippedInvalidPolicies, nil, err
}
@ -218,7 +225,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
if err != nil {
return nil, nil, skippedInvalidPolicies, nil, err
}
resources, err := c.loadResources(out, c.ResourcePaths, policies, vaps, dClient)
resources, jsonPayloads, err := c.loadResources(out, c.ResourcePaths, policies, vaps, dClient)
if err != nil {
return nil, nil, skippedInvalidPolicies, nil, err
}
@ -247,10 +254,11 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
policyRulesCount += len(vps)
exceptionsCount := len(exceptions)
exceptionsCount += len(celexceptions)
resourceCount := len(resources) + len(jsonPayloads)
if exceptionsCount > 0 {
fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s) with %d exception(s)...\n", policyRulesCount, len(resources), exceptionsCount)
fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s) with %d exception(s)...\n", policyRulesCount, resourceCount, exceptionsCount)
} else {
fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources))
fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, resourceCount)
}
}
rc, resources1, responses1, err := c.applyPolicies(
@ -272,7 +280,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
if err != nil {
return rc, resources1, skippedInvalidPolicies, responses1, err
}
responses3, err := c.applyValidatingPolicies(vps, celexceptions, resources1, variables.Namespace, rc, dClient)
responses3, err := c.applyValidatingPolicies(vps, jsonPayloads, celexceptions, resources1, variables.Namespace, rc, dClient)
if err != nil {
return rc, resources1, skippedInvalidPolicies, responses1, err
}
@ -325,6 +333,7 @@ func (c *ApplyCommandConfig) applyValidatingAdmissionPolicies(
func (c *ApplyCommandConfig) applyValidatingPolicies(
vps []policiesv1alpha1.ValidatingPolicy,
jsonPayloads []*unstructured.Unstructured,
exceptions []*policiesv1alpha1.CELPolicyException,
resources []*unstructured.Unstructured,
namespaceProvider func(string) *corev1.Namespace,
@ -355,6 +364,7 @@ func (c *ApplyCommandConfig) applyValidatingPolicies(
}
restMapper := restmapper.NewDiscoveryRESTMapper(apiGroupResources)
responses := make([]engineapi.EngineResponse, 0)
responsesTemp := make([]engine.EngineResponse, 0)
for _, resource := range resources {
// get gvk from resource
gvk := resource.GroupVersionKind()
@ -384,7 +394,7 @@ func (c *ApplyCommandConfig) applyValidatingPolicies(
false,
nil,
)
response, err := eng.Handle(ctx, request)
reps, err := eng.Handle(ctx, request)
if err != nil {
if c.ContinueOnFail {
fmt.Printf("failed to apply validating policies on resource %s (%v)\n", resource.GetName(), err)
@ -392,7 +402,25 @@ func (c *ApplyCommandConfig) applyValidatingPolicies(
}
return responses, fmt.Errorf("failed to apply validating policies on resource %s (%w)", resource.GetName(), err)
}
// transform response into legacy engine responses
responsesTemp = append(responsesTemp, reps)
}
for _, json := range jsonPayloads {
eng = engine.NewEngine(provider, nil, nil)
request := engine.RequestFromJSON(contextProvider, json)
reps, err := eng.Handle(ctx, request)
if err != nil {
if c.ContinueOnFail {
fmt.Printf("failed to apply validating policies on JSON payloads (%v)\n", err)
continue
}
return responses, fmt.Errorf("failed to apply validating policies on JSON payloads (%w)", err)
}
responsesTemp = append(responsesTemp, reps)
}
// transform response into legacy engine responses
for _, response := range responsesTemp {
for _, r := range response.Policies {
engineResponse := engineapi.EngineResponse{
Resource: *response.Resource,
@ -482,12 +510,24 @@ func (c *ApplyCommandConfig) applyPolicies(
return &rc, resources, responses, nil
}
func (c *ApplyCommandConfig) loadResources(out io.Writer, paths []string, policies []kyvernov1.PolicyInterface, vap []admissionregistrationv1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) {
func (c *ApplyCommandConfig) loadResources(out io.Writer, paths []string, policies []kyvernov1.PolicyInterface, vap []admissionregistrationv1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, []*unstructured.Unstructured, error) {
resources, err := common.GetResourceAccordingToResourcePath(out, nil, paths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "")
if err != nil {
return resources, fmt.Errorf("failed to load resources (%w)", err)
return resources, nil, fmt.Errorf("failed to load resources (%w)", err)
}
return resources, nil
var jsonPayloads []*unstructured.Unstructured
if len(c.JSONPaths) > 0 {
for _, path := range c.JSONPaths {
payload, err := payload.Load(path)
if err != nil {
return nil, nil, fmt.Errorf("failed to load JSON payload (%w)", err)
}
jsonPayloads = append(jsonPayloads, &unstructured.Unstructured{Object: payload.(map[string]interface{})})
}
}
return resources, jsonPayloads, nil
}
func (c *ApplyCommandConfig) loadPolicies() (
@ -632,7 +672,7 @@ func (c *ApplyCommandConfig) checkArguments() error {
if (len(c.PolicyPaths) > 0 && c.PolicyPaths[0] == "-") && len(c.ResourcePaths) > 0 && c.ResourcePaths[0] == "-" {
return fmt.Errorf("a stdin pipe can be used for either policies or resources, not both")
}
if len(c.ResourcePaths) == 0 && !c.Cluster {
if len(c.ResourcePaths) == 0 && !c.Cluster && len(c.JSONPaths) == 0 {
return fmt.Errorf("resource file(s) or cluster required")
}
return nil

View file

@ -145,24 +145,6 @@ func Test_Apply(t *testing.T) {
},
}},
},
// {
// // TODO
// config: ApplyCommandConfig{
// PolicyPaths: []string{"https://github.com/kyverno/policies/openshift/team-validate-ns-name/"},
// ResourcePaths: []string{"../../../../../test/openshift/team-validate-ns-name.yaml"},
// GitBranch: "main",
// PolicyReport: true,
// },
// expectedPolicyReports: []policyreportv1alpha2.PolicyReport{{
// Summary: policyreportv1alpha2.PolicyReportSummary{
// Pass: 2,
// Fail: 0,
// Skip: 0,
// Error: 0,
// Warn: 0,
// },
// }},
// },
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../../test/cli/apply/policies-set"},
@ -537,6 +519,22 @@ func Test_Apply_ValidatingPolicies(t *testing.T) {
},
}},
},
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../../test/cli/test-validating-policy/json-check-dockerfile/policy.yaml"},
JSONPaths: []string{"../../../../../test/cli/test-validating-policy/json-check-dockerfile/payload.json"},
PolicyReport: true,
},
expectedPolicyReports: []policyreportv1alpha2.PolicyReport{{
Summary: policyreportv1alpha2.PolicyReportSummary{
Pass: 1,
Fail: 1,
Skip: 0,
Error: 0,
Warn: 0,
},
}},
},
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../../test/cli/test-cel-exceptions/check-deployment-labels/policy.yaml"},
@ -610,7 +608,7 @@ func Test_Apply_ValidatingPolicies(t *testing.T) {
_ = input.Close()
}()
}
desc := fmt.Sprintf("Policies: [%s], / Resources: [%s]", strings.Join(tc.config.PolicyPaths, ","), strings.Join(tc.config.ResourcePaths, ","))
desc := fmt.Sprintf("Policies: [%s], / Resources: [%s], JSON payload: [%s]", strings.Join(tc.config.PolicyPaths, ","), strings.Join(tc.config.ResourcePaths, ","), strings.Join(tc.config.JSONPaths, ","))
_, _, _, responses, err := tc.config.applyCommandHelper(os.Stdout)
assert.NoError(t, err, desc)

View file

@ -37,7 +37,7 @@ func GetResourceAccordingToResourcePath(
policyResourcePath string,
) (resources []*unstructured.Unstructured, err error) {
if fs != nil {
resources, err = GetResourcesWithTest(out, fs, policies, resourcePaths, policyResourcePath)
resources, err = GetResourcesWithTest(out, fs, resourcePaths, policyResourcePath)
if err != nil {
return nil, fmt.Errorf("failed to extract the resources (%w)", err)
}

View file

@ -13,7 +13,6 @@ import (
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/resource"
"github.com/kyverno/kyverno/pkg/admissionpolicy"
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/clients/dclient"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
@ -126,16 +125,8 @@ func whenClusterIsFalse(out io.Writer, resourcePaths []string, policyReport bool
}
// GetResourcesWithTest with gets matched resources by the given policies
func GetResourcesWithTest(out io.Writer, fs billy.Filesystem, policies []kyvernov1.PolicyInterface, resourcePaths []string, policyResourcePath string) ([]*unstructured.Unstructured, error) {
func GetResourcesWithTest(out io.Writer, fs billy.Filesystem, resourcePaths []string, policyResourcePath string) ([]*unstructured.Unstructured, error) {
resources := make([]*unstructured.Unstructured, 0)
resourceTypesMap := make(map[string]bool)
for _, policy := range policies {
for _, rule := range autogen.Default.ComputeRules(policy, "") {
for _, kind := range rule.MatchResources.Kinds {
resourceTypesMap[kind] = true
}
}
}
if len(resourcePaths) > 0 {
for _, resourcePath := range resourcePaths {
var resourceBytes []byte

View file

@ -49,6 +49,7 @@ kyverno apply [flags]
--generated-exception-ttl duration Default TTL for generated exceptions (default 720h0m0s)
-b, --git-branch string test git repository branch
-h, --help help for apply
--json strings Path to JSON payload files
--kubeconfig string path to kubeconfig file with authorization and master location information
-n, --namespace string Optional Policy parameter passed with cluster flag
-o, --output string Prints the mutated/generated resources in provided file/directory

View file

@ -8,6 +8,7 @@ import (
vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen"
contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context"
"github.com/kyverno/kyverno/pkg/cel/matching"
celpolicy "github.com/kyverno/kyverno/pkg/cel/policy"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
@ -25,8 +26,9 @@ import (
)
type EngineRequest struct {
request admissionv1.AdmissionRequest
context contextlib.ContextInterface
jsonPayload *unstructured.Unstructured
request admissionv1.AdmissionRequest
context contextlib.ContextInterface
}
func RequestFromAdmission(context contextlib.ContextInterface, request admissionv1.AdmissionRequest) EngineRequest {
@ -36,6 +38,13 @@ func RequestFromAdmission(context contextlib.ContextInterface, request admission
}
}
func RequestFromJSON(context contextlib.ContextInterface, jsonPayload *unstructured.Unstructured) EngineRequest {
return EngineRequest{
jsonPayload: jsonPayload,
context: context,
}
}
func Request(
context contextlib.ContextInterface,
gvk schema.GroupVersionKind,
@ -111,6 +120,15 @@ func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineRespo
if err != nil {
return response, err
}
if request.jsonPayload != nil {
response.Resource = request.jsonPayload
for _, policy := range policies {
response.Policies = append(response.Policies, e.handlePolicy(ctx, policy, request.jsonPayload.Object, nil, nil, nil, request.context))
}
return response, nil
}
// load objects
object, oldObject, err := admissionutils.ExtractResources(nil, request.request)
if err != nil {
@ -147,7 +165,7 @@ func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineRespo
}
// evaluate policies
for _, policy := range policies {
response.Policies = append(response.Policies, e.handlePolicy(ctx, policy, attr, &request.request, namespace, request.context))
response.Policies = append(response.Policies, e.handlePolicy(ctx, policy, nil, attr, &request.request, namespace, request.context))
}
return response, nil
}
@ -163,12 +181,14 @@ func (e *engine) matchPolicy(policy CompiledPolicy, attr admission.Attributes, n
}
// match against main policy constraints
matches, err := match(policy.Policy.Spec.MatchConstraints)
if err != nil {
return false, -1, err
}
if matches {
return true, -1, nil
if policy.Policy.GetSpec().MatchConstraints != nil {
matches, err := match(policy.Policy.Spec.MatchConstraints)
if err != nil {
return false, -1, err
}
if matches {
return true, -1, nil
}
}
// match against autogen rules
@ -185,7 +205,7 @@ func (e *engine) matchPolicy(policy CompiledPolicy, attr admission.Attributes, n
return false, -1, nil
}
func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) PolicyResponse {
func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, jsonPayload interface{}, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) PolicyResponse {
response := PolicyResponse{
Actions: policy.Actions,
Policy: policy.Policy,
@ -201,7 +221,15 @@ func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, attr a
}
autogenIndex = index
}
result, err := policy.CompiledPolicy.Evaluate(ctx, nil, attr, request, namespace, context, autogenIndex)
var result *celpolicy.EvaluationResult
var err error
if jsonPayload != nil {
result, err = policy.CompiledPolicy.Evaluate(ctx, jsonPayload, nil, nil, nil, context, -1)
} else {
result, err = policy.CompiledPolicy.Evaluate(ctx, nil, attr, request, namespace, context, autogenIndex)
}
// TODO: error is about match conditions here ?
if err != nil {
response.Rules = handlers.WithResponses(engineapi.RuleError("evaluation", engineapi.Validation, "failed to load context", err, nil))

View file

@ -25,14 +25,6 @@ func Test_evaluateJson(t *testing.T) {
{
"message": "HTTP calls are not allowed",
"expression": "!object.Stages.exists(s, \n s.Commands.exists(c, \n c.Args.exists(a, \n a.Value.contains('http://') || a.Value.contains('https://')\n )\n )\n)"
},
{
"message": "curl is not allowed",
"expression": "!object.Stages.exists(s, \n s.Commands.exists(c, \n c.CmdLine.contains('curl')\n )\n)"
},
{
"message": "wget is not allowed",
"expression": "!object.Stages.exists(s, \n s.Commands.exists(c, \n c.CmdLine.contains('wget')\n )\n)"
}
]
}
@ -242,5 +234,5 @@ func Test_evaluateJson(t *testing.T) {
assert.NilError(t, err)
}
t.Log(result)
assert.Assert(t, result.Result == false)
}

View file

@ -0,0 +1,176 @@
{
"MetaArgs": [
{
"Key": "BUILD_PLATFORM",
"DefaultValue": "\"linux/amd64\"",
"ProvidedValue": null,
"Value": "\"linux/amd64\""
},
{
"Key": "BUILDER_IMAGE",
"DefaultValue": "\"golang:1.20.6-alpine3.18\"",
"ProvidedValue": null,
"Value": "\"golang:1.20.6-alpine3.18\""
}
],
"Stages": [
{
"Name": "builder",
"BaseName": "\"golang:1.20.6-alpine3.18\"",
"Platform": "$BUILD_PLATFORM",
"Comment": "",
"SourceCode": "FROM --platform=$BUILD_PLATFORM $BUILDER_IMAGE as builder",
"Location": [
{
"Start": {
"Line": 4,
"Character": 0
},
"End": {
"Line": 4,
"Character": 0
}
}
],
"As": "builder",
"From": {
"Image": "\"golang:1.20.6-alpine3.18\""
},
"Commands": [
{
"Name": "WORKDIR",
"Path": "/"
},
{
"Chmod": "",
"Chown": "",
"DestPath": "./",
"From": "",
"Link": false,
"Name": "COPY",
"SourceContents": null,
"SourcePaths": [
"."
]
},
{
"Args": [
{
"Comment": "",
"Key": "SIGNER_BINARY_LINK",
"Value": "\"https://d2hvyiie56hcat.cloudfront.net/linux/amd64/plugin/latest/notation-aws-signer-plugin.zip\""
}
],
"Name": "ARG"
},
{
"Args": [
{
"Comment": "",
"Key": "SIGNER_BINARY_FILE",
"Value": "\"notation-aws-signer-plugin.zip\""
}
],
"Name": "ARG"
},
{
"CmdLine": [
"wget -O ${SIGNER_BINARY_FILE} ${SIGNER_BINARY_LINK}"
],
"Files": null,
"FlagsUsed": [],
"Name": "RUN",
"PrependShell": true
},
{
"CmdLine": [
"apk update && apk add unzip && unzip -o ${SIGNER_BINARY_FILE}"
],
"Files": null,
"FlagsUsed": [],
"Name": "RUN",
"PrependShell": true
},
{
"CmdLine": [
"GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags=\"-w -s\" -o kyverno-notation-aws ."
],
"Files": null,
"FlagsUsed": [],
"Name": "RUN",
"PrependShell": true
}
]
},
{
"Name": "",
"BaseName": "gcr.io/distroless/static:nonroot",
"Platform": "",
"Comment": "",
"SourceCode": "FROM gcr.io/distroless/static:nonroot",
"Location": [
{
"Start": {
"Line": 20,
"Character": 0
},
"End": {
"Line": 20,
"Character": 0
}
}
],
"From": {
"Image": "gcr.io/distroless/static:nonroot"
},
"Commands": [
{
"Name": "WORKDIR",
"Path": "/"
},
{
"Env": [
{
"Key": "PLUGINS_DIR",
"Value": "/plugins"
}
],
"Name": "ENV"
},
{
"Chmod": "",
"Chown": "",
"DestPath": "plugins/com.amazonaws.signer.notation.plugin/notation-com.amazonaws.signer.notation.plugin",
"From": "builder",
"Link": false,
"Name": "COPY",
"SourceContents": null,
"SourcePaths": [
"notation-com.amazonaws.signer.notation.plugin"
]
},
{
"Chmod": "",
"Chown": "",
"DestPath": "kyverno-notation-aws",
"From": "builder",
"Link": false,
"Name": "COPY",
"SourceContents": null,
"SourcePaths": [
"kyverno-notation-aws"
]
},
{
"CmdLine": [
"/kyverno-notation-aws"
],
"Files": null,
"Name": "ENTRYPOINT",
"PrependShell": false
}
]
}
]
}

View file

@ -0,0 +1,40 @@
apiVersion: policies.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: check-dockerfile-disallow-curl
spec:
evaluation:
mode: JSON
validations:
- message: "curl is not allowed"
expression: >-
!object.Stages.exists(s,
s.Commands.exists(c,
has(c.CmdLine) && c.CmdLine.exists(cmd, string(cmd).contains('curl'))
)
)
---
apiVersion: policies.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: check-dockerfile-disallow-wget
spec:
evaluation:
mode: JSON
validations:
- message: "wget is not allowed"
expression: >-
!object.Stages.exists(s,
s.Commands.exists(c,
has(c.CmdLine) && c.CmdLine.exists(cmd, string(cmd).contains('wget'))
)
)
- message: "HTTP calls are not allowed"
expression: >-
!object.Stages.exists(s,
s.Commands.exists(c,
c.Args.exists(a,
a.Value.contains('http://') || a.Value.contains('https://')
)
)
)