mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-06 16:06:56 +00:00
commit
e9e7d8faff
43 changed files with 1711 additions and 528 deletions
|
@ -22,6 +22,12 @@ spec:
|
|||
required:
|
||||
- rules
|
||||
properties:
|
||||
# default values to be handled by user
|
||||
validationFailureAction:
|
||||
type: string
|
||||
enum:
|
||||
- block
|
||||
- report
|
||||
rules:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -22,6 +22,12 @@ spec:
|
|||
required:
|
||||
- rules
|
||||
properties:
|
||||
# default values to be handled by user
|
||||
validationFailureAction:
|
||||
type: string
|
||||
enum:
|
||||
- block
|
||||
- report
|
||||
rules:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -20,4 +20,4 @@ spec:
|
|||
image: nginx:1.7.9
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- containerPort: 80
|
|
@ -35,4 +35,3 @@ spec :
|
|||
containers:
|
||||
- (image): "nginx*"
|
||||
imagePullPolicy: Always
|
||||
|
||||
|
|
|
@ -5,37 +5,37 @@ metadata:
|
|||
spec:
|
||||
validationFailureAction: "report"
|
||||
rules:
|
||||
- name: check-cpu-memory-limits
|
||||
resource:
|
||||
kinds:
|
||||
- Deployment
|
||||
validate:
|
||||
message: "Resource limits are required for CPU and memory"
|
||||
pattern:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
# match all contianers
|
||||
- (name): "*"
|
||||
resources:
|
||||
limits:
|
||||
# cpu and memory are required
|
||||
memory: "?*"
|
||||
cpu: "?*"
|
||||
# - name: add-memory-limit
|
||||
# resource:
|
||||
# kinds:
|
||||
# - Deployment
|
||||
# mutate:
|
||||
# overlay:
|
||||
# spec:
|
||||
# template:
|
||||
# spec:
|
||||
# containers:
|
||||
# # the wildcard * will match all containers in the list
|
||||
# - (name): "*"
|
||||
# resources:
|
||||
# limits:
|
||||
# # add memory limit if it is not exist
|
||||
# "+(memory)": "300Mi"
|
||||
- name: add-memory-limit
|
||||
resource:
|
||||
kinds:
|
||||
- Deployment
|
||||
mutate:
|
||||
overlay:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
# the wildcard * will match all containers in the list
|
||||
- (name): "*"
|
||||
resources:
|
||||
limits:
|
||||
# add memory limit if it is not exist
|
||||
"+(memory)": "300Mi"
|
||||
- name: check-cpu-memory-limits
|
||||
resource:
|
||||
kinds:
|
||||
- Deployment
|
||||
validate:
|
||||
message: "Resource limits are required for CPU and memory"
|
||||
pattern:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
# match all contianers
|
||||
- (name): "*"
|
||||
resources:
|
||||
limits:
|
||||
# cpu and memory are required
|
||||
memory: "?*"
|
||||
cpu: "?*"
|
|
@ -22,4 +22,4 @@ spec:
|
|||
ports:
|
||||
- containerPort: 80
|
||||
- name: ghost
|
||||
image: ghost:latest
|
||||
image: ghost:latest
|
|
@ -16,4 +16,4 @@ spec:
|
|||
containers:
|
||||
# if the image tag is latest, set the imagePullPolicy to Always
|
||||
- (image): "*:latest"
|
||||
imagePullPolicy: "IfNotPresent"
|
||||
imagePullPolicy: "IfNotPresent"
|
|
@ -10,4 +10,4 @@ subsets:
|
|||
ports:
|
||||
- name: secure-connection
|
||||
port: 443
|
||||
protocol: TCP
|
||||
protocol: TCP
|
19
main.go
19
main.go
|
@ -4,6 +4,7 @@ import (
|
|||
"flag"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/nirmata/kyverno/pkg/annotations"
|
||||
"github.com/nirmata/kyverno/pkg/config"
|
||||
controller "github.com/nirmata/kyverno/pkg/controller"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
|
@ -24,7 +25,6 @@ var (
|
|||
|
||||
func main() {
|
||||
defer glog.Flush()
|
||||
|
||||
printVersionInfo()
|
||||
clientConfig, err := createClientConfig(kubeconfig)
|
||||
if err != nil {
|
||||
|
@ -43,19 +43,20 @@ func main() {
|
|||
kubeInformer := utils.NewKubeInformerFactory(clientConfig)
|
||||
eventController := event.NewEventController(client, policyInformerFactory)
|
||||
violationBuilder := violation.NewPolicyViolationBuilder(client, policyInformerFactory, eventController)
|
||||
|
||||
annotationsController := annotations.NewAnnotationControler(client)
|
||||
policyController := controller.NewPolicyController(
|
||||
client,
|
||||
policyInformerFactory,
|
||||
violationBuilder,
|
||||
eventController)
|
||||
eventController,
|
||||
annotationsController)
|
||||
|
||||
genControler := gencontroller.NewGenController(client, eventController, policyInformerFactory, violationBuilder, kubeInformer.Core().V1().Namespaces())
|
||||
tlsPair, err := initTLSPemPair(clientConfig, client)
|
||||
if err != nil {
|
||||
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 {
|
||||
glog.Fatalf("Unable to create webhook server: %v\n", err)
|
||||
}
|
||||
|
@ -67,23 +68,25 @@ func main() {
|
|||
|
||||
stopCh := signals.SetupSignalHandler()
|
||||
|
||||
if err = webhookRegistrationClient.Register(); err != nil {
|
||||
glog.Fatalf("Failed registering Admission Webhooks: %v\n", err)
|
||||
}
|
||||
|
||||
policyInformerFactory.Run(stopCh)
|
||||
kubeInformer.Start(stopCh)
|
||||
eventController.Run(stopCh)
|
||||
genControler.Run(stopCh)
|
||||
annotationsController.Run(stopCh)
|
||||
if err = policyController.Run(stopCh); err != nil {
|
||||
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()
|
||||
<-stopCh
|
||||
server.Stop()
|
||||
genControler.Stop()
|
||||
eventController.Stop()
|
||||
annotationsController.Stop()
|
||||
policyController.Stop()
|
||||
}
|
||||
|
||||
|
|
302
pkg/annotations/annotations.go
Normal file
302
pkg/annotations/annotations.go
Normal 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)
|
||||
}
|
36
pkg/annotations/annotations_test.go
Normal file
36
pkg/annotations/annotations_test.go
Normal 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)
|
||||
}
|
||||
}
|
115
pkg/annotations/controller.go
Normal file
115
pkg/annotations/controller.go
Normal 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
18
pkg/annotations/info.go
Normal 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
16
pkg/annotations/utils.go
Normal 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
|
||||
}
|
|
@ -18,7 +18,8 @@ type Policy struct {
|
|||
|
||||
// Spec describes policy behavior by its rules
|
||||
type Spec struct {
|
||||
Rules []Rule `json:"rules"`
|
||||
Rules []Rule `json:"rules"`
|
||||
ValidationFailureAction string `json:"validationFailureAction"`
|
||||
}
|
||||
|
||||
// Rule is set of mutation, validation and generation actions
|
||||
|
@ -77,16 +78,24 @@ type CloneFrom struct {
|
|||
|
||||
// Status contains violations for existing resources
|
||||
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
|
||||
type Violation struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Rules []FailedRule `json:"rules"`
|
||||
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
|
||||
|
|
|
@ -104,3 +104,44 @@ func (in *Generation) DeepCopyInto(out *Generation) {
|
|||
*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
|
||||
}
|
||||
|
|
|
@ -41,6 +41,22 @@ func (in *CloneFrom) DeepCopy() *CloneFrom {
|
|||
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.
|
||||
func (in *Generation) DeepCopy() *Generation {
|
||||
if in == nil {
|
||||
|
@ -135,6 +151,11 @@ func (in *ResourceDescription) DeepCopyInto(out *ResourceDescription) {
|
|||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.Namespace != nil {
|
||||
in, out := &in.Namespace, &out.Namespace
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.Selector != nil {
|
||||
in, out := &in.Selector, &out.Selector
|
||||
*out = new(v1.LabelSelector)
|
||||
|
@ -210,8 +231,10 @@ func (in *Status) DeepCopyInto(out *Status) {
|
|||
*out = *in
|
||||
if in.Violations != nil {
|
||||
in, out := &in.Violations, &out.Violations
|
||||
*out = make([]Violation, len(*in))
|
||||
copy(*out, *in)
|
||||
*out = make(map[string]Violation, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
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.
|
||||
func (in *Violation) DeepCopyInto(out *Violation) {
|
||||
*out = *in
|
||||
if in.Rules != nil {
|
||||
in, out := &in.Rules, &out.Rules
|
||||
*out = make([]FailedRule, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
77
pkg/controller/cleanup.go
Normal file
77
pkg/controller/cleanup.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,15 +3,17 @@ package controller
|
|||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
|
||||
"github.com/nirmata/kyverno/pkg/annotations"
|
||||
"github.com/nirmata/kyverno/pkg/info"
|
||||
|
||||
"github.com/nirmata/kyverno/pkg/engine"
|
||||
|
||||
"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"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
"github.com/nirmata/kyverno/pkg/event"
|
||||
|
@ -27,27 +29,30 @@ import (
|
|||
|
||||
//PolicyController to manage Policy CRD
|
||||
type PolicyController struct {
|
||||
client *client.Client
|
||||
policyLister lister.PolicyLister
|
||||
policySynced cache.InformerSynced
|
||||
violationBuilder violation.Generator
|
||||
eventController event.Generator
|
||||
queue workqueue.RateLimitingInterface
|
||||
client *client.Client
|
||||
policyLister lister.PolicyLister
|
||||
policySynced cache.InformerSynced
|
||||
violationBuilder violation.Generator
|
||||
eventController event.Generator
|
||||
annotationsController annotations.Controller
|
||||
queue workqueue.RateLimitingInterface
|
||||
}
|
||||
|
||||
// NewPolicyController from cmd args
|
||||
func NewPolicyController(client *client.Client,
|
||||
policyInformer sharedinformer.PolicyInformer,
|
||||
violationBuilder violation.Generator,
|
||||
eventController event.Generator) *PolicyController {
|
||||
eventController event.Generator,
|
||||
annotationsController annotations.Controller) *PolicyController {
|
||||
|
||||
controller := &PolicyController{
|
||||
client: client,
|
||||
policyLister: policyInformer.GetLister(),
|
||||
policySynced: policyInformer.GetInfomer().HasSynced,
|
||||
violationBuilder: violationBuilder,
|
||||
eventController: eventController,
|
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName),
|
||||
client: client,
|
||||
policyLister: policyInformer.GetLister(),
|
||||
policySynced: policyInformer.GetInfomer().HasSynced,
|
||||
violationBuilder: violationBuilder,
|
||||
eventController: eventController,
|
||||
annotationsController: annotationsController,
|
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName),
|
||||
}
|
||||
|
||||
policyInformer.GetInfomer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
|
@ -63,13 +68,13 @@ func (pc *PolicyController) createPolicyHandler(resource interface{}) {
|
|||
}
|
||||
|
||||
func (pc *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) {
|
||||
newPolicy := newResource.(*types.Policy)
|
||||
oldPolicy := oldResource.(*types.Policy)
|
||||
newPolicy.Status = types.Status{}
|
||||
oldPolicy.Status = types.Status{}
|
||||
newPolicy := newResource.(*v1alpha1.Policy)
|
||||
oldPolicy := oldResource.(*v1alpha1.Policy)
|
||||
newPolicy.Status = v1alpha1.Status{}
|
||||
oldPolicy.Status = v1alpha1.Status{}
|
||||
newPolicy.ResourceVersion = ""
|
||||
oldPolicy.ResourceVersion = ""
|
||||
if reflect.DeepEqual(newPolicy.ResourceVersion, oldPolicy.ResourceVersion) {
|
||||
if reflect.DeepEqual(newPolicy, oldPolicy) {
|
||||
return
|
||||
}
|
||||
pc.enqueuePolicy(newResource)
|
||||
|
@ -82,6 +87,7 @@ func (pc *PolicyController) deletePolicyHandler(resource interface{}) {
|
|||
glog.Error("error decoding object, invalid type")
|
||||
return
|
||||
}
|
||||
cleanAnnotations(pc.client, resource)
|
||||
glog.Infof("policy deleted: %s", object.GetName())
|
||||
}
|
||||
|
||||
|
@ -155,7 +161,7 @@ func (pc *PolicyController) handleErr(err error, key interface{}) {
|
|||
}
|
||||
pc.queue.Forget(key)
|
||||
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 {
|
||||
|
@ -169,8 +175,7 @@ func (pc *PolicyController) syncHandler(obj interface{}) error {
|
|||
glog.Errorf("invalid policy key: %s", key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get Policy resource with namespace/name
|
||||
// Get Policy
|
||||
policy, err := pc.policyLister.Get(name)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
|
@ -179,40 +184,103 @@ func (pc *PolicyController) syncHandler(obj interface{}) error {
|
|||
}
|
||||
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())
|
||||
// Process policy on existing resources
|
||||
policyInfos := engine.ProcessExisting(pc.client, policy)
|
||||
events, violations := createEventsAndViolations(pc.eventController, policyInfos)
|
||||
|
||||
events, violations := pc.createEventsAndViolations(policyInfos)
|
||||
// Events, Violations
|
||||
pc.eventController.Add(events...)
|
||||
err = pc.violationBuilder.Add(violations...)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
|
||||
// Annotations
|
||||
pc.createAnnotations(policyInfos)
|
||||
|
||||
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{}
|
||||
violations := []*violation.Info{}
|
||||
// Create events from the policyInfo
|
||||
for _, policyInfo := range policyInfos {
|
||||
fruleNames := []string{}
|
||||
frules := []v1alpha1.FailedRule{}
|
||||
sruleNames := []string{}
|
||||
|
||||
for _, rule := range policyInfo.Rules {
|
||||
if !rule.IsSuccessful() {
|
||||
e := &event.Info{}
|
||||
fruleNames = append(fruleNames, rule.Name)
|
||||
frule := v1alpha1.FailedRule{Name: rule.Name}
|
||||
switch rule.RuleType {
|
||||
case info.Mutation, info.Validation, info.Generation:
|
||||
// Events
|
||||
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:
|
||||
glog.Info("Unsupported Rule type")
|
||||
}
|
||||
frule.Error = rule.GetErrorString()
|
||||
frules = append(frules, frule)
|
||||
events = append(events, e)
|
||||
} else {
|
||||
sruleNames = append(sruleNames, rule.Name)
|
||||
|
@ -220,22 +288,16 @@ func createEventsAndViolations(eventController event.Generator, policyInfos []*i
|
|||
}
|
||||
|
||||
if !policyInfo.IsSuccessful() {
|
||||
// Event
|
||||
// list of failed rules : ruleNames
|
||||
e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, strings.Join(fruleNames, ";"))
|
||||
e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, concatFailedRules(frules))
|
||||
events = append(events, e)
|
||||
// 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)
|
||||
} 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
|
||||
}
|
||||
|
|
|
@ -41,7 +41,8 @@ func (f *fixture) runControler(policyName string) {
|
|||
f.Client,
|
||||
policyInformerFactory,
|
||||
violationBuilder,
|
||||
eventController)
|
||||
eventController,
|
||||
nil)
|
||||
|
||||
stopCh := signals.SetupSignalHandler()
|
||||
// start informer & controller
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
)
|
||||
|
||||
const policyWorkQueueName = "policyworkqueue"
|
||||
|
||||
const policyWorkQueueRetryLimit = 5
|
||||
const policyWorkQueueRetryLimit = 3
|
||||
|
||||
const policyControllerWorkerCount = 2
|
||||
|
||||
func concatFailedRules(frules []v1alpha1.FailedRule) string {
|
||||
var buffer bytes.Buffer
|
||||
for _, frule := range frules {
|
||||
buffer.WriteString(frule.Name + ";")
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
patchTypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/discovery/cached/memory"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
@ -40,7 +41,6 @@ func NewClient(config *rest.Config) (*Client, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kclient, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
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...)
|
||||
}
|
||||
|
||||
//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
|
||||
// Access items using []Items
|
||||
func (c *Client) ListResource(kind string, namespace string, lselector *meta.LabelSelector) (*unstructured.UnstructuredList, error) {
|
||||
|
|
|
@ -14,11 +14,11 @@ import (
|
|||
// 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 caller is responsible to apply the changes to the resource
|
||||
|
||||
// ProcessExisting checks for mutation and validation violations of existing resources
|
||||
func ProcessExisting(client *client.Client, policy *types.Policy) []*info.PolicyInfo {
|
||||
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 _, k := range rule.Kinds {
|
||||
|
@ -34,7 +34,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
|
|||
if rule.ResourceDescription.Namespace != nil {
|
||||
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 {
|
||||
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)
|
||||
|
@ -52,18 +52,19 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
|
|||
ri := &resourceInfo{resource: &res, gvk: &metav1.GroupVersionKind{Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind}}
|
||||
resources = append(resources, ri)
|
||||
|
||||
resourceMap[string(res.GetUID())] = ri
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
policyInfos := []*info.PolicyInfo{}
|
||||
// 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 {
|
||||
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)
|
||||
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) {
|
||||
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))
|
||||
rawResource, err := res.resource.MarshalJSON()
|
||||
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) {
|
||||
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)
|
||||
mergePatches := JoinPatches(patches)
|
||||
// merge the patches
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
// Mutate performs mutation. Overlay first and then mutation patches
|
||||
func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) {
|
||||
var allPatches [][]byte
|
||||
var err error
|
||||
patchedDocument := rawResource
|
||||
ris := []*info.RuleInfo{}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
// Apply the JSON patches from the rule to the resource
|
||||
rawResource, err = ApplyPatches(rawResource, 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...)
|
||||
}
|
||||
ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name)
|
||||
allPatches = append(allPatches, overlayPatches...)
|
||||
}
|
||||
}
|
||||
|
||||
// Process Patches
|
||||
if len(rule.Mutation.Patches) != 0 {
|
||||
rulePatches, errs := ProcessPatches(rule, rawResource)
|
||||
rulePatches, errs := ProcessPatches(rule, patchedDocument)
|
||||
if len(errs) > 0 {
|
||||
ri.Fail()
|
||||
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 {
|
||||
// Apply the JSON patches from the rule to the resource
|
||||
rawResource, err = ApplyPatches(rawResource, 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...)
|
||||
}
|
||||
ri.Addf("Rule %s: Patches succesfully applied.", rule.Name)
|
||||
allPatches = append(allPatches, rulePatches...)
|
||||
}
|
||||
}
|
||||
ris = append(ris, ri)
|
||||
|
|
|
@ -41,7 +41,7 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
|
|||
err := validateResourceWithPattern(resource, rule.Validation.Pattern)
|
||||
if err != nil {
|
||||
ri.Fail()
|
||||
ri.Addf("Rule %s: Validation has failed, err %s.", rule.Name, err)
|
||||
ri.Addf("validation has failed, err %v.", err)
|
||||
} else {
|
||||
ri.Addf("Rule %s: Validation succesfully.", rule.Name)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
@ -42,13 +41,12 @@ type Controller interface {
|
|||
func NewEventController(client *client.Client,
|
||||
shareInformer sharedinformer.PolicyInformer) Controller {
|
||||
|
||||
controller := &controller{
|
||||
return &controller{
|
||||
client: client,
|
||||
policyLister: shareInformer.GetLister(),
|
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), eventWorkQueueName),
|
||||
recorder: initRecorder(client),
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
func initRecorder(client *client.Client) record.EventRecorder {
|
||||
|
@ -93,40 +91,58 @@ func (c *controller) Stop() {
|
|||
defer c.queue.ShutDown()
|
||||
glog.Info("Shutting down eventbuilder controller workers")
|
||||
}
|
||||
|
||||
func (c *controller) runWorker() {
|
||||
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 {
|
||||
obj, shutdown := c.queue.Get()
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
|
||||
err := func(obj interface{}) error {
|
||||
defer c.queue.Done(obj)
|
||||
var key Info
|
||||
var ok bool
|
||||
|
||||
if key, ok = obj.(Info); !ok {
|
||||
c.queue.Forget(obj)
|
||||
glog.Warningf("Expecting type info by got %v\n", obj)
|
||||
return nil
|
||||
}
|
||||
// Run the syncHandler, passing the resource and the policy
|
||||
if err := c.SyncHandler(key); err != nil {
|
||||
c.queue.AddRateLimited(key)
|
||||
return fmt.Errorf("error syncing '%s' : %s, requeuing event creation request", key.Namespace+"/"+key.Name, err.Error())
|
||||
}
|
||||
err := c.syncHandler(key)
|
||||
c.handleErr(err, obj)
|
||||
return nil
|
||||
}(obj)
|
||||
|
||||
if err != nil {
|
||||
glog.Warning(err)
|
||||
glog.Error(err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *controller) SyncHandler(key Info) error {
|
||||
func (c *controller) syncHandler(key Info) error {
|
||||
var robj runtime.Object
|
||||
var err error
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ const eventWorkQueueName = "policy-controller-events"
|
|||
|
||||
const eventWorkerThreadCount = 1
|
||||
|
||||
const workQueueRetryLimit = 1
|
||||
|
||||
//Info defines the event details
|
||||
type Info struct {
|
||||
Kind string
|
||||
|
|
|
@ -59,7 +59,8 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) {
|
|||
policyInfo := info.NewPolicyInfo(p.Name,
|
||||
"Namespace",
|
||||
ns.Name,
|
||||
"") // Namespace has no namespace..WOW
|
||||
"",
|
||||
p.Spec.ValidationFailureAction) // Namespace has no namespace..WOW
|
||||
|
||||
ruleInfos := engine.GenerateNew(c.client, p, ns)
|
||||
policyInfo.AddRuleInfos(ruleInfos)
|
||||
|
@ -77,9 +78,7 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) {
|
|||
|
||||
if onViolation {
|
||||
glog.Infof("Adding violation for generation rule of policy %s\n", policyInfo.Name)
|
||||
|
||||
v := violation.NewViolation(event.PolicyViolation, policyInfo.Name, policyInfo.RKind, policyInfo.RName,
|
||||
policyInfo.RNamespace, msg)
|
||||
v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules())
|
||||
c.violationBuilder.Add(v)
|
||||
} else {
|
||||
eventInfo = event.NewEvent(policyKind, "", policyInfo.Name, event.RequestBlocked,
|
||||
|
|
|
@ -3,6 +3,8 @@ package info
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
)
|
||||
|
||||
//PolicyInfo defines policy information
|
||||
|
@ -16,25 +18,68 @@ type PolicyInfo struct {
|
|||
// Namespace is the ns of resource
|
||||
// empty on non-namespaced resources
|
||||
RNamespace string
|
||||
Rules []*RuleInfo
|
||||
success bool
|
||||
//TODO: add check/enum for types
|
||||
ValidationFailureAction string // BlockChanges, ReportViolation
|
||||
Rules []*RuleInfo
|
||||
success bool
|
||||
}
|
||||
|
||||
//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{
|
||||
Name: policyName,
|
||||
RKind: rKind,
|
||||
RName: rName,
|
||||
RNamespace: rNamespace,
|
||||
success: true, // fail to be set explicity
|
||||
Name: policyName,
|
||||
RKind: rKind,
|
||||
RName: rName,
|
||||
RNamespace: rNamespace,
|
||||
success: true, // fail to be set explicity
|
||||
ValidationFailureAction: validationFailureAction,
|
||||
}
|
||||
}
|
||||
|
||||
//IsSuccessful checks if policy is succesful
|
||||
// the policy is set to fail, if any of the rules have failed
|
||||
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
|
||||
|
@ -73,12 +118,18 @@ type RuleInfo struct {
|
|||
}
|
||||
|
||||
//ToString reule information
|
||||
//TODO: check if this is needed
|
||||
func (ri *RuleInfo) ToString() string {
|
||||
str := "rulename: " + ri.Name
|
||||
msgs := strings.Join(ri.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
|
||||
func NewRuleInfo(ruleName string, ruleType RuleType) *RuleInfo {
|
||||
return &RuleInfo{
|
||||
|
@ -121,6 +172,9 @@ func RulesSuccesfuly(rules []*RuleInfo) bool {
|
|||
|
||||
//AddRuleInfos sets the rule information
|
||||
func (pi *PolicyInfo) AddRuleInfos(rules []*RuleInfo) {
|
||||
if rules == nil {
|
||||
return
|
||||
}
|
||||
if !RulesSuccesfuly(rules) {
|
||||
pi.success = false
|
||||
}
|
||||
|
|
|
@ -103,7 +103,8 @@ func applyPolicyOnRaw(policy *kubepolicy.Policy, rawResource []byte, gvk *metav1
|
|||
policyInfo := info.NewPolicyInfo(policy.Name,
|
||||
gvk.Kind,
|
||||
rname,
|
||||
rns)
|
||||
rns,
|
||||
policy.Spec.ValidationFailureAction)
|
||||
|
||||
// Process Mutation
|
||||
patches, ruleInfos := engine.Mutate(*policy, rawResource, *gvk)
|
||||
|
|
|
@ -80,7 +80,7 @@ func scanDir(dir string) ([]string, error) {
|
|||
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
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)
|
||||
|
|
|
@ -173,7 +173,8 @@ func (t *test) applyPolicy(policy *pt.Policy,
|
|||
policyInfo := info.NewPolicyInfo(policy.Name,
|
||||
rkind,
|
||||
rname,
|
||||
rns)
|
||||
rns,
|
||||
policy.Spec.ValidationFailureAction)
|
||||
// Apply Mutation Rules
|
||||
patches, ruleInfos := engine.Mutate(*policy, rawResource, *tresource.gvk)
|
||||
policyInfo.AddRuleInfos(ruleInfos)
|
||||
|
|
|
@ -2,24 +2,26 @@ package violation
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/golang/glog"
|
||||
types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
v1alpha1 "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
event "github.com/nirmata/kyverno/pkg/event"
|
||||
"github.com/nirmata/kyverno/pkg/info"
|
||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
//Generator to generate policy violation
|
||||
type Generator interface {
|
||||
Add(infos ...*Info) error
|
||||
RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error
|
||||
ResourceRemoval(policy, rKind, rNs, rName string) error
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
client *client.Client
|
||||
policyLister v1alpha1.PolicyLister
|
||||
policyLister lister.PolicyLister
|
||||
eventBuilder event.Generator
|
||||
}
|
||||
|
||||
|
@ -27,7 +29,6 @@ type builder struct {
|
|||
type Builder interface {
|
||||
Generator
|
||||
processViolation(info *Info) error
|
||||
isActive(kind string, rname string, rnamespace string) (bool, error)
|
||||
}
|
||||
|
||||
//NewPolicyViolationBuilder returns new violation builder
|
||||
|
@ -43,7 +44,24 @@ func NewPolicyViolationBuilder(client *client.Client,
|
|||
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 {
|
||||
if infos == nil {
|
||||
return nil
|
||||
}
|
||||
for _, info := range infos {
|
||||
return b.processViolation(info)
|
||||
}
|
||||
|
@ -51,96 +69,203 @@ func (b *builder) Add(infos ...*Info) error {
|
|||
}
|
||||
|
||||
func (b *builder) processViolation(info *Info) error {
|
||||
currentViolations := []interface{}{}
|
||||
statusMap := map[string]interface{}{}
|
||||
var ok bool
|
||||
//TODO: hack get from client
|
||||
p1, err := b.client.GetResource("Policy", "", info.Policy, "status")
|
||||
violationsMap := map[string]interface{}{}
|
||||
violationMap := map[string]interface{}{}
|
||||
var violations interface{}
|
||||
var violation interface{}
|
||||
// Get Policy
|
||||
obj, err := b.client.GetResource("Policy", "", info.Policy, "status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unstr := p1.UnstructuredContent()
|
||||
// check if "status" field exists
|
||||
unstr := obj.UnstructuredContent()
|
||||
// get "status" subresource
|
||||
status, ok := unstr["status"]
|
||||
if ok {
|
||||
// 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")
|
||||
}
|
||||
violations, ok := statusMap["violations"]
|
||||
// get policy violations
|
||||
violations, ok = statusMap["violations"]
|
||||
if !ok {
|
||||
glog.Info("violation not present")
|
||||
return nil
|
||||
}
|
||||
glog.Info(reflect.TypeOf(violations))
|
||||
if currentViolations, ok = violations.([]interface{}); !ok {
|
||||
return errors.New("Unable to parse violations")
|
||||
violationsMap, ok = violations.(map[string]interface{})
|
||||
if !ok {
|
||||
return errors.New("Unable to get status.violations subresource")
|
||||
}
|
||||
}
|
||||
newViolation := info.Violation
|
||||
for _, violation := range currentViolations {
|
||||
glog.Info(reflect.TypeOf(violation))
|
||||
if v, ok := violation.(map[string]interface{}); ok {
|
||||
if name, ok := v["name"].(string); ok {
|
||||
if namespace, ok := v["namespace"].(string); ok {
|
||||
ok, err := b.isActive(info.Kind, name, namespace)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
//TODO remove the violation as it corresponds to resource that does not exist
|
||||
glog.Info("removed violation")
|
||||
}
|
||||
}
|
||||
// check if the resource has a violation
|
||||
violation, ok = violationsMap[info.getKey()]
|
||||
if !ok {
|
||||
// add resource violation
|
||||
violationsMap[info.getKey()] = info.Violation
|
||||
statusMap["violations"] = violationsMap
|
||||
unstr["status"] = statusMap
|
||||
} else {
|
||||
violationMap, ok = violation.(map[string]interface{})
|
||||
if !ok {
|
||||
return errors.New("Unable to get status.violations.violation subresource")
|
||||
}
|
||||
// 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
|
||||
// set the updated status
|
||||
statusMap["violations"] = currentViolations
|
||||
unstr["status"] = statusMap
|
||||
p1.SetUnstructuredContent(unstr)
|
||||
_, err = b.client.UpdateStatusResource("policies", "", p1, false)
|
||||
|
||||
obj.SetUnstructuredContent(unstr)
|
||||
// update the status sub-resource for policy
|
||||
_, err = b.client.UpdateStatusResource("Policy", "", obj, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builder) isActive(kind, rname, rnamespace string) (bool, error) {
|
||||
// Generate Merge Patch
|
||||
_, err := b.client.GetResource(b.client.DiscoveryClient.GetGVRFromKind(kind).Resource, rnamespace, rname)
|
||||
//RemoveInactiveViolation
|
||||
func (b *builder) RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error {
|
||||
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 {
|
||||
glog.Errorf("unable to get resource %s/%s ", rnamespace, rname)
|
||||
return false, err
|
||||
return 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
|
||||
func NewViolation(reason event.Reason, policyName, kind, rname, rnamespace, msg string) *Info {
|
||||
return &Info{Policy: policyName,
|
||||
Violation: types.Violation{
|
||||
Kind: kind,
|
||||
Name: rname,
|
||||
Namespace: rnamespace,
|
||||
Reason: reason.String(),
|
||||
Message: msg,
|
||||
},
|
||||
// ResourceRemoval on resources reoval we remove the policy violation in the policy
|
||||
func (b *builder) ResourceRemoval(policy, rKind, rNs, rName string) error {
|
||||
statusMap := map[string]interface{}{}
|
||||
violationsMap := map[string]interface{}{}
|
||||
var violations interface{}
|
||||
// Get Policy
|
||||
obj, err := b.client.GetResource("Policy", "", policy, "status")
|
||||
if err != nil {
|
||||
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
|
||||
func NewViolationFromEvent(e *event.Info, pName, rKind, rName, rnamespace string) *Info {
|
||||
return &Info{
|
||||
Policy: pName,
|
||||
Violation: types.Violation{
|
||||
Kind: rKind,
|
||||
Name: rName,
|
||||
Namespace: rnamespace,
|
||||
Reason: e.Reason,
|
||||
Message: e.Message,
|
||||
},
|
||||
// check if the resource has a violation
|
||||
_, ok = violationsMap[BuildKey(rKind, rNs, rName)]
|
||||
if !ok {
|
||||
// no violation for this resource
|
||||
return nil
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -11,8 +11,17 @@ const workqueueViolationName = "Policy-Violations"
|
|||
// Event Reason
|
||||
const violationEventResrouce = "Violation"
|
||||
|
||||
//ViolationInfo describes the policyviolation details
|
||||
//Info describes the policyviolation details
|
||||
type Info struct {
|
||||
Policy string
|
||||
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
|
||||
}
|
||||
|
|
40
pkg/webhooks/deleteresource.go
Normal file
40
pkg/webhooks/deleteresource.go
Normal 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
113
pkg/webhooks/mutation.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
45
pkg/webhooks/policyvalidation.go
Normal file
45
pkg/webhooks/policyvalidation.go
Normal 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,
|
||||
}
|
||||
}
|
|
@ -139,7 +139,8 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati
|
|||
constructWebhook(
|
||||
config.MutatingWebhookName,
|
||||
config.MutatingWebhookServicePath,
|
||||
caData),
|
||||
caData,
|
||||
false),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -157,7 +158,8 @@ func (wrc *WebhookRegistrationClient) contructDebugMutatingWebhookConfig(caData
|
|||
constructDebugWebhook(
|
||||
config.MutatingWebhookName,
|
||||
url,
|
||||
caData),
|
||||
caData,
|
||||
false),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +192,8 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura
|
|||
constructWebhook(
|
||||
config.ValidatingWebhookName,
|
||||
config.ValidatingWebhookServicePath,
|
||||
caData),
|
||||
caData,
|
||||
true),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -208,7 +211,8 @@ func (wrc *WebhookRegistrationClient) contructDebugValidatingWebhookConfig(caDat
|
|||
constructDebugWebhook(
|
||||
config.ValidatingWebhookName,
|
||||
url,
|
||||
caData),
|
||||
caData,
|
||||
true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +245,8 @@ func (wrc *WebhookRegistrationClient) contructPolicyValidatingWebhookConfig() (*
|
|||
constructWebhook(
|
||||
config.PolicyValidatingWebhookName,
|
||||
config.PolicyValidatingWebhookServicePath,
|
||||
caData),
|
||||
caData,
|
||||
true),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -259,12 +264,13 @@ func (wrc *WebhookRegistrationClient) contructDebugPolicyValidatingWebhookConfig
|
|||
constructDebugWebhook(
|
||||
config.PolicyValidatingWebhookName,
|
||||
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 := "*/*"
|
||||
apiGroups := "*"
|
||||
apiversions := "*"
|
||||
|
@ -273,6 +279,15 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
|
|||
apiGroups = "kyverno.io"
|
||||
apiversions = "v1alpha1"
|
||||
}
|
||||
operationtypes := []admregapi.OperationType{
|
||||
admregapi.Create,
|
||||
admregapi.Update,
|
||||
}
|
||||
// Add operation DELETE for validation
|
||||
if validation {
|
||||
operationtypes = append(operationtypes, admregapi.Delete)
|
||||
|
||||
}
|
||||
|
||||
return admregapi.Webhook{
|
||||
Name: name,
|
||||
|
@ -286,10 +301,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
|
|||
},
|
||||
Rules: []admregapi.RuleWithOperations{
|
||||
admregapi.RuleWithOperations{
|
||||
Operations: []admregapi.OperationType{
|
||||
admregapi.Create,
|
||||
admregapi.Update,
|
||||
},
|
||||
Operations: operationtypes,
|
||||
Rule: admregapi.Rule{
|
||||
APIGroups: []string{
|
||||
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 := "*/*"
|
||||
apiGroups := "*"
|
||||
apiversions := "*"
|
||||
|
@ -316,6 +328,14 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
|
|||
apiGroups = "kyverno.io"
|
||||
apiversions = "v1alpha1"
|
||||
}
|
||||
operationtypes := []admregapi.OperationType{
|
||||
admregapi.Create,
|
||||
admregapi.Update,
|
||||
}
|
||||
// Add operation DELETE for validation
|
||||
if validation {
|
||||
operationtypes = append(operationtypes, admregapi.Delete)
|
||||
}
|
||||
|
||||
return admregapi.Webhook{
|
||||
Name: name,
|
||||
|
@ -325,10 +345,7 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
|
|||
},
|
||||
Rules: []admregapi.RuleWithOperations{
|
||||
admregapi.RuleWithOperations{
|
||||
Operations: []admregapi.OperationType{
|
||||
admregapi.Create,
|
||||
admregapi.Update,
|
||||
},
|
||||
Operations: operationtypes,
|
||||
Rule: admregapi.Rule{
|
||||
APIGroups: []string{
|
||||
apiGroups,
|
||||
|
|
77
pkg/webhooks/report.go
Normal file
77
pkg/webhooks/report.go
Normal 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
|
||||
}
|
|
@ -8,35 +8,30 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/config"
|
||||
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/info"
|
||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||
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"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
const policyKind = "Policy"
|
||||
|
||||
// WebhookServer contains configured TLS server with MutationWebhook.
|
||||
// MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient.
|
||||
type WebhookServer struct {
|
||||
server http.Server
|
||||
client *client.Client
|
||||
policyLister v1alpha1.PolicyLister
|
||||
eventController event.Generator
|
||||
filterKinds []string
|
||||
server http.Server
|
||||
client *client.Client
|
||||
policyLister v1alpha1.PolicyLister
|
||||
eventController event.Generator
|
||||
violationBuilder violation.Generator
|
||||
annotationsController annotations.Controller
|
||||
filterKinds []string
|
||||
}
|
||||
|
||||
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
|
||||
|
@ -46,6 +41,8 @@ func NewWebhookServer(
|
|||
tlsPair *tlsutils.TlsPemPair,
|
||||
shareInformer sharedinformer.PolicyInformer,
|
||||
eventController event.Generator,
|
||||
violationBuilder violation.Generator,
|
||||
annotationsController annotations.Controller,
|
||||
filterKinds []string) (*WebhookServer, error) {
|
||||
|
||||
if tlsPair == nil {
|
||||
|
@ -60,10 +57,12 @@ func NewWebhookServer(
|
|||
tlsConfig.Certificates = []tls.Certificate{pair}
|
||||
|
||||
ws := &WebhookServer{
|
||||
client: client,
|
||||
policyLister: shareInformer.GetLister(),
|
||||
eventController: eventController,
|
||||
filterKinds: parseKinds(filterKinds),
|
||||
client: client,
|
||||
policyLister: shareInformer.GetLister(),
|
||||
eventController: eventController,
|
||||
violationBuilder: violationBuilder,
|
||||
annotationsController: annotationsController,
|
||||
filterKinds: parseKinds(filterKinds),
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
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
|
||||
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
|
||||
// Answers to the http.ResponseWriter if request is not valid
|
||||
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
|
||||
}
|
||||
|
||||
//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, ",")
|
||||
}
|
||||
|
|
|
@ -1,11 +1,34 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"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
|
||||
func StringInSlice(kind string, list []string) bool {
|
||||
for _, b := range list {
|
||||
|
@ -63,3 +86,50 @@ func getApplicableKindsForPolicy(p *v1alpha1.Policy) []string {
|
|||
}
|
||||
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
126
pkg/webhooks/validation.go
Normal 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
|
||||
}
|
|
@ -28,7 +28,7 @@ spec:
|
|||
message: "This SS is broken"
|
||||
pattern:
|
||||
spec:
|
||||
replicas: ">20"
|
||||
replicas: ">2"
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: www
|
||||
|
|
Loading…
Add table
Reference in a new issue