1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

522 supporting crd validation

This commit is contained in:
shravan 2020-03-05 22:50:32 +05:30
parent b27a62b6bf
commit 7aa1e1515b
5 changed files with 100 additions and 18 deletions

View file

@ -3,6 +3,8 @@ package client
import ( import (
"strings" "strings"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -74,6 +76,10 @@ func (c *fakeDiscoveryClient) GetGVRFromKind(kind string) schema.GroupVersionRes
return c.getGVR(resource) return c.getGVR(resource)
} }
func (c *fakeDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
return nil, nil
}
func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{ return &unstructured.Unstructured{
Object: map[string]interface{}{ Object: map[string]interface{}{

View file

@ -1,14 +1,34 @@
package openapi package openapi
import ( import (
"encoding/json"
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
"gopkg.in/yaml.v2"
"github.com/googleapis/gnostic/compiler"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
client "github.com/nirmata/kyverno/pkg/dclient" client "github.com/nirmata/kyverno/pkg/dclient"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
) )
type crdDefinition struct {
Spec struct {
Names struct {
Kind string `json:"kind"`
} `json:"names"`
Versions []struct {
Schema struct {
OpenAPIV3Schema interface{} `json:"openAPIV3Schema"`
} `json:"schema"`
} `json:"versions"`
} `json:"spec"`
}
type crdSync struct { type crdSync struct {
client *client.Client client *client.Client
} }
@ -20,20 +40,54 @@ func NewCRDSync(client *client.Client) *crdSync {
} }
func (c *crdSync) Run(workers int, stopCh <-chan struct{}) { func (c *crdSync) Run(workers int, stopCh <-chan struct{}) {
for i := 0; i < workers; i++ {
go wait.Until(c.syncCrd, time.Second*10, stopCh)
}
<-stopCh
}
func (c *crdSync) syncCrd() {
newDoc, err := c.client.DiscoveryClient.OpenAPISchema() newDoc, err := c.client.DiscoveryClient.OpenAPISchema()
if err != nil { if err != nil {
glog.V(4).Infof("cannot get openapi schema: %v", err) glog.V(4).Infof("cannot get openapi schema: %v", err)
} }
err = useCustomOpenApiDocument(newDoc) err = useOpenApiDocument(newDoc)
if err != nil { if err != nil {
glog.V(4).Infof("Could not set custom OpenApi document: %v\n", err) glog.V(4).Infof("Could not set custom OpenApi document: %v\n", err)
} }
for i := 0; i < workers; i++ {
go wait.Until(c.sync, time.Second*10, stopCh)
}
<-stopCh
}
func (c *crdSync) sync() {
openApiGlobalState.mutex.Lock()
defer openApiGlobalState.mutex.Unlock()
crds, err := c.client.ListResource("CustomResourceDefinition", "", nil)
if err != nil {
glog.V(4).Infof("could not fetch crd's from server: %v", err)
return
}
for _, crd := range crds.Items {
var crdDefinition crdDefinition
crdRaw, _ := json.Marshal(crd.Object)
_ = json.Unmarshal(crdRaw, &crdDefinition)
crdName := crdDefinition.Spec.Names.Kind
if len(crdDefinition.Spec.Versions) < 1 {
glog.V(4).Infof("could not parse crd schema, no versions present")
continue
}
var schema yaml.MapSlice
schemaRaw, _ := json.Marshal(crdDefinition.Spec.Versions[0].Schema.OpenAPIV3Schema)
_ = yaml.Unmarshal(schemaRaw, &schema)
parsedSchema, err := openapi_v2.NewSchema(schema, compiler.NewContext("schema", nil))
if err != nil {
glog.V(4).Infof("could not parse crd schema:%v", err)
continue
}
openApiGlobalState.kindToDefinitionName[crdName] = crdName
openApiGlobalState.definitions[crdName] = parsedSchema
}
} }

View file

@ -40,7 +40,7 @@ func init() {
panic(err) panic(err)
} }
err = useCustomOpenApiDocument(defaultDoc) err = useOpenApiDocument(defaultDoc)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -75,15 +75,18 @@ func ValidatePolicyMutation(policy v1.ClusterPolicy) error {
for kind, rules := range kindToRules { for kind, rules := range kindToRules {
newPolicy := policy newPolicy := policy
newPolicy.Spec.Rules = rules newPolicy.Spec.Rules = rules
resource, _ := generateEmptyResource(openApiGlobalState.definitions[openApiGlobalState.kindToDefinitionName[kind]]).(map[string]interface{}) resource, _ := generateEmptyResource(openApiGlobalState.definitions[openApiGlobalState.kindToDefinitionName[kind]]).(map[string]interface{})
if resource == nil {
glog.V(4).Infof("Cannot Validate policy: openApi definition now found for %v", kind)
return nil
}
newResource := unstructured.Unstructured{Object: resource} newResource := unstructured.Unstructured{Object: resource}
newResource.SetKind(kind) newResource.SetKind(kind)
ctx := context.NewContext() ctx := context.NewContext()
err := ctx.AddSA("kyvernoDummyUsername") err := ctx.AddSA("kyvernoDummyUsername")
if err != nil { if err != nil {
glog.Infof("Failed to load service account in context:%v", err) glog.V(4).Infof("Failed to load service account in context:%v", err)
} }
policyContext := engine.PolicyContext{ policyContext := engine.PolicyContext{
@ -101,7 +104,7 @@ func ValidatePolicyMutation(policy v1.ClusterPolicy) error {
} }
return fmt.Errorf(strings.Join(errMessages, "\n")) return fmt.Errorf(strings.Join(errMessages, "\n"))
} }
err = ValidateResource(resp.PatchedResource.UnstructuredContent(), kind) err = ValidateResource(*resp.PatchedResource.DeepCopy(), kind)
if err != nil { if err != nil {
return err return err
} }
@ -110,9 +113,16 @@ func ValidatePolicyMutation(policy v1.ClusterPolicy) error {
return nil return nil
} }
func ValidateResource(patchedResource interface{}, kind string) error { // For crd, we do not store definition in document
func getSchemaFromDefinitions(kind string) (proto.Schema, error) {
path := proto.NewPath(kind)
return (&proto.Definitions{}).ParseSchema(openApiGlobalState.definitions[kind], &path)
}
func ValidateResource(patchedResource unstructured.Unstructured, kind string) error {
openApiGlobalState.mutex.RLock() openApiGlobalState.mutex.RLock()
defer openApiGlobalState.mutex.RUnlock() defer openApiGlobalState.mutex.RUnlock()
var err error
if !openApiGlobalState.isSet { if !openApiGlobalState.isSet {
glog.V(4).Info("Cannot Validate resource: Validation global state not set") glog.V(4).Info("Cannot Validate resource: Validation global state not set")
@ -122,10 +132,14 @@ func ValidateResource(patchedResource interface{}, kind string) error {
kind = openApiGlobalState.kindToDefinitionName[kind] kind = openApiGlobalState.kindToDefinitionName[kind]
schema := openApiGlobalState.models.LookupModel(kind) schema := openApiGlobalState.models.LookupModel(kind)
if schema == nil { if schema == nil {
return fmt.Errorf("pre-validation: couldn't find model %s", kind) schema, err = getSchemaFromDefinitions(kind)
if err != nil || schema == nil {
return fmt.Errorf("pre-validation: couldn't find model %s", kind)
}
delete(patchedResource.Object, "kind")
} }
if errs := validation.ValidateModel(patchedResource, schema, kind); len(errs) > 0 { if errs := validation.ValidateModel(patchedResource.UnstructuredContent(), schema, kind); len(errs) > 0 {
var errorMessages []string var errorMessages []string
for i := range errs { for i := range errs {
errorMessages = append(errorMessages, errs[i].Error()) errorMessages = append(errorMessages, errs[i].Error())
@ -137,7 +151,7 @@ func ValidateResource(patchedResource interface{}, kind string) error {
return nil return nil
} }
func useCustomOpenApiDocument(customDoc *openapi_v2.Document) error { func useOpenApiDocument(customDoc *openapi_v2.Document) error {
openApiGlobalState.mutex.Lock() openApiGlobalState.mutex.Lock()
defer openApiGlobalState.mutex.Unlock() defer openApiGlobalState.mutex.Unlock()
@ -181,7 +195,11 @@ func generateEmptyResource(kindSchema *openapi_v2.Schema) interface{} {
} }
if len(types) != 1 { if len(types) != 1 {
return nil if len(kindSchema.GetProperties().GetAdditionalProperties()) > 0 {
types = []string{"object"}
} else {
return nil
}
} }
switch types[0] { switch types[0] {

View file

@ -41,6 +41,10 @@ func Test_ValidateMutationPolicy(t *testing.T) {
policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"mutate-pod-disable-automoutingapicred"},"spec":{"rules":[{"name":"pod-disable-automoutingapicred","match":{"resources":{"kinds":["Pod"]}},"mutate":{"overlay":{"spec":{"(serviceAccountName)":"*","automountServiceAccountToken":80}}}}]}}`), policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"mutate-pod-disable-automoutingapicred"},"spec":{"rules":[{"name":"pod-disable-automoutingapicred","match":{"resources":{"kinds":["Pod"]}},"mutate":{"overlay":{"spec":{"(serviceAccountName)":"*","automountServiceAccountToken":80}}}}]}}`),
errMessage: `ValidationError(io.k8s.api.core.v1.Pod.spec.automountServiceAccountToken): invalid type for io.k8s.api.core.v1.PodSpec.automountServiceAccountToken: got "integer", expected "boolean"`, errMessage: `ValidationError(io.k8s.api.core.v1.Pod.spec.automountServiceAccountToken): invalid type for io.k8s.api.core.v1.PodSpec.automountServiceAccountToken: got "integer", expected "boolean"`,
}, },
{
description: "Testing Policies with substitute variables",
policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"add-ns-access-controls","annotations":{"policies.kyverno.io/category":"Workload Isolation","policies.kyverno.io/description":"Create roles and role bindings for a new namespace"}},"spec":{"background":false,"rules":[{"name":"add-sa-annotation","match":{"resources":{"kinds":["Namespace"]}},"mutate":{"overlay":{"metadata":{"annotations":{"nirmata.io/ns-creator":"{{serviceAccountName}}"}}}}},{"name":"generate-owner-role","match":{"resources":{"kinds":["Namespace"]}},"preconditions":[{"key":"{{request.userInfo.username}}","operator":"NotEqual","value":""},{"key":"{{serviceAccountName}}","operator":"NotEqual","value":""},{"key":"{{serviceAccountNamespace}}","operator":"NotEqual","value":""}],"generate":{"kind":"ClusterRole","name":"ns-owner-{{request.object.metadata.name}}-{{request.userInfo.username}}","data":{"metadata":{"annotations":{"nirmata.io/ns-creator":"{{serviceAccountName}}"}},"rules":[{"apiGroups":[""],"resources":["namespaces"],"verbs":["delete"],"resourceNames":["{{request.object.metadata.name}}"]}]}}},{"name":"generate-owner-role-binding","match":{"resources":{"kinds":["Namespace"]}},"preconditions":[{"key":"{{request.userInfo.username}}","operator":"NotEqual","value":""},{"key":"{{serviceAccountName}}","operator":"NotEqual","value":""},{"key":"{{serviceAccountNamespace}}","operator":"NotEqual","value":""}],"generate":{"kind":"ClusterRoleBinding","name":"ns-owner-{{request.object.metadata.name}}-{{request.userInfo.username}}-binding","data":{"metadata":{"annotations":{"nirmata.io/ns-creator":"{{serviceAccountName}}"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"ns-owner-{{request.object.metadata.name}}-{{request.userInfo.username}}"},"subjects":[{"kind":"ServiceAccount","name":"{{serviceAccountName}}","namespace":"{{serviceAccountNamespace}}"}]}}},{"name":"generate-admin-role-binding","match":{"resources":{"kinds":["Namespace"]}},"preconditions":[{"key":"{{request.userInfo.username}}","operator":"NotEqual","value":""},{"key":"{{serviceAccountName}}","operator":"NotEqual","value":""},{"key":"{{serviceAccountNamespace}}","operator":"NotEqual","value":""}],"generate":{"kind":"RoleBinding","name":"ns-admin-{{request.object.metadata.name}}-{{request.userInfo.username}}-binding","namespace":"{{request.object.metadata.name}}","data":{"metadata":{"annotations":{"nirmata.io/ns-creator":"{{serviceAccountName}}"}},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"admin"},"subjects":[{"kind":"ServiceAccount","name":"{{serviceAccountName}}","namespace":"{{serviceAccountNamespace}}"}]}}}]}}`),
},
} }
for i, tc := range tcs { for i, tc := range tcs {

View file

@ -103,7 +103,7 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest, resou
glog.V(4).Infof("Failed to apply policy %s on resource %s/%s\n", policy.Name, resource.GetNamespace(), resource.GetName()) glog.V(4).Infof("Failed to apply policy %s on resource %s/%s\n", policy.Name, resource.GetNamespace(), resource.GetName())
continue continue
} }
err := openapi.ValidateResource(engineResponse.PatchedResource.UnstructuredContent(), engineResponse.PatchedResource.GetKind()) err := openapi.ValidateResource(*engineResponse.PatchedResource.DeepCopy(), engineResponse.PatchedResource.GetKind())
if err != nil { if err != nil {
glog.V(4).Infoln(err) glog.V(4).Infoln(err)
continue continue