1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 16:06:56 +00:00

Merge pull request #220 from nirmata/217_feature

217 feature
This commit is contained in:
shuting 2019-07-20 16:21:37 -07:00 committed by GitHub
commit e9e7d8faff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1711 additions and 528 deletions

View file

@ -22,6 +22,12 @@ spec:
required: required:
- rules - rules
properties: properties:
# default values to be handled by user
validationFailureAction:
type: string
enum:
- block
- report
rules: rules:
type: array type: array
items: items:

View file

@ -22,6 +22,12 @@ spec:
required: required:
- rules - rules
properties: properties:
# default values to be handled by user
validationFailureAction:
type: string
enum:
- block
- report
rules: rules:
type: array type: array
items: items:

View file

@ -20,4 +20,4 @@ spec:
image: nginx:1.7.9 image: nginx:1.7.9
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 80 - containerPort: 80

View file

@ -35,4 +35,3 @@ spec :
containers: containers:
- (image): "nginx*" - (image): "nginx*"
imagePullPolicy: Always imagePullPolicy: Always

View file

@ -5,37 +5,37 @@ metadata:
spec: spec:
validationFailureAction: "report" validationFailureAction: "report"
rules: rules:
- name: check-cpu-memory-limits - name: add-memory-limit
resource: resource:
kinds: kinds:
- Deployment - Deployment
validate: mutate:
message: "Resource limits are required for CPU and memory" overlay:
pattern: spec:
spec: template:
template: spec:
spec: containers:
containers: # the wildcard * will match all containers in the list
# match all contianers - (name): "*"
- (name): "*" resources:
resources: limits:
limits: # add memory limit if it is not exist
# cpu and memory are required "+(memory)": "300Mi"
memory: "?*" - name: check-cpu-memory-limits
cpu: "?*" resource:
# - name: add-memory-limit kinds:
# resource: - Deployment
# kinds: validate:
# - Deployment message: "Resource limits are required for CPU and memory"
# mutate: pattern:
# overlay: spec:
# spec: template:
# template: spec:
# spec: containers:
# containers: # match all contianers
# # the wildcard * will match all containers in the list - (name): "*"
# - (name): "*" resources:
# resources: limits:
# limits: # cpu and memory are required
# # add memory limit if it is not exist memory: "?*"
# "+(memory)": "300Mi" cpu: "?*"

View file

@ -22,4 +22,4 @@ spec:
ports: ports:
- containerPort: 80 - containerPort: 80
- name: ghost - name: ghost
image: ghost:latest image: ghost:latest

View file

@ -16,4 +16,4 @@ spec:
containers: containers:
# if the image tag is latest, set the imagePullPolicy to Always # if the image tag is latest, set the imagePullPolicy to Always
- (image): "*:latest" - (image): "*:latest"
imagePullPolicy: "IfNotPresent" imagePullPolicy: "IfNotPresent"

View file

@ -10,4 +10,4 @@ subsets:
ports: ports:
- name: secure-connection - name: secure-connection
port: 443 port: 443
protocol: TCP protocol: TCP

19
main.go
View file

@ -4,6 +4,7 @@ import (
"flag" "flag"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/annotations"
"github.com/nirmata/kyverno/pkg/config" "github.com/nirmata/kyverno/pkg/config"
controller "github.com/nirmata/kyverno/pkg/controller" controller "github.com/nirmata/kyverno/pkg/controller"
client "github.com/nirmata/kyverno/pkg/dclient" client "github.com/nirmata/kyverno/pkg/dclient"
@ -24,7 +25,6 @@ var (
func main() { func main() {
defer glog.Flush() defer glog.Flush()
printVersionInfo() printVersionInfo()
clientConfig, err := createClientConfig(kubeconfig) clientConfig, err := createClientConfig(kubeconfig)
if err != nil { if err != nil {
@ -43,19 +43,20 @@ func main() {
kubeInformer := utils.NewKubeInformerFactory(clientConfig) kubeInformer := utils.NewKubeInformerFactory(clientConfig)
eventController := event.NewEventController(client, policyInformerFactory) eventController := event.NewEventController(client, policyInformerFactory)
violationBuilder := violation.NewPolicyViolationBuilder(client, policyInformerFactory, eventController) violationBuilder := violation.NewPolicyViolationBuilder(client, policyInformerFactory, eventController)
annotationsController := annotations.NewAnnotationControler(client)
policyController := controller.NewPolicyController( policyController := controller.NewPolicyController(
client, client,
policyInformerFactory, policyInformerFactory,
violationBuilder, violationBuilder,
eventController) eventController,
annotationsController)
genControler := gencontroller.NewGenController(client, eventController, policyInformerFactory, violationBuilder, kubeInformer.Core().V1().Namespaces()) genControler := gencontroller.NewGenController(client, eventController, policyInformerFactory, violationBuilder, kubeInformer.Core().V1().Namespaces())
tlsPair, err := initTLSPemPair(clientConfig, client) tlsPair, err := initTLSPemPair(clientConfig, client)
if err != nil { if err != nil {
glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err) glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
} }
server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, eventController, filterK8Kinds) server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, eventController, violationBuilder, annotationsController, filterK8Kinds)
if err != nil { if err != nil {
glog.Fatalf("Unable to create webhook server: %v\n", err) glog.Fatalf("Unable to create webhook server: %v\n", err)
} }
@ -67,23 +68,25 @@ func main() {
stopCh := signals.SetupSignalHandler() stopCh := signals.SetupSignalHandler()
if err = webhookRegistrationClient.Register(); err != nil {
glog.Fatalf("Failed registering Admission Webhooks: %v\n", err)
}
policyInformerFactory.Run(stopCh) policyInformerFactory.Run(stopCh)
kubeInformer.Start(stopCh) kubeInformer.Start(stopCh)
eventController.Run(stopCh) eventController.Run(stopCh)
genControler.Run(stopCh) genControler.Run(stopCh)
annotationsController.Run(stopCh)
if err = policyController.Run(stopCh); err != nil { if err = policyController.Run(stopCh); err != nil {
glog.Fatalf("Error running PolicyController: %v\n", err) glog.Fatalf("Error running PolicyController: %v\n", err)
} }
if err = webhookRegistrationClient.Register(); err != nil {
glog.Fatalf("Failed registering Admission Webhooks: %v\n", err)
}
server.RunAsync() server.RunAsync()
<-stopCh <-stopCh
server.Stop() server.Stop()
genControler.Stop() genControler.Stop()
eventController.Stop() eventController.Stop()
annotationsController.Stop()
policyController.Stop() policyController.Stop()
} }

View file

@ -0,0 +1,302 @@
package annotations
import (
"encoding/json"
"reflect"
"github.com/golang/glog"
pinfo "github.com/nirmata/kyverno/pkg/info"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
//Policy information for annotations
type Policy struct {
Status string `json:"status"`
// Key Type/Name
MutationRules map[string]Rule `json:"mutationrules,omitempty"`
ValidationRules map[string]Rule `json:"validationrules,omitempty"`
GenerationRules map[string]Rule `json:"generationrules,omitempty"`
}
//Rule information for annotations
type Rule struct {
Status string `json:"status"`
Changes string `json:"changes,omitempty"` // TODO for mutation changes
Error string `json:"error,omitempty"`
}
func (p *Policy) getOverAllStatus() string {
// mutation
for _, v := range p.MutationRules {
if v.Status == "Failure" {
return "Failure"
}
}
// validation
for _, v := range p.ValidationRules {
if v.Status == "Failure" {
return "Failure"
}
}
// generation
for _, v := range p.GenerationRules {
if v.Status == "Failure" {
return "Failure"
}
}
return "Success"
}
func getRules(rules []*pinfo.RuleInfo, ruleType pinfo.RuleType) map[string]Rule {
if len(rules) == 0 {
return nil
}
annrules := make(map[string]Rule, 0)
// var annrules map[string]Rule
for _, r := range rules {
if r.RuleType != ruleType {
continue
}
rule := Rule{Status: getStatus(r.IsSuccessful())}
if !r.IsSuccessful() {
rule.Error = r.GetErrorString()
}
annrules[r.Name] = rule
}
return annrules
}
func (p *Policy) updatePolicy(obj *Policy, ruleType pinfo.RuleType) bool {
updates := false
// Check Mutation rules
switch ruleType {
case pinfo.Mutation:
if p.compareMutationRules(obj.MutationRules) {
updates = true
}
case pinfo.Validation:
if p.compareValidationRules(obj.ValidationRules) {
updates = true
}
case pinfo.Generation:
if p.compareGenerationRules(obj.GenerationRules) {
updates = true
}
if p.Status != obj.Status {
updates = true
}
}
// check if any rules failed
p.Status = p.getOverAllStatus()
// If there are any updates then the annotation can be updated, can skip
return updates
}
func (p *Policy) compareMutationRules(rules map[string]Rule) bool {
// check if the rules have changed
if !reflect.DeepEqual(p.MutationRules, rules) {
p.MutationRules = rules
return true
}
return false
}
func (p *Policy) compareValidationRules(rules map[string]Rule) bool {
// check if the rules have changed
if !reflect.DeepEqual(p.ValidationRules, rules) {
p.ValidationRules = rules
return true
}
return false
}
func (p *Policy) compareGenerationRules(rules map[string]Rule) bool {
// check if the rules have changed
if !reflect.DeepEqual(p.GenerationRules, rules) {
p.GenerationRules = rules
return true
}
return false
}
func newAnnotationForPolicy(pi *pinfo.PolicyInfo) *Policy {
return &Policy{Status: getStatus(pi.IsSuccessful()),
MutationRules: getRules(pi.Rules, pinfo.Mutation),
ValidationRules: getRules(pi.Rules, pinfo.Validation),
GenerationRules: getRules(pi.Rules, pinfo.Generation),
}
}
//AddPolicy will add policy annotation if not present or update if present
// modifies obj
// returns true, if there is any update -> caller need to update the obj
// returns false, if there is no change -> caller can skip the update
func AddPolicy(obj *unstructured.Unstructured, pi *pinfo.PolicyInfo, ruleType pinfo.RuleType) bool {
PolicyObj := newAnnotationForPolicy(pi)
// get annotation
ann := obj.GetAnnotations()
// check if policy already has annotation
cPolicy, ok := ann[BuildKey(pi.Name)]
if !ok {
PolicyByte, err := json.Marshal(PolicyObj)
if err != nil {
glog.Error(err)
return false
}
// insert policy information
ann[BuildKey(pi.Name)] = string(PolicyByte)
// set annotation back to unstr
obj.SetAnnotations(ann)
return true
}
cPolicyObj := Policy{}
err := json.Unmarshal([]byte(cPolicy), &cPolicyObj)
if err != nil {
return false
}
// update policy information inside the annotation
// 1> policy status
// 2> Mutation, Validation, Generation
if cPolicyObj.updatePolicy(PolicyObj, ruleType) {
cPolicyByte, err := json.Marshal(cPolicyObj)
if err != nil {
return false
}
// update policy information
ann[BuildKey(pi.Name)] = string(cPolicyByte)
// set annotation back to unstr
obj.SetAnnotations(ann)
return true
}
return false
}
//RemovePolicy to remove annotations
// return true -> if there was an entry and we deleted it
// return false -> if there is no entry, caller need not update
func RemovePolicy(obj *unstructured.Unstructured, policy string) bool {
// get annotations
ann := obj.GetAnnotations()
if ann == nil {
return false
}
if _, ok := ann[BuildKey(policy)]; !ok {
return false
}
delete(ann, BuildKey(policy))
// set annotation back to unstr
obj.SetAnnotations(ann)
return true
}
//ParseAnnotationsFromObject extracts annotations from the JSON obj
func ParseAnnotationsFromObject(bytes []byte) map[string]string {
var objectJSON map[string]interface{}
json.Unmarshal(bytes, &objectJSON)
meta, ok := objectJSON["metadata"].(map[string]interface{})
if !ok {
glog.Error("unable to parse")
return nil
}
ann, ok, err := unstructured.NestedStringMap(meta, "annotations")
if err != nil || !ok {
return nil
}
return ann
}
//AddPolicyJSONPatch generate JSON Patch to add policy informatino JSON patch
func AddPolicyJSONPatch(ann map[string]string, pi *pinfo.PolicyInfo, ruleType pinfo.RuleType) (map[string]string, []byte, error) {
if ann == nil {
ann = make(map[string]string, 0)
}
PolicyObj := newAnnotationForPolicy(pi)
cPolicy, ok := ann[BuildKey(pi.Name)]
if !ok {
PolicyByte, err := json.Marshal(PolicyObj)
if err != nil {
return nil, nil, err
}
// insert policy information
ann[BuildKey(pi.Name)] = string(PolicyByte)
// create add JSON patch
jsonPatch, err := createAddJSONPatch(ann)
return ann, jsonPatch, err
}
cPolicyObj := Policy{}
err := json.Unmarshal([]byte(cPolicy), &cPolicyObj)
// update policy information inside the annotation
// 1> policy status
// 2> rule (name, status,changes,type)
update := cPolicyObj.updatePolicy(PolicyObj, ruleType)
if !update {
return nil, nil, err
}
cPolicyByte, err := json.Marshal(cPolicyObj)
if err != nil {
return nil, nil, err
}
// update policy information
ann[BuildKey(pi.Name)] = string(cPolicyByte)
// create update JSON patch
jsonPatch, err := createReplaceJSONPatch(ann)
return ann, jsonPatch, err
}
//RemovePolicyJSONPatch remove JSON patch
func RemovePolicyJSONPatch(ann map[string]string, policy string) (map[string]string, []byte, error) {
if ann == nil {
return nil, nil, nil
}
delete(ann, policy)
if len(ann) == 0 {
jsonPatch, err := createRemoveJSONPatch(ann)
return nil, jsonPatch, err
}
jsonPatch, err := createReplaceJSONPatch(ann)
return ann, jsonPatch, err
}
type patchMapValue struct {
Op string `json:"op"`
Path string `json:"path"`
Value map[string]string `json:"value"`
}
func createRemoveJSONPatch(ann map[string]string) ([]byte, error) {
payload := []patchMapValue{{
Op: "remove",
Path: "/metadata/annotations",
}}
return json.Marshal(payload)
}
func createAddJSONPatch(ann map[string]string) ([]byte, error) {
if ann == nil {
ann = make(map[string]string, 0)
}
payload := []patchMapValue{{
Op: "add",
Path: "/metadata/annotations",
Value: ann,
}}
return json.Marshal(payload)
}
func createReplaceJSONPatch(ann map[string]string) ([]byte, error) {
if ann == nil {
ann = make(map[string]string, 0)
}
payload := []patchMapValue{{
Op: "replace",
Path: "/metadata/annotations",
Value: ann,
}}
return json.Marshal(payload)
}

View file

@ -0,0 +1,36 @@
package annotations
import (
"encoding/json"
"testing"
pinfo "github.com/nirmata/kyverno/pkg/info"
)
func TestAddPatch(t *testing.T) {
// Create
objRaw := []byte(`{"kind":"Deployment","apiVersion":"apps/v1","metadata":{"name":"nginx-deployment","namespace":"default","creationTimestamp":null,"labels":{"app":"nginx"}},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"nginx"}},"spec":{"containers":[{"name":"nginx","image":"nginx:latest","ports":[{"containerPort":80,"protocol":"TCP"}],"resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"},{"name":"ghost","image":"ghost:latest","resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","securityContext":{},"schedulerName":"default-scheduler"}},"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxUnavailable":"25%","maxSurge":"25%"}},"revisionHistoryLimit":10,"progressDeadlineSeconds":600},"status":{}}`)
piRaw := []byte(`{"Name":"set-image-pull-policy","RKind":"Deployment","RName":"nginx-deployment","RNamespace":"default","ValidationFailureAction":"","Rules":[{"Name":"nginx-deployment","Msgs":["Rule nginx-deployment: Overlay succesfully applied."],"RuleType":0}]}`)
ann := ParseAnnotationsFromObject(objRaw)
pi := pinfo.PolicyInfo{}
err := json.Unmarshal(piRaw, &pi)
if err != nil {
panic(err)
}
ann, _, err = AddPolicyJSONPatch(ann, &pi, pinfo.Mutation)
if err != nil {
panic(err)
}
// Update
piRaw = []byte(`{"Name":"set-image-pull-policy","RKind":"Deployment","RName":"nginx-deployment","RNamespace":"default","ValidationFailureAction":"","Rules":[{"Name":"nginx-deployment","Msgs":["Rule nginx-deployment1: Overlay succesfully applied."],"RuleType":0}]}`)
// ann = ParseAnnotationsFromObject(objRaw)
pi = pinfo.PolicyInfo{}
err = json.Unmarshal(piRaw, &pi)
if err != nil {
panic(err)
}
ann, _, err = AddPolicyJSONPatch(ann, &pi, pinfo.Mutation)
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,115 @@
package annotations
import (
"fmt"
"time"
"github.com/golang/glog"
client "github.com/nirmata/kyverno/pkg/dclient"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/workqueue"
)
type controller struct {
client *client.Client
queue workqueue.RateLimitingInterface
}
type Interface interface {
Add(rkind, rns, rname string, patch []byte)
}
type Controller interface {
Interface
Run(stopCh <-chan struct{})
Stop()
}
func NewAnnotationControler(client *client.Client) Controller {
return &controller{
client: client,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), annotationQueueName),
}
}
func (c *controller) Add(rkind, rns, rname string, patch []byte) {
c.queue.Add(newInfo(rkind, rns, rname, patch))
}
func (c *controller) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
for i := 0; i < workerThreadCount; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
glog.Info("Started annotation controller workers")
}
func (c *controller) Stop() {
defer c.queue.ShutDown()
glog.Info("Shutting down annotation controller workers")
}
func (c *controller) runWorker() {
for c.processNextWorkItem() {
}
}
func (pc *controller) processNextWorkItem() bool {
obj, shutdown := pc.queue.Get()
if shutdown {
return false
}
err := func(obj interface{}) error {
defer pc.queue.Done(obj)
err := pc.syncHandler(obj)
pc.handleErr(err, obj)
return nil
}(obj)
if err != nil {
glog.Error(err)
return true
}
return true
}
func (pc *controller) handleErr(err error, key interface{}) {
if err == nil {
pc.queue.Forget(key)
return
}
// This controller retries if something goes wrong. After that, it stops trying.
if pc.queue.NumRequeues(key) < workQueueRetryLimit {
glog.Warningf("Error syncing events %v: %v", key, err)
// Re-enqueue the key rate limited. Based on the rate limiter on the
// queue and the re-enqueue history, the key will be processed later again.
pc.queue.AddRateLimited(key)
return
}
pc.queue.Forget(key)
glog.Error(err)
glog.Warningf("Dropping the key out of the queue: %v", err)
}
func (c *controller) syncHandler(obj interface{}) error {
var key info
var ok bool
if key, ok = obj.(info); !ok {
return fmt.Errorf("expected string in workqueue but got %#v", obj)
}
var err error
// check if the resource is created
_, err = c.client.GetResource(key.RKind, key.RNs, key.RName)
if err != nil {
glog.Errorf("Error creating annotation: unable to get resource %s/%s/%s, will retry: %s ", key.RKind, key.RNs, key.RName, err)
return err
}
// if it is patch the resource
_, err = c.client.PatchResource(key.RKind, key.RNs, key.RName, *key.Patch)
if err != nil {
glog.Errorf("Error creating annotation: unable to get resource %s/%s/%s, will retry: %s", key.RKind, key.RNs, key.RName, err)
return err
}
return nil
}

18
pkg/annotations/info.go Normal file
View file

@ -0,0 +1,18 @@
package annotations
type info struct {
RKind string
RNs string
RName string
//TODO:Hack as slice makes the struct unhasable
Patch *[]byte
}
func newInfo(rkind, rns, rname string, patch []byte) info {
return info{
RKind: rkind,
RNs: rns,
RName: rname,
Patch: &patch,
}
}

16
pkg/annotations/utils.go Normal file
View file

@ -0,0 +1,16 @@
package annotations
const annotationQueueName = "annotation-queue"
const workerThreadCount = 1
const workQueueRetryLimit = 3
func getStatus(status bool) string {
if status {
return "Success"
}
return "Failure"
}
func BuildKey(policyName string) string {
return "policies.kyverno.io." + policyName
}

View file

@ -18,7 +18,8 @@ type Policy struct {
// Spec describes policy behavior by its rules // Spec describes policy behavior by its rules
type Spec struct { type Spec struct {
Rules []Rule `json:"rules"` Rules []Rule `json:"rules"`
ValidationFailureAction string `json:"validationFailureAction"`
} }
// Rule is set of mutation, validation and generation actions // Rule is set of mutation, validation and generation actions
@ -77,16 +78,24 @@ type CloneFrom struct {
// Status contains violations for existing resources // Status contains violations for existing resources
type Status struct { type Status struct {
Violations []Violation `json:"violations,omitempty"` // Violations map[kind/namespace/resource]Violation
Violations map[string]Violation `json:"violations,omitempty"`
} }
// Violation for the policy // Violation for the policy
type Violation struct { type Violation struct {
Kind string `json:"kind,omitempty"` Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"` Namespace string `json:"namespace,omitempty"`
Reason string `json:"reason,omitempty"` Rules []FailedRule `json:"rules"`
Message string `json:"message,omitempty"` Reason string `json:"reason,omitempty"`
}
// FailedRule stored info and type of failed rules
type FailedRule struct {
Name string `json:"name"`
Type string `json:"type"` //Mutation, Validation, Genertaion
Error string `json:"error"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View file

@ -104,3 +104,44 @@ func (in *Generation) DeepCopyInto(out *Generation) {
*out = *in *out = *in
} }
} }
// return true -> if there were any removals
// return false -> if it looks the same
func (v *Violation) RemoveRulesOfType(ruleType string) bool {
removed := false
updatedRules := []FailedRule{}
for _, r := range v.Rules {
if r.Type == ruleType {
removed = true
continue
}
updatedRules = append(updatedRules, r)
}
if removed {
v.Rules = updatedRules
return true
}
return false
}
//IsEqual Check if violatiosn are equal
func (v *Violation) IsEqual(nv Violation) bool {
// We do not need to compare resource info as it will be same
// Reason
if v.Reason != nv.Reason {
return false
}
// Rule
if len(v.Rules) != len(nv.Rules) {
return false
}
// assumes the rules will be in order, as the rule are proceeed in order
// if the rule order changes, it means the policy has changed.. as it will afffect the order in which mutation rules are applied
for i, r := range v.Rules {
if r != nv.Rules[i] {
return false
}
}
return true
}

View file

@ -41,6 +41,22 @@ func (in *CloneFrom) DeepCopy() *CloneFrom {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FailedRule) DeepCopyInto(out *FailedRule) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FailedRule.
func (in *FailedRule) DeepCopy() *FailedRule {
if in == nil {
return nil
}
out := new(FailedRule)
in.DeepCopyInto(out)
return out
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generation. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generation.
func (in *Generation) DeepCopy() *Generation { func (in *Generation) DeepCopy() *Generation {
if in == nil { if in == nil {
@ -135,6 +151,11 @@ func (in *ResourceDescription) DeepCopyInto(out *ResourceDescription) {
*out = new(string) *out = new(string)
**out = **in **out = **in
} }
if in.Namespace != nil {
in, out := &in.Namespace, &out.Namespace
*out = new(string)
**out = **in
}
if in.Selector != nil { if in.Selector != nil {
in, out := &in.Selector, &out.Selector in, out := &in.Selector, &out.Selector
*out = new(v1.LabelSelector) *out = new(v1.LabelSelector)
@ -210,8 +231,10 @@ func (in *Status) DeepCopyInto(out *Status) {
*out = *in *out = *in
if in.Violations != nil { if in.Violations != nil {
in, out := &in.Violations, &out.Violations in, out := &in.Violations, &out.Violations
*out = make([]Violation, len(*in)) *out = make(map[string]Violation, len(*in))
copy(*out, *in) for key, val := range *in {
(*out)[key] = *val.DeepCopy()
}
} }
return return
} }
@ -239,6 +262,11 @@ func (in *Validation) DeepCopy() *Validation {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Violation) DeepCopyInto(out *Violation) { func (in *Violation) DeepCopyInto(out *Violation) {
*out = *in *out = *in
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]FailedRule, len(*in))
copy(*out, *in)
}
return return
} }

77
pkg/controller/cleanup.go Normal file
View file

@ -0,0 +1,77 @@
package controller
import (
"github.com/golang/glog"
"github.com/minio/minio/pkg/wildcard"
"github.com/nirmata/kyverno/pkg/annotations"
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
client "github.com/nirmata/kyverno/pkg/dclient"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func cleanAnnotations(client *client.Client, obj interface{}) {
// get the policy struct from interface
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
glog.Error(err)
return
}
policy := v1alpha1.Policy{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr, &policy); err != nil {
glog.Error(err)
return
}
// Get the resources that apply to the policy
// key uid
resourceMap := map[string]unstructured.Unstructured{}
for _, rule := range policy.Spec.Rules {
for _, k := range rule.Kinds {
if k == "Namespace" {
continue
}
// kind -> resource
gvr := client.DiscoveryClient.GetGVRFromKind(k)
// label selectors
// namespace ? should it be default or allow policy to specify it
namespace := "default"
if rule.ResourceDescription.Namespace != nil {
namespace = *rule.ResourceDescription.Namespace
}
list, err := client.ListResource(k, namespace, rule.ResourceDescription.Selector)
if err != nil {
glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String())
glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err)
continue
}
for _, res := range list.Items {
name := rule.ResourceDescription.Name
if name != nil {
// wild card matching
if !wildcard.Match(*name, res.GetName()) {
continue
}
}
resourceMap[string(res.GetUID())] = res
}
}
}
// remove annotations for the resources
for _, obj := range resourceMap {
// get annotations
ann := obj.GetAnnotations()
_, patch, err := annotations.RemovePolicyJSONPatch(ann, annotations.BuildKey(policy.Name))
if err != nil {
glog.Error(err)
continue
}
// patch the resource
_, err = client.PatchResource(obj.GetKind(), obj.GetNamespace(), obj.GetName(), patch)
if err != nil {
glog.Error(err)
continue
}
}
}

View file

@ -3,15 +3,17 @@ package controller
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"time" "time"
jsonpatch "github.com/evanphx/json-patch"
"github.com/nirmata/kyverno/pkg/annotations"
"github.com/nirmata/kyverno/pkg/info" "github.com/nirmata/kyverno/pkg/info"
"github.com/nirmata/kyverno/pkg/engine" "github.com/nirmata/kyverno/pkg/engine"
"github.com/golang/glog" "github.com/golang/glog"
types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1" lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
client "github.com/nirmata/kyverno/pkg/dclient" client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/event" "github.com/nirmata/kyverno/pkg/event"
@ -27,27 +29,30 @@ import (
//PolicyController to manage Policy CRD //PolicyController to manage Policy CRD
type PolicyController struct { type PolicyController struct {
client *client.Client client *client.Client
policyLister lister.PolicyLister policyLister lister.PolicyLister
policySynced cache.InformerSynced policySynced cache.InformerSynced
violationBuilder violation.Generator violationBuilder violation.Generator
eventController event.Generator eventController event.Generator
queue workqueue.RateLimitingInterface annotationsController annotations.Controller
queue workqueue.RateLimitingInterface
} }
// NewPolicyController from cmd args // NewPolicyController from cmd args
func NewPolicyController(client *client.Client, func NewPolicyController(client *client.Client,
policyInformer sharedinformer.PolicyInformer, policyInformer sharedinformer.PolicyInformer,
violationBuilder violation.Generator, violationBuilder violation.Generator,
eventController event.Generator) *PolicyController { eventController event.Generator,
annotationsController annotations.Controller) *PolicyController {
controller := &PolicyController{ controller := &PolicyController{
client: client, client: client,
policyLister: policyInformer.GetLister(), policyLister: policyInformer.GetLister(),
policySynced: policyInformer.GetInfomer().HasSynced, policySynced: policyInformer.GetInfomer().HasSynced,
violationBuilder: violationBuilder, violationBuilder: violationBuilder,
eventController: eventController, eventController: eventController,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName), annotationsController: annotationsController,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName),
} }
policyInformer.GetInfomer().AddEventHandler(cache.ResourceEventHandlerFuncs{ policyInformer.GetInfomer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@ -63,13 +68,13 @@ func (pc *PolicyController) createPolicyHandler(resource interface{}) {
} }
func (pc *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) { func (pc *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) {
newPolicy := newResource.(*types.Policy) newPolicy := newResource.(*v1alpha1.Policy)
oldPolicy := oldResource.(*types.Policy) oldPolicy := oldResource.(*v1alpha1.Policy)
newPolicy.Status = types.Status{} newPolicy.Status = v1alpha1.Status{}
oldPolicy.Status = types.Status{} oldPolicy.Status = v1alpha1.Status{}
newPolicy.ResourceVersion = "" newPolicy.ResourceVersion = ""
oldPolicy.ResourceVersion = "" oldPolicy.ResourceVersion = ""
if reflect.DeepEqual(newPolicy.ResourceVersion, oldPolicy.ResourceVersion) { if reflect.DeepEqual(newPolicy, oldPolicy) {
return return
} }
pc.enqueuePolicy(newResource) pc.enqueuePolicy(newResource)
@ -82,6 +87,7 @@ func (pc *PolicyController) deletePolicyHandler(resource interface{}) {
glog.Error("error decoding object, invalid type") glog.Error("error decoding object, invalid type")
return return
} }
cleanAnnotations(pc.client, resource)
glog.Infof("policy deleted: %s", object.GetName()) glog.Infof("policy deleted: %s", object.GetName())
} }
@ -155,7 +161,7 @@ func (pc *PolicyController) handleErr(err error, key interface{}) {
} }
pc.queue.Forget(key) pc.queue.Forget(key)
glog.Error(err) glog.Error(err)
glog.Warningf("Dropping the key %q out of the queue: %v", key, err) glog.Warningf("Dropping the key out of the queue: %v", err)
} }
func (pc *PolicyController) syncHandler(obj interface{}) error { func (pc *PolicyController) syncHandler(obj interface{}) error {
@ -169,8 +175,7 @@ func (pc *PolicyController) syncHandler(obj interface{}) error {
glog.Errorf("invalid policy key: %s", key) glog.Errorf("invalid policy key: %s", key)
return nil return nil
} }
// Get Policy
// Get Policy resource with namespace/name
policy, err := pc.policyLister.Get(name) policy, err := pc.policyLister.Get(name)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
@ -179,40 +184,103 @@ func (pc *PolicyController) syncHandler(obj interface{}) error {
} }
return err return err
} }
// process policy on existing resource
// get the violations and pass to violation Builder
// get the events and pass to event Builder
//TODO: processPolicy
glog.Infof("process policy %s on existing resources", policy.GetName()) glog.Infof("process policy %s on existing resources", policy.GetName())
// Process policy on existing resources
policyInfos := engine.ProcessExisting(pc.client, policy) policyInfos := engine.ProcessExisting(pc.client, policy)
events, violations := createEventsAndViolations(pc.eventController, policyInfos)
events, violations := pc.createEventsAndViolations(policyInfos)
// Events, Violations
pc.eventController.Add(events...) pc.eventController.Add(events...)
err = pc.violationBuilder.Add(violations...) err = pc.violationBuilder.Add(violations...)
if err != nil { if err != nil {
glog.Error(err) glog.Error(err)
} }
// Annotations
pc.createAnnotations(policyInfos)
return nil return nil
} }
func createEventsAndViolations(eventController event.Generator, policyInfos []*info.PolicyInfo) ([]*event.Info, []*violation.Info) { func (pc *PolicyController) createAnnotations(policyInfos []*info.PolicyInfo) {
for _, pi := range policyInfos {
var patch []byte
//get resource
obj, err := pc.client.GetResource(pi.RKind, pi.RNamespace, pi.RName)
if err != nil {
glog.Error(err)
continue
}
// add annotation for policy application
ann := obj.GetAnnotations()
// Mutation rules
ann, mpatch, err := annotations.AddPolicyJSONPatch(ann, pi, info.Mutation)
if err != nil {
glog.Error(err)
continue
}
// Validation rules
ann, vpatch, err := annotations.AddPolicyJSONPatch(ann, pi, info.Validation)
if err != nil {
glog.Error(err)
}
if mpatch == nil && vpatch == nil {
//nothing to patch
continue
}
// merge the patches
if mpatch != nil && vpatch != nil {
patch, err = jsonpatch.MergePatch(mpatch, vpatch)
if err != nil {
glog.Error(err)
continue
}
}
if mpatch == nil {
patch = vpatch
} else {
patch = mpatch
}
// add the anotation to the resource
_, err = pc.client.PatchResource(pi.RKind, pi.RNamespace, pi.RName, patch)
if err != nil {
glog.Error(err)
continue
}
}
}
func (pc *PolicyController) createEventsAndViolations(policyInfos []*info.PolicyInfo) ([]*event.Info, []*violation.Info) {
events := []*event.Info{} events := []*event.Info{}
violations := []*violation.Info{} violations := []*violation.Info{}
// Create events from the policyInfo // Create events from the policyInfo
for _, policyInfo := range policyInfos { for _, policyInfo := range policyInfos {
fruleNames := []string{} frules := []v1alpha1.FailedRule{}
sruleNames := []string{} sruleNames := []string{}
for _, rule := range policyInfo.Rules { for _, rule := range policyInfo.Rules {
if !rule.IsSuccessful() { if !rule.IsSuccessful() {
e := &event.Info{} e := &event.Info{}
fruleNames = append(fruleNames, rule.Name) frule := v1alpha1.FailedRule{Name: rule.Name}
switch rule.RuleType { switch rule.RuleType {
case info.Mutation, info.Validation, info.Generation: case info.Mutation, info.Validation, info.Generation:
// Events // Events
e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation, event.FProcessRule, rule.Name, policyInfo.Name) e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation, event.FProcessRule, rule.Name, policyInfo.Name)
switch rule.RuleType {
case info.Mutation:
frule.Type = info.Mutation.String()
case info.Validation:
frule.Type = info.Validation.String()
case info.Generation:
frule.Type = info.Generation.String()
}
frule.Error = rule.GetErrorString()
default: default:
glog.Info("Unsupported Rule type") glog.Info("Unsupported Rule type")
} }
frule.Error = rule.GetErrorString()
frules = append(frules, frule)
events = append(events, e) events = append(events, e)
} else { } else {
sruleNames = append(sruleNames, rule.Name) sruleNames = append(sruleNames, rule.Name)
@ -220,22 +288,16 @@ func createEventsAndViolations(eventController event.Generator, policyInfos []*i
} }
if !policyInfo.IsSuccessful() { if !policyInfo.IsSuccessful() {
// Event e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, concatFailedRules(frules))
// list of failed rules : ruleNames
e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, strings.Join(fruleNames, ";"))
events = append(events, e) events = append(events, e)
// Violation // Violation
v := violation.NewViolationFromEvent(e, policyInfo.Name, policyInfo.RKind, policyInfo.RName, policyInfo.RNamespace) v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules())
violations = append(violations, v) violations = append(violations, v)
} else {
// clean up violations
pc.violationBuilder.RemoveInactiveViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, info.Mutation)
pc.violationBuilder.RemoveInactiveViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, info.Validation)
} }
// else {
// // Policy was processed succesfully
// e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyApplied, event.SPolicyApply, policyInfo.Name)
// events = append(events, e)
// // Policy applied succesfully on resource
// e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyApplied, event.SRuleApply, strings.Join(sruleNames, ";"), policyInfo.RName)
// events = append(events, e)
// }
} }
return events, violations return events, violations
} }

View file

@ -41,7 +41,8 @@ func (f *fixture) runControler(policyName string) {
f.Client, f.Client,
policyInformerFactory, policyInformerFactory,
violationBuilder, violationBuilder,
eventController) eventController,
nil)
stopCh := signals.SetupSignalHandler() stopCh := signals.SetupSignalHandler()
// start informer & controller // start informer & controller

View file

@ -1,7 +1,21 @@
package controller package controller
import (
"bytes"
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
)
const policyWorkQueueName = "policyworkqueue" const policyWorkQueueName = "policyworkqueue"
const policyWorkQueueRetryLimit = 5 const policyWorkQueueRetryLimit = 3
const policyControllerWorkerCount = 2 const policyControllerWorkerCount = 2
func concatFailedRules(frules []v1alpha1.FailedRule) string {
var buffer bytes.Buffer
for _, frule := range frules {
buffer.WriteString(frule.Name + ";")
}
return buffer.String()
}

View file

@ -16,6 +16,7 @@ import (
"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"
patchTypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
@ -40,7 +41,6 @@ func NewClient(config *rest.Config) (*Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
kclient, err := kubernetes.NewForConfig(config) kclient, err := kubernetes.NewForConfig(config)
if err != nil { if err != nil {
return nil, err return nil, err
@ -110,6 +110,11 @@ func (c *Client) GetResource(kind string, namespace string, name string, subreso
return c.getResourceInterface(kind, namespace).Get(name, meta.GetOptions{}, subresources...) return c.getResourceInterface(kind, namespace).Get(name, meta.GetOptions{}, subresources...)
} }
//Patch
func (c *Client) PatchResource(kind string, namespace string, name string, patch []byte) (*unstructured.Unstructured, error) {
return c.getResourceInterface(kind, namespace).Patch(name, patchTypes.JSONPatchType, patch, meta.PatchOptions{})
}
// ListResource returns the list of resources in unstructured/json format // ListResource returns the list of resources in unstructured/json format
// Access items using []Items // Access items using []Items
func (c *Client) ListResource(kind string, namespace string, lselector *meta.LabelSelector) (*unstructured.UnstructuredList, error) { func (c *Client) ListResource(kind string, namespace string, lselector *meta.LabelSelector) (*unstructured.UnstructuredList, error) {

View file

@ -14,11 +14,11 @@ import (
// Instead we expose them as standalone functions passing the required atrributes // Instead we expose them as standalone functions passing the required atrributes
// The each function returns the changes that need to be applied on the resource // The each function returns the changes that need to be applied on the resource
// the caller is responsible to apply the changes to the resource // the caller is responsible to apply the changes to the resource
// ProcessExisting checks for mutation and validation violations of existing resources // ProcessExisting checks for mutation and validation violations of existing resources
func ProcessExisting(client *client.Client, policy *types.Policy) []*info.PolicyInfo { func ProcessExisting(client *client.Client, policy *types.Policy) []*info.PolicyInfo {
glog.Infof("Applying policy %s on existing resources", policy.Name) glog.Infof("Applying policy %s on existing resources", policy.Name)
resources := []*resourceInfo{} // key uid
resourceMap := map[string]*resourceInfo{}
for _, rule := range policy.Spec.Rules { for _, rule := range policy.Spec.Rules {
for _, k := range rule.Kinds { for _, k := range rule.Kinds {
@ -34,7 +34,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
if rule.ResourceDescription.Namespace != nil { if rule.ResourceDescription.Namespace != nil {
namespace = *rule.ResourceDescription.Namespace namespace = *rule.ResourceDescription.Namespace
} }
list, err := client.ListResource(gvr.Resource, namespace, rule.ResourceDescription.Selector) list, err := client.ListResource(k, namespace, rule.ResourceDescription.Selector)
if err != nil { if err != nil {
glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String()) glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String())
glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err) glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err)
@ -52,18 +52,19 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
ri := &resourceInfo{resource: &res, gvk: &metav1.GroupVersionKind{Group: gvk.Group, ri := &resourceInfo{resource: &res, gvk: &metav1.GroupVersionKind{Group: gvk.Group,
Version: gvk.Version, Version: gvk.Version,
Kind: gvk.Kind}} Kind: gvk.Kind}}
resources = append(resources, ri)
resourceMap[string(res.GetUID())] = ri
} }
} }
} }
policyInfos := []*info.PolicyInfo{} policyInfos := []*info.PolicyInfo{}
// for the filtered resource apply policy // for the filtered resource apply policy
for _, r := range resources { for _, v := range resourceMap {
policyInfo, err := applyPolicy(client, policy, r) policyInfo, err := applyPolicy(client, policy, v)
if err != nil { if err != nil {
glog.Errorf("unable to apply policy %s on resource %s/%s", policy.Name, r.resource.GetName(), r.resource.GetNamespace()) glog.Errorf("unable to apply policy %s on resource %s/%s", policy.Name, v.resource.GetName(), v.resource.GetNamespace())
glog.Error(err) glog.Error(err)
continue continue
} }
@ -74,7 +75,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
} }
func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo) (*info.PolicyInfo, error) { func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo) (*info.PolicyInfo, error) {
policyInfo := info.NewPolicyInfo(policy.Name, res.gvk.Kind, res.resource.GetName(), res.resource.GetNamespace()) policyInfo := info.NewPolicyInfo(policy.Name, res.gvk.Kind, res.resource.GetName(), res.resource.GetNamespace(), policy.Spec.ValidationFailureAction)
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules)) glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
rawResource, err := res.resource.MarshalJSON() rawResource, err := res.resource.MarshalJSON()
if err != nil { if err != nil {
@ -99,6 +100,10 @@ func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo)
func mutation(p *types.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]*info.RuleInfo, error) { func mutation(p *types.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]*info.RuleInfo, error) {
patches, ruleInfos := Mutate(*p, rawResource, *gvk) patches, ruleInfos := Mutate(*p, rawResource, *gvk)
if len(ruleInfos) == 0 {
// no rules were processed
return nil, nil
}
// option 2: (original Resource + patch) compare with (original resource) // option 2: (original Resource + patch) compare with (original resource)
mergePatches := JoinPatches(patches) mergePatches := JoinPatches(patches)
// merge the patches // merge the patches

View file

@ -10,7 +10,7 @@ import (
// Mutate performs mutation. Overlay first and then mutation patches // Mutate performs mutation. Overlay first and then mutation patches
func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) { func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) {
var allPatches [][]byte var allPatches [][]byte
var err error patchedDocument := rawResource
ris := []*info.RuleInfo{} ris := []*info.RuleInfo{}
for _, rule := range policy.Spec.Rules { for _, rule := range policy.Spec.Rules {
@ -29,38 +29,24 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio
overlayPatches, err := ProcessOverlay(rule, rawResource, gvk) overlayPatches, err := ProcessOverlay(rule, rawResource, gvk)
if err != nil { if err != nil {
ri.Fail() ri.Fail()
ri.Addf("Rule %s: Overlay application has failed, err %s.", rule.Name, err) ri.Addf("overlay application has failed, err %v.", err)
} else { } else {
// Apply the JSON patches from the rule to the resource ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name)
rawResource, err = ApplyPatches(rawResource, overlayPatches) allPatches = append(allPatches, overlayPatches...)
if err != nil {
ri.Fail()
ri.Addf("Unable to apply JSON patch to resource, err %s.", err)
} else {
ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name)
allPatches = append(allPatches, overlayPatches...)
}
} }
} }
// Process Patches // Process Patches
if len(rule.Mutation.Patches) != 0 { if len(rule.Mutation.Patches) != 0 {
rulePatches, errs := ProcessPatches(rule, rawResource) rulePatches, errs := ProcessPatches(rule, patchedDocument)
if len(errs) > 0 { if len(errs) > 0 {
ri.Fail() ri.Fail()
for _, err := range errs { for _, err := range errs {
ri.Addf("Rule %s: Patches application has failed, err %s.", rule.Name, err) ri.Addf("patches application has failed, err %v.", err)
} }
} else { } else {
// Apply the JSON patches from the rule to the resource ri.Addf("Rule %s: Patches succesfully applied.", rule.Name)
rawResource, err = ApplyPatches(rawResource, rulePatches) allPatches = append(allPatches, rulePatches...)
if err != nil {
ri.Fail()
ri.Addf("Unable to apply JSON patch to resource, err %s.", err)
} else {
ri.Addf("Rule %s: Patches succesfully applied.", rule.Name)
allPatches = append(allPatches, rulePatches...)
}
} }
} }
ris = append(ris, ri) ris = append(ris, ri)

View file

@ -41,7 +41,7 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
err := validateResourceWithPattern(resource, rule.Validation.Pattern) err := validateResourceWithPattern(resource, rule.Validation.Pattern)
if err != nil { if err != nil {
ri.Fail() ri.Fail()
ri.Addf("Rule %s: Validation has failed, err %s.", rule.Name, err) ri.Addf("validation has failed, err %v.", err)
} else { } else {
ri.Addf("Rule %s: Validation succesfully.", rule.Name) ri.Addf("Rule %s: Validation succesfully.", rule.Name)

View file

@ -1,7 +1,6 @@
package event package event
import ( import (
"fmt"
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
@ -42,13 +41,12 @@ type Controller interface {
func NewEventController(client *client.Client, func NewEventController(client *client.Client,
shareInformer sharedinformer.PolicyInformer) Controller { shareInformer sharedinformer.PolicyInformer) Controller {
controller := &controller{ return &controller{
client: client, client: client,
policyLister: shareInformer.GetLister(), policyLister: shareInformer.GetLister(),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), eventWorkQueueName), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), eventWorkQueueName),
recorder: initRecorder(client), recorder: initRecorder(client),
} }
return controller
} }
func initRecorder(client *client.Client) record.EventRecorder { func initRecorder(client *client.Client) record.EventRecorder {
@ -93,40 +91,58 @@ func (c *controller) Stop() {
defer c.queue.ShutDown() defer c.queue.ShutDown()
glog.Info("Shutting down eventbuilder controller workers") glog.Info("Shutting down eventbuilder controller workers")
} }
func (c *controller) runWorker() { func (c *controller) runWorker() {
for c.processNextWorkItem() { for c.processNextWorkItem() {
} }
} }
func (c *controller) handleErr(err error, key interface{}) {
if err == nil {
c.queue.Forget(key)
return
}
// This controller retries if something goes wrong. After that, it stops trying.
if c.queue.NumRequeues(key) < workQueueRetryLimit {
glog.Warningf("Error syncing events %v: %v", key, err)
// Re-enqueue the key rate limited. Based on the rate limiter on the
// queue and the re-enqueue history, the key will be processed later again.
c.queue.AddRateLimited(key)
return
}
c.queue.Forget(key)
glog.Error(err)
glog.Warningf("Dropping the key out of the queue: %v", err)
}
func (c *controller) processNextWorkItem() bool { func (c *controller) processNextWorkItem() bool {
obj, shutdown := c.queue.Get() obj, shutdown := c.queue.Get()
if shutdown { if shutdown {
return false return false
} }
err := func(obj interface{}) error { err := func(obj interface{}) error {
defer c.queue.Done(obj) defer c.queue.Done(obj)
var key Info var key Info
var ok bool var ok bool
if key, ok = obj.(Info); !ok { if key, ok = obj.(Info); !ok {
c.queue.Forget(obj) c.queue.Forget(obj)
glog.Warningf("Expecting type info by got %v\n", obj) glog.Warningf("Expecting type info by got %v\n", obj)
return nil return nil
} }
// Run the syncHandler, passing the resource and the policy err := c.syncHandler(key)
if err := c.SyncHandler(key); err != nil { c.handleErr(err, obj)
c.queue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s' : %s, requeuing event creation request", key.Namespace+"/"+key.Name, err.Error())
}
return nil return nil
}(obj) }(obj)
if err != nil { if err != nil {
glog.Warning(err) glog.Error(err)
return true
} }
return true return true
} }
func (c *controller) SyncHandler(key Info) error { func (c *controller) syncHandler(key Info) error {
var robj runtime.Object var robj runtime.Object
var err error var err error

View file

@ -6,6 +6,8 @@ const eventWorkQueueName = "policy-controller-events"
const eventWorkerThreadCount = 1 const eventWorkerThreadCount = 1
const workQueueRetryLimit = 1
//Info defines the event details //Info defines the event details
type Info struct { type Info struct {
Kind string Kind string

View file

@ -59,7 +59,8 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) {
policyInfo := info.NewPolicyInfo(p.Name, policyInfo := info.NewPolicyInfo(p.Name,
"Namespace", "Namespace",
ns.Name, ns.Name,
"") // Namespace has no namespace..WOW "",
p.Spec.ValidationFailureAction) // Namespace has no namespace..WOW
ruleInfos := engine.GenerateNew(c.client, p, ns) ruleInfos := engine.GenerateNew(c.client, p, ns)
policyInfo.AddRuleInfos(ruleInfos) policyInfo.AddRuleInfos(ruleInfos)
@ -77,9 +78,7 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) {
if onViolation { if onViolation {
glog.Infof("Adding violation for generation rule of policy %s\n", policyInfo.Name) glog.Infof("Adding violation for generation rule of policy %s\n", policyInfo.Name)
v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules())
v := violation.NewViolation(event.PolicyViolation, policyInfo.Name, policyInfo.RKind, policyInfo.RName,
policyInfo.RNamespace, msg)
c.violationBuilder.Add(v) c.violationBuilder.Add(v)
} else { } else {
eventInfo = event.NewEvent(policyKind, "", policyInfo.Name, event.RequestBlocked, eventInfo = event.NewEvent(policyKind, "", policyInfo.Name, event.RequestBlocked,

View file

@ -3,6 +3,8 @@ package info
import ( import (
"fmt" "fmt"
"strings" "strings"
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
) )
//PolicyInfo defines policy information //PolicyInfo defines policy information
@ -16,25 +18,68 @@ type PolicyInfo struct {
// Namespace is the ns of resource // Namespace is the ns of resource
// empty on non-namespaced resources // empty on non-namespaced resources
RNamespace string RNamespace string
Rules []*RuleInfo //TODO: add check/enum for types
success bool ValidationFailureAction string // BlockChanges, ReportViolation
Rules []*RuleInfo
success bool
} }
//NewPolicyInfo returns a new policy info //NewPolicyInfo returns a new policy info
func NewPolicyInfo(policyName string, rKind string, rName string, rNamespace string) *PolicyInfo { func NewPolicyInfo(policyName, rKind, rName, rNamespace, validationFailureAction string) *PolicyInfo {
return &PolicyInfo{ return &PolicyInfo{
Name: policyName, Name: policyName,
RKind: rKind, RKind: rKind,
RName: rName, RName: rName,
RNamespace: rNamespace, RNamespace: rNamespace,
success: true, // fail to be set explicity success: true, // fail to be set explicity
ValidationFailureAction: validationFailureAction,
} }
} }
//IsSuccessful checks if policy is succesful //IsSuccessful checks if policy is succesful
// the policy is set to fail, if any of the rules have failed // the policy is set to fail, if any of the rules have failed
func (pi *PolicyInfo) IsSuccessful() bool { func (pi *PolicyInfo) IsSuccessful() bool {
return pi.success for _, r := range pi.Rules {
if !r.success {
pi.success = false
return false
}
}
pi.success = true
return true
}
// SuccessfulRules returns list of successful rule names
func (pi *PolicyInfo) SuccessfulRules() []string {
var rules []string
for _, r := range pi.Rules {
if r.IsSuccessful() {
rules = append(rules, r.Name)
}
}
return rules
}
// FailedRules returns list of failed rule names
func (pi *PolicyInfo) FailedRules() []string {
var rules []string
for _, r := range pi.Rules {
if !r.IsSuccessful() {
rules = append(rules, r.Name)
}
}
return rules
}
//GetFailedRules returns the failed rules with rule type
func (pi *PolicyInfo) GetFailedRules() []v1alpha1.FailedRule {
var rules []v1alpha1.FailedRule
for _, r := range pi.Rules {
if !r.IsSuccessful() {
rules = append(rules, v1alpha1.FailedRule{Name: r.Name, Type: r.RuleType.String(), Error: r.GetErrorString()})
}
}
return rules
} }
//ErrorRules returns error msgs from all rule //ErrorRules returns error msgs from all rule
@ -73,12 +118,18 @@ type RuleInfo struct {
} }
//ToString reule information //ToString reule information
//TODO: check if this is needed
func (ri *RuleInfo) ToString() string { func (ri *RuleInfo) ToString() string {
str := "rulename: " + ri.Name str := "rulename: " + ri.Name
msgs := strings.Join(ri.Msgs, ";") msgs := strings.Join(ri.Msgs, ";")
return strings.Join([]string{str, msgs}, ";") return strings.Join([]string{str, msgs}, ";")
} }
//GetErrorString returns the error message for a rule
func (ri *RuleInfo) GetErrorString() string {
return strings.Join(ri.Msgs, ";")
}
//NewRuleInfo creates a new RuleInfo //NewRuleInfo creates a new RuleInfo
func NewRuleInfo(ruleName string, ruleType RuleType) *RuleInfo { func NewRuleInfo(ruleName string, ruleType RuleType) *RuleInfo {
return &RuleInfo{ return &RuleInfo{
@ -121,6 +172,9 @@ func RulesSuccesfuly(rules []*RuleInfo) bool {
//AddRuleInfos sets the rule information //AddRuleInfos sets the rule information
func (pi *PolicyInfo) AddRuleInfos(rules []*RuleInfo) { func (pi *PolicyInfo) AddRuleInfos(rules []*RuleInfo) {
if rules == nil {
return
}
if !RulesSuccesfuly(rules) { if !RulesSuccesfuly(rules) {
pi.success = false pi.success = false
} }

View file

@ -103,7 +103,8 @@ func applyPolicyOnRaw(policy *kubepolicy.Policy, rawResource []byte, gvk *metav1
policyInfo := info.NewPolicyInfo(policy.Name, policyInfo := info.NewPolicyInfo(policy.Name,
gvk.Kind, gvk.Kind,
rname, rname,
rns) rns,
policy.Spec.ValidationFailureAction)
// Process Mutation // Process Mutation
patches, ruleInfos := engine.Mutate(*policy, rawResource, *gvk) patches, ruleInfos := engine.Mutate(*policy, rawResource, *gvk)

View file

@ -80,7 +80,7 @@ func scanDir(dir string) ([]string, error) {
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v\n", dir, err) return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v", dir, err)
} }
res = append(res, path) res = append(res, path)

View file

@ -173,7 +173,8 @@ func (t *test) applyPolicy(policy *pt.Policy,
policyInfo := info.NewPolicyInfo(policy.Name, policyInfo := info.NewPolicyInfo(policy.Name,
rkind, rkind,
rname, rname,
rns) rns,
policy.Spec.ValidationFailureAction)
// Apply Mutation Rules // Apply Mutation Rules
patches, ruleInfos := engine.Mutate(*policy, rawResource, *tresource.gvk) patches, ruleInfos := engine.Mutate(*policy, rawResource, *tresource.gvk)
policyInfo.AddRuleInfos(ruleInfos) policyInfo.AddRuleInfos(ruleInfos)

View file

@ -2,24 +2,26 @@ package violation
import ( import (
"errors" "errors"
"reflect"
"github.com/golang/glog" v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
v1alpha1 "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
client "github.com/nirmata/kyverno/pkg/dclient" client "github.com/nirmata/kyverno/pkg/dclient"
event "github.com/nirmata/kyverno/pkg/event" event "github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/info"
"github.com/nirmata/kyverno/pkg/sharedinformer" "github.com/nirmata/kyverno/pkg/sharedinformer"
"k8s.io/apimachinery/pkg/runtime"
) )
//Generator to generate policy violation //Generator to generate policy violation
type Generator interface { type Generator interface {
Add(infos ...*Info) error Add(infos ...*Info) error
RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error
ResourceRemoval(policy, rKind, rNs, rName string) error
} }
type builder struct { type builder struct {
client *client.Client client *client.Client
policyLister v1alpha1.PolicyLister policyLister lister.PolicyLister
eventBuilder event.Generator eventBuilder event.Generator
} }
@ -27,7 +29,6 @@ type builder struct {
type Builder interface { type Builder interface {
Generator Generator
processViolation(info *Info) error processViolation(info *Info) error
isActive(kind string, rname string, rnamespace string) (bool, error)
} }
//NewPolicyViolationBuilder returns new violation builder //NewPolicyViolationBuilder returns new violation builder
@ -43,7 +44,24 @@ func NewPolicyViolationBuilder(client *client.Client,
return builder return builder
} }
//BuldNewViolation returns a new violation
func BuldNewViolation(pName string, rKind string, rNs string, rName string, reason string, frules []v1alpha1.FailedRule) *Info {
return &Info{
Policy: pName,
Violation: v1alpha1.Violation{
Kind: rKind,
Namespace: rNs,
Name: rName,
Reason: reason,
Rules: frules,
},
}
}
func (b *builder) Add(infos ...*Info) error { func (b *builder) Add(infos ...*Info) error {
if infos == nil {
return nil
}
for _, info := range infos { for _, info := range infos {
return b.processViolation(info) return b.processViolation(info)
} }
@ -51,96 +69,203 @@ func (b *builder) Add(infos ...*Info) error {
} }
func (b *builder) processViolation(info *Info) error { func (b *builder) processViolation(info *Info) error {
currentViolations := []interface{}{}
statusMap := map[string]interface{}{} statusMap := map[string]interface{}{}
var ok bool violationsMap := map[string]interface{}{}
//TODO: hack get from client violationMap := map[string]interface{}{}
p1, err := b.client.GetResource("Policy", "", info.Policy, "status") var violations interface{}
var violation interface{}
// Get Policy
obj, err := b.client.GetResource("Policy", "", info.Policy, "status")
if err != nil { if err != nil {
return err return err
} }
unstr := p1.UnstructuredContent() unstr := obj.UnstructuredContent()
// check if "status" field exists // get "status" subresource
status, ok := unstr["status"] status, ok := unstr["status"]
if ok { if ok {
// status exists
// status is already present then we append violations // status is already present then we append violations
if statusMap, ok = status.(map[string]interface{}); !ok { if statusMap, ok = status.(map[string]interface{}); !ok {
return errors.New("Unable to parse status subresource") return errors.New("Unable to parse status subresource")
} }
violations, ok := statusMap["violations"] // get policy violations
violations, ok = statusMap["violations"]
if !ok { if !ok {
glog.Info("violation not present") return nil
} }
glog.Info(reflect.TypeOf(violations)) violationsMap, ok = violations.(map[string]interface{})
if currentViolations, ok = violations.([]interface{}); !ok { if !ok {
return errors.New("Unable to parse violations") return errors.New("Unable to get status.violations subresource")
} }
} // check if the resource has a violation
newViolation := info.Violation violation, ok = violationsMap[info.getKey()]
for _, violation := range currentViolations { if !ok {
glog.Info(reflect.TypeOf(violation)) // add resource violation
if v, ok := violation.(map[string]interface{}); ok { violationsMap[info.getKey()] = info.Violation
if name, ok := v["name"].(string); ok { statusMap["violations"] = violationsMap
if namespace, ok := v["namespace"].(string); ok { unstr["status"] = statusMap
ok, err := b.isActive(info.Kind, name, namespace) } else {
if err != nil { violationMap, ok = violation.(map[string]interface{})
glog.Error(err) if !ok {
continue return errors.New("Unable to get status.violations.violation subresource")
}
if !ok {
//TODO remove the violation as it corresponds to resource that does not exist
glog.Info("removed violation")
}
}
} }
// we check if the new violation updates are different from stored violation info
v := v1alpha1.Violation{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(violationMap, &v)
if err != nil {
return err
}
// compare v & info.Violation
if v.IsEqual(info.Violation) {
// no updates to violation
// do nothing
return nil
}
// update the violation
violationsMap[info.getKey()] = info.Violation
statusMap["violations"] = violationsMap
unstr["status"] = statusMap
} }
} else {
violationsMap[info.getKey()] = info.Violation
statusMap["violations"] = violationsMap
unstr["status"] = statusMap
} }
currentViolations = append(currentViolations, newViolation)
// update violations obj.SetUnstructuredContent(unstr)
// set the updated status // update the status sub-resource for policy
statusMap["violations"] = currentViolations _, err = b.client.UpdateStatusResource("Policy", "", obj, false)
unstr["status"] = statusMap
p1.SetUnstructuredContent(unstr)
_, err = b.client.UpdateStatusResource("policies", "", p1, false)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (b *builder) isActive(kind, rname, rnamespace string) (bool, error) { //RemoveInactiveViolation
// Generate Merge Patch func (b *builder) RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error {
_, err := b.client.GetResource(b.client.DiscoveryClient.GetGVRFromKind(kind).Resource, rnamespace, rname) statusMap := map[string]interface{}{}
violationsMap := map[string]interface{}{}
violationMap := map[string]interface{}{}
var violations interface{}
var violation interface{}
// Get Policy
obj, err := b.client.GetResource("Policy", "", policy, "status")
if err != nil { if err != nil {
glog.Errorf("unable to get resource %s/%s ", rnamespace, rname) return err
return false, err
} }
return true, nil unstr := obj.UnstructuredContent()
// get "status" subresource
status, ok := unstr["status"]
if !ok {
return nil
}
// status exists
// status is already present then we append violations
if statusMap, ok = status.(map[string]interface{}); !ok {
return errors.New("Unable to parse status subresource")
}
// get policy violations
violations, ok = statusMap["violations"]
if !ok {
return nil
}
violationsMap, ok = violations.(map[string]interface{})
if !ok {
return errors.New("Unable to get status.violations subresource")
}
// check if the resource has a violation
violation, ok = violationsMap[BuildKey(rKind, rNs, rName)]
if !ok {
// no violation for this resource
return nil
}
violationMap, ok = violation.(map[string]interface{})
if !ok {
return errors.New("Unable to get status.violations.violation subresource")
}
// check remove the rules of the given type
// this is called when the policy is applied succesfully, so we can remove the previous failed rules
// if all rules are to be removed, the deleted the violation
v := v1alpha1.Violation{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(violationMap, &v)
if err != nil {
return err
}
if !v.RemoveRulesOfType(ruleType.String()) {
// no rule of given type found,
// no need to remove rule
return nil
}
// if there are no faile rules remove the violation
if len(v.Rules) == 0 {
delete(violationsMap, BuildKey(rKind, rNs, rName))
} else {
// update the rules
violationsMap[BuildKey(rKind, rNs, rName)] = v
}
statusMap["violations"] = violationsMap
unstr["status"] = statusMap
obj.SetUnstructuredContent(unstr)
// update the status sub-resource for policy
_, err = b.client.UpdateStatusResource("Policy", "", obj, false)
if err != nil {
return err
}
return nil
} }
//NewViolation return new policy violation // ResourceRemoval on resources reoval we remove the policy violation in the policy
func NewViolation(reason event.Reason, policyName, kind, rname, rnamespace, msg string) *Info { func (b *builder) ResourceRemoval(policy, rKind, rNs, rName string) error {
return &Info{Policy: policyName, statusMap := map[string]interface{}{}
Violation: types.Violation{ violationsMap := map[string]interface{}{}
Kind: kind, var violations interface{}
Name: rname, // Get Policy
Namespace: rnamespace, obj, err := b.client.GetResource("Policy", "", policy, "status")
Reason: reason.String(), if err != nil {
Message: msg, return err
}, }
unstr := obj.UnstructuredContent()
// get "status" subresource
status, ok := unstr["status"]
if !ok {
return nil
}
// status exists
// status is already present then we append violations
if statusMap, ok = status.(map[string]interface{}); !ok {
return errors.New("Unable to parse status subresource")
}
// get policy violations
violations, ok = statusMap["violations"]
if !ok {
return nil
}
violationsMap, ok = violations.(map[string]interface{})
if !ok {
return errors.New("Unable to get status.violations subresource")
} }
}
//NewViolationFromEvent returns violation info from event // check if the resource has a violation
func NewViolationFromEvent(e *event.Info, pName, rKind, rName, rnamespace string) *Info { _, ok = violationsMap[BuildKey(rKind, rNs, rName)]
return &Info{ if !ok {
Policy: pName, // no violation for this resource
Violation: types.Violation{ return nil
Kind: rKind,
Name: rName,
Namespace: rnamespace,
Reason: e.Reason,
Message: e.Message,
},
} }
// remove the pair from the map
delete(violationsMap, BuildKey(rKind, rNs, rName))
if len(violationsMap) == 0 {
delete(statusMap, "violations")
} else {
statusMap["violations"] = violationsMap
}
unstr["status"] = statusMap
obj.SetUnstructuredContent(unstr)
// update the status sub-resource for policy
_, err = b.client.UpdateStatusResource("Policy", "", obj, false)
if err != nil {
return err
}
return nil
} }

View file

@ -11,8 +11,17 @@ const workqueueViolationName = "Policy-Violations"
// Event Reason // Event Reason
const violationEventResrouce = "Violation" const violationEventResrouce = "Violation"
//ViolationInfo describes the policyviolation details //Info describes the policyviolation details
type Info struct { type Info struct {
Policy string Policy string
policytype.Violation policytype.Violation
} }
func (i Info) getKey() string {
return i.Kind + "/" + i.Namespace + "/" + i.Name
}
//BuildKey returns the key format
func BuildKey(rKind, rNs, rName string) string {
return rKind + "/" + rNs + "/" + rName
}

View file

@ -0,0 +1,40 @@
package webhooks
import (
"errors"
engine "github.com/nirmata/kyverno/pkg/engine"
v1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/labels"
)
func (ws *WebhookServer) removePolicyViolation(request *v1beta1.AdmissionRequest) error {
// Get the list of policies that apply on the resource
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
// Unable to connect to policy Lister to access policies
return errors.New("Unable to connect to policy controller to access policies. Clean Up of Policy Violations is not being done")
}
for _, policy := range policies {
// check if policy has a rule for the admission request kind
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
// get the details from the request
rname := request.Name
rns := request.Namespace
rkind := request.Kind.Kind
// check if the resource meets the policy Resource description
for _, rule := range policy.Spec.Rules {
ok := engine.ResourceMeetsDescription(request.Object.Raw, rule.ResourceDescription, request.Kind)
if ok {
// Check if the policy has a violation for this resource
err := ws.violationBuilder.ResourceRemoval(policy.Name, rkind, rns, rname)
if err != nil {
return err
}
}
}
}
return nil
}

113
pkg/webhooks/mutation.go Normal file
View file

@ -0,0 +1,113 @@
package webhooks
import (
jsonpatch "github.com/evanphx/json-patch"
"github.com/golang/glog"
engine "github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/info"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
// HandleMutation handles mutating webhook admission request
func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
// Unable to connect to policy Lister to access policies
glog.Error("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied")
glog.Warning(err)
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
rname := engine.ParseNameFromObject(request.Object.Raw)
rns := engine.ParseNamespaceFromObject(request.Object.Raw)
rkind := engine.ParseKindFromObject(request.Object.Raw)
var allPatches [][]byte
var annPatches []byte
policyInfos := []*info.PolicyInfo{}
for _, policy := range policies {
// check if policy has a rule for the admission request kind
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
//TODO: HACK Check if an update of annotations
if checkIfOnlyAnnotationsUpdate(request) {
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
policyInfo := info.NewPolicyInfo(policy.Name,
rkind,
rname,
rns,
policy.Spec.ValidationFailureAction)
glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, rns, rname, request.UID, request.Operation)
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
policyPatches, ruleInfos := engine.Mutate(*policy, request.Object.Raw, request.Kind)
policyInfo.AddRuleInfos(ruleInfos)
if !policyInfo.IsSuccessful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range ruleInfos {
glog.Warningf("%s: %s\n", r.Name, r.Msgs)
}
} else {
// TODO
// // CleanUp Violations if exists
// err := ws.violationBuilder.RemoveInactiveViolation(policy.Name, request.Kind.Kind, rns, rname, info.Validation)
// if err != nil {
// glog.Info(err)
// }
allPatches = append(allPatches, policyPatches...)
glog.Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns)
}
policyInfos = append(policyInfos, policyInfo)
annPatch := addAnnotationsToResource(request.Object.Raw, policyInfo, info.Mutation)
if annPatch != nil {
if annPatches == nil {
annPatches = annPatch
} else {
annPatches, err = jsonpatch.MergePatch(annPatches, annPatch)
if err != nil {
glog.Error(err)
}
}
}
}
if len(allPatches) > 0 {
eventsInfo, _ := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Mutation)
ws.eventController.Add(eventsInfo...)
}
// add annotations
if annPatches != nil {
// fmt.Println(string(annPatches))
ws.annotationsController.Add(rkind, rns, rname, annPatches)
}
ok, msg := isAdmSuccesful(policyInfos)
if ok {
patchType := v1beta1.PatchTypeJSONPatch
return &v1beta1.AdmissionResponse{
Allowed: true,
Patch: engine.JoinPatches(allPatches),
PatchType: &patchType,
}
}
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: msg,
},
}
}

View file

@ -0,0 +1,45 @@
package webhooks
import (
"encoding/json"
"fmt"
"github.com/golang/glog"
policyv1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
//HandlePolicyValidation performs the validation check on policy resource
func (ws *WebhookServer) HandlePolicyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
return ws.validateUniqueRuleName(request.Object.Raw)
}
// Verify if the Rule names are unique within a policy
func (ws *WebhookServer) validateUniqueRuleName(rawPolicy []byte) *v1beta1.AdmissionResponse {
var policy *policyv1.Policy
var ruleNames []string
json.Unmarshal(rawPolicy, &policy)
for _, rule := range policy.Spec.Rules {
if utils.Contains(ruleNames, rule.Name) {
msg := fmt.Sprintf(`The policy "%s" is invalid: duplicate rule name: "%s"`, policy.Name, rule.Name)
glog.Errorln(msg)
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: msg,
},
}
}
ruleNames = append(ruleNames, rule.Name)
}
glog.V(3).Infof("Policy validation passed")
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}

View file

@ -139,7 +139,8 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati
constructWebhook( constructWebhook(
config.MutatingWebhookName, config.MutatingWebhookName,
config.MutatingWebhookServicePath, config.MutatingWebhookServicePath,
caData), caData,
false),
}, },
}, nil }, nil
} }
@ -157,7 +158,8 @@ func (wrc *WebhookRegistrationClient) contructDebugMutatingWebhookConfig(caData
constructDebugWebhook( constructDebugWebhook(
config.MutatingWebhookName, config.MutatingWebhookName,
url, url,
caData), caData,
false),
}, },
} }
} }
@ -190,7 +192,8 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura
constructWebhook( constructWebhook(
config.ValidatingWebhookName, config.ValidatingWebhookName,
config.ValidatingWebhookServicePath, config.ValidatingWebhookServicePath,
caData), caData,
true),
}, },
}, nil }, nil
} }
@ -208,7 +211,8 @@ func (wrc *WebhookRegistrationClient) contructDebugValidatingWebhookConfig(caDat
constructDebugWebhook( constructDebugWebhook(
config.ValidatingWebhookName, config.ValidatingWebhookName,
url, url,
caData), caData,
true),
}, },
} }
} }
@ -241,7 +245,8 @@ func (wrc *WebhookRegistrationClient) contructPolicyValidatingWebhookConfig() (*
constructWebhook( constructWebhook(
config.PolicyValidatingWebhookName, config.PolicyValidatingWebhookName,
config.PolicyValidatingWebhookServicePath, config.PolicyValidatingWebhookServicePath,
caData), caData,
true),
}, },
}, nil }, nil
} }
@ -259,12 +264,13 @@ func (wrc *WebhookRegistrationClient) contructDebugPolicyValidatingWebhookConfig
constructDebugWebhook( constructDebugWebhook(
config.PolicyValidatingWebhookName, config.PolicyValidatingWebhookName,
url, url,
caData), caData,
true),
}, },
} }
} }
func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook { func constructWebhook(name, servicePath string, caData []byte, validation bool) admregapi.Webhook {
resource := "*/*" resource := "*/*"
apiGroups := "*" apiGroups := "*"
apiversions := "*" apiversions := "*"
@ -273,6 +279,15 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
apiGroups = "kyverno.io" apiGroups = "kyverno.io"
apiversions = "v1alpha1" apiversions = "v1alpha1"
} }
operationtypes := []admregapi.OperationType{
admregapi.Create,
admregapi.Update,
}
// Add operation DELETE for validation
if validation {
operationtypes = append(operationtypes, admregapi.Delete)
}
return admregapi.Webhook{ return admregapi.Webhook{
Name: name, Name: name,
@ -286,10 +301,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
}, },
Rules: []admregapi.RuleWithOperations{ Rules: []admregapi.RuleWithOperations{
admregapi.RuleWithOperations{ admregapi.RuleWithOperations{
Operations: []admregapi.OperationType{ Operations: operationtypes,
admregapi.Create,
admregapi.Update,
},
Rule: admregapi.Rule{ Rule: admregapi.Rule{
APIGroups: []string{ APIGroups: []string{
apiGroups, apiGroups,
@ -306,7 +318,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
} }
} }
func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook { func constructDebugWebhook(name, url string, caData []byte, validation bool) admregapi.Webhook {
resource := "*/*" resource := "*/*"
apiGroups := "*" apiGroups := "*"
apiversions := "*" apiversions := "*"
@ -316,6 +328,14 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
apiGroups = "kyverno.io" apiGroups = "kyverno.io"
apiversions = "v1alpha1" apiversions = "v1alpha1"
} }
operationtypes := []admregapi.OperationType{
admregapi.Create,
admregapi.Update,
}
// Add operation DELETE for validation
if validation {
operationtypes = append(operationtypes, admregapi.Delete)
}
return admregapi.Webhook{ return admregapi.Webhook{
Name: name, Name: name,
@ -325,10 +345,7 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
}, },
Rules: []admregapi.RuleWithOperations{ Rules: []admregapi.RuleWithOperations{
admregapi.RuleWithOperations{ admregapi.RuleWithOperations{
Operations: []admregapi.OperationType{ Operations: operationtypes,
admregapi.Create,
admregapi.Update,
},
Rule: admregapi.Rule{ Rule: admregapi.Rule{
APIGroups: []string{ APIGroups: []string{
apiGroups, apiGroups,

77
pkg/webhooks/report.go Normal file
View file

@ -0,0 +1,77 @@
package webhooks
import (
"strings"
"github.com/nirmata/kyverno/pkg/annotations"
"github.com/nirmata/kyverno/pkg/violation"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/info"
)
//TODO: change validation from bool -> enum(validation, mutation)
func newEventInfoFromPolicyInfo(policyInfoList []*info.PolicyInfo, onUpdate bool, ruleType info.RuleType) ([]*event.Info, []*violation.Info) {
var eventsInfo []*event.Info
var violations []*violation.Info
ok, msg := isAdmSuccesful(policyInfoList)
// Some policies failed to apply succesfully
if !ok {
for _, pi := range policyInfoList {
if pi.IsSuccessful() {
continue
}
rules := pi.FailedRules()
ruleNames := strings.Join(rules, ";")
if !onUpdate {
// CREATE
eventsInfo = append(eventsInfo,
event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyApplyBlockCreate, pi.RName, ruleNames))
glog.V(3).Infof("Rule(s) %s of policy %s blocked resource creation, error: %s\n", ruleNames, pi.Name, msg)
} else {
// UPDATE
eventsInfo = append(eventsInfo,
event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.RequestBlocked, event.FPolicyApplyBlockUpdate, ruleNames, pi.Name))
eventsInfo = append(eventsInfo,
event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyBlockResourceUpdate, pi.RName, ruleNames))
glog.V(3).Infof("Request blocked events info has prepared for %s/%s and %s/%s\n", policyKind, pi.Name, pi.RKind, pi.RName)
}
// if report flag is set
if pi.ValidationFailureAction == ReportViolation && ruleType == info.Validation {
// Create Violations
v := violation.BuldNewViolation(pi.Name, pi.RKind, pi.RNamespace, pi.RName, event.PolicyViolation.String(), pi.GetFailedRules())
violations = append(violations, v)
}
}
} else {
if !onUpdate {
// All policies were applied succesfully
// CREATE
for _, pi := range policyInfoList {
rules := pi.SuccessfulRules()
ruleNames := strings.Join(rules, ";")
eventsInfo = append(eventsInfo,
event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.PolicyApplied, event.SRulesApply, ruleNames, pi.Name))
glog.V(3).Infof("Success event info has prepared for %s/%s\n", pi.RKind, pi.RName)
}
}
}
return eventsInfo, violations
}
func addAnnotationsToResource(rawResource []byte, pi *info.PolicyInfo, ruleType info.RuleType) []byte {
if len(pi.Rules) == 0 {
return nil
}
// get annotations
ann := annotations.ParseAnnotationsFromObject(rawResource)
ann, patch, err := annotations.AddPolicyJSONPatch(ann, pi, ruleType)
if err != nil {
glog.Error(err)
return nil
}
return patch
}

View file

@ -8,35 +8,30 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
policyv1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" "github.com/nirmata/kyverno/pkg/annotations"
"github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1" "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
"github.com/nirmata/kyverno/pkg/config" "github.com/nirmata/kyverno/pkg/config"
client "github.com/nirmata/kyverno/pkg/dclient" client "github.com/nirmata/kyverno/pkg/dclient"
engine "github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/event" "github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/info"
"github.com/nirmata/kyverno/pkg/sharedinformer" "github.com/nirmata/kyverno/pkg/sharedinformer"
tlsutils "github.com/nirmata/kyverno/pkg/tls" tlsutils "github.com/nirmata/kyverno/pkg/tls"
"github.com/nirmata/kyverno/pkg/utils" "github.com/nirmata/kyverno/pkg/violation"
v1beta1 "k8s.io/api/admission/v1beta1" v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
) )
const policyKind = "Policy"
// WebhookServer contains configured TLS server with MutationWebhook. // WebhookServer contains configured TLS server with MutationWebhook.
// MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient. // MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient.
type WebhookServer struct { type WebhookServer struct {
server http.Server server http.Server
client *client.Client client *client.Client
policyLister v1alpha1.PolicyLister policyLister v1alpha1.PolicyLister
eventController event.Generator eventController event.Generator
filterKinds []string violationBuilder violation.Generator
annotationsController annotations.Controller
filterKinds []string
} }
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration // NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
@ -46,6 +41,8 @@ func NewWebhookServer(
tlsPair *tlsutils.TlsPemPair, tlsPair *tlsutils.TlsPemPair,
shareInformer sharedinformer.PolicyInformer, shareInformer sharedinformer.PolicyInformer,
eventController event.Generator, eventController event.Generator,
violationBuilder violation.Generator,
annotationsController annotations.Controller,
filterKinds []string) (*WebhookServer, error) { filterKinds []string) (*WebhookServer, error) {
if tlsPair == nil { if tlsPair == nil {
@ -60,10 +57,12 @@ func NewWebhookServer(
tlsConfig.Certificates = []tls.Certificate{pair} tlsConfig.Certificates = []tls.Certificate{pair}
ws := &WebhookServer{ ws := &WebhookServer{
client: client, client: client,
policyLister: shareInformer.GetLister(), policyLister: shareInformer.GetLister(),
eventController: eventController, eventController: eventController,
filterKinds: parseKinds(filterKinds), violationBuilder: violationBuilder,
annotationsController: annotationsController,
filterKinds: parseKinds(filterKinds),
} }
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve) mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve)
@ -94,14 +93,30 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
// Do not process the admission requests for kinds that are in filterKinds for filtering // Do not process the admission requests for kinds that are in filterKinds for filtering
if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) { if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) {
// if the resource is being deleted we need to clear any existing Policy Violations
// TODO: can report to the user that we clear the violation corresponding to this resource
if admissionReview.Request.Operation == v1beta1.Delete {
// Resource DELETE
err := ws.removePolicyViolation(admissionReview.Request)
if err != nil {
glog.Info(err)
}
admissionReview.Response = &v1beta1.AdmissionResponse{
Allowed: true,
}
admissionReview.Response.UID = admissionReview.Request.UID
} else {
// Resource CREATE
// Resource UPDATE
switch r.URL.Path {
case config.MutatingWebhookServicePath:
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
case config.ValidatingWebhookServicePath:
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
case config.PolicyValidatingWebhookServicePath:
admissionReview.Response = ws.HandlePolicyValidation(admissionReview.Request)
}
switch r.URL.Path {
case config.MutatingWebhookServicePath:
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
case config.ValidatingWebhookServicePath:
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
case config.PolicyValidatingWebhookServicePath:
admissionReview.Response = ws.HandlePolicyValidation(admissionReview.Request)
} }
} }
@ -140,169 +155,6 @@ func (ws *WebhookServer) Stop() {
} }
} }
// HandleMutation handles mutating webhook admission request
func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
// Unable to connect to policy Lister to access policies
glog.Error("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied")
glog.Warning(err)
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
var allPatches [][]byte
policyInfos := []*info.PolicyInfo{}
for _, policy := range policies {
// check if policy has a rule for the admission request kind
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
rname := engine.ParseNameFromObject(request.Object.Raw)
rns := engine.ParseNamespaceFromObject(request.Object.Raw)
rkind := engine.ParseKindFromObject(request.Object.Raw)
policyInfo := info.NewPolicyInfo(policy.Name,
rkind,
rname,
rns)
glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, rns, rname, request.UID, request.Operation)
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
policyPatches, ruleInfos := engine.Mutate(*policy, request.Object.Raw, request.Kind)
policyInfo.AddRuleInfos(ruleInfos)
if !policyInfo.IsSuccessful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range ruleInfos {
glog.Warning(r.Msgs)
}
} else if len(policyPatches) > 0 {
allPatches = append(allPatches, policyPatches...)
glog.Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns)
}
policyInfos = append(policyInfos, policyInfo)
}
if len(allPatches) > 0 {
eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update))
ws.eventController.Add(eventsInfo...)
}
ok, msg := isAdmSuccesful(policyInfos)
if ok {
patchType := v1beta1.PatchTypeJSONPatch
return &v1beta1.AdmissionResponse{
Allowed: true,
Patch: engine.JoinPatches(allPatches),
PatchType: &patchType,
}
}
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: msg,
},
}
}
func isAdmSuccesful(policyInfos []*info.PolicyInfo) (bool, string) {
var admSuccess = true
var errMsgs []string
for _, pi := range policyInfos {
if !pi.IsSuccessful() {
admSuccess = false
errMsgs = append(errMsgs, fmt.Sprintf("\nPolicy %s failed with following rules", pi.Name))
// Get the error rules
errorRules := pi.ErrorRules()
errMsgs = append(errMsgs, errorRules)
}
}
return admSuccess, strings.Join(errMsgs, ";")
}
// HandleValidation handles validating webhook admission request
// If there are no errors in validating rule we apply generation rules
func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
policyInfos := []*info.PolicyInfo{}
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
// Unable to connect to policy Lister to access policies
glog.Error("Unable to connect to policy controller to access policies. Validation Rules are NOT being applied")
glog.Warning(err)
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
for _, policy := range policies {
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
rname := engine.ParseNameFromObject(request.Object.Raw)
rns := engine.ParseNamespaceFromObject(request.Object.Raw)
rkind := engine.ParseKindFromObject(request.Object.Raw)
policyInfo := info.NewPolicyInfo(policy.Name,
rkind,
rname,
rns)
glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, rns, rname, request.UID, request.Operation)
glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
ruleInfos, err := engine.Validate(*policy, request.Object.Raw, request.Kind)
if err != nil {
// This is not policy error
// but if unable to parse request raw resource
// TODO : create event ? dont think so
glog.Error(err)
continue
}
policyInfo.AddRuleInfos(ruleInfos)
if !policyInfo.IsSuccessful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range ruleInfos {
glog.Warning(r.Msgs)
}
} else if len(ruleInfos) > 0 {
glog.Infof("Validation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns)
}
policyInfos = append(policyInfos, policyInfo)
}
if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 {
eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update))
ws.eventController.Add(eventsInfo...)
}
// If Validation fails then reject the request
ok, msg := isAdmSuccesful(policyInfos)
if !ok {
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: msg,
},
}
}
return &v1beta1.AdmissionResponse{
Allowed: true,
}
// Generation rules applied via generation controller
}
// bodyToAdmissionReview creates AdmissionReview object from request body // bodyToAdmissionReview creates AdmissionReview object from request body
// Answers to the http.ResponseWriter if request is not valid // Answers to the http.ResponseWriter if request is not valid
func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview { func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview {
@ -334,96 +186,3 @@ func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer htt
return admissionReview return admissionReview
} }
//HandlePolicyValidation performs the validation check on policy resource
func (ws *WebhookServer) HandlePolicyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
return ws.validateUniqueRuleName(request.Object.Raw)
}
func (ws *WebhookServer) validateUniqueRuleName(rawPolicy []byte) *v1beta1.AdmissionResponse {
var policy *policyv1.Policy
var ruleNames []string
json.Unmarshal(rawPolicy, &policy)
for _, rule := range policy.Spec.Rules {
if utils.Contains(ruleNames, rule.Name) {
msg := fmt.Sprintf(`The policy "%s" is invalid: duplicate rule name: "%s"`, policy.Name, rule.Name)
glog.Errorln(msg)
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: msg,
},
}
}
ruleNames = append(ruleNames, rule.Name)
}
glog.V(3).Infof("Policy validation passed")
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
func newEventInfoFromPolicyInfo(policyInfoList []*info.PolicyInfo, onUpdate bool) []*event.Info {
var eventsInfo []*event.Info
ok, msg := isAdmSuccesful(policyInfoList)
// create events on operation UPDATE
if onUpdate {
if !ok {
for _, pi := range policyInfoList {
ruleNames := getRuleNames(*pi, false)
eventsInfo = append(eventsInfo,
event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.RequestBlocked, event.FPolicyApplyBlockUpdate, ruleNames, pi.Name))
eventsInfo = append(eventsInfo,
event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyBlockResourceUpdate, pi.RName, ruleNames))
glog.V(3).Infof("Request blocked events info has prepared for %s/%s and %s/%s\n", policyKind, pi.Name, pi.RKind, pi.RName)
}
}
return eventsInfo
}
// create events on operation CREATE
if ok {
for _, pi := range policyInfoList {
ruleNames := getRuleNames(*pi, true)
eventsInfo = append(eventsInfo,
event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.PolicyApplied, event.SRulesApply, ruleNames, pi.Name))
glog.V(3).Infof("Success event info has prepared for %s/%s\n", pi.RKind, pi.RName)
}
return eventsInfo
}
for _, pi := range policyInfoList {
ruleNames := getRuleNames(*pi, false)
eventsInfo = append(eventsInfo,
event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyApplyBlockCreate, pi.RName, ruleNames))
glog.V(3).Infof("Rule(s) %s of policy %s blocked resource creation, error: %s\n", ruleNames, pi.Name, msg)
}
return eventsInfo
}
func getRuleNames(policyInfo info.PolicyInfo, onSuccess bool) string {
var ruleNames []string
for _, rule := range policyInfo.Rules {
if onSuccess {
if rule.IsSuccessful() {
ruleNames = append(ruleNames, rule.Name)
}
} else {
if !rule.IsSuccessful() {
ruleNames = append(ruleNames, rule.Name)
}
}
}
return strings.Join(ruleNames, ",")
}

View file

@ -1,11 +1,34 @@
package webhooks package webhooks
import ( import (
"fmt"
"reflect"
"strings" "strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1" "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
"github.com/nirmata/kyverno/pkg/info"
v1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
const policyKind = "Policy"
func isAdmSuccesful(policyInfos []*info.PolicyInfo) (bool, string) {
var admSuccess = true
var errMsgs []string
for _, pi := range policyInfos {
if !pi.IsSuccessful() {
admSuccess = false
errMsgs = append(errMsgs, fmt.Sprintf("\nPolicy %s failed with following rules", pi.Name))
// Get the error rules
errorRules := pi.ErrorRules()
errMsgs = append(errMsgs, errorRules)
}
}
return admSuccess, strings.Join(errMsgs, ";")
}
//StringInSlice checks if string is present in slice of strings //StringInSlice checks if string is present in slice of strings
func StringInSlice(kind string, list []string) bool { func StringInSlice(kind string, list []string) bool {
for _, b := range list { for _, b := range list {
@ -63,3 +86,50 @@ func getApplicableKindsForPolicy(p *v1alpha1.Policy) []string {
} }
return kinds return kinds
} }
// Policy Reporting Modes
const (
BlockChanges = "block"
ReportViolation = "report"
)
// returns true -> if there is even one policy that blocks resource requst
// returns false -> if all the policies are meant to report only, we dont block resource request
func toBlock(pis []*info.PolicyInfo) bool {
for _, pi := range pis {
if pi.ValidationFailureAction != ReportViolation {
return true
}
}
return false
}
func checkIfOnlyAnnotationsUpdate(request *v1beta1.AdmissionRequest) bool {
// process only if its for existing resources
if request.Operation != v1beta1.Update {
return false
}
// updated resoruce
obj := request.Object
objUnstr := unstructured.Unstructured{}
err := objUnstr.UnmarshalJSON(obj.Raw)
if err != nil {
glog.Error(err)
return false
}
objUnstr.SetAnnotations(nil)
objUnstr.SetGeneration(0)
oldobj := request.OldObject
oldobjUnstr := unstructured.Unstructured{}
err = oldobjUnstr.UnmarshalJSON(oldobj.Raw)
if err != nil {
glog.Error(err)
return false
}
oldobjUnstr.SetAnnotations(nil)
oldobjUnstr.SetGeneration(0)
if reflect.DeepEqual(objUnstr, oldobjUnstr) {
return true
}
return false
}

126
pkg/webhooks/validation.go Normal file
View file

@ -0,0 +1,126 @@
package webhooks
import (
jsonpatch "github.com/evanphx/json-patch"
"github.com/golang/glog"
engine "github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/info"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
// HandleValidation handles validating webhook admission request
// If there are no errors in validating rule we apply generation rules
func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
policyInfos := []*info.PolicyInfo{}
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
// Unable to connect to policy Lister to access policies
glog.Error("Unable to connect to policy controller to access policies. Validation Rules are NOT being applied")
glog.Warning(err)
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
rname := engine.ParseNameFromObject(request.Object.Raw)
rns := engine.ParseNamespaceFromObject(request.Object.Raw)
rkind := engine.ParseKindFromObject(request.Object.Raw)
var annPatches []byte
for _, policy := range policies {
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
//TODO: HACK Check if an update of annotations
if checkIfOnlyAnnotationsUpdate(request) {
// allow the update of resource to add annotations
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
policyInfo := info.NewPolicyInfo(policy.Name,
rkind,
rname,
rns,
policy.Spec.ValidationFailureAction)
glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, rns, rname, request.UID, request.Operation)
glog.Infof("Validating resource %s/%s/%s with policy %s with %d rules", rkind, rns, rname, policy.ObjectMeta.Name, len(policy.Spec.Rules))
ruleInfos, err := engine.Validate(*policy, request.Object.Raw, request.Kind)
if err != nil {
// This is not policy error
// but if unable to parse request raw resource
// TODO : create event ? dont think so
glog.Error(err)
continue
}
policyInfo.AddRuleInfos(ruleInfos)
if !policyInfo.IsSuccessful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range ruleInfos {
glog.Warningf("%s: %s\n", r.Name, r.Msgs)
}
} else {
// CleanUp Violations if exists
err := ws.violationBuilder.RemoveInactiveViolation(policy.Name, request.Kind.Kind, rns, rname, info.Validation)
if err != nil {
glog.Info(err)
}
if len(ruleInfos) > 0 {
glog.Infof("Validation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns)
}
}
policyInfos = append(policyInfos, policyInfo)
// annotations
annPatch := addAnnotationsToResource(request.Object.Raw, policyInfo, info.Validation)
if annPatch != nil {
if annPatches == nil {
annPatches = annPatch
} else {
annPatches, err = jsonpatch.MergePatch(annPatches, annPatch)
if err != nil {
glog.Error(err)
}
}
}
}
if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 {
eventsInfo, violations := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update), info.Validation)
// If the validationFailureAction flag is set "report",
// then we dont block the request and report the violations
ws.violationBuilder.Add(violations...)
ws.eventController.Add(eventsInfo...)
}
// add annotations
if annPatches != nil {
ws.annotationsController.Add(rkind, rns, rname, annPatches)
}
// If Validation fails then reject the request
ok, msg := isAdmSuccesful(policyInfos)
// violations are created if "report" flag is set
// and if there are any then we dont bock the resource creation
// Even if one the policy being applied
if !ok && toBlock(policyInfos) {
return &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: msg,
},
}
}
return &v1beta1.AdmissionResponse{
Allowed: true,
}
// Generation rules applied via generation controller
}

View file

@ -28,7 +28,7 @@ spec:
message: "This SS is broken" message: "This SS is broken"
pattern: pattern:
spec: spec:
replicas: ">20" replicas: ">2"
volumeClaimTemplates: volumeClaimTemplates:
- metadata: - metadata:
name: www name: www