diff --git a/cmd/cli/kubectl-kyverno/commands/test/test.go b/cmd/cli/kubectl-kyverno/commands/test/test.go index 79aaa4b334..05b7bcfb7f 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/test.go +++ b/cmd/cli/kubectl-kyverno/commands/test/test.go @@ -88,22 +88,31 @@ func runTest(out io.Writer, testCase test.TestCase, auditWarn bool) ([]engineapi } if rule.Name == res.Rule { if rule.HasGenerate() { - ruleUnstr, err := generate.GetUnstrRule(rule.Generation.DeepCopy()) - if err != nil { - fmt.Fprintf(out, " Error: failed to get unstructured rule (%s)\n", err) - break - } - genClone, _, err := unstructured.NestedMap(ruleUnstr.Object, "clone") - if err != nil { - fmt.Fprintf(out, " Error: failed to read data (%s)\n", err) - break - } - if len(genClone) != 0 { + if len(rule.Generation.CloneList.Kinds) != 0 { // cloneList + // We cannot cast this to an unstructured object because it doesn't have a kind. if isGit { ruleToCloneSourceResource[rule.Name] = res.CloneSourceResource } else { ruleToCloneSourceResource[rule.Name] = path.GetFullPath(res.CloneSourceResource, testDir) } + } else { // clone or data + ruleUnstr, err := generate.GetUnstrRule(rule.Generation.DeepCopy()) + if err != nil { + fmt.Fprintf(out, " Error: failed to get unstructured rule (%s)\n", err) + break + } + genClone, _, err := unstructured.NestedMap(ruleUnstr.Object, "clone") + if err != nil { + fmt.Fprintf(out, " Error: failed to read data (%s)\n", err) + break + } + if len(genClone) != 0 { + if isGit { + ruleToCloneSourceResource[rule.Name] = res.CloneSourceResource + } else { + ruleToCloneSourceResource[rule.Name] = path.GetFullPath(res.CloneSourceResource, testDir) + } + } } } break diff --git a/cmd/cli/kubectl-kyverno/processor/generate.go b/cmd/cli/kubectl-kyverno/processor/generate.go index d646877abd..ce767e52b8 100644 --- a/cmd/cli/kubectl-kyverno/processor/generate.go +++ b/cmd/cli/kubectl-kyverno/processor/generate.go @@ -10,6 +10,7 @@ import ( "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/resource" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/store" + "github.com/kyverno/kyverno/pkg/autogen" "github.com/kyverno/kyverno/pkg/background/generate" "github.com/kyverno/kyverno/pkg/clients/dclient" "github.com/kyverno/kyverno/pkg/config" @@ -18,6 +19,7 @@ import ( engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/imageverifycache" + kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" @@ -43,7 +45,36 @@ func handleGeneratePolicy(out io.Writer, generateResponse *engineapi.EngineRespo } } - c, err := initializeMockController(out, objects) + listKinds := map[schema.GroupVersionResource]string{} + + // Collect items in a potential cloneList to provide list kinds to the fake dynamic client. + for _, rule := range autogen.ComputeRules(policyContext.Policy()) { + if !rule.HasGenerate() || len(rule.Generation.CloneList.Kinds) == 0 { + continue + } + + for _, kind := range rule.Generation.CloneList.Kinds { + apiVersion, kind := kubeutils.GetKindFromGVK(kind) + + if apiVersion == "" || kind == "" { + continue + } + + gv, err := schema.ParseGroupVersion(apiVersion) + if err != nil { + fmt.Fprintf(out, "failed to parse group and version from clone list kind %s: %v\n", apiVersion, err) + continue + } + + listKinds[schema.GroupVersionResource{ + Group: gv.Group, + Version: gv.Version, + Resource: strings.ToLower(kind) + "s", + }] = kind + "List" + } + } + + c, err := initializeMockController(out, listKinds, objects) if err != nil { fmt.Fprintln(out, "error at controller") return nil, err @@ -82,8 +113,8 @@ func handleGeneratePolicy(out io.Writer, generateResponse *engineapi.EngineRespo return newRuleResponse, nil } -func initializeMockController(out io.Writer, objects []runtime.Object) (*generate.GenerateController, error) { - client, err := dclient.NewFakeClient(runtime.NewScheme(), nil, objects...) +func initializeMockController(out io.Writer, gvrToListKind map[schema.GroupVersionResource]string, objects []runtime.Object) (*generate.GenerateController, error) { + client, err := dclient.NewFakeClient(runtime.NewScheme(), gvrToListKind, objects...) if err != nil { fmt.Fprintf(out, "Failed to mock dynamic client") return nil, err diff --git a/test/cli/test-generate/clone-list/cloneSourceResources.yaml b/test/cli/test-generate/clone-list/cloneSourceResources.yaml new file mode 100644 index 0000000000..0027def781 --- /dev/null +++ b/test/cli/test-generate/clone-list/cloneSourceResources.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Secret +metadata: + name: regcred + namespace: default + labels: + allowedToBeCloned: "true" +type: Opaque +data: + password: MWYyZDFlMmU2N2Rm + +--- +apiVersion: v1 +kind: Secret +metadata: + name: missing-label + namespace: default +type: Opaque +data: + password: MWYyZDFlMmU2N2Rm diff --git a/test/cli/test-generate/clone-list/generatedResource.yaml b/test/cli/test-generate/clone-list/generatedResource.yaml new file mode 100644 index 0000000000..012c280a67 --- /dev/null +++ b/test/cli/test-generate/clone-list/generatedResource.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: regcred + namespace: hello-world-namespace + labels: + allowedToBeCloned: "true" +type: Opaque +data: + password: MWYyZDFlMmU2N2Rm diff --git a/test/cli/test-generate/clone-list/kyverno-test.yaml b/test/cli/test-generate/clone-list/kyverno-test.yaml new file mode 100644 index 0000000000..a84cfe690d --- /dev/null +++ b/test/cli/test-generate/clone-list/kyverno-test.yaml @@ -0,0 +1,17 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: kyverno-test.yaml +policies: + - policy.yaml +resources: + - resource.yaml +results: + - policy: clone-list-secrets + rule: clone-list-labelled-secrets + resources: + - hello-world-namespace + cloneSourceResource: cloneSourceResources.yaml + generatedResource: generatedResource.yaml + kind: Namespace + result: pass diff --git a/test/cli/test-generate/clone-list/policy.yaml b/test/cli/test-generate/clone-list/policy.yaml new file mode 100644 index 0000000000..d2c5a02762 --- /dev/null +++ b/test/cli/test-generate/clone-list/policy.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + annotations: + policies.kyverno.io/category: Sample + policies.kyverno.io/description: 'Secrets like registry credentials often need + to exist in multiple Namespaces so Pods there have access. Manually duplicating + those Secrets is time consuming and error prone. This policy will copy all Secrets + with the appropriate label which exists in the `default` Namespace to new Namespaces + when they are created. It will also push updates to the copied Secrets should the + source Secret be changed.' + policies.kyverno.io/subject: Secret + policies.kyverno.io/title: Clone List Secrets + name: clone-list-secrets +spec: + admission: true + background: true + rules: + - generate: + cloneList: + namespace: default + kinds: + - v1/Secret + - v1/ConfigMap + selector: + matchLabels: + allowedToBeCloned: "true" + namespace: '{{request.object.metadata.name}}' + synchronize: true + match: + any: + - resources: + kinds: + - Namespace + name: clone-list-labelled-secrets + validationFailureAction: Audit diff --git a/test/cli/test-generate/clone-list/resource.yaml b/test/cli/test-generate/clone-list/resource.yaml new file mode 100644 index 0000000000..9c419f9933 --- /dev/null +++ b/test/cli/test-generate/clone-list/resource.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world-namespace + namespace: hello-world-namespace