diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go index e5c6f35b2e..4934bdd073 100644 --- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go +++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go @@ -24,6 +24,9 @@ type Test struct { // Resources are the resource to be used in the test Resources []string `json:"resources,omitempty"` + // Target Resources are for policies that have mutate existing + TargetResources []string `json:"targetResources,omitempty"` + // Variables is the values to be used in the test Variables string `json:"variables,omitempty"` @@ -54,6 +57,15 @@ type CheckResult struct { Error v1alpha1.Any `json:"error"` } +type TestResourceSpec struct { + Group string `json:"group,omitempty"` + Version string `json:"version,omitempty"` + Kind string `json:"kind,omitempty"` + Namespace string `json:"namespace,omitempty"` + Subresource string `json:"subresource,omitempty"` + Name string `json:"name,omitempty"` +} + type CheckMatch struct { // Resource filters engine responses Resource *v1alpha1.Any `json:"resource,omitempty"` diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go index 6a9f2939cf..2f544723c6 100644 --- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go +++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go @@ -67,5 +67,5 @@ type TestResult struct { TestResultDeprecated `json:",inline,omitempty"` // Resources gives us the list of resources on which the policy is going to be applied. - Resources []string `json:"resources"` + Resources []any `json:"resources"` } diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command.go b/cmd/cli/kubectl-kyverno/commands/apply/command.go index 4a1e3881d3..c17a740a47 100644 --- a/cmd/cli/kubectl-kyverno/commands/apply/command.go +++ b/cmd/cli/kubectl-kyverno/commands/apply/command.go @@ -34,6 +34,8 @@ import ( "github.com/spf13/cobra" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) @@ -60,6 +62,7 @@ type ApplyCommandConfig struct { AuditWarn bool ResourcePaths []string PolicyPaths []string + TargetResourcePaths []string GitBranch string warnExitCode int warnNoPassed bool @@ -134,6 +137,8 @@ func Command() *cobra.Command { } 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") + cmd.Flags().StringSliceVarP(&applyCommandConfig.TargetResourcePaths, "target-resources", "", []string{}, "Path to a directory containing target resources files for policies that have mutate existing") cmd.Flags().BoolVarP(&applyCommandConfig.Cluster, "cluster", "c", false, "Checks if policies should be applied to cluster in the current context") cmd.Flags().StringVarP(&applyCommandConfig.MutateLogPath, "output", "o", "", "Prints the mutated/generated resources in provided file/directory") // currently `set` flag supports variable for single policy applied on single resource @@ -189,18 +194,28 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul return nil, nil, skipInvalidPolicies, nil, fmt.Errorf("failed to decode yaml (%w)", err) } var store store.Store - rc, resources1, skipInvalidPolicies, responses1, dClient, err := c.initStoreAndClusterClient(&store, skipInvalidPolicies) - if err != nil { - return rc, resources1, skipInvalidPolicies, responses1, err - } rc, resources1, skipInvalidPolicies, responses1, policies, vaps, vapBindings, err := c.loadPolicies(skipInvalidPolicies) if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } - resources, err := c.loadResources(out, policies, vaps, dClient) + var targetResources []*unstructured.Unstructured + if len(c.TargetResourcePaths) > 0 { + targetResources, err = c.loadResources(out, c.TargetResourcePaths, policies, vaps, nil) + if err != nil { + return rc, resources1, skipInvalidPolicies, responses1, err + } + } + + rc, resources1, skipInvalidPolicies, responses1, dClient, err := c.initStoreAndClusterClient(&store, skipInvalidPolicies, targetResources...) if err != nil { return rc, resources1, skipInvalidPolicies, responses1, err } + + resources, err := c.loadResources(out, c.ResourcePaths, policies, vaps, dClient) + if err != nil { + return rc, resources1, skipInvalidPolicies, responses1, err + } + var exceptions []*kyvernov2.PolicyException if c.inlineExceptions { exceptions = exception.SelectFrom(resources) @@ -339,6 +354,7 @@ func (c *ApplyCommandConfig) applyPolicytoResource( Stdin: c.Stdin, Rc: &rc, PrintPatchResource: true, + Cluster: c.Cluster, Client: dClient, AuditWarn: c.AuditWarn, Subresources: vars.Subresources(), @@ -362,8 +378,8 @@ func (c *ApplyCommandConfig) applyPolicytoResource( return &rc, resources, responses, nil } -func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []admissionregistrationv1beta1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) { - resources, err := common.GetResourceAccordingToResourcePath(out, nil, c.ResourcePaths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "") +func (c *ApplyCommandConfig) loadResources(out io.Writer, paths []string, policies []kyvernov1.PolicyInterface, vap []admissionregistrationv1beta1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*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) } @@ -429,7 +445,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, dclient.Interface, error) { +func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies SkippedInvalidPolicies, targetResources ...*unstructured.Unstructured) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, dclient.Interface, error) { store.SetLocal(true) store.SetRegistryAccess(c.RegistryAccess) if c.Cluster { @@ -455,6 +471,18 @@ func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipI return nil, nil, skipInvalidPolicies, nil, nil, err } } + if len(targetResources) > 0 && !c.Cluster { + var targets []runtime.Object + for _, t := range targetResources { + targets = append(targets, t) + } + + dClient, err = dclient.NewFakeClient(runtime.NewScheme(), map[schema.GroupVersionResource]string{}, targets...) + dClient.SetDiscovery(dclient.NewFakeDiscoveryClient(nil)) + if err != nil { + return nil, nil, skipInvalidPolicies, nil, nil, err + } + } return nil, nil, skipInvalidPolicies, nil, dClient, err } diff --git a/cmd/cli/kubectl-kyverno/commands/test/command.go b/cmd/cli/kubectl-kyverno/commands/test/command.go index 87670876f8..6676c0789d 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/command.go +++ b/cmd/cli/kubectl-kyverno/commands/test/command.go @@ -17,7 +17,6 @@ import ( "github.com/sergi/go-diff/diffmatchpatch" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/tools/cache" ) func Command() *cobra.Command { @@ -127,7 +126,7 @@ func testCommandExecute( if err := printTestResult(filteredResults, responses, rc, &resultsTable, test.Fs, resourcePath); err != nil { return fmt.Errorf("failed to print test result (%w)", err) } - if err := printCheckResult(test.Test.Checks, responses, rc, &resultsTable); err != nil { + if err := printCheckResult(test.Test.Checks, *responses, rc, &resultsTable); err != nil { return fmt.Errorf("failed to print test result (%w)", err) } fullTable.AddFailed(resultsTable.RawRows...) @@ -152,19 +151,15 @@ func testCommandExecute( return nil } -func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath string, response engineapi.EngineResponse, rule engineapi.RuleResponse) (bool, string, string) { +func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath string, response engineapi.EngineResponse, rule engineapi.RuleResponse, actualResource unstructured.Unstructured) (bool, string, string) { expected := test.Result // fallback to the deprecated field if expected == "" { expected = test.Status } // fallback on deprecated field - patchedResource := test.PatchedResource - if test.PatchedResources != "" { - patchedResource = test.PatchedResources - } - if patchedResource != "" { - equals, diff, err := getAndCompareResource([]*unstructured.Unstructured{&response.PatchedResource}, fs, filepath.Join(resoucePath, patchedResource)) + if test.PatchedResource != "" { + equals, diff, err := getAndCompareResource(actualResource, fs, filepath.Join(resoucePath, test.PatchedResource)) if err != nil { return false, err.Error(), "Resource error" } @@ -175,14 +170,14 @@ func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath stri } } if test.GeneratedResource != "" { - equals, diff, err := getAndCompareResource(rule.GeneratedResources(), fs, filepath.Join(resoucePath, test.GeneratedResource)) + equals, diff, err := getAndCompareResource(actualResource, fs, filepath.Join(resoucePath, test.GeneratedResource)) if err != nil { return false, err.Error(), "Resource error" } if !equals { dmp := diffmatchpatch.New() legend := dmp.DiffPrettyText(dmp.DiffMain("only in expected", "only in actual", false)) - return false, fmt.Sprintf("Generated resource didn't match the generated resource in the test result\n(%s)\n\n%s", legend, diff), "Resource diff" + return false, fmt.Sprintf("Patched resource didn't match the generated resource in the test result\n(%s)\n\n%s", legend, diff), "Resource diff" } } result := report.ComputePolicyReportResult(false, response, rule) @@ -192,27 +187,6 @@ func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath stri return true, result.Message, "Ok" } -func lookupEngineResponses(test v1alpha1.TestResult, resourceName string, responses ...engineapi.EngineResponse) []engineapi.EngineResponse { - matches := make([]engineapi.EngineResponse, 0, len(responses)) - for _, response := range responses { - policy := response.Policy() - resource := response.Resource - pName := cache.MetaObjectToName(policy.MetaObject()).String() - rName := cache.MetaObjectToName(&resource).String() - if test.Kind != resource.GetKind() { - continue - } - if pName != test.Policy { - continue - } - if resourceName != "" && rName != resourceName && resource.GetName() != resourceName { - continue - } - matches = append(matches, response) - } - return matches -} - func lookupRuleResponses(test v1alpha1.TestResult, responses ...engineapi.RuleResponse) []engineapi.RuleResponse { var matches []engineapi.RuleResponse // Since there are no rules in case of validating admission policies, responses are returned without checking rule names. diff --git a/cmd/cli/kubectl-kyverno/commands/test/compare.go b/cmd/cli/kubectl-kyverno/commands/test/compare.go index 5feaa28aac..26aa307434 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/compare.go +++ b/cmd/cli/kubectl-kyverno/commands/test/compare.go @@ -11,41 +11,26 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func getAndCompareResource(actualResources []*unstructured.Unstructured, fs billy.Filesystem, path string) (bool, string, error) { - expectedResources, err := resource.GetResourceFromPath(fs, path) +func getAndCompareResource(actualResource unstructured.Unstructured, fs billy.Filesystem, path string) (bool, string, error) { + expectedResource, err := resource.GetResourceFromPath(fs, path, actualResource.GetAPIVersion(), actualResource.GetKind(), actualResource.GetNamespace(), actualResource.GetName()) if err != nil { return false, "", fmt.Errorf("error: failed to load resource (%s)", err) } + resource.FixupGenerateLabels(actualResource) + resource.FixupGenerateLabels(*expectedResource) - expectedResourcesMap := map[string]unstructured.Unstructured{} - for _, expectedResource := range expectedResources { - if expectedResource == nil { - continue - } - r := *expectedResource - resource.FixupGenerateLabels(r) - expectedResourcesMap[expectedResource.GetNamespace()+"/"+expectedResource.GetName()] = r + equals, err := resource.Compare(actualResource, *expectedResource, true) + if err != nil { + return false, "", fmt.Errorf("error: failed to compare resources (%s)", err) } - - for _, actualResource := range actualResources { - if actualResource == nil { - continue - } - r := *actualResource - resource.FixupGenerateLabels(r) - equals, err := resource.Compare(r, expectedResourcesMap[r.GetNamespace()+"/"+r.GetName()], true) - if err != nil { - return false, "", fmt.Errorf("error: failed to compare resources (%s)", err) - } - if !equals { - log.Log.V(4).Info("Resource diff", "expected", expectedResourcesMap[r.GetNamespace()+"/"+r.GetName()], "actual", r) - es, _ := yaml.Marshal(expectedResourcesMap[r.GetNamespace()+"/"+r.GetName()]) - as, _ := yaml.Marshal(r) - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(es), string(as), false) - log.Log.V(4).Info("\n" + dmp.DiffPrettyText(diffs) + "\n") - return false, dmp.DiffPrettyText(diffs), nil - } + if !equals { + log.Log.V(4).Info("Resource diff", "expected", expectedResource, "actual", actualResource) + es, _ := yaml.Marshal(expectedResource) + as, _ := yaml.Marshal(actualResource) + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(string(es), string(as), false) + log.Log.V(4).Info("\n" + dmp.DiffPrettyText(diffs) + "\n") + return false, dmp.DiffPrettyText(diffs), nil } return true, "", nil } diff --git a/cmd/cli/kubectl-kyverno/commands/test/output.go b/cmd/cli/kubectl-kyverno/commands/test/output.go index eee1cb17e1..2a20b0d35e 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/output.go +++ b/cmd/cli/kubectl-kyverno/commands/test/output.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "strings" "github.com/go-git/go-billy/v5" "github.com/kyverno/kyverno-json/pkg/engine/assert" @@ -12,12 +13,13 @@ import ( "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/color" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/table" engineapi "github.com/kyverno/kyverno/pkg/engine/api" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) func printCheckResult( checks []v1alpha1.CheckResult, - responses []engineapi.EngineResponse, + responses TestResponse, rc *resultCounts, resultsTable *table.Table, ) error { @@ -25,7 +27,10 @@ func printCheckResult( testCount := 1 for _, check := range checks { // filter engine responses - matchingEngineResponses := responses + var matchingEngineResponses []engineapi.EngineResponse + for _, engineresponses := range responses.Trigger { + matchingEngineResponses = append(matchingEngineResponses, engineresponses...) + } // 1. by resource if check.Match.Resource != nil { var filtered []engineapi.EngineResponse @@ -160,9 +165,10 @@ func printCheckResult( return nil } +// a test that contains a policy that may contain several rules func printTestResult( tests []v1alpha1.TestResult, - responses []engineapi.EngineResponse, + responses *TestResponse, rc *resultCounts, resultsTable *table.Table, fs billy.Filesystem, @@ -170,80 +176,144 @@ func printTestResult( ) error { testCount := 1 for _, test := range tests { - // lookup matching engine responses (without the resource name) - // to reduce the search scope - responses := lookupEngineResponses(test, "", responses...) - // TODO fix deprecated fields - // identify the resources to be looked up var resources []string - if test.Resources != nil { - resources = append(resources, test.Resources...) - } else if test.Resource != "" { - resources = append(resources, test.Resource) + // The test specifies certain resources to check, results will be checked for those resources only + if test.Resource != "" { + test.Resources = append(test.Resources, test.Resource) } - for _, resource := range resources { - var rows []table.Row - // lookup matching engine responses (with the resource name this time) - for _, response := range lookupEngineResponses(test, resource, responses...) { - // lookup matching rule responses - for _, rule := range lookupRuleResponses(test, response.PolicyResponse.Rules...) { - // perform test checks - ok, message, reason := checkResult(test, fs, resoucePath, response, rule) - // if checks failed but we were expecting a fail it's considered a success - success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail) - row := table.Row{ - RowCompact: table.RowCompact{ - ID: testCount, - Policy: color.Policy("", test.Policy), - Rule: color.Rule(test.Rule), - Resource: color.Resource(test.Kind, test.Namespace, resource), - Reason: reason, - IsFailure: !success, - }, - Message: message, - } - if success { - row.Result = color.ResultPass() - if test.Result == policyreportv1alpha2.StatusSkip { - rc.Skip++ - } else { - rc.Pass++ + if test.Resources != nil { + for _, r := range test.Resources { + for _, m := range []map[string][]engineapi.EngineResponse{responses.Target, responses.Trigger} { + for resourceGVKAndName := range m { + nameParts := strings.Split(resourceGVKAndName, ",") + if resourceString, ok := r.(string); ok { + nsAndName := strings.Split(resourceString, "/") + if len(nsAndName) == 1 { + if resourceString == nameParts[len(nameParts)-1] { + resources = append(resources, resourceGVKAndName) + } + } + if len(nsAndName) == 2 { + if nsAndName[0] == nameParts[len(nameParts)-2] && nsAndName[1] == nameParts[len(nameParts)-1] { + resources = append(resources, resourceGVKAndName) + } + } } - } else { - row.Result = color.ResultFail() - rc.Fail++ - } - testCount++ - rows = append(rows, row) - } - // if there are no RuleResponse, the resource has been excluded. This is a pass. - if len(rows) == 0 { - row := table.Row{ - RowCompact: table.RowCompact{ - ID: testCount, - Policy: color.Policy("", test.Policy), - Rule: color.Rule(test.Rule), - Resource: color.Resource(test.Kind, test.Namespace, resource), - Result: color.ResultPass(), - Reason: color.Excluded(), - IsFailure: false, - }, - Message: color.Excluded(), + if resourceSpec, ok := r.(v1alpha1.TestResourceSpec); ok { + if resourceSpec.Group == "" { + if resourceSpec.Version != nameParts[0] { + continue + } + } else { + if resourceSpec.Group+"/"+resourceSpec.Version != nameParts[0] { + continue + } + } + if resourceSpec.Namespace != nameParts[len(nameParts)-2] { + continue + } + if resourceSpec.Name == nameParts[len(nameParts)-1] { + resources = append(resources, resourceGVKAndName) + } + } } - rc.Skip++ - testCount++ - rows = append(rows, row) } } - // if not found - if len(rows) == 0 { + } + + // The test specifies no resources, check all results + if len(resources) == 0 { + for r := range responses.Target { + resources = append(resources, r) + } + for r := range responses.Trigger { + resources = append(resources, r) + } + } + + for _, resource := range resources { + var rows []table.Row + var resourceSkipped bool + if _, ok := responses.Trigger[resource]; ok { + for _, response := range responses.Trigger[resource] { + polNameNs := strings.Split(test.Policy, "/") + if response.Policy().GetName() != polNameNs[len(polNameNs)-1] { + continue + } + for _, rule := range lookupRuleResponses(test, response.PolicyResponse.Rules...) { + r := response.Resource + + if rule.RuleType() != "Generation" { + if rule.RuleType() == "Mutation" { + r = response.PatchedResource + } + + ok, message, reason := checkResult(test, fs, resoucePath, response, rule, r) + if strings.Contains(message, "not found in manifest") { + resourceSkipped = true + continue + } + + success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail) + resourceRows := createRowsAccordingToResults(test, rc, &testCount, success, message, reason, strings.Replace(resource, ",", "/", -1)) + rows = append(rows, resourceRows...) + } else { + generatedResources := rule.GeneratedResources() + for _, r := range generatedResources { + ok, message, reason := checkResult(test, fs, resoucePath, response, rule, *r) + + success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail) + resourceRows := createRowsAccordingToResults(test, rc, &testCount, success, message, reason, r.GetName()) + rows = append(rows, resourceRows...) + } + } + } + + // if there are no RuleResponse, the resource has been excluded. This is a pass. + if len(rows) == 0 && !resourceSkipped { + row := table.Row{ + RowCompact: table.RowCompact{ + ID: testCount, + Policy: color.Policy("", test.Policy), + Rule: color.Rule(test.Rule), + Resource: color.Resource(test.Kind, test.Namespace, strings.Replace(resource, ",", "/", -1)), + Result: color.ResultPass(), + Reason: color.Excluded(), + IsFailure: false, + }, + Message: color.Excluded(), + } + rc.Skip++ + testCount++ + rows = append(rows, row) + } + } + } + + // Check if the resource specified exists in the targets + if _, ok := responses.Target[resource]; ok { + for _, response := range responses.Target[resource] { + // we are doing this twice which is kinda not nice + nameParts := strings.Split(resource, ",") + name, ns, kind, apiVersion := nameParts[len(nameParts)-1], nameParts[len(nameParts)-2], nameParts[len(nameParts)-3], nameParts[len(nameParts)-4] + + r, rule := extractPatchedTargetFromEngineResponse(apiVersion, kind, name, ns, response) + ok, message, reason := checkResult(test, fs, resoucePath, response, *rule, *r) + + success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail) + resourceRows := createRowsAccordingToResults(test, rc, &testCount, success, message, reason, strings.Replace(resource, ",", "/", -1)) + rows = append(rows, resourceRows...) + } + } + + if len(rows) == 0 && !resourceSkipped { row := table.Row{ RowCompact: table.RowCompact{ ID: testCount, Policy: color.Policy("", test.Policy), Rule: color.Rule(test.Rule), - Resource: color.Resource(test.Kind, test.Namespace, resource), + Resource: color.Resource(test.Kind, test.Namespace, strings.Replace(resource, ",", "/", -1)), IsFailure: true, Result: color.ResultFail(), Reason: color.NotFound(), @@ -261,6 +331,70 @@ func printTestResult( return nil } +func createRowsAccordingToResults(test v1alpha1.TestResult, rc *resultCounts, globalTestCounter *int, success bool, message string, reason string, resourceGVKAndName string) []table.Row { + resourceParts := strings.Split(resourceGVKAndName, "/") + rows := []table.Row{} + row := table.Row{ + RowCompact: table.RowCompact{ + ID: *globalTestCounter, + Policy: color.Policy("", test.Policy), + Rule: color.Rule(test.Rule), + Resource: color.Resource(strings.Join(resourceParts[:len(resourceParts)-1], "/"), test.Namespace, resourceParts[len(resourceParts)-1]), + Reason: reason, + IsFailure: !success, + }, + Message: message, + } + if success { + row.Result = color.ResultPass() + if test.Result == policyreportv1alpha2.StatusSkip { + rc.Skip++ + } else { + rc.Pass++ + } + } else { + row.Result = color.ResultFail() + rc.Fail++ + } + *globalTestCounter++ + rows = append(rows, row) + + // if there are no RuleResponse, the resource has been excluded. This is a pass. + if len(rows) == 0 { + row := table.Row{ + RowCompact: table.RowCompact{ + ID: *globalTestCounter, + Policy: color.Policy("", test.Policy), + Rule: color.Rule(test.Rule), + Resource: color.Resource(strings.Join(resourceParts[:len(resourceParts)-1], "/"), test.Namespace, resourceParts[len(resourceParts)-1]), // todo: handle namespace + Result: color.ResultPass(), + Reason: color.Excluded(), + IsFailure: false, + }, + Message: color.Excluded(), + } + rc.Skip++ + *globalTestCounter++ + rows = append(rows, row) + } + return rows +} + +func extractPatchedTargetFromEngineResponse(apiVersion, kind, resourceName, resourceNamespace string, response engineapi.EngineResponse) (*unstructured.Unstructured, *engineapi.RuleResponse) { + for _, rule := range response.PolicyResponse.Rules { + r, _, _ := rule.PatchedTarget() + if r != nil { + if resourceNamespace == "" { + resourceNamespace = r.GetNamespace() + } + if r.GetAPIVersion() == apiVersion && r.GetKind() == kind && r.GetName() == resourceName && r.GetNamespace() == resourceNamespace { + return r, &rule + } + } + } + return nil, nil +} + func printFailedTestResult(out io.Writer, resultsTable table.Table, detailedResults bool) { printer := table.NewTablePrinter(out) for i := range resultsTable.RawRows { diff --git a/cmd/cli/kubectl-kyverno/commands/test/test.go b/cmd/cli/kubectl-kyverno/commands/test/test.go index ea543b59ed..cb4c453369 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/test.go +++ b/cmd/cli/kubectl-kyverno/commands/test/test.go @@ -26,9 +26,16 @@ import ( engineapi "github.com/kyverno/kyverno/pkg/engine/api" policyvalidation "github.com/kyverno/kyverno/pkg/validation/policy" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ) -func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engineapi.EngineResponse, error) { +type TestResponse struct { + Trigger map[string][]engineapi.EngineResponse + Target map[string][]engineapi.EngineResponse +} + +func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) (*TestResponse, error) { // don't process test case with errors if testCase.Err != nil { return nil, testCase.Err @@ -75,6 +82,25 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi fmt.Fprintln(out, " warning: found duplicated resource", dup.Kind, dup.Name, dup.Namespace) } } + + targetResourcesPath := path.GetFullPaths(testCase.Test.TargetResources, testDir, isGit) + targetResources, err := common.GetResourceAccordingToResourcePath(out, testCase.Fs, targetResourcesPath, false, results.Policies, results.VAPs, dClient, "", false, testDir) + if err != nil { + return nil, fmt.Errorf("error: failed to load target resources (%s)", err) + } + + targets := []runtime.Object{} + for _, t := range targetResources { + targets = append(targets, t) + } + + // this will be a dclient containing all target resources. a policy may not do anything with any targets in these + dClient, err = dclient.NewFakeClient(runtime.NewScheme(), map[schema.GroupVersionResource]string{}, targets...) + if err != nil { + return nil, err + } + dClient.SetDiscovery(dclient.NewFakeDiscoveryClient(nil)) + // exceptions fmt.Fprintln(out, " Loading exceptions", "...") exceptionFullPath := path.GetFullPaths(testCase.Test.PolicyExceptions, testDir, isGit) @@ -164,7 +190,12 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi // execute engine var engineResponses []engineapi.EngineResponse var resultCounts processor.ResultCounts + testResponse := TestResponse{ + Trigger: map[string][]engineapi.EngineResponse{}, + Target: map[string][]engineapi.EngineResponse{}, + } for _, resource := range uniques { + // the policy processor is for multiple policies at once processor := processor.PolicyProcessor{ Store: &store, Policies: validPolicies, @@ -177,16 +208,28 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi NamespaceSelectorMap: vars.NamespaceSelectors(), Rc: &resultCounts, RuleToCloneSourceResource: ruleToCloneSourceResource, + Cluster: false, Client: dClient, Subresources: vars.Subresources(), - Out: out, + Out: io.Discard, } ers, err := processor.ApplyPoliciesOnResource() if err != nil { return nil, fmt.Errorf("failed to apply policies on resource %v (%w)", resource.GetName(), err) } + resourceKey := generateResourceKey(resource) engineResponses = append(engineResponses, ers...) + testResponse.Trigger[resourceKey] = ers } + for _, targetResource := range targetResources { + for _, engineResponse := range engineResponses { + if r, _ := extractPatchedTargetFromEngineResponse(targetResource.GetAPIVersion(), targetResource.GetKind(), targetResource.GetName(), targetResource.GetNamespace(), engineResponse); r != nil { + resourceKey := generateResourceKey(targetResource) + testResponse.Target[resourceKey] = append(testResponse.Target[resourceKey], engineResponse) + } + } + } + for _, resource := range uniques { processor := processor.ValidatingAdmissionPolicyProcessor{ Policies: results.VAPs, @@ -200,7 +243,13 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi if err != nil { return nil, fmt.Errorf("failed to apply policies on resource %s (%w)", resource.GetName(), err) } - engineResponses = append(engineResponses, ers...) + resourceKey := generateResourceKey(resource) + testResponse.Trigger[resourceKey] = append(testResponse.Trigger[resourceKey], ers...) } - return engineResponses, nil + // this is an array of responses of all policies, generated by all of their rules + return &testResponse, nil +} + +func generateResourceKey(resource *unstructured.Unstructured) string { + return resource.GetAPIVersion() + "," + resource.GetKind() + "," + resource.GetNamespace() + "," + resource.GetName() } 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 1357fd3002..14157d633b 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: (devel) name: tests.cli.kyverno.io spec: group: cli.kyverno.io @@ -182,6 +182,11 @@ spec: - result type: object type: array + targetResources: + description: Target Resources are for policies that have mutate existing + items: + type: string + type: array userinfo: description: UserInfo is the user info to be used in the test type: string diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml index 6bd9aca6db..7820dd44f8 100644 --- a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml +++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: (devel) name: userinfoes.cli.kyverno.io spec: group: cli.kyverno.io diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml index b227d23d93..f14203c22a 100644 --- a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml +++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: (devel) name: values.cli.kyverno.io spec: group: cli.kyverno.io 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 1357fd3002..14157d633b 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: (devel) name: tests.cli.kyverno.io spec: group: cli.kyverno.io @@ -182,6 +182,11 @@ spec: - result type: object type: array + targetResources: + description: Target Resources are for policies that have mutate existing + items: + type: string + type: array userinfo: description: UserInfo is the user info to be used in the test type: string diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml index 6bd9aca6db..7820dd44f8 100644 --- a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml +++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: (devel) name: userinfoes.cli.kyverno.io spec: group: cli.kyverno.io diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml index b227d23d93..f14203c22a 100644 --- a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml +++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: (devel) name: values.cli.kyverno.io spec: group: cli.kyverno.io diff --git a/cmd/cli/kubectl-kyverno/fix/test.go b/cmd/cli/kubectl-kyverno/fix/test.go index 1409aa2ea6..c6ab462e97 100644 --- a/cmd/cli/kubectl-kyverno/fix/test.go +++ b/cmd/cli/kubectl-kyverno/fix/test.go @@ -37,7 +37,7 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error) messages = append(messages, "test result should not use both `resource` and `resources` fields") } if result.Resource != "" { - var resources []string + var resources []any messages = append(messages, "test result uses deprecated `resource` field, moving it into the `resources` field") resources = append(resources, result.Resources...) resources = append(resources, result.Resource) @@ -67,7 +67,13 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error) if compress { compressed := map[v1alpha1.TestResultBase][]string{} for _, result := range results { - compressed[result.TestResultBase] = append(compressed[result.TestResultBase], result.Resources...) + resourcesAsStringArray := []string{} + for _, resource := range result.Resources { + if r, ok := resource.(string); ok { + resourcesAsStringArray = append(resourcesAsStringArray, r) + } + } + compressed[result.TestResultBase] = append(compressed[result.TestResultBase], resourcesAsStringArray...) } results = nil for k, v := range compressed { @@ -76,9 +82,13 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error) messages = append(messages, "test results contains duplicate resources") v = unique.UnsortedList() } + anyArray := make([]interface{}, len(v)) + for i, r := range v { + anyArray[i] = r + } results = append(results, v1alpha1.TestResult{ TestResultBase: k, - Resources: v, + Resources: anyArray, }) } } @@ -104,14 +114,25 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error) if x := cmp.Compare(a.CloneSourceResource, b.CloneSourceResource); x != 0 { return x } - slices.Sort(a.Resources) - slices.Sort(b.Resources) + asArray1 := []string{} + for _, r := range a.Resources { + resourceString, _ := r.(string) + asArray1 = append(asArray1, resourceString) + } + asArray2 := []string{} + for _, r := range b.Resources { + resourceString, _ := r.(string) + asArray2 = append(asArray1, resourceString) + } + slices.Sort(asArray1) + slices.Sort(asArray2) + if x := cmp.Compare(len(a.Resources), len(b.Resources)); x != 0 { return x } if len(a.Resources) == len(b.Resources) { for i := range a.Resources { - if x := cmp.Compare(a.Resources[i], b.Resources[i]); x != 0 { + if x := cmp.Compare(asArray1[i], asArray2[i]); x != 0 { return x } } diff --git a/cmd/cli/kubectl-kyverno/processor/generate.go b/cmd/cli/kubectl-kyverno/processor/generate.go index 7d6d5411e0..d311273237 100644 --- a/cmd/cli/kubectl-kyverno/processor/generate.go +++ b/cmd/cli/kubectl-kyverno/processor/generate.go @@ -122,6 +122,7 @@ func initializeMockController(out io.Writer, s *store.Store, gvrToListKind map[s imageverifycache.DisabledImageVerifyCache(), store.ContextLoaderFactory(s, nil), nil, + nil, )) return c, nil } diff --git a/cmd/cli/kubectl-kyverno/processor/policy_processor.go b/cmd/cli/kubectl-kyverno/processor/policy_processor.go index 5e00cd8fef..95308d4c5d 100644 --- a/cmd/cli/kubectl-kyverno/processor/policy_processor.go +++ b/cmd/cli/kubectl-kyverno/processor/policy_processor.go @@ -43,6 +43,7 @@ type PolicyProcessor struct { MutateLogPath string MutateLogPathIsDir bool Variables *variables.Variables + Cluster bool UserInfo *kyvernov2.RequestInfo PolicyReport bool NamespaceSelectorMap map[string]map[string]string @@ -72,6 +73,7 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse, if rclient == nil { rclient = registryclient.NewOrDie() } + isCluster := false eng := engine.NewEngine( cfg, config.NewDefaultMetricsConfiguration(), @@ -81,13 +83,14 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse, imageverifycache.DisabledImageVerifyCache(), store.ContextLoaderFactory(p.Store, nil), exceptions.New(policyExceptionLister), + &isCluster, ) gvk, subresource := resource.GroupVersionKind(), "" resourceKind := resource.GetKind() resourceName := resource.GetName() resourceNamespace := resource.GetNamespace() // If --cluster flag is not set, then we need to find the top level resource GVK and subresource - if p.Client == nil { + if !p.Cluster { for _, s := range p.Subresources { subgvk := schema.GroupVersionKind{ Group: s.Subresource.Group, @@ -377,6 +380,21 @@ func (p *PolicyProcessor) printOutput(resource interface{}, response engineapi.E return fmt.Errorf("failed to marshal (%w)", err) } + var yamlEncodedTargetResources [][]byte + for _, ruleResponese := range response.PolicyResponse.Rules { + patchedTarget, _, _ := ruleResponese.PatchedTarget() + + if patchedTarget != nil { + yamlEncodedResource, err := yamlv2.Marshal(patchedTarget.Object) + if err != nil { + return fmt.Errorf("failed to marshal (%w)", err) + } + + yamlEncodedResource = append(yamlEncodedResource, []byte("\n---\n")...) + yamlEncodedTargetResources = append(yamlEncodedTargetResources, yamlEncodedResource) + } + } + if p.MutateLogPath == "" { resource := string(yamlEncodedResource) + string("\n---") if len(strings.TrimSpace(resource)) > 0 { @@ -384,6 +402,12 @@ func (p *PolicyProcessor) printOutput(resource interface{}, response engineapi.E fmt.Fprintf(p.Out, "\npolicy %s applied to %s:", response.Policy().GetName(), resourcePath) } fmt.Fprintf(p.Out, "\n"+resource+"\n") //nolint:govet + if len(yamlEncodedTargetResources) > 0 { + fmt.Fprintf(p.Out, "patched targets: \n") + for _, patchedTarget := range yamlEncodedTargetResources { + fmt.Fprintf(p.Out, "\n"+string(patchedTarget)+"\n") + } + } } return nil } @@ -409,11 +433,14 @@ func (p *PolicyProcessor) printOutput(resource interface{}, response engineapi.E file = f } if _, err := file.Write([]byte(string(yamlEncodedResource) + "\n---\n\n")); err != nil { - if err := file.Close(); err != nil { - log.Log.Error(err, "failed to close file") - } return err } + + for _, patchedTarget := range yamlEncodedTargetResources { + if _, err := file.Write(patchedTarget); err != nil { + return err + } + } if err := file.Close(); err != nil { return err } diff --git a/cmd/cli/kubectl-kyverno/resource/resource.go b/cmd/cli/kubectl-kyverno/resource/resource.go index 177dea42a4..fbc574a94f 100644 --- a/cmd/cli/kubectl-kyverno/resource/resource.go +++ b/cmd/cli/kubectl-kyverno/resource/resource.go @@ -59,7 +59,8 @@ func YamlToUnstructured(resourceYaml []byte) (*unstructured.Unstructured, error) return resource, nil } -func GetResourceFromPath(fs billy.Filesystem, path string) ([]*unstructured.Unstructured, error) { +// should be able to specify a single resource in a multi yaml file, dont error out when there are multiple resource +func GetResourceFromPath(fs billy.Filesystem, path string, apiVersion, kind, resourceNamespace, resourceName string) (*unstructured.Unstructured, error) { var resourceBytes []byte if fs == nil { data, err := GetFileBytes(path) @@ -83,10 +84,18 @@ func GetResourceFromPath(fs billy.Filesystem, path string) ([]*unstructured.Unst if err != nil { return nil, err } - if len(resources) == 0 { - return nil, fmt.Errorf("no resources found") + + for _, r := range resources { + name := r.GetName() + ns := r.GetNamespace() + apiv := r.GetAPIVersion() + k := r.GetKind() + + if apiv == apiVersion && k == kind && name == resourceName && ns == resourceNamespace { + return r, nil + } } - return resources, nil + return nil, fmt.Errorf("resource with name %s not found in manifest", resourceName) } func GetFileBytes(path string) ([]byte, error) { diff --git a/cmd/cli/kubectl-kyverno/test/load.go b/cmd/cli/kubectl-kyverno/test/load.go index 155d964bc7..6865c2cf69 100644 --- a/cmd/cli/kubectl-kyverno/test/load.go +++ b/cmd/cli/kubectl-kyverno/test/load.go @@ -84,9 +84,6 @@ func LoadTest(fs billy.Filesystem, path string) TestCase { func cleanTest(test *v1alpha1.Test) { test.Policies = removeDuplicateStrings(test.Policies) test.Resources = removeDuplicateStrings(test.Resources) - for index, result := range test.Results { - test.Results[index].Resources = removeDuplicateStrings(result.Resources) - } } func removeDuplicateStrings(strings []string) []string { diff --git a/cmd/cli/kubectl-kyverno/test/load_test.go b/cmd/cli/kubectl-kyverno/test/load_test.go index 17d90e544f..5f9661dad7 100644 --- a/cmd/cli/kubectl-kyverno/test/load_test.go +++ b/cmd/cli/kubectl-kyverno/test/load_test.go @@ -65,7 +65,7 @@ func TestLoadTests(t *testing.T) { Result: policyreportv1alpha2.StatusPass, Rule: "only-allow-trusted-images", }, - Resources: []string{ + Resources: []any{ "test-pod-with-non-root-user-image", "test-pod-with-trusted-registry", }, @@ -97,7 +97,7 @@ func TestLoadTests(t *testing.T) { Rule: "generate-limitrange", GeneratedResource: "generatedLimitRange.yaml", }, - Resources: []string{"hello-world-namespace"}, + Resources: []any{"hello-world-namespace"}, }, { TestResultBase: v1alpha1.TestResultBase{ Kind: "Namespace", @@ -106,7 +106,7 @@ func TestLoadTests(t *testing.T) { Rule: "generate-resourcequota", GeneratedResource: "generatedResourceQuota.yaml", }, - Resources: []string{"hello-world-namespace"}, + Resources: []any{"hello-world-namespace"}, }}, }, }}, @@ -134,7 +134,7 @@ func TestLoadTests(t *testing.T) { Result: policyreportv1alpha2.StatusPass, Rule: "only-allow-trusted-images", }, - Resources: []string{ + Resources: []any{ "test-pod-with-non-root-user-image", "test-pod-with-trusted-registry", }, @@ -160,7 +160,7 @@ func TestLoadTests(t *testing.T) { Rule: "generate-limitrange", GeneratedResource: "generatedLimitRange.yaml", }, - Resources: []string{"hello-world-namespace"}, + Resources: []any{"hello-world-namespace"}, }, { TestResultBase: v1alpha1.TestResultBase{ Kind: "Namespace", @@ -169,7 +169,7 @@ func TestLoadTests(t *testing.T) { Rule: "generate-resourcequota", GeneratedResource: "generatedResourceQuota.yaml", }, - Resources: []string{"hello-world-namespace"}, + Resources: []any{"hello-world-namespace"}, }}, }, }}, @@ -234,7 +234,7 @@ func TestLoadTest(t *testing.T) { Result: policyreportv1alpha2.StatusPass, Rule: "only-allow-trusted-images", }, - Resources: []string{ + Resources: []any{ "test-pod-with-non-root-user-image", "test-pod-with-trusted-registry", }, @@ -263,7 +263,7 @@ func TestLoadTest(t *testing.T) { Result: policyreportv1alpha2.StatusPass, Rule: "only-allow-trusted-images", }, - Resources: []string{ + Resources: []any{ "test-pod-with-non-root-user-image", "test-pod-with-trusted-registry", }, diff --git a/cmd/internal/engine.go b/cmd/internal/engine.go index 4745e4c24d..777dbe3917 100644 --- a/cmd/internal/engine.go +++ b/cmd/internal/engine.go @@ -53,6 +53,7 @@ func NewEngine( ivCache, factories.DefaultContextLoaderFactory(configMapResolver, factories.WithAPICallConfig(apiCallConfig), factories.WithGlobalContextStore(gctxStore)), exceptionsSelector, + nil, ) } diff --git a/docs/user/cli/commands/kyverno_apply.md b/docs/user/cli/commands/kyverno_apply.md index 582e6b7ab9..c1976beba2 100644 --- a/docs/user/cli/commands/kyverno_apply.md +++ b/docs/user/cli/commands/kyverno_apply.md @@ -60,6 +60,8 @@ kyverno apply [flags] -s, --set strings Variables that are required -i, --stdin Optional mutate policy parameter to pipe directly through to kubectl -t, --table Show results in table format + --target-resource strings Path to individual files containing target resources files for policies that have mutate existing + --target-resources strings Path to a directory containing target resources files for policies that have mutate existing -u, --userinfo string Admission Info including Roles, Cluster Roles and Subjects -f, --values-file string File containing values for policy variables --warn-exit-code int Set the exit code for warnings; if failures or errors are found, will exit 1 diff --git a/docs/user/cli/crd/index.html b/docs/user/cli/crd/index.html index 2271866e61..6be608f0a2 100644 --- a/docs/user/cli/crd/index.html +++ b/docs/user/cli/crd/index.html @@ -112,6 +112,17 @@ This field is deprecated, use <code>metadata.name</code> instead</p> </tr> <tr> <td> +<code>targetResources</code><br/> +<em> +[]string +</em> +</td> +<td> +<p>Target Resources are for policies that have mutate existing</p> +</td> +</tr> +<tr> +<td> <code>variables</code><br/> <em> string @@ -655,6 +666,81 @@ Kubernetes meta/v1.APIResource </tbody> </table> <hr /> +<h3 id="cli.kyverno.io/v1alpha1.TestResourceSpec">TestResourceSpec +</h3> +<p> +</p> +<table class="table table-striped"> +<thead class="thead-dark"> +<tr> +<th>Field</th> +<th>Description</th> +</tr> +</thead> +<tbody> +<tr> +<td> +<code>group</code><br/> +<em> +string +</em> +</td> +<td> +</td> +</tr> +<tr> +<td> +<code>version</code><br/> +<em> +string +</em> +</td> +<td> +</td> +</tr> +<tr> +<td> +<code>kind</code><br/> +<em> +string +</em> +</td> +<td> +</td> +</tr> +<tr> +<td> +<code>namespace</code><br/> +<em> +string +</em> +</td> +<td> +</td> +</tr> +<tr> +<td> +<code>subresource</code><br/> +<em> +string +</em> +</td> +<td> +</td> +</tr> +<tr> +<td> +<code>name</code><br/> +<em> +string +</em> +</td> +<td> +</td> +</tr> +</tbody> +</table> +<hr /> <h3 id="cli.kyverno.io/v1alpha1.TestResult">TestResult </h3> <p> @@ -706,7 +792,7 @@ TestResultDeprecated <td> <code>resources</code><br/> <em> -[]string +[]any </em> </td> <td> diff --git a/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html b/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html index 00fad5c552..61460d8278 100644 --- a/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html +++ b/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html @@ -193,6 +193,35 @@ This field is deprecated, use <code>metadata.name</code> instead</p> + <tr> + <td><code>targetResources</code> + + <span style="color:blue;"> *</span> + + </br> + + + + + <span style="font-family: monospace">[]string</span> + + + </td> + <td> + + + <p>Target Resources are for policies that have mutate existing</p> + + + + + + </td> + </tr> + + + + <tr> <td><code>variables</code> @@ -1475,7 +1504,7 @@ This field is deprecated, use <code>metadata.name</code> instead</p> - <span style="font-family: monospace">[]string</span> + <span style="font-family: monospace">[]any</span> </td> diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 8a1d0cb008..0dec3ebc49 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -31,6 +31,7 @@ type engine struct { metricsConfiguration config.MetricsConfiguration jp jmespath.Interface client engineapi.Client + isCluster bool rclientFactory engineapi.RegistryClientFactory ivCache imageverifycache.Client contextLoader engineapi.ContextLoaderFactory @@ -51,7 +52,12 @@ func NewEngine( ivCache imageverifycache.Client, contextLoader engineapi.ContextLoaderFactory, exceptionSelector engineapi.PolicyExceptionSelector, + isCluster *bool, ) engineapi.Engine { + if isCluster == nil { + defaultCluster := true + isCluster = &defaultCluster + } meter := otel.GetMeterProvider().Meter(metrics.MeterName) resultCounter, err := meter.Int64Counter( "kyverno_policy_results", @@ -74,6 +80,7 @@ func NewEngine( client: client, rclientFactory: rclientFactory, ivCache: ivCache, + isCluster: *isCluster, contextLoader: contextLoader, exceptionSelector: exceptionSelector, resultCounter: resultCounter, @@ -107,8 +114,8 @@ func (e *engine) Mutate( if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) { policyResponse, patchedResource := e.mutate(ctx, logger, policyContext) response = response. - WithPolicyResponse(policyResponse). - WithPatchedResource(patchedResource) + WithPatchedResource(patchedResource). + WithPolicyResponse(policyResponse) } response = response.WithStats(engineapi.NewExecutionStats(startTime, time.Now())) e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response) diff --git a/pkg/engine/fuzz_test.go b/pkg/engine/fuzz_test.go index 64656e5be2..5ff73847f0 100644 --- a/pkg/engine/fuzz_test.go +++ b/pkg/engine/fuzz_test.go @@ -43,6 +43,7 @@ var ( imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(nil), nil, + nil, ) initter sync.Once ) @@ -126,6 +127,7 @@ func FuzzVerifyImageAndPatchTest(f *testing.F) { imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(nil), nil, + nil, ) _, _ = verifyImageAndPatchEngine.VerifyAndPatchImages( @@ -271,6 +273,7 @@ func FuzzMutateTest(f *testing.F) { imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(nil), nil, + nil, ) e.Mutate( context.Background(), diff --git a/pkg/engine/handlers/validation/validate_cel.go b/pkg/engine/handlers/validation/validate_cel.go index 49d9303354..f6c3c4e443 100644 --- a/pkg/engine/handlers/validation/validate_cel.go +++ b/pkg/engine/handlers/validation/validate_cel.go @@ -30,12 +30,14 @@ import ( ) type validateCELHandler struct { - client engineapi.Client + client engineapi.Client + isCluster bool } -func NewValidateCELHandler(client engineapi.Client) (handlers.Handler, error) { +func NewValidateCELHandler(client engineapi.Client, isCluster bool) (handlers.Handler, error) { return validateCELHandler{ - client: client, + client: client, + isCluster: isCluster, }, nil } @@ -140,7 +142,7 @@ func (h validateCELHandler) Process( ns = "" } if ns != "" { - if h.client != nil { + if h.client != nil && h.isCluster { namespace, err = h.client.GetNamespace(ctx, ns, metav1.GetOptions{}) if err != nil { return resource, handlers.WithResponses( diff --git a/pkg/engine/image_verify_test.go b/pkg/engine/image_verify_test.go index 1bf2e51bbc..6d63988315 100644 --- a/pkg/engine/image_verify_test.go +++ b/pkg/engine/image_verify_test.go @@ -324,6 +324,7 @@ func testVerifyAndPatchImages( imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(cmResolver), nil, + nil, ) return e.VerifyAndPatchImages( ctx, @@ -1059,6 +1060,7 @@ func testImageVerifyCache( ivCache, factories.DefaultContextLoaderFactory(cmResolver), nil, + nil, ) return e.VerifyAndPatchImages( ctx, diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go index 12d3c99064..fe3b5aca7e 100644 --- a/pkg/engine/mutation_test.go +++ b/pkg/engine/mutation_test.go @@ -42,6 +42,7 @@ func testMutate( imageverifycache.DisabledImageVerifyCache(), contextLoader, nil, + nil, ) return e.Mutate( ctx, diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 165343a713..7bdc087790 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -51,7 +51,7 @@ func (e *engine) validate( } else if hasValidatePss { return validation.NewValidatePssHandler() } else if hasValidateCEL { - return validation.NewValidateCELHandler(e.client) + return validation.NewValidateCELHandler(e.client, e.isCluster) } else { return validation.NewValidateResourceHandler() } diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index de2ec61c49..8a2a299ae7 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -41,6 +41,7 @@ func testValidate( imageverifycache.DisabledImageVerifyCache(), contextLoader, nil, + nil, ) return e.Validate( ctx, diff --git a/pkg/webhooks/resource/fake.go b/pkg/webhooks/resource/fake.go index 51f7fbe677..09e2073c50 100644 --- a/pkg/webhooks/resource/fake.go +++ b/pkg/webhooks/resource/fake.go @@ -66,6 +66,7 @@ func NewFakeHandlers(ctx context.Context, policyCache policycache.Cache) *resour imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(configMapResolver), exceptions.New(peLister), + nil, ), } } diff --git a/pkg/webhooks/resource/validation_test.go b/pkg/webhooks/resource/validation_test.go index f13c84abac..d0a28167d1 100644 --- a/pkg/webhooks/resource/validation_test.go +++ b/pkg/webhooks/resource/validation_test.go @@ -2086,6 +2086,7 @@ func TestValidate_failure_action_overrides(t *testing.T) { imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(nil), nil, + nil, ) for i, tc := range testcases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { @@ -2188,6 +2189,7 @@ func Test_RuleSelector(t *testing.T) { imageverifycache.DisabledImageVerifyCache(), factories.DefaultContextLoaderFactory(nil), nil, + nil, ) resp := eng.Validate( context.TODO(), diff --git a/test/cli/test-mutate/add-default-resources/kyverno-test.yaml b/test/cli/test-mutate/add-default-resources/kyverno-test.yaml index a31438600c..343ba724b3 100644 --- a/test/cli/test-mutate/add-default-resources/kyverno-test.yaml +++ b/test/cli/test-mutate/add-default-resources/kyverno-test.yaml @@ -7,13 +7,6 @@ policies: resources: - resource.yaml results: -- kind: Pod - patchedResources: patchedResource1.yaml - policy: add-default-resources - resources: - - nginx-demo1 - result: pass - rule: add-default-requests - kind: Pod patchedResources: patchedResource3.yaml policy: add-default-resources @@ -21,6 +14,13 @@ results: - nginx-demo3 result: pass rule: add-default-requests +- kind: Pod + patchedResources: patchedResource1.yaml + policy: add-default-resources + resources: + - nginx-demo1 + result: pass + rule: add-default-requests - kind: Pod patchedResources: patchedResource2.yaml policy: add-default-resources diff --git a/test/cli/test-mutate/global-anchor/kyverno-test.yaml b/test/cli/test-mutate/global-anchor/kyverno-test.yaml index e413c72631..57d1fba0b7 100644 --- a/test/cli/test-mutate/global-anchor/kyverno-test.yaml +++ b/test/cli/test-mutate/global-anchor/kyverno-test.yaml @@ -7,13 +7,6 @@ policies: resources: - resources.yaml results: -- kind: Pod - patchedResources: patchedResource.yaml - policy: add-safe-to-evict - resources: - - pod-with-emptydir-hostpath - result: pass - rule: annotate-empty-dir - kind: Pod patchedResources: patchedResourceWithVolume.yaml policy: add-safe-to-evict @@ -21,6 +14,13 @@ results: - pod-with-emptydir-hostpath-1 result: pass rule: annotate-empty-dir +- kind: Pod + patchedResources: patchedResource.yaml + policy: add-safe-to-evict + resources: + - pod-with-emptydir-hostpath + result: pass + rule: annotate-empty-dir - kind: Pod policy: add-safe-to-evict resources: diff --git a/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml b/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml index 6d88fcb8a4..241eadfaa2 100644 --- a/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml +++ b/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml @@ -7,6 +7,12 @@ policies: resources: - resource.yaml results: +- kind: Pod + policy: karpenter-annotations-to-nodeselector + resources: + - soft-pod-antiaffinity-1-copy + result: pass + rule: hard-nodeselector-lifecycle-on-demand - kind: Pod patchedResources: patched.yaml policy: karpenter-annotations-to-nodeselector @@ -14,9 +20,3 @@ results: - soft-pod-antiaffinity-1 result: pass rule: hard-nodeselector-lifecycle-on-demand -- kind: Pod - policy: karpenter-annotations-to-nodeselector - resources: - - soft-pod-antiaffinity-1-copy - result: pass - rule: hard-nodeselector-lifecycle-on-demand diff --git a/test/cli/test-mutate/kyverno-test.yaml b/test/cli/test-mutate/kyverno-test.yaml index df8be89492..fc70882001 100644 --- a/test/cli/test-mutate/kyverno-test.yaml +++ b/test/cli/test-mutate/kyverno-test.yaml @@ -14,13 +14,6 @@ results: - mydeploy result: pass rule: add-label -- kind: Pod - patchedResources: patchedResource3.yaml - policy: add-label - resources: - - production/same-name-but-diff-namespace - result: pass - rule: add-label - kind: Pod patchedResources: patchedResource6.yaml policy: add-label @@ -35,6 +28,13 @@ results: - testing/same-name-but-diff-namespace result: pass rule: add-label +- kind: Pod + patchedResources: patchedResource3.yaml + policy: add-label + resources: + - production/same-name-but-diff-namespace + result: pass + rule: add-label - kind: Pod patchedResources: patchedResource1.yaml policy: add-label diff --git a/test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml b/test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml new file mode 100644 index 0000000000..37619aba43 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml @@ -0,0 +1,18 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: kyverno-test.yaml +policies: +- policy.yaml +resources: +- trigger-cm.yaml +results: +- kind: "" + patchedResources: mutated-secret.yaml + policy: mutate-existing-secret + resources: + - secret-1 + result: fail + rule: mutate-secret-on-configmap-create +targetResources: +- raw-secret.yaml diff --git a/test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml b/test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml new file mode 100755 index 0000000000..51e4b6ab0b --- /dev/null +++ b/test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + labels: + otherlabel: bar + name: secret-1 + namespace: staging diff --git a/test/cli/test-mutate/mutate-existing-fail/policy.yaml b/test/cli/test-mutate/mutate-existing-fail/policy.yaml new file mode 100755 index 0000000000..00731ebc95 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing-fail/policy.yaml @@ -0,0 +1,27 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: mutate-existing-secret +spec: + rules: + - match: + any: + - resources: + kinds: + - ConfigMap + names: + - dictionary-1 + namespaces: + - staging + mutate: + mutateExistingOnPolicyUpdate: false + patchStrategicMerge: + metadata: + labels: + foo: bar + targets: + - apiVersion: v1 + kind: Secret + name: secret-1 + namespace: staging + name: mutate-secret-on-configmap-create diff --git a/test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml b/test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml new file mode 100644 index 0000000000..935cb1be92 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-1 + namespace: staging diff --git a/test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml b/test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml new file mode 100755 index 0000000000..b458868bc4 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + name: dictionary-1 + namespace: staging diff --git a/test/cli/test-mutate/mutate-existing/kyverno-test.yaml b/test/cli/test-mutate/mutate-existing/kyverno-test.yaml new file mode 100644 index 0000000000..c446c3a4a3 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing/kyverno-test.yaml @@ -0,0 +1,17 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: kyverno-test.yaml +policies: +- policy.yaml +resources: +- trigger-cm.yaml +results: +- kind: "" + patchedResources: mutated-secret.yaml + policy: mutate-existing-secret + resources: [] + result: pass + rule: mutate-secret-on-configmap-create +targetResources: +- raw-secret.yaml diff --git a/test/cli/test-mutate/mutate-existing/mutated-secret.yaml b/test/cli/test-mutate/mutate-existing/mutated-secret.yaml new file mode 100755 index 0000000000..769daa4ab5 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing/mutated-secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + name: dictionary-1 + namespace: staging +--- +apiVersion: v1 +kind: Secret +metadata: + labels: + foo: bar + name: secret-1 + namespace: staging diff --git a/test/cli/test-mutate/mutate-existing/policy.yaml b/test/cli/test-mutate/mutate-existing/policy.yaml new file mode 100755 index 0000000000..00731ebc95 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing/policy.yaml @@ -0,0 +1,27 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: mutate-existing-secret +spec: + rules: + - match: + any: + - resources: + kinds: + - ConfigMap + names: + - dictionary-1 + namespaces: + - staging + mutate: + mutateExistingOnPolicyUpdate: false + patchStrategicMerge: + metadata: + labels: + foo: bar + targets: + - apiVersion: v1 + kind: Secret + name: secret-1 + namespace: staging + name: mutate-secret-on-configmap-create diff --git a/test/cli/test-mutate/mutate-existing/raw-secret.yaml b/test/cli/test-mutate/mutate-existing/raw-secret.yaml new file mode 100644 index 0000000000..935cb1be92 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing/raw-secret.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-1 + namespace: staging diff --git a/test/cli/test-mutate/mutate-existing/trigger-cm.yaml b/test/cli/test-mutate/mutate-existing/trigger-cm.yaml new file mode 100755 index 0000000000..b458868bc4 --- /dev/null +++ b/test/cli/test-mutate/mutate-existing/trigger-cm.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + name: dictionary-1 + namespace: staging diff --git a/test/cli/test-mutate/same-name-mutate-existing/cm.yaml b/test/cli/test-mutate/same-name-mutate-existing/cm.yaml new file mode 100755 index 0000000000..b458868bc4 --- /dev/null +++ b/test/cli/test-mutate/same-name-mutate-existing/cm.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + foo: bar +kind: ConfigMap +metadata: + name: dictionary-1 + namespace: staging diff --git a/test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml b/test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml new file mode 100644 index 0000000000..656eaa0316 --- /dev/null +++ b/test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml @@ -0,0 +1,20 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: kyverno-test.yaml +policies: +- policy.yaml +resources: +- cm.yaml +results: +- kind: "" + patchedResources: mutated-resources.yaml + policy: mutate-existing-secret + resources: + - staging/secret-1 + - prod/secret-1 + result: pass + rule: mutate-secret-on-configmap-create +targetResources: +- secret-1.yaml +- secret-2.yaml diff --git a/test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml b/test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml new file mode 100755 index 0000000000..e8515724f4 --- /dev/null +++ b/test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + labels: + foo: bar + name: secret-1 + namespace: staging +--- +apiVersion: v1 +kind: Secret +metadata: + labels: + foo: bar + name: secret-1 + namespace: prod + diff --git a/test/cli/test-mutate/same-name-mutate-existing/policy.yaml b/test/cli/test-mutate/same-name-mutate-existing/policy.yaml new file mode 100755 index 0000000000..4fc8b6afa0 --- /dev/null +++ b/test/cli/test-mutate/same-name-mutate-existing/policy.yaml @@ -0,0 +1,26 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: mutate-existing-secret +spec: + rules: + - match: + any: + - resources: + kinds: + - ConfigMap + names: + - dictionary-1 + namespaces: + - staging + mutate: + mutateExistingOnPolicyUpdate: false + patchStrategicMerge: + metadata: + labels: + foo: bar + targets: + - apiVersion: v1 + kind: Secret + name: secret-1 + name: mutate-secret-on-configmap-create diff --git a/test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml b/test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml new file mode 100644 index 0000000000..935cb1be92 --- /dev/null +++ b/test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-1 + namespace: staging diff --git a/test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml b/test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml new file mode 100644 index 0000000000..00f314554d --- /dev/null +++ b/test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-1 + namespace: prod diff --git a/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml b/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml index 4e44f2a671..bcf45a4156 100644 --- a/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml +++ b/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml @@ -7,13 +7,6 @@ policies: resources: - resources.yaml results: -- kind: ScaledObject - patchedResources: patchedResource1.yaml - policy: keda-prometheus-serveraddress - resources: - - service-1 - result: pass - rule: keda-prometheus-serveraddress - kind: ScaledObject patchedResources: patchedResource2.yaml policy: keda-prometheus-serveraddress @@ -21,6 +14,13 @@ results: - service-2 result: pass rule: keda-prometheus-serveraddress +- kind: ScaledObject + patchedResources: patchedResource1.yaml + policy: keda-prometheus-serveraddress + resources: + - service-1 + result: pass + rule: keda-prometheus-serveraddress - kind: ScaledObject policy: keda-prometheus-serveraddress resources: