diff --git a/pkg/autogenv2/autogen.go b/pkg/autogenv2/autogen.go new file mode 100644 index 0000000000..8c694fe4d2 --- /dev/null +++ b/pkg/autogenv2/autogen.go @@ -0,0 +1,200 @@ +package autogenv2 + +import ( + "fmt" + "sort" + "strings" + + "github.com/kyverno/kyverno/api/kyverno" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/sets" +) + +const ( + // PodControllerCronJob represent CronJob string + PodControllerCronJob = "CronJob" +) + +var ( + PodControllers = sets.New("DaemonSet", "Deployment", "Job", "StatefulSet", "ReplicaSet", "ReplicationController", "CronJob") + podControllersKindsSet = PodControllers.Union(sets.New("Pod")) +) + +// AutogenV2 defines the interface for the new autogeneration strategy. +type AutogenV2 interface { + ExtractPodSpec(resource unstructured.Unstructured) (*unstructured.Unstructured, error) +} + +// ImplAutogenV2 is the implementation of the AutogenV2 interface. +type ImplAutogenV2 struct{} + +// NewAutogenV2 creates a new instance of AutogenV2. +func NewAutogenV2() AutogenV2 { + return &ImplAutogenV2{} +} + +func splitKinds(controllers, separator string) []string { + kinds := strings.Split(controllers, separator) + sort.Strings(kinds) + return kinds +} + +func isKindOtherthanPod(kinds []string) bool { + if len(kinds) > 1 && kubeutils.ContainsKind(kinds, "Pod") { + return true + } + return false +} + +func checkAutogenSupport(needed *bool, subjects ...kyvernov1.ResourceDescription) bool { + for _, subject := range subjects { + if subject.Name != "" || len(subject.Names) > 0 || subject.Selector != nil || subject.Annotations != nil || isKindOtherthanPod(subject.Kinds) { + return false + } + if needed != nil { + *needed = *needed || podControllersKindsSet.HasAny(subject.Kinds...) + } + } + return true +} + +// CanAutoGen checks whether the rule(s) (in policy) can be applied to Pod controllers +// returns controllers as: +// - "" if: +// - name or selector is defined +// - mixed kinds (Pod + pod controller) is defined +// - Pod and PodControllers are not defined +// - mutate.Patches/mutate.PatchesJSON6902/validate.deny/generate rule is defined +// +// - otherwise it returns all pod controllers +func CanAutoGen(spec *kyvernov1.Spec) (applyAutoGen bool, controllers sets.Set[string]) { + needed := false + for _, rule := range spec.Rules { + if rule.HasGenerate() { + return false, sets.New("none") + } + if rule.Mutation != nil { + if rule.Mutation.PatchesJSON6902 != "" { + return false, sets.New("none") + } + for _, foreach := range rule.Mutation.ForEachMutation { + if foreach.PatchesJSON6902 != "" { + return false, sets.New("none") + } + } + } + match := rule.MatchResources + if !checkAutogenSupport(&needed, match.ResourceDescription) { + debug.Info("skip generating rule on pod controllers: Name / Selector in resource description may not be applicable.", "rule", rule.Name) + return false, sets.New[string]() + } + for _, value := range match.Any { + if !checkAutogenSupport(&needed, value.ResourceDescription) { + debug.Info("skip generating rule on pod controllers: Name / Selector in match any block is not applicable.", "rule", rule.Name) + return false, sets.New[string]() + } + } + for _, value := range match.All { + if !checkAutogenSupport(&needed, value.ResourceDescription) { + debug.Info("skip generating rule on pod controllers: Name / Selector in match all block is not applicable.", "rule", rule.Name) + return false, sets.New[string]() + } + } + if exclude := rule.ExcludeResources; exclude != nil { + if !checkAutogenSupport(&needed, exclude.ResourceDescription) { + debug.Info("skip generating rule on pod controllers: Name / Selector in resource description may not be applicable.", "rule", rule.Name) + return false, sets.New[string]() + } + for _, value := range exclude.Any { + if !checkAutogenSupport(&needed, value.ResourceDescription) { + debug.Info("skip generating rule on pod controllers: Name / Selector in exclude any block is not applicable.", "rule", rule.Name) + return false, sets.New[string]() + } + } + for _, value := range exclude.All { + if !checkAutogenSupport(&needed, value.ResourceDescription) { + debug.Info("skip generating rule on pod controllers: Name / Selector in exclud all block is not applicable.", "rule", rule.Name) + return false, sets.New[string]() + } + } + } + } + if !needed { + return false, sets.New[string]() + } + return true, PodControllers +} + +func GetSupportedControllers(spec *kyvernov1.Spec) sets.Set[string] { + apply, controllers := CanAutoGen(spec) + if !apply || (controllers.Len() == 1 && controllers.Has("none")) { + return nil + } + return controllers +} + +// GetRequestedControllers returns the requested autogen controllers based on object annotations. +func GetRequestedControllers(meta *metav1.ObjectMeta) sets.Set[string] { + annotations := meta.GetAnnotations() + if annotations == nil { + return nil + } + controllers, ok := annotations[kyverno.AnnotationAutogenControllers] + if !ok || controllers == "" { + return nil + } + if controllers == "none" { + return sets.New[string]() + } + return sets.New(splitKinds(controllers, ",")...) +} + +// GetControllers computes the autogen controllers that should be applied to a policy. +// It returns the requested, supported and effective controllers (intersection of requested and supported ones). +func GetControllers(meta *metav1.ObjectMeta, spec *kyvernov1.Spec) ([]string, []string, []string) { + // compute supported and requested controllers + supported, requested := GetSupportedControllers(spec), GetRequestedControllers(meta) + // no specific request, we can return supported controllers without further filtering + if requested == nil { + return requested.UnsortedList(), supported.UnsortedList(), supported.UnsortedList() + } + // filter supported controllers, keeping only those that have been requested + var activated []string + for _, controller := range supported.UnsortedList() { + if requested.Has(controller) { + activated = append(activated, controller) + } + } + + return requested.UnsortedList(), supported.UnsortedList(), activated +} + +// ExtractPodSpec extracts the PodSpec from an unstructured resource if the controller supports autogen. +func (a *ImplAutogenV2) ExtractPodSpec(resource unstructured.Unstructured) (*unstructured.Unstructured, error) { + kind := resource.GetKind() + var podSpec map[string]interface{} + var found bool + var err error + switch kind { + case "Deployment", "StatefulSet", "DaemonSet", "Job", "ReplicaSet", "ReplicationController": + podSpec, found, err = unstructured.NestedMap(resource.Object, "spec", "template", "spec") + if err != nil || !found { + return nil, fmt.Errorf("error extracting pod spec: %v", err) + } + + case "CronJob": + jobTemplate, found, err := unstructured.NestedMap(resource.Object, "spec", "jobTemplate", "spec", "template", "spec") + if err != nil || !found { + return nil, fmt.Errorf("error extracting pod spec from CronJob: %v", err) + } + podSpec = jobTemplate + + default: + return nil, nil // No pod spec for this kind of resource + } + + return &unstructured.Unstructured{Object: podSpec}, nil +} diff --git a/pkg/autogenv2/autogen_test.go b/pkg/autogenv2/autogen_test.go new file mode 100644 index 0000000000..5195cf71e0 --- /dev/null +++ b/pkg/autogenv2/autogen_test.go @@ -0,0 +1,410 @@ +package autogenv2 + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/kyverno/kyverno/api/kyverno" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + "gotest.tools/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/sets" +) + +func Test_CanAutoGen(t *testing.T) { + testCases := []struct { + name string + policy []byte + expectedControllers sets.Set[string] + }{ + { + name: "rule-with-match-name", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"test","match":{"resources":{"kinds":["Namespace"],"name":"*"}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-match-selector", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"],"selector":{"matchLabels":{"foo":"bar"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-exclude-name", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"test"}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-exclude-names", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"names":["test"]}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-exclude-selector", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"selector":{"matchLabels":{"foo":"bar"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-deny", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"validate":{"message":"testpolicy","deny":{"conditions":[{"key":"{{request.object.metadata.labels.foo}}","operator":"Equals","value":"bar"}]}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-match-mixed-kinds-pod-podcontrollers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod","Deployment"]}},"preconditions":{"any":[{"key":"{{request.operation}}","operator":"Equals","value":"CREATE"}]},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-exclude-mixed-kinds-pod-podcontrollers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"kinds":["Pod","Deployment"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-match-kinds-pod-only", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"validate":{"message":"testpolicy","pattern":{"metadata":{"labels":{"foo":"bar"}}}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-exclude-kinds-pod-only", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"kinds":["Pod"],"namespaces":["test"]}},"validate":{"message":"testpolicy","pattern":{"metadata":{"labels":{"foo":"bar"}}}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-mutate-patches", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"test","match":{"resources":{"kinds":["Pod"]}},"mutate":{"patchesJson6902":"-op:add\npath:/spec/containers/0/env/-1\nvalue:{\"name\":\"SERVICE\",\"value\":{{request.object.spec.template.metadata.labels.app}}}"}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-generate", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"add-networkpolicy"},"spec":{"rules":[{"name":"default-deny-ingress","match":{"resources":{"kinds":["Namespace"],"name":"*"}},"exclude":{"resources":{"namespaces":["kube-system","default","kube-public","kyverno"]}},"generate":{"kind":"NetworkPolicy","name":"default-deny-ingress","namespace":"{{request.object.metadata.name}}","synchronize":true,"data":{"spec":{"podSelector":{},"policyTypes":["Ingress"]}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-predefined-invalid-controllers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"annotations":null,"pod-policies.kyverno.io/autogen-controllers":"DaemonSet,Deployment,StatefulSet","spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod","Deployment"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-predefined-valid-controllers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"annotations":null,"pod-policies.kyverno.io/autogen-controllers":"none","spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod","Deployment"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-only-predefined-valid-controllers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"annotations":null,"pod-policies.kyverno.io/autogen-controllers":"none","spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Namespace"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New("none"), + }, + { + name: "rule-with-match-kinds-pod-only-validate-exclude", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"validate":{"message":"testpolicy","podSecurity": {"level": "baseline","version":"v1.24","exclude":[{"controlName":"SELinux","restrictedField":"spec.containers[*].securityContext.seLinuxOptions.role","images":["nginx"],"values":["baz"]}, {"controlName":"SELinux","restrictedField":"spec.initContainers[*].securityContext.seLinuxOptions.role","images":["nodejs"],"values":["init-baz"]}]}}}]}}`), + expectedControllers: PodControllers, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + var policy kyvernov1.ClusterPolicy + err := json.Unmarshal(test.policy, &policy) + assert.NilError(t, err) + + applyAutoGen, controllers := CanAutoGen(&policy.Spec) + if !applyAutoGen { + controllers = sets.New("none") + } + + equalityTest := test.expectedControllers.Equal(controllers) + assert.Assert(t, equalityTest, fmt.Sprintf("expected: %v, got: %v", test.expectedControllers, controllers)) + }) + } +} + +func Test_GetSupportedControllers(t *testing.T) { + testCases := []struct { + name string + policy []byte + expectedControllers sets.Set[string] + }{ + { + name: "rule-with-match-name", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"test","match":{"resources":{"kinds":["Namespace"],"name":"*"}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-match-selector", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"],"selector":{"matchLabels":{"foo":"bar"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-exclude-name", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"test"}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-exclude-selector", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test-getcontrollers"},"spec":{"background":false,"rules":[{"name":"test-getcontrollers","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"selector":{"matchLabels":{"foo":"bar"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-deny", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"validate":{"message":"testpolicy","deny":{"conditions":[{"key":"{{request.object.metadata.labels.foo}}","operator":"Equals","value":"bar"}]}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-match-mixed-kinds-pod-podcontrollers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod","Deployment"]}},"preconditions":{"any":[{"key":"{{request.operation}}","operator":"Equals","value":"CREATE"}]},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-exclude-mixed-kinds-pod-podcontrollers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"kinds":["Pod","Deployment"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-match-kinds-pod-only", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"validate":{"message":"testpolicy","pattern":{"metadata":{"labels":{"foo":"bar"}}}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-exclude-kinds-pod-only", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"kinds":["Pod"],"namespaces":["test"]}},"validate":{"message":"testpolicy","pattern":{"metadata":{"labels":{"foo":"bar"}}}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-mutate-patches", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"test","match":{"resources":{"kinds":["Pod"]}},"mutate":{"patchesJson6902":"-op:add\npath:/spec/containers/0/env/-1\nvalue:{\"name\":\"SERVICE\",\"value\":{{request.object.spec.template.metadata.labels.app}}}"}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-generate", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"add-networkpolicy"},"spec":{"rules":[{"name":"default-deny-ingress","match":{"resources":{"kinds":["Namespace"],"name":"*"}},"exclude":{"resources":{"namespaces":["kube-system","default","kube-public","kyverno"]}},"generate":{"kind":"NetworkPolicy","name":"default-deny-ingress","namespace":"{{request.object.metadata.name}}","synchronize":true,"data":{"spec":{"podSelector":{},"policyTypes":["Ingress"]}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-predefined-invalid-controllers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"annotations":null,"pod-policies.kyverno.io/autogen-controllers":"DaemonSet,Deployment,StatefulSet","spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod","Deployment"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-predefined-valid-controllers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"annotations":null,"pod-policies.kyverno.io/autogen-controllers":"none","spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Pod","Deployment"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-only-predefined-valid-controllers", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"set-service-labels-env"},"annotations":null,"pod-policies.kyverno.io/autogen-controllers":"none","spec":{"background":false,"rules":[{"name":"set-service-label","match":{"resources":{"kinds":["Namespace"]}},"mutate":{"patchStrategicMerge":{"metadata":{"labels":{"+(service)":"{{request.object.spec.template.metadata.labels.app}}"}}}}}]}}`), + expectedControllers: sets.New[string](), + }, + { + name: "rule-with-match-kinds-pod-only-validate-exclude", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"test"},"spec":{"rules":[{"name":"require-network-policy","match":{"resources":{"kinds":["Pod"]}},"validate":{"message":"testpolicy","podSecurity": {"level": "baseline","version":"v1.24","exclude":[{"controlName":"SELinux","restrictedField":"spec.containers[*].securityContext.seLinuxOptions.role","images":["nginx"],"values":["baz"]}, {"controlName":"SELinux","restrictedField":"spec.initContainers[*].securityContext.seLinuxOptions.role","images":["nodejs"],"values":["init-baz"]}]}}}]}}`), + expectedControllers: PodControllers, + }, + { + name: "rule-with-validate-podsecurity", + policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"pod-security"},"spec":{"rules":[{"name":"restricted","match":{"all":[{"resources":{"kinds":["Pod"]}}]},"validate":{"failureAction":"enforce","podSecurity":{"level":"restricted","version":"v1.24"}}}]}}`), + expectedControllers: PodControllers, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + var policy kyvernov1.ClusterPolicy + err := json.Unmarshal(test.policy, &policy) + assert.NilError(t, err) + + controllers := GetSupportedControllers(&policy.Spec) + + equalityTest := test.expectedControllers.Equal(controllers) + assert.Assert(t, equalityTest, fmt.Sprintf("expected: %v, got: %v", test.expectedControllers, controllers)) + }) + } +} + +func Test_GetRequestedControllers(t *testing.T) { + testCases := []struct { + name string + meta metav1.ObjectMeta + expectedControllers sets.Set[string] + }{ + { + name: "annotations-nil", + meta: metav1.ObjectMeta{}, + expectedControllers: nil, + }, + { + name: "annotation-not-set", + meta: metav1.ObjectMeta{Annotations: map[string]string{}}, + expectedControllers: nil, + }, + { + name: "annotation-empty", + meta: metav1.ObjectMeta{Annotations: map[string]string{kyverno.AnnotationAutogenControllers: ""}}, + expectedControllers: nil, + }, + { + name: "annotation-none", + meta: metav1.ObjectMeta{Annotations: map[string]string{kyverno.AnnotationAutogenControllers: "none"}}, + expectedControllers: sets.New[string](), + }, + { + name: "annotation-job", + meta: metav1.ObjectMeta{Annotations: map[string]string{kyverno.AnnotationAutogenControllers: "Job"}}, + expectedControllers: sets.New[string]("Job"), + }, + { + name: "annotation-job-deployment", + meta: metav1.ObjectMeta{Annotations: map[string]string{kyverno.AnnotationAutogenControllers: "Job,Deployment"}}, + expectedControllers: sets.New[string]("Job", "Deployment"), + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + controllers := GetRequestedControllers(&test.meta) + + equalityTest := test.expectedControllers.Equal(controllers) + assert.Assert(t, equalityTest, fmt.Sprintf("expected: %v, got: %v", test.expectedControllers, controllers)) + }) + } +} + +func TestExtractPodSpec(t *testing.T) { + testCases := []struct { + name string + resource unstructured.Unstructured // The input resource + expectedPod *unstructured.Unstructured // Expected pod spec + expectError bool // Whether an error is expected + }{ + { + name: "extract pod spec from deployment", + resource: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + }, + }, + }, + expectedPod: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + expectError: false, + }, + { + name: "extract pod spec from cronjob", + resource: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "CronJob", + "metadata": map[string]interface{}{ + "name": "test-cronjob", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "schedule": "* * * * *", + "jobTemplate": map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedPod: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "nginx", + "image": "nginx", + }, + }, + }, + }, + expectError: false, + }, + { + name: "no pod spec in configmap", + resource: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-configmap", + "namespace": "default", + }, + "data": map[string]interface{}{ + "key": "value", + }, + }, + }, + expectedPod: nil, + expectError: false, + }, + { + name: "invalid resource structure", + resource: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": nil, // missing metadata + "spec": nil, // missing spec + }, + }, + expectedPod: nil, + expectError: true, + }, + } + + autogen := NewAutogenV2() + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + // Call the function under test + podSpec, err := autogen.ExtractPodSpec(test.resource) + + // Check for errors + if test.expectError { + assert.ErrorContains(t, err, "error extracting pod spec") + } else { + assert.NilError(t, err) + } + + // Check for pod spec correctness + if test.expectedPod != nil { + assert.Assert(t, podSpec != nil, "expected pod spec but got nil") + assert.DeepEqual(t, test.expectedPod.Object, podSpec.Object) + } else { + assert.Assert(t, podSpec == nil, "expected nil pod spec but got a non-nil value") + } + }) + } +} diff --git a/pkg/autogenv2/log.go b/pkg/autogenv2/log.go new file mode 100644 index 0000000000..fd7fd1e3cb --- /dev/null +++ b/pkg/autogenv2/log.go @@ -0,0 +1,8 @@ +package autogenv2 + +import "github.com/kyverno/kyverno/pkg/logging" + +var ( + logger = logging.WithName("autogen-v2") + debug = logger.V(5) +)