From 231e7a681e6628647faba37349eb03e34e01ae80 Mon Sep 17 00:00:00 2001 From: Sanskar Gurdasani <92817635+Sanskarzz@users.noreply.github.com> Date: Thu, 1 Feb 2024 03:58:14 +0530 Subject: [PATCH] Support PolicyExceptions with CLI (#9525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * loding policyExecptions from func Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * adding PolicyExceptions in crds Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * adding PolicyExceptions in GetPolicy function Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * adding policyexceptions in Load function Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * resolve error becuase of now Getpolicy return policyexceptions Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * added -exception flag loaded policyexception Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * added policyexceptions in processor and NewEngine Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * Revert "added -exception flag loaded policyexception" This reverts commit f53b205c089e780033b14c9a6343a141c20875b6. * Revert "Added support for PolicyExceptions for apply command " This reverts commit 82689ea0c1b914c566ac8ef30ab863f33d9b5460. * Update cmd/cli/kubectl-kyverno/commands/test/test.go loading exceptions with policies Co-authored-by: Mariam Fahmy <mariamfahmy66@gmail.com> Signed-off-by: Sanskar Gurdasani <92817635+Sanskarzz@users.noreply.github.com> * updated GetFullPaths function and remove unnecessary code Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * added tests for loading exceptions in GetPolicy function Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * added tests for loading policy exceptions Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * Used selector in List function Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * generated cli crd Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * updated loadpolicy_test tests and corrected kind Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * resolved unit test error in path_test.go file Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * limiting the usage of exceptions to ValidatingAdmissionPolicies Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> * remove changes in common code Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * fixes Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> * codegen Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> --------- Signed-off-by: Sanskarzz <sanskar.gur@gmail.com> Signed-off-by: Sanskar Gurdasani <92817635+Sanskarzz@users.noreply.github.com> Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> Co-authored-by: Mariam Fahmy <mariamfahmy66@gmail.com> Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> --- cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go | 3 ++ .../kubectl-kyverno/commands/apply/command.go | 49 ++++++++++++------- .../commands/apply/command_test.go | 34 ++++++------- cmd/cli/kubectl-kyverno/commands/test/test.go | 19 ++++++- .../config/crds/cli.kyverno.io_tests.yaml | 6 +++ .../data/crds/cli.kyverno.io_tests.yaml | 6 +++ cmd/cli/kubectl-kyverno/exception/load.go | 20 +++++++- .../kubectl-kyverno/exception/load_test.go | 4 +- .../kubectl-kyverno/processor/exceptions.go | 21 ++++++++ .../processor/policy_processor.go | 8 ++- docs/user/cli/commands/kyverno_apply.md | 1 + 11 files changed, 130 insertions(+), 41 deletions(-) create mode 100644 cmd/cli/kubectl-kyverno/processor/exceptions.go diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go index 4595a01431..cece9cba50 100644 --- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go +++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go @@ -38,6 +38,9 @@ type Test struct { // Values are the values to be used in the test Values *ValuesSpec `json:"values,omitempty"` + + // Policy Exceptions are the policy exceptions to be used in the test + PolicyExceptions []string `json:"exceptions,omitempty"` } type CheckResult struct { diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command.go b/cmd/cli/kubectl-kyverno/commands/apply/command.go index 7281d4caad..e51ac1d439 100644 --- a/cmd/cli/kubectl-kyverno/commands/apply/command.go +++ b/cmd/cli/kubectl-kyverno/commands/apply/command.go @@ -13,8 +13,10 @@ import ( "github.com/go-git/go-billy/v5/memfs" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/api/kyverno/v1beta1" + kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/deprecations" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/exception" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/color" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy" @@ -41,12 +43,12 @@ import ( const divider = "----------------------------------------------------------------------" -type SkippedInvalidPolicies struct { +type skippedInvalidPolicies struct { skipped []string invalid []string } -type ApplyCommandConfig struct { +type applyCommandConfig struct { KubeConfig string Context string Namespace string @@ -64,11 +66,12 @@ type ApplyCommandConfig struct { GitBranch string warnExitCode int warnNoPassed bool + exception []string } func Command() *cobra.Command { var removeColor, detailedResults, table bool - applyCommandConfig := &ApplyCommandConfig{} + applyCommandConfig := &applyCommandConfig{} cmd := &cobra.Command{ Use: "apply", Short: command.FormatDescription(true, websiteUrl, false, description...), @@ -114,10 +117,11 @@ func Command() *cobra.Command { cmd.Flags().BoolVar(&removeColor, "remove-color", false, "Remove any color from output") cmd.Flags().BoolVar(&detailedResults, "detailed-results", false, "If set to true, display detailed results") cmd.Flags().BoolVarP(&table, "table", "t", false, "Show results in table format") + cmd.Flags().StringSliceVar(&applyCommandConfig.exception, "exception", nil, "Policy exception to be considered when evaluating policies against resources") return cmd } -func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error) { +func (c *applyCommandConfig) applyCommandHelper(out io.Writer) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error) { rc, resources1, skipInvalidPolicies, responses1, err := c.checkArguments() if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err @@ -156,13 +160,21 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } + exceptions, err := exception.Load(c.exception...) + if err != nil { + return rc, resources1, skipInvalidPolicies, responses1, fmt.Errorf("Error: failed to load exceptions (%s)", err) + } if !c.Stdin { var policyRulesCount int for _, policy := range policies { policyRulesCount += len(autogen.ComputeRules(policy)) } policyRulesCount += len(vaps) - fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources)) + if len(exceptions) > 0 { + fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s) with %d exception(s)...\n", policyRulesCount, len(resources), len(exceptions)) + } else { + fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources)) + } } rc, resources1, responses1, err = c.applyPolicytoResource( @@ -171,6 +183,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul variables, policies, resources, + exceptions, &skipInvalidPolicies, dClient, userInfo, @@ -189,7 +202,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul return rc, resources1, skipInvalidPolicies, responses, nil } -func (c *ApplyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error, bool) { +func (c *applyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error, bool) { mutateLogPathIsDir, err := checkMutateLogPath(c.MutateLogPath) if err != nil { return nil, nil, skipInvalidPolicies, nil, fmt.Errorf("failed to create file/folder (%w)", err), false @@ -197,7 +210,7 @@ func (c *ApplyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies SkippedIn return nil, nil, skipInvalidPolicies, nil, err, mutateLogPathIsDir } -func (c *ApplyCommandConfig) applyValidatingAdmissionPolicytoResource( +func (c *applyCommandConfig) applyValidatingAdmissionPolicytoResource( vaps []v1alpha1.ValidatingAdmissionPolicy, vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding, resources []*unstructured.Unstructured, @@ -223,13 +236,14 @@ func (c *ApplyCommandConfig) applyValidatingAdmissionPolicytoResource( return responses, nil } -func (c *ApplyCommandConfig) applyPolicytoResource( +func (c *applyCommandConfig) applyPolicytoResource( out io.Writer, store *store.Store, vars *variables.Variables, policies []kyvernov1.PolicyInterface, resources []*unstructured.Unstructured, - skipInvalidPolicies *SkippedInvalidPolicies, + exceptions []*kyvernov2beta1.PolicyException, + skipInvalidPolicies *skippedInvalidPolicies, dClient dclient.Interface, userInfo *v1beta1.RequestInfo, mutateLogPathIsDir bool, @@ -262,6 +276,7 @@ func (c *ApplyCommandConfig) applyPolicytoResource( Store: store, Policies: validPolicies, Resource: *resource, + PolicyExceptions: exceptions, MutateLogPath: c.MutateLogPath, MutateLogPathIsDir: mutateLogPathIsDir, Variables: vars, @@ -285,7 +300,7 @@ func (c *ApplyCommandConfig) applyPolicytoResource( return &rc, resources, responses, nil } -func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []v1alpha1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) { +func (c *applyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []v1alpha1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) { resources, err := common.GetResourceAccordingToResourcePath(out, nil, c.ResourcePaths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "") if err != nil { return resources, fmt.Errorf("failed to load resources (%w)", err) @@ -293,7 +308,7 @@ func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.P return resources, nil } -func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, []kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { +func (c *applyCommandConfig) loadPolicies(skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, []kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) { // load policies var policies []kyvernov1.PolicyInterface var vaps []v1alpha1.ValidatingAdmissionPolicy @@ -301,13 +316,11 @@ func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPoli for _, path := range c.PolicyPaths { isGit := source.IsGit(path) - if isGit { gitSourceURL, err := url.Parse(path) if err != nil { return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to load policies (%w)", err) } - pathElems := strings.Split(gitSourceURL.Path[1:], "/") if len(pathElems) <= 1 { err := fmt.Errorf("invalid URL path %s - expected https://<any_git_source_domain>/:owner/:repository/:branch (without --git-branch flag) OR https://<any_git_source_domain>/:owner/:repository/:directory (with --git-branch flag)", gitSourceURL.Path) @@ -349,7 +362,7 @@ func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPoli return nil, nil, skipInvalidPolicies, nil, policies, vaps, vapBindings, nil } -func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error, dclient.Interface) { +func (c *applyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error, dclient.Interface) { store.SetLocal(true) store.SetRegistryAccess(c.RegistryAccess) if c.Cluster { @@ -378,7 +391,7 @@ func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipI return nil, nil, skipInvalidPolicies, nil, err, dClient } -func (c *ApplyCommandConfig) cleanPreviousContent(mutateLogPathIsDir bool, skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error) { +func (c *applyCommandConfig) cleanPreviousContent(mutateLogPathIsDir bool, skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error) { // empty the previous contents of the file just in case if the file already existed before with some content(so as to perform overwrites) // the truncation of files for the case when mutateLogPath is dir, is handled under pkg/kyverno/apply/common.go if !mutateLogPathIsDir && c.MutateLogPath != "" { @@ -392,8 +405,8 @@ func (c *ApplyCommandConfig) cleanPreviousContent(mutateLogPathIsDir bool, skipI return nil, nil, skipInvalidPolicies, nil, nil } -func (c *ApplyCommandConfig) checkArguments() (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error) { - var skipInvalidPolicies SkippedInvalidPolicies +func (c *applyCommandConfig) checkArguments() (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error) { + var skipInvalidPolicies skippedInvalidPolicies if c.ValuesFile != "" && c.Variables != nil { return nil, nil, skipInvalidPolicies, nil, fmt.Errorf("pass the values either using set flag or values_file flag") } @@ -409,7 +422,7 @@ func (c *ApplyCommandConfig) checkArguments() (*processor.ResultCounts, []*unstr return nil, nil, skipInvalidPolicies, nil, nil } -func printSkippedAndInvalidPolicies(out io.Writer, skipInvalidPolicies SkippedInvalidPolicies) { +func printSkippedAndInvalidPolicies(out io.Writer, skipInvalidPolicies skippedInvalidPolicies) { if len(skipInvalidPolicies.skipped) > 0 { fmt.Fprintln(out, divider) fmt.Fprintln(out, "Policies Skipped (as required variables are not provided by the user):") diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command_test.go b/cmd/cli/kubectl-kyverno/commands/apply/command_test.go index dcb95742d5..e32ef1a30b 100644 --- a/cmd/cli/kubectl-kyverno/commands/apply/command_test.go +++ b/cmd/cli/kubectl-kyverno/commands/apply/command_test.go @@ -18,7 +18,7 @@ func Test_Apply(t *testing.T) { type TestCase struct { gitBranch string expectedPolicyReports []policyreportv1alpha2.PolicyReport - config ApplyCommandConfig + config applyCommandConfig stdinFile string } // copy disallow_latest_tag.yaml to local path @@ -28,7 +28,7 @@ func Test_Apply(t *testing.T) { testcases := []*TestCase{ { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"}, ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"}, PolicyReport: true, @@ -44,7 +44,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{localFileName}, ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"}, PolicyReport: true, @@ -60,7 +60,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"}, ResourcePaths: []string{"../../../../../test/resources/pod_with_latest_tag.yaml"}, PolicyReport: true, @@ -76,7 +76,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/apply/policies"}, ResourcePaths: []string{"../../../../../test/cli/apply/resource"}, PolicyReport: true, @@ -92,7 +92,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"}, ResourcePaths: []string{"../../../../../test/resources/pod_with_latest_tag.yaml"}, PolicyReport: true, @@ -109,7 +109,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"-"}, ResourcePaths: []string{"../../../../../test/resources/pod_with_latest_tag.yaml"}, PolicyReport: true, @@ -127,7 +127,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"}, ResourcePaths: []string{"-"}, PolicyReport: true, @@ -163,7 +163,7 @@ func Test_Apply(t *testing.T) { // }}, // }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/apply/policies-set"}, ResourcePaths: []string{"../../../../../test/cli/apply/resources-set"}, Variables: []string{"request.operation=UPDATE"}, @@ -180,7 +180,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/policy.yaml"}, ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/deployment1.yaml"}, PolicyReport: true, @@ -196,7 +196,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/policy.yaml"}, ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/deployment2.yaml"}, PolicyReport: true, @@ -212,7 +212,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/policy.yaml"}, ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/pod1.yaml"}, PolicyReport: true, @@ -228,7 +228,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/policy.yaml"}, ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/pod2.yaml"}, PolicyReport: true, @@ -244,7 +244,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/policy.yaml"}, ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/deployment1.yaml"}, PolicyReport: true, @@ -260,7 +260,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/policy.yaml"}, ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/deployment2.yaml"}, PolicyReport: true, @@ -276,7 +276,7 @@ func Test_Apply(t *testing.T) { }}, }, { - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"https://github.com/kyverno/policies/best-practices/require-labels/", "../../../../../test/best_practices/disallow_latest_tag.yaml"}, ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"}, GitBranch: "main", @@ -294,7 +294,7 @@ func Test_Apply(t *testing.T) { }, { // Same as the above test case but the policy paths are reordered - config: ApplyCommandConfig{ + config: applyCommandConfig{ PolicyPaths: []string{"../../../../../test/best_practices/disallow_latest_tag.yaml", "https://github.com/kyverno/policies/best-practices/require-labels/"}, ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"}, GitBranch: "main", diff --git a/cmd/cli/kubectl-kyverno/commands/test/test.go b/cmd/cli/kubectl-kyverno/commands/test/test.go index 711efad54e..0dc44e789a 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/test.go +++ b/cmd/cli/kubectl-kyverno/commands/test/test.go @@ -7,6 +7,7 @@ import ( kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/api/kyverno/v1beta1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/deprecations" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/exception" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/path" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy" @@ -74,6 +75,17 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa fmt.Fprintln(out, " Warning: found duplicated resource", dup.Kind, dup.Name, dup.Namespace) } } + // exceptions + fmt.Fprintln(out, " Loading exceptions", "...") + exceptionFullPath := path.GetFullPaths(testCase.Test.PolicyExceptions, testDir, isGit) + exceptions, err := exception.Load(exceptionFullPath...) + if err != nil { + return nil, fmt.Errorf("Error: failed to load exceptions (%s)", err) + } + // Validates that exceptions cannot be used with ValidatingAdmissionPolicies. + if len(validatingAdmissionPolicies) > 0 && len(exceptions) > 0 { + return nil, fmt.Errorf("Error: Currently, the use of exceptions in conjunction with ValidatingAdmissionPolicies is not supported.") + } // init store var store store.Store store.SetLocal(true) @@ -81,7 +93,11 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa if vars != nil { vars.SetInStore(&store) } - fmt.Fprintln(out, " Applying", len(policies)+len(validatingAdmissionPolicies), pluralize.Pluralize(len(policies)+len(validatingAdmissionPolicies), "policy", "policies"), "to", len(uniques), pluralize.Pluralize(len(uniques), "resource", "resources"), "...") + if len(exceptions) > 0 { + fmt.Fprintln(out, " Applying", len(policies)+len(validatingAdmissionPolicies), pluralize.Pluralize(len(policies)+len(validatingAdmissionPolicies), "policy", "policies"), "to", len(uniques), pluralize.Pluralize(len(uniques), "resource", "resources"), "with", len(exceptions), pluralize.Pluralize(len(exceptions), "exception", "exceptions"), "...") + } else { + fmt.Fprintln(out, " Applying", len(policies)+len(validatingAdmissionPolicies), pluralize.Pluralize(len(policies)+len(validatingAdmissionPolicies), "policy", "policies"), "to", len(uniques), pluralize.Pluralize(len(uniques), "resource", "resources"), "...") + } // TODO document the code below ruleToCloneSourceResource := map[string]string{} for _, policy := range policies { @@ -144,6 +160,7 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa Store: &store, Policies: validPolicies, Resource: *resource, + PolicyExceptions: exceptions, MutateLogPath: "", Variables: vars, UserInfo: userInfo, diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml index 43e3639148..d5ae056ea1 100644 --- a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml +++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml @@ -24,6 +24,12 @@ spec: 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 + exceptions: + description: Policy Exceptions are the policy exceptions to be used in + the test + items: + type: string + type: array kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml index 43e3639148..d5ae056ea1 100644 --- a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml +++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml @@ -24,6 +24,12 @@ spec: 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 + exceptions: + description: Policy Exceptions are the policy exceptions to be used in + the test + items: + type: string + type: array kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client diff --git a/cmd/cli/kubectl-kyverno/exception/load.go b/cmd/cli/kubectl-kyverno/exception/load.go index 641f1b5083..ddc5c126ca 100644 --- a/cmd/cli/kubectl-kyverno/exception/load.go +++ b/cmd/cli/kubectl-kyverno/exception/load.go @@ -2,6 +2,8 @@ package exception import ( "fmt" + "os" + "path/filepath" kyvernov2 "github.com/kyverno/kyverno/api/kyverno/v2" kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1" @@ -19,7 +21,23 @@ var ( exceptionV2 = schema.GroupVersion(kyvernov2.GroupVersion).WithKind("PolicyException") ) -func Load(content []byte) ([]*kyvernov2beta1.PolicyException, error) { +func Load(paths ...string) ([]*kyvernov2beta1.PolicyException, error) { + var out []*kyvernov2beta1.PolicyException + for _, path := range paths { + bytes, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, fmt.Errorf("unable to read yaml (%w)", err) + } + exceptions, err := load(bytes) + if err != nil { + return nil, fmt.Errorf("unable to load exceptions (%w)", err) + } + out = append(out, exceptions...) + } + return out, nil +} + +func load(content []byte) ([]*kyvernov2beta1.PolicyException, error) { documents, err := yamlutils.SplitDocuments(content) if err != nil { return nil, err diff --git a/cmd/cli/kubectl-kyverno/exception/load_test.go b/cmd/cli/kubectl-kyverno/exception/load_test.go index 733300f29a..31fb276d25 100644 --- a/cmd/cli/kubectl-kyverno/exception/load_test.go +++ b/cmd/cli/kubectl-kyverno/exception/load_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_Load(t *testing.T) { +func Test_load(t *testing.T) { tests := []struct { name string policies string @@ -31,7 +31,7 @@ func Test_Load(t *testing.T) { bytes, err := os.ReadFile(tt.policies) require.NoError(t, err) require.NoError(t, err) - if res, err := Load(bytes); (err != nil) != tt.wantErr { + if res, err := load(bytes); (err != nil) != tt.wantErr { t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) } else if len(res) != tt.wantLoaded { t.Errorf("Load() loaded amount = %v, wantLoaded %v", len(res), tt.wantLoaded) diff --git a/cmd/cli/kubectl-kyverno/processor/exceptions.go b/cmd/cli/kubectl-kyverno/processor/exceptions.go new file mode 100644 index 0000000000..bf6cb2b4c7 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/processor/exceptions.go @@ -0,0 +1,21 @@ +package processor + +import ( + kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1" + "k8s.io/apimachinery/pkg/labels" +) + +type policyExceptionLister struct { + exceptions []*kyvernov2beta1.PolicyException +} + +func (l *policyExceptionLister) List(selector labels.Selector) ([]*kyvernov2beta1.PolicyException, error) { + var out []*kyvernov2beta1.PolicyException + for _, exception := range l.exceptions { + exceptionLabels := labels.Set(exception.GetLabels()) + if selector.Matches(exceptionLabels) { + out = append(out, exception) + } + } + return out, nil +} diff --git a/cmd/cli/kubectl-kyverno/processor/policy_processor.go b/cmd/cli/kubectl-kyverno/processor/policy_processor.go index e95356fad5..7b592a8a57 100644 --- a/cmd/cli/kubectl-kyverno/processor/policy_processor.go +++ b/cmd/cli/kubectl-kyverno/processor/policy_processor.go @@ -11,6 +11,7 @@ import ( json_patch "github.com/evanphx/json-patch/v5" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" + kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/v1alpha1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/store" @@ -38,6 +39,7 @@ type PolicyProcessor struct { Store *store.Store Policies []kyvernov1.PolicyInterface Resource unstructured.Unstructured + PolicyExceptions []*kyvernov2beta1.PolicyException MutateLogPath string MutateLogPathIsDir bool Variables *variables.Variables @@ -59,11 +61,13 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse, jp := jmespath.New(cfg) resource := p.Resource namespaceLabels := p.NamespaceSelectorMap[p.Resource.GetNamespace()] + policyExceptionLister := &policyExceptionLister{ + exceptions: p.PolicyExceptions, + } var client engineapi.Client if p.Client != nil { client = adapters.Client(p.Client) } - rclient := p.Store.GetRegistryClient() if rclient == nil { rclient = registryclient.NewOrDie() @@ -76,7 +80,7 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse, factories.DefaultRegistryClientFactory(adapters.RegistryClient(rclient), nil), imageverifycache.DisabledImageVerifyCache(), store.ContextLoaderFactory(p.Store, nil), - nil, + policyExceptionLister, "", ) gvk, subresource := resource.GroupVersionKind(), "" diff --git a/docs/user/cli/commands/kyverno_apply.md b/docs/user/cli/commands/kyverno_apply.md index b609ba2d78..1771b3acd9 100644 --- a/docs/user/cli/commands/kyverno_apply.md +++ b/docs/user/cli/commands/kyverno_apply.md @@ -41,6 +41,7 @@ kyverno apply [flags] -c, --cluster Checks if policies should be applied to cluster in the current context --context string The name of the kubeconfig context to use --detailed-results If set to true, display detailed results + --exception strings Policy exception to be considered when evaluating policies against resources -b, --git-branch string test git repository branch -h, --help help for apply --kubeconfig string path to kubeconfig file with authorization and master location information