diff --git a/backend/pkg/engine/processor.go b/backend/pkg/engine/processor.go index e38e019a..53161ddd 100644 --- a/backend/pkg/engine/processor.go +++ b/backend/pkg/engine/processor.go @@ -22,8 +22,10 @@ import ( "github.com/kyverno/kyverno/pkg/registryclient" "github.com/kyverno/kyverno/pkg/toggle" jsonutils "github.com/kyverno/kyverno/pkg/utils/json" + "github.com/kyverno/kyverno/pkg/validatingadmissionpolicy" "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" + "k8s.io/api/admissionregistration/v1alpha1" authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -47,6 +49,7 @@ type Processor struct { func (p *Processor) Run( ctx context.Context, policies []kyvernov1.PolicyInterface, + vaps []v1alpha1.ValidatingAdmissionPolicy, resources []unstructured.Unstructured, oldResources []unstructured.Unstructured, ) (*models.Results, error) { @@ -121,7 +124,21 @@ func (p *Processor) Run( response.Generation = append(response.Generation, result) } } - + // validating admissin policies + for i := range resources { + var resource unstructured.Unstructured + if p.params.Context.Operation == kyvernov1.Delete { + resource = resources[i] + } else if p.params.Context.Operation == kyvernov1.Update { + resource = resources[i] + } else { + resource = resources[i] + } + for _, policy := range vaps { + result := validatingadmissionpolicy.Validate(policy, resource) + response.Validation = append(response.Validation, models.ConvertResponse(result)) + } + } return response, nil } diff --git a/backend/pkg/server/api/engine/handler.go b/backend/pkg/server/api/engine/handler.go index c2c4aa7e..8b2d28b3 100644 --- a/backend/pkg/server/api/engine/handler.go +++ b/backend/pkg/server/api/engine/handler.go @@ -28,8 +28,7 @@ func newEngineHandler(cl cluster.Cluster, config APIConfiguration) (gin.HandlerF return nil, fmt.Errorf("unable to load params: %w", err) } params.ImageData = in.ImageData - - policies, err := in.LoadPolicies(policyLoader) + policies, vaps, err := in.LoadPolicies(policyLoader) if err != nil { return nil, fmt.Errorf("unable to load policies: %w", err) } @@ -74,7 +73,7 @@ func newEngineHandler(cl cluster.Cluster, config APIConfiguration) (gin.HandlerF if err != nil { return nil, err } - results, err := processor.Run(ctx, policies, resources, oldResources) + results, err := processor.Run(ctx, policies, vaps, resources, oldResources) if err != nil { return nil, err } diff --git a/backend/pkg/server/api/engine/request.go b/backend/pkg/server/api/engine/request.go index 941fd60c..d8bd06a3 100644 --- a/backend/pkg/server/api/engine/request.go +++ b/backend/pkg/server/api/engine/request.go @@ -5,6 +5,7 @@ import ( kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1" + "k8s.io/api/admissionregistration/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/openapi" @@ -38,7 +39,7 @@ func (r *EngineRequest) LoadParameters() (*models.Parameters, error) { return ¶ms, nil } -func (r *EngineRequest) LoadPolicies(policyLoader loader.Loader) ([]kyvernov1.PolicyInterface, error) { +func (r *EngineRequest) LoadPolicies(policyLoader loader.Loader) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { return utils.LoadPolicies(policyLoader, []byte(r.Policies)) } diff --git a/backend/pkg/utils/policy.go b/backend/pkg/utils/policy.go index 0e92e55e..cafabcbe 100644 --- a/backend/pkg/utils/policy.go +++ b/backend/pkg/utils/policy.go @@ -4,42 +4,53 @@ import ( "fmt" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1" + "k8s.io/api/admissionregistration/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/kyverno/playground/backend/pkg/resource/convert" "github.com/kyverno/playground/backend/pkg/resource/loader" ) -func ToPolicyInterface(untyped unstructured.Unstructured) (kyvernov1.PolicyInterface, error) { - kind := untyped.GetKind() - if kind == "Policy" { - policy, err := convert.To[kyvernov1.Policy](untyped) - if err != nil { - return nil, err - } - return policy, nil - } else if kind == "ClusterPolicy" { - policy, err := convert.To[kyvernov1.ClusterPolicy](untyped) - if err != nil { - return nil, err - } - return policy, nil - } - return nil, fmt.Errorf("invalid kind: %s", kind) -} +var ( + policyV1 = schema.GroupVersion(kyvernov1.GroupVersion).WithKind("Policy") + policyV2 = schema.GroupVersion(kyvernov2beta1.GroupVersion).WithKind("Policy") + clusterPolicyV1 = schema.GroupVersion(kyvernov1.GroupVersion).WithKind("ClusterPolicy") + clusterPolicyV2 = schema.GroupVersion(kyvernov2beta1.GroupVersion).WithKind("ClusterPolicy") + vapV1alpha1 = v1alpha1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy") +) -func LoadPolicies(l loader.Loader, content []byte) ([]kyvernov1.PolicyInterface, error) { +func LoadPolicies(l loader.Loader, content []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) { untyped, err := loader.LoadResources(l, content) if err != nil { - return nil, err + return nil, nil, err } var policies []kyvernov1.PolicyInterface + var vaps []v1alpha1.ValidatingAdmissionPolicy for _, policy := range untyped { - policy, err := ToPolicyInterface(policy) - if err != nil { - return nil, err + gvk := policy.GroupVersionKind() + switch gvk { + case policyV1, policyV2: + var typed kyvernov1.Policy + if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(policy.UnstructuredContent(), &typed, true); err != nil { + return nil, nil, err + } + policies = append(policies, &typed) + case clusterPolicyV1, clusterPolicyV2: + var typed kyvernov1.ClusterPolicy + if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(policy.UnstructuredContent(), &typed, true); err != nil { + return nil, nil, err + } + policies = append(policies, &typed) + case vapV1alpha1: + var typed v1alpha1.ValidatingAdmissionPolicy + if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(policy.UnstructuredContent(), &typed, true); err != nil { + return nil, nil, err + } + vaps = append(vaps, typed) + default: + return nil, nil, fmt.Errorf("policy type not supported %s", gvk) } - policies = append(policies, policy) } - return policies, nil + return policies, vaps, nil } diff --git a/backend/pkg/utils/policy_test.go b/backend/pkg/utils/policy_test.go index 6b01725c..1f9b07d5 100644 --- a/backend/pkg/utils/policy_test.go +++ b/backend/pkg/utils/policy_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/kubectl-validate/pkg/openapiclient" "github.com/kyverno/playground/backend/data" @@ -18,14 +17,17 @@ const ( singleResource string = "../../testdata/namespace.yaml" multiplePolicy string = "../../testdata/multiple-policies.yaml" policyWithComment string = "../../testdata/multiple-policies-with-comment.yaml" + vap string = "../../testdata/vap.yaml" + policyAndVap string = "../../testdata/policy-and-vap.yaml" ) func Test_LoadPolicies(t *testing.T) { tests := []struct { - name string - policies string - wantLoaded int - wantErr bool + name string + policies string + wantPolicies int + wantVaps int + wantErr bool }{{ name: "invalid policy", policies: "../../testdata/invalid-policy.yaml", @@ -42,17 +44,27 @@ func Test_LoadPolicies(t *testing.T) { policies: singleResource, wantErr: true, }, { - name: "load single policy", - policies: "../../testdata/single-policy.yaml", - wantLoaded: 1, + name: "load single policy", + policies: "../../testdata/single-policy.yaml", + wantPolicies: 1, }, { - name: "load multiple resources", - policies: multiplePolicy, - wantLoaded: 2, + name: "load multiple resources", + policies: multiplePolicy, + wantPolicies: 2, }, { - name: "load policy with comment", - policies: policyWithComment, - wantLoaded: 1, + name: "load policy with comment", + policies: policyWithComment, + wantPolicies: 1, + }, { + name: "vap", + policies: vap, + wantPolicies: 0, + wantVaps: 1, + }, { + name: "policy and vap", + policies: policyAndVap, + wantPolicies: 1, + wantVaps: 1, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -64,47 +76,12 @@ func Test_LoadPolicies(t *testing.T) { ), ) require.NoError(t, err) - if res, err := utils.LoadPolicies(loader, bytes); (err != nil) != tt.wantErr { + if policies, vaps, err := utils.LoadPolicies(loader, bytes); (err != nil) != tt.wantErr { t.Errorf("loader.LoadPolicies() error = %v, wantErr %v", err, tt.wantErr) - } else if len(res) != tt.wantLoaded { - t.Errorf("loader.LoadPolicies() loaded amount = %v, wantLoaded %v", len(res), tt.wantLoaded) - } - }) - } -} - -func TestToPolicyInterface(t *testing.T) { - tests := []struct { - name string - file string - wantErr bool - }{{ - name: "load single policy", - file: "../../testdata/single-policy.yaml", - wantErr: true, - }, { - name: "load single cluster policy", - file: "../../testdata/single-cluster-policy.yaml", - wantErr: true, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bytes, err := os.ReadFile(tt.file) - require.NoError(t, err) - loader, err := loader.New( - openapiclient.NewComposite( - openapiclient.NewLocalSchemaFiles(data.Schemas(), "schemas"), - ), - ) - require.NoError(t, err) - resource, err := loader.Load(bytes) - require.NoError(t, err) - err = unstructured.SetNestedField(resource.UnstructuredContent(), "foo", "spec", "bar") - require.NoError(t, err) - _, err = utils.ToPolicyInterface(resource) - if (err != nil) != tt.wantErr { - t.Errorf("ToPolicyInterface() error = %v, wantErr %v", err, tt.wantErr) - return + } else if len(policies) != tt.wantPolicies { + t.Errorf("loader.LoadPolicies() loaded amount = %v, wantLoaded %v", len(policies), tt.wantPolicies) + } else if len(vaps) != tt.wantVaps { + t.Errorf("loader.LoadPolicies() loaded amount = %v, wantLoaded %v", len(vaps), tt.wantVaps) } }) } diff --git a/backend/testdata/policy-and-vap.yaml b/backend/testdata/policy-and-vap.yaml new file mode 100644 index 00000000..806d919e --- /dev/null +++ b/backend/testdata/policy-and-vap.yaml @@ -0,0 +1,36 @@ +apiVersion: admissionregistration.k8s.io/v1alpha1 +kind: ValidatingAdmissionPolicy +metadata: + name: disallow-host-path +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + validations: + - expression: "!has(object.spec.template.spec.volumes) || object.spec.template.spec.volumes.all(volume, !has(volume.hostPath))" + message: "HostPath volumes are forbidden. The field spec.template.spec.volumes[*].hostPath must be unset." +--- +apiVersion: kyverno.io/v1 +kind: Policy +metadata: + name: require-ns-purpose-label + namespace: test +spec: + validationFailureAction: Enforce + rules: + - name: require-ns-purpose-label + match: + any: + - resources: + kinds: + - Namespace + validate: + message: "You must have label 'purpose' with value 'production' set on all new namespaces." + pattern: + metadata: + labels: + purpose: production diff --git a/backend/testdata/vap.yaml b/backend/testdata/vap.yaml new file mode 100644 index 00000000..78d44a49 --- /dev/null +++ b/backend/testdata/vap.yaml @@ -0,0 +1,15 @@ +apiVersion: admissionregistration.k8s.io/v1alpha1 +kind: ValidatingAdmissionPolicy +metadata: + name: disallow-host-path +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + validations: + - expression: "!has(object.spec.template.spec.volumes) || object.spec.template.spec.volumes.all(volume, !has(volume.hostPath))" + message: "HostPath volumes are forbidden. The field spec.template.spec.volumes[*].hostPath must be unset." diff --git a/release-notes/main.md b/release-notes/main.md index 16d8586e..f9f85683 100644 --- a/release-notes/main.md +++ b/release-notes/main.md @@ -3,11 +3,12 @@ Release notes for `TODO`. ## :dizzy: New features :dizzy: -- Added Validating Admission Policy autocompletion in Policies panel +- Added Validating Admission Policies support -