Skip to content

Commit

Permalink
feat: add validating admission policy support (#496)
Browse files Browse the repository at this point in the history
* feat: add validating admission policy support

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* unit tests

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fmt

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
  • Loading branch information
eddycharly authored Sep 21, 2023
1 parent cf46cce commit c323703
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 87 deletions.
19 changes: 18 additions & 1 deletion backend/pkg/engine/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}

Expand Down
5 changes: 2 additions & 3 deletions backend/pkg/server/api/engine/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion backend/pkg/server/api/engine/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -38,7 +39,7 @@ func (r *EngineRequest) LoadParameters() (*models.Parameters, error) {
return &params, 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))
}

Expand Down
63 changes: 37 additions & 26 deletions backend/pkg/utils/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
85 changes: 31 additions & 54 deletions backend/pkg/utils/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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) {
Expand All @@ -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)
}
})
}
Expand Down
36 changes: 36 additions & 0 deletions backend/testdata/policy-and-vap.yaml
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions backend/testdata/vap.yaml
Original file line number Diff line number Diff line change
@@ -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."
5 changes: 3 additions & 2 deletions release-notes/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!--
## :sparkles: UI changes :sparkles:
- Added Validating Admission Policy autocompletion in Policies panel

<!--
## :star: Examples :star:
## :boat: Tutorials :boat:
Expand Down

0 comments on commit c323703

Please sign in to comment.