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:
|
required:
|
||||||
- rules
|
- rules
|
||||||
properties:
|
properties:
|
||||||
|
# default values to be handled by user
|
||||||
|
validationFailureAction:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- block
|
||||||
|
- report
|
||||||
rules:
|
rules:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|
|
@ -22,6 +22,12 @@ spec:
|
||||||
required:
|
required:
|
||||||
- rules
|
- rules
|
||||||
properties:
|
properties:
|
||||||
|
# default values to be handled by user
|
||||||
|
validationFailureAction:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- block
|
||||||
|
- report
|
||||||
rules:
|
rules:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|
|
@ -20,4 +20,4 @@ spec:
|
||||||
image: nginx:1.7.9
|
image: nginx:1.7.9
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
|
@ -35,4 +35,3 @@ spec :
|
||||||
containers:
|
containers:
|
||||||
- (image): "nginx*"
|
- (image): "nginx*"
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
|
|
||||||
|
|
|
@ -5,37 +5,37 @@ metadata:
|
||||||
spec:
|
spec:
|
||||||
validationFailureAction: "report"
|
validationFailureAction: "report"
|
||||||
rules:
|
rules:
|
||||||
- name: check-cpu-memory-limits
|
- name: add-memory-limit
|
||||||
resource:
|
resource:
|
||||||
kinds:
|
kinds:
|
||||||
- Deployment
|
- Deployment
|
||||||
validate:
|
mutate:
|
||||||
message: "Resource limits are required for CPU and memory"
|
overlay:
|
||||||
pattern:
|
spec:
|
||||||
spec:
|
template:
|
||||||
template:
|
spec:
|
||||||
spec:
|
containers:
|
||||||
containers:
|
# the wildcard * will match all containers in the list
|
||||||
# match all contianers
|
- (name): "*"
|
||||||
- (name): "*"
|
resources:
|
||||||
resources:
|
limits:
|
||||||
limits:
|
# add memory limit if it is not exist
|
||||||
# cpu and memory are required
|
"+(memory)": "300Mi"
|
||||||
memory: "?*"
|
- name: check-cpu-memory-limits
|
||||||
cpu: "?*"
|
resource:
|
||||||
# - name: add-memory-limit
|
kinds:
|
||||||
# resource:
|
- Deployment
|
||||||
# kinds:
|
validate:
|
||||||
# - Deployment
|
message: "Resource limits are required for CPU and memory"
|
||||||
# mutate:
|
pattern:
|
||||||
# overlay:
|
spec:
|
||||||
# spec:
|
template:
|
||||||
# template:
|
spec:
|
||||||
# spec:
|
containers:
|
||||||
# containers:
|
# match all contianers
|
||||||
# # the wildcard * will match all containers in the list
|
- (name): "*"
|
||||||
# - (name): "*"
|
resources:
|
||||||
# resources:
|
limits:
|
||||||
# limits:
|
# cpu and memory are required
|
||||||
# # add memory limit if it is not exist
|
memory: "?*"
|
||||||
# "+(memory)": "300Mi"
|
cpu: "?*"
|
|
@ -22,4 +22,4 @@ spec:
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
- name: ghost
|
- name: ghost
|
||||||
image: ghost:latest
|
image: ghost:latest
|
|
@ -16,4 +16,4 @@ spec:
|
||||||
containers:
|
containers:
|
||||||
# if the image tag is latest, set the imagePullPolicy to Always
|
# if the image tag is latest, set the imagePullPolicy to Always
|
||||||
- (image): "*:latest"
|
- (image): "*:latest"
|
||||||
imagePullPolicy: "IfNotPresent"
|
imagePullPolicy: "IfNotPresent"
|
|
@ -10,4 +10,4 @@ subsets:
|
||||||
ports:
|
ports:
|
||||||
- name: secure-connection
|
- name: secure-connection
|
||||||
port: 443
|
port: 443
|
||||||
protocol: TCP
|
protocol: TCP
|
19
main.go
19
main.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
|
"github.com/nirmata/kyverno/pkg/annotations"
|
||||||
"github.com/nirmata/kyverno/pkg/config"
|
"github.com/nirmata/kyverno/pkg/config"
|
||||||
controller "github.com/nirmata/kyverno/pkg/controller"
|
controller "github.com/nirmata/kyverno/pkg/controller"
|
||||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||||
|
@ -24,7 +25,6 @@ var (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer glog.Flush()
|
defer glog.Flush()
|
||||||
|
|
||||||
printVersionInfo()
|
printVersionInfo()
|
||||||
clientConfig, err := createClientConfig(kubeconfig)
|
clientConfig, err := createClientConfig(kubeconfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -43,19 +43,20 @@ func main() {
|
||||||
kubeInformer := utils.NewKubeInformerFactory(clientConfig)
|
kubeInformer := utils.NewKubeInformerFactory(clientConfig)
|
||||||
eventController := event.NewEventController(client, policyInformerFactory)
|
eventController := event.NewEventController(client, policyInformerFactory)
|
||||||
violationBuilder := violation.NewPolicyViolationBuilder(client, policyInformerFactory, eventController)
|
violationBuilder := violation.NewPolicyViolationBuilder(client, policyInformerFactory, eventController)
|
||||||
|
annotationsController := annotations.NewAnnotationControler(client)
|
||||||
policyController := controller.NewPolicyController(
|
policyController := controller.NewPolicyController(
|
||||||
client,
|
client,
|
||||||
policyInformerFactory,
|
policyInformerFactory,
|
||||||
violationBuilder,
|
violationBuilder,
|
||||||
eventController)
|
eventController,
|
||||||
|
annotationsController)
|
||||||
|
|
||||||
genControler := gencontroller.NewGenController(client, eventController, policyInformerFactory, violationBuilder, kubeInformer.Core().V1().Namespaces())
|
genControler := gencontroller.NewGenController(client, eventController, policyInformerFactory, violationBuilder, kubeInformer.Core().V1().Namespaces())
|
||||||
tlsPair, err := initTLSPemPair(clientConfig, client)
|
tlsPair, err := initTLSPemPair(clientConfig, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
|
glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
|
||||||
}
|
}
|
||||||
server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, eventController, filterK8Kinds)
|
server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, eventController, violationBuilder, annotationsController, filterK8Kinds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("Unable to create webhook server: %v\n", err)
|
glog.Fatalf("Unable to create webhook server: %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -67,23 +68,25 @@ func main() {
|
||||||
|
|
||||||
stopCh := signals.SetupSignalHandler()
|
stopCh := signals.SetupSignalHandler()
|
||||||
|
|
||||||
|
if err = webhookRegistrationClient.Register(); err != nil {
|
||||||
|
glog.Fatalf("Failed registering Admission Webhooks: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
policyInformerFactory.Run(stopCh)
|
policyInformerFactory.Run(stopCh)
|
||||||
kubeInformer.Start(stopCh)
|
kubeInformer.Start(stopCh)
|
||||||
eventController.Run(stopCh)
|
eventController.Run(stopCh)
|
||||||
genControler.Run(stopCh)
|
genControler.Run(stopCh)
|
||||||
|
annotationsController.Run(stopCh)
|
||||||
if err = policyController.Run(stopCh); err != nil {
|
if err = policyController.Run(stopCh); err != nil {
|
||||||
glog.Fatalf("Error running PolicyController: %v\n", err)
|
glog.Fatalf("Error running PolicyController: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = webhookRegistrationClient.Register(); err != nil {
|
|
||||||
glog.Fatalf("Failed registering Admission Webhooks: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server.RunAsync()
|
server.RunAsync()
|
||||||
<-stopCh
|
<-stopCh
|
||||||
server.Stop()
|
server.Stop()
|
||||||
genControler.Stop()
|
genControler.Stop()
|
||||||
eventController.Stop()
|
eventController.Stop()
|
||||||
|
annotationsController.Stop()
|
||||||
policyController.Stop()
|
policyController.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
|
// Spec describes policy behavior by its rules
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Rules []Rule `json:"rules"`
|
Rules []Rule `json:"rules"`
|
||||||
|
ValidationFailureAction string `json:"validationFailureAction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule is set of mutation, validation and generation actions
|
// Rule is set of mutation, validation and generation actions
|
||||||
|
@ -77,16 +78,24 @@ type CloneFrom struct {
|
||||||
|
|
||||||
// Status contains violations for existing resources
|
// Status contains violations for existing resources
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Violations []Violation `json:"violations,omitempty"`
|
// Violations map[kind/namespace/resource]Violation
|
||||||
|
Violations map[string]Violation `json:"violations,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Violation for the policy
|
// Violation for the policy
|
||||||
type Violation struct {
|
type Violation struct {
|
||||||
Kind string `json:"kind,omitempty"`
|
Kind string `json:"kind,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Namespace string `json:"namespace,omitempty"`
|
Namespace string `json:"namespace,omitempty"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Rules []FailedRule `json:"rules"`
|
||||||
Message string `json:"message,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailedRule stored info and type of failed rules
|
||||||
|
type FailedRule struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"` //Mutation, Validation, Genertaion
|
||||||
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
|
@ -104,3 +104,44 @@ func (in *Generation) DeepCopyInto(out *Generation) {
|
||||||
*out = *in
|
*out = *in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return true -> if there were any removals
|
||||||
|
// return false -> if it looks the same
|
||||||
|
func (v *Violation) RemoveRulesOfType(ruleType string) bool {
|
||||||
|
removed := false
|
||||||
|
updatedRules := []FailedRule{}
|
||||||
|
for _, r := range v.Rules {
|
||||||
|
if r.Type == ruleType {
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updatedRules = append(updatedRules, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if removed {
|
||||||
|
v.Rules = updatedRules
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//IsEqual Check if violatiosn are equal
|
||||||
|
func (v *Violation) IsEqual(nv Violation) bool {
|
||||||
|
// We do not need to compare resource info as it will be same
|
||||||
|
// Reason
|
||||||
|
if v.Reason != nv.Reason {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Rule
|
||||||
|
if len(v.Rules) != len(nv.Rules) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// assumes the rules will be in order, as the rule are proceeed in order
|
||||||
|
// if the rule order changes, it means the policy has changed.. as it will afffect the order in which mutation rules are applied
|
||||||
|
for i, r := range v.Rules {
|
||||||
|
if r != nv.Rules[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,22 @@ func (in *CloneFrom) DeepCopy() *CloneFrom {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *FailedRule) DeepCopyInto(out *FailedRule) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FailedRule.
|
||||||
|
func (in *FailedRule) DeepCopy() *FailedRule {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(FailedRule)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generation.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generation.
|
||||||
func (in *Generation) DeepCopy() *Generation {
|
func (in *Generation) DeepCopy() *Generation {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
|
@ -135,6 +151,11 @@ func (in *ResourceDescription) DeepCopyInto(out *ResourceDescription) {
|
||||||
*out = new(string)
|
*out = new(string)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.Namespace != nil {
|
||||||
|
in, out := &in.Namespace, &out.Namespace
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
if in.Selector != nil {
|
if in.Selector != nil {
|
||||||
in, out := &in.Selector, &out.Selector
|
in, out := &in.Selector, &out.Selector
|
||||||
*out = new(v1.LabelSelector)
|
*out = new(v1.LabelSelector)
|
||||||
|
@ -210,8 +231,10 @@ func (in *Status) DeepCopyInto(out *Status) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.Violations != nil {
|
if in.Violations != nil {
|
||||||
in, out := &in.Violations, &out.Violations
|
in, out := &in.Violations, &out.Violations
|
||||||
*out = make([]Violation, len(*in))
|
*out = make(map[string]Violation, len(*in))
|
||||||
copy(*out, *in)
|
for key, val := range *in {
|
||||||
|
(*out)[key] = *val.DeepCopy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -239,6 +262,11 @@ func (in *Validation) DeepCopy() *Validation {
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *Violation) DeepCopyInto(out *Violation) {
|
func (in *Violation) DeepCopyInto(out *Violation) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.Rules != nil {
|
||||||
|
in, out := &in.Rules, &out.Rules
|
||||||
|
*out = make([]FailedRule, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
77
pkg/controller/cleanup.go
Normal file
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
|
|
||||||
|
"github.com/nirmata/kyverno/pkg/annotations"
|
||||||
"github.com/nirmata/kyverno/pkg/info"
|
"github.com/nirmata/kyverno/pkg/info"
|
||||||
|
|
||||||
"github.com/nirmata/kyverno/pkg/engine"
|
"github.com/nirmata/kyverno/pkg/engine"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||||
lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||||
"github.com/nirmata/kyverno/pkg/event"
|
"github.com/nirmata/kyverno/pkg/event"
|
||||||
|
@ -27,27 +29,30 @@ import (
|
||||||
|
|
||||||
//PolicyController to manage Policy CRD
|
//PolicyController to manage Policy CRD
|
||||||
type PolicyController struct {
|
type PolicyController struct {
|
||||||
client *client.Client
|
client *client.Client
|
||||||
policyLister lister.PolicyLister
|
policyLister lister.PolicyLister
|
||||||
policySynced cache.InformerSynced
|
policySynced cache.InformerSynced
|
||||||
violationBuilder violation.Generator
|
violationBuilder violation.Generator
|
||||||
eventController event.Generator
|
eventController event.Generator
|
||||||
queue workqueue.RateLimitingInterface
|
annotationsController annotations.Controller
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPolicyController from cmd args
|
// NewPolicyController from cmd args
|
||||||
func NewPolicyController(client *client.Client,
|
func NewPolicyController(client *client.Client,
|
||||||
policyInformer sharedinformer.PolicyInformer,
|
policyInformer sharedinformer.PolicyInformer,
|
||||||
violationBuilder violation.Generator,
|
violationBuilder violation.Generator,
|
||||||
eventController event.Generator) *PolicyController {
|
eventController event.Generator,
|
||||||
|
annotationsController annotations.Controller) *PolicyController {
|
||||||
|
|
||||||
controller := &PolicyController{
|
controller := &PolicyController{
|
||||||
client: client,
|
client: client,
|
||||||
policyLister: policyInformer.GetLister(),
|
policyLister: policyInformer.GetLister(),
|
||||||
policySynced: policyInformer.GetInfomer().HasSynced,
|
policySynced: policyInformer.GetInfomer().HasSynced,
|
||||||
violationBuilder: violationBuilder,
|
violationBuilder: violationBuilder,
|
||||||
eventController: eventController,
|
eventController: eventController,
|
||||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName),
|
annotationsController: annotationsController,
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), policyWorkQueueName),
|
||||||
}
|
}
|
||||||
|
|
||||||
policyInformer.GetInfomer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
policyInformer.GetInfomer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
|
@ -63,13 +68,13 @@ func (pc *PolicyController) createPolicyHandler(resource interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) {
|
func (pc *PolicyController) updatePolicyHandler(oldResource, newResource interface{}) {
|
||||||
newPolicy := newResource.(*types.Policy)
|
newPolicy := newResource.(*v1alpha1.Policy)
|
||||||
oldPolicy := oldResource.(*types.Policy)
|
oldPolicy := oldResource.(*v1alpha1.Policy)
|
||||||
newPolicy.Status = types.Status{}
|
newPolicy.Status = v1alpha1.Status{}
|
||||||
oldPolicy.Status = types.Status{}
|
oldPolicy.Status = v1alpha1.Status{}
|
||||||
newPolicy.ResourceVersion = ""
|
newPolicy.ResourceVersion = ""
|
||||||
oldPolicy.ResourceVersion = ""
|
oldPolicy.ResourceVersion = ""
|
||||||
if reflect.DeepEqual(newPolicy.ResourceVersion, oldPolicy.ResourceVersion) {
|
if reflect.DeepEqual(newPolicy, oldPolicy) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pc.enqueuePolicy(newResource)
|
pc.enqueuePolicy(newResource)
|
||||||
|
@ -82,6 +87,7 @@ func (pc *PolicyController) deletePolicyHandler(resource interface{}) {
|
||||||
glog.Error("error decoding object, invalid type")
|
glog.Error("error decoding object, invalid type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cleanAnnotations(pc.client, resource)
|
||||||
glog.Infof("policy deleted: %s", object.GetName())
|
glog.Infof("policy deleted: %s", object.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +161,7 @@ func (pc *PolicyController) handleErr(err error, key interface{}) {
|
||||||
}
|
}
|
||||||
pc.queue.Forget(key)
|
pc.queue.Forget(key)
|
||||||
glog.Error(err)
|
glog.Error(err)
|
||||||
glog.Warningf("Dropping the key %q out of the queue: %v", key, err)
|
glog.Warningf("Dropping the key out of the queue: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PolicyController) syncHandler(obj interface{}) error {
|
func (pc *PolicyController) syncHandler(obj interface{}) error {
|
||||||
|
@ -169,8 +175,7 @@ func (pc *PolicyController) syncHandler(obj interface{}) error {
|
||||||
glog.Errorf("invalid policy key: %s", key)
|
glog.Errorf("invalid policy key: %s", key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Get Policy
|
||||||
// Get Policy resource with namespace/name
|
|
||||||
policy, err := pc.policyLister.Get(name)
|
policy, err := pc.policyLister.Get(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
|
@ -179,40 +184,103 @@ func (pc *PolicyController) syncHandler(obj interface{}) error {
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// process policy on existing resource
|
|
||||||
// get the violations and pass to violation Builder
|
|
||||||
// get the events and pass to event Builder
|
|
||||||
//TODO: processPolicy
|
|
||||||
glog.Infof("process policy %s on existing resources", policy.GetName())
|
glog.Infof("process policy %s on existing resources", policy.GetName())
|
||||||
|
// Process policy on existing resources
|
||||||
policyInfos := engine.ProcessExisting(pc.client, policy)
|
policyInfos := engine.ProcessExisting(pc.client, policy)
|
||||||
events, violations := createEventsAndViolations(pc.eventController, policyInfos)
|
|
||||||
|
events, violations := pc.createEventsAndViolations(policyInfos)
|
||||||
|
// Events, Violations
|
||||||
pc.eventController.Add(events...)
|
pc.eventController.Add(events...)
|
||||||
err = pc.violationBuilder.Add(violations...)
|
err = pc.violationBuilder.Add(violations...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Error(err)
|
glog.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Annotations
|
||||||
|
pc.createAnnotations(policyInfos)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createEventsAndViolations(eventController event.Generator, policyInfos []*info.PolicyInfo) ([]*event.Info, []*violation.Info) {
|
func (pc *PolicyController) createAnnotations(policyInfos []*info.PolicyInfo) {
|
||||||
|
for _, pi := range policyInfos {
|
||||||
|
var patch []byte
|
||||||
|
//get resource
|
||||||
|
obj, err := pc.client.GetResource(pi.RKind, pi.RNamespace, pi.RName)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// add annotation for policy application
|
||||||
|
ann := obj.GetAnnotations()
|
||||||
|
// Mutation rules
|
||||||
|
ann, mpatch, err := annotations.AddPolicyJSONPatch(ann, pi, info.Mutation)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Validation rules
|
||||||
|
ann, vpatch, err := annotations.AddPolicyJSONPatch(ann, pi, info.Validation)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
}
|
||||||
|
if mpatch == nil && vpatch == nil {
|
||||||
|
//nothing to patch
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// merge the patches
|
||||||
|
if mpatch != nil && vpatch != nil {
|
||||||
|
patch, err = jsonpatch.MergePatch(mpatch, vpatch)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mpatch == nil {
|
||||||
|
patch = vpatch
|
||||||
|
} else {
|
||||||
|
patch = mpatch
|
||||||
|
}
|
||||||
|
// add the anotation to the resource
|
||||||
|
_, err = pc.client.PatchResource(pi.RKind, pi.RNamespace, pi.RName, patch)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PolicyController) createEventsAndViolations(policyInfos []*info.PolicyInfo) ([]*event.Info, []*violation.Info) {
|
||||||
events := []*event.Info{}
|
events := []*event.Info{}
|
||||||
violations := []*violation.Info{}
|
violations := []*violation.Info{}
|
||||||
// Create events from the policyInfo
|
// Create events from the policyInfo
|
||||||
for _, policyInfo := range policyInfos {
|
for _, policyInfo := range policyInfos {
|
||||||
fruleNames := []string{}
|
frules := []v1alpha1.FailedRule{}
|
||||||
sruleNames := []string{}
|
sruleNames := []string{}
|
||||||
|
|
||||||
for _, rule := range policyInfo.Rules {
|
for _, rule := range policyInfo.Rules {
|
||||||
if !rule.IsSuccessful() {
|
if !rule.IsSuccessful() {
|
||||||
e := &event.Info{}
|
e := &event.Info{}
|
||||||
fruleNames = append(fruleNames, rule.Name)
|
frule := v1alpha1.FailedRule{Name: rule.Name}
|
||||||
switch rule.RuleType {
|
switch rule.RuleType {
|
||||||
case info.Mutation, info.Validation, info.Generation:
|
case info.Mutation, info.Validation, info.Generation:
|
||||||
// Events
|
// Events
|
||||||
e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation, event.FProcessRule, rule.Name, policyInfo.Name)
|
e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation, event.FProcessRule, rule.Name, policyInfo.Name)
|
||||||
|
switch rule.RuleType {
|
||||||
|
case info.Mutation:
|
||||||
|
frule.Type = info.Mutation.String()
|
||||||
|
case info.Validation:
|
||||||
|
frule.Type = info.Validation.String()
|
||||||
|
case info.Generation:
|
||||||
|
frule.Type = info.Generation.String()
|
||||||
|
}
|
||||||
|
frule.Error = rule.GetErrorString()
|
||||||
default:
|
default:
|
||||||
glog.Info("Unsupported Rule type")
|
glog.Info("Unsupported Rule type")
|
||||||
}
|
}
|
||||||
|
frule.Error = rule.GetErrorString()
|
||||||
|
frules = append(frules, frule)
|
||||||
events = append(events, e)
|
events = append(events, e)
|
||||||
} else {
|
} else {
|
||||||
sruleNames = append(sruleNames, rule.Name)
|
sruleNames = append(sruleNames, rule.Name)
|
||||||
|
@ -220,22 +288,16 @@ func createEventsAndViolations(eventController event.Generator, policyInfos []*i
|
||||||
}
|
}
|
||||||
|
|
||||||
if !policyInfo.IsSuccessful() {
|
if !policyInfo.IsSuccessful() {
|
||||||
// Event
|
e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, concatFailedRules(frules))
|
||||||
// list of failed rules : ruleNames
|
|
||||||
e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyViolation, event.FResourcePolcy, policyInfo.RNamespace+"/"+policyInfo.RName, strings.Join(fruleNames, ";"))
|
|
||||||
events = append(events, e)
|
events = append(events, e)
|
||||||
// Violation
|
// Violation
|
||||||
v := violation.NewViolationFromEvent(e, policyInfo.Name, policyInfo.RKind, policyInfo.RName, policyInfo.RNamespace)
|
v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules())
|
||||||
violations = append(violations, v)
|
violations = append(violations, v)
|
||||||
|
} else {
|
||||||
|
// clean up violations
|
||||||
|
pc.violationBuilder.RemoveInactiveViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, info.Mutation)
|
||||||
|
pc.violationBuilder.RemoveInactiveViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, info.Validation)
|
||||||
}
|
}
|
||||||
// else {
|
|
||||||
// // Policy was processed succesfully
|
|
||||||
// e := event.NewEvent("Policy", "", policyInfo.Name, event.PolicyApplied, event.SPolicyApply, policyInfo.Name)
|
|
||||||
// events = append(events, e)
|
|
||||||
// // Policy applied succesfully on resource
|
|
||||||
// e = event.NewEvent(policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyApplied, event.SRuleApply, strings.Join(sruleNames, ";"), policyInfo.RName)
|
|
||||||
// events = append(events, e)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
return events, violations
|
return events, violations
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,8 @@ func (f *fixture) runControler(policyName string) {
|
||||||
f.Client,
|
f.Client,
|
||||||
policyInformerFactory,
|
policyInformerFactory,
|
||||||
violationBuilder,
|
violationBuilder,
|
||||||
eventController)
|
eventController,
|
||||||
|
nil)
|
||||||
|
|
||||||
stopCh := signals.SetupSignalHandler()
|
stopCh := signals.SetupSignalHandler()
|
||||||
// start informer & controller
|
// start informer & controller
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
const policyWorkQueueName = "policyworkqueue"
|
const policyWorkQueueName = "policyworkqueue"
|
||||||
|
|
||||||
const policyWorkQueueRetryLimit = 5
|
const policyWorkQueueRetryLimit = 3
|
||||||
|
|
||||||
const policyControllerWorkerCount = 2
|
const policyControllerWorkerCount = 2
|
||||||
|
|
||||||
|
func concatFailedRules(frules []v1alpha1.FailedRule) string {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, frule := range frules {
|
||||||
|
buffer.WriteString(frule.Name + ";")
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
patchTypes "k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/discovery"
|
"k8s.io/client-go/discovery"
|
||||||
"k8s.io/client-go/discovery/cached/memory"
|
"k8s.io/client-go/discovery/cached/memory"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
|
@ -40,7 +41,6 @@ func NewClient(config *rest.Config) (*Client, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
kclient, err := kubernetes.NewForConfig(config)
|
kclient, err := kubernetes.NewForConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -110,6 +110,11 @@ func (c *Client) GetResource(kind string, namespace string, name string, subreso
|
||||||
return c.getResourceInterface(kind, namespace).Get(name, meta.GetOptions{}, subresources...)
|
return c.getResourceInterface(kind, namespace).Get(name, meta.GetOptions{}, subresources...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Patch
|
||||||
|
func (c *Client) PatchResource(kind string, namespace string, name string, patch []byte) (*unstructured.Unstructured, error) {
|
||||||
|
return c.getResourceInterface(kind, namespace).Patch(name, patchTypes.JSONPatchType, patch, meta.PatchOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
// ListResource returns the list of resources in unstructured/json format
|
// ListResource returns the list of resources in unstructured/json format
|
||||||
// Access items using []Items
|
// Access items using []Items
|
||||||
func (c *Client) ListResource(kind string, namespace string, lselector *meta.LabelSelector) (*unstructured.UnstructuredList, error) {
|
func (c *Client) ListResource(kind string, namespace string, lselector *meta.LabelSelector) (*unstructured.UnstructuredList, error) {
|
||||||
|
|
|
@ -14,11 +14,11 @@ import (
|
||||||
// Instead we expose them as standalone functions passing the required atrributes
|
// Instead we expose them as standalone functions passing the required atrributes
|
||||||
// The each function returns the changes that need to be applied on the resource
|
// The each function returns the changes that need to be applied on the resource
|
||||||
// the caller is responsible to apply the changes to the resource
|
// the caller is responsible to apply the changes to the resource
|
||||||
|
|
||||||
// ProcessExisting checks for mutation and validation violations of existing resources
|
// ProcessExisting checks for mutation and validation violations of existing resources
|
||||||
func ProcessExisting(client *client.Client, policy *types.Policy) []*info.PolicyInfo {
|
func ProcessExisting(client *client.Client, policy *types.Policy) []*info.PolicyInfo {
|
||||||
glog.Infof("Applying policy %s on existing resources", policy.Name)
|
glog.Infof("Applying policy %s on existing resources", policy.Name)
|
||||||
resources := []*resourceInfo{}
|
// key uid
|
||||||
|
resourceMap := map[string]*resourceInfo{}
|
||||||
|
|
||||||
for _, rule := range policy.Spec.Rules {
|
for _, rule := range policy.Spec.Rules {
|
||||||
for _, k := range rule.Kinds {
|
for _, k := range rule.Kinds {
|
||||||
|
@ -34,7 +34,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
|
||||||
if rule.ResourceDescription.Namespace != nil {
|
if rule.ResourceDescription.Namespace != nil {
|
||||||
namespace = *rule.ResourceDescription.Namespace
|
namespace = *rule.ResourceDescription.Namespace
|
||||||
}
|
}
|
||||||
list, err := client.ListResource(gvr.Resource, namespace, rule.ResourceDescription.Selector)
|
list, err := client.ListResource(k, namespace, rule.ResourceDescription.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String())
|
glog.Errorf("unable to list resource for %s with label selector %s", gvr.Resource, rule.Selector.String())
|
||||||
glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err)
|
glog.Errorf("unable to apply policy %s rule %s. err: %s", policy.Name, rule.Name, err)
|
||||||
|
@ -52,18 +52,19 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
|
||||||
ri := &resourceInfo{resource: &res, gvk: &metav1.GroupVersionKind{Group: gvk.Group,
|
ri := &resourceInfo{resource: &res, gvk: &metav1.GroupVersionKind{Group: gvk.Group,
|
||||||
Version: gvk.Version,
|
Version: gvk.Version,
|
||||||
Kind: gvk.Kind}}
|
Kind: gvk.Kind}}
|
||||||
resources = append(resources, ri)
|
|
||||||
|
resourceMap[string(res.GetUID())] = ri
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
policyInfos := []*info.PolicyInfo{}
|
policyInfos := []*info.PolicyInfo{}
|
||||||
// for the filtered resource apply policy
|
// for the filtered resource apply policy
|
||||||
for _, r := range resources {
|
for _, v := range resourceMap {
|
||||||
|
|
||||||
policyInfo, err := applyPolicy(client, policy, r)
|
policyInfo, err := applyPolicy(client, policy, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("unable to apply policy %s on resource %s/%s", policy.Name, r.resource.GetName(), r.resource.GetNamespace())
|
glog.Errorf("unable to apply policy %s on resource %s/%s", policy.Name, v.resource.GetName(), v.resource.GetNamespace())
|
||||||
glog.Error(err)
|
glog.Error(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -74,7 +75,7 @@ func ProcessExisting(client *client.Client, policy *types.Policy) []*info.Policy
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo) (*info.PolicyInfo, error) {
|
func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo) (*info.PolicyInfo, error) {
|
||||||
policyInfo := info.NewPolicyInfo(policy.Name, res.gvk.Kind, res.resource.GetName(), res.resource.GetNamespace())
|
policyInfo := info.NewPolicyInfo(policy.Name, res.gvk.Kind, res.resource.GetName(), res.resource.GetNamespace(), policy.Spec.ValidationFailureAction)
|
||||||
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
||||||
rawResource, err := res.resource.MarshalJSON()
|
rawResource, err := res.resource.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -99,6 +100,10 @@ func applyPolicy(client *client.Client, policy *types.Policy, res *resourceInfo)
|
||||||
|
|
||||||
func mutation(p *types.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]*info.RuleInfo, error) {
|
func mutation(p *types.Policy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]*info.RuleInfo, error) {
|
||||||
patches, ruleInfos := Mutate(*p, rawResource, *gvk)
|
patches, ruleInfos := Mutate(*p, rawResource, *gvk)
|
||||||
|
if len(ruleInfos) == 0 {
|
||||||
|
// no rules were processed
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
// option 2: (original Resource + patch) compare with (original resource)
|
// option 2: (original Resource + patch) compare with (original resource)
|
||||||
mergePatches := JoinPatches(patches)
|
mergePatches := JoinPatches(patches)
|
||||||
// merge the patches
|
// merge the patches
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
// Mutate performs mutation. Overlay first and then mutation patches
|
// Mutate performs mutation. Overlay first and then mutation patches
|
||||||
func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) {
|
func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) ([][]byte, []*info.RuleInfo) {
|
||||||
var allPatches [][]byte
|
var allPatches [][]byte
|
||||||
var err error
|
patchedDocument := rawResource
|
||||||
ris := []*info.RuleInfo{}
|
ris := []*info.RuleInfo{}
|
||||||
|
|
||||||
for _, rule := range policy.Spec.Rules {
|
for _, rule := range policy.Spec.Rules {
|
||||||
|
@ -29,38 +29,24 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio
|
||||||
overlayPatches, err := ProcessOverlay(rule, rawResource, gvk)
|
overlayPatches, err := ProcessOverlay(rule, rawResource, gvk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ri.Fail()
|
ri.Fail()
|
||||||
ri.Addf("Rule %s: Overlay application has failed, err %s.", rule.Name, err)
|
ri.Addf("overlay application has failed, err %v.", err)
|
||||||
} else {
|
} else {
|
||||||
// Apply the JSON patches from the rule to the resource
|
ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name)
|
||||||
rawResource, err = ApplyPatches(rawResource, overlayPatches)
|
allPatches = append(allPatches, overlayPatches...)
|
||||||
if err != nil {
|
|
||||||
ri.Fail()
|
|
||||||
ri.Addf("Unable to apply JSON patch to resource, err %s.", err)
|
|
||||||
} else {
|
|
||||||
ri.Addf("Rule %s: Overlay succesfully applied.", rule.Name)
|
|
||||||
allPatches = append(allPatches, overlayPatches...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process Patches
|
// Process Patches
|
||||||
if len(rule.Mutation.Patches) != 0 {
|
if len(rule.Mutation.Patches) != 0 {
|
||||||
rulePatches, errs := ProcessPatches(rule, rawResource)
|
rulePatches, errs := ProcessPatches(rule, patchedDocument)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
ri.Fail()
|
ri.Fail()
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
ri.Addf("Rule %s: Patches application has failed, err %s.", rule.Name, err)
|
ri.Addf("patches application has failed, err %v.", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Apply the JSON patches from the rule to the resource
|
ri.Addf("Rule %s: Patches succesfully applied.", rule.Name)
|
||||||
rawResource, err = ApplyPatches(rawResource, rulePatches)
|
allPatches = append(allPatches, rulePatches...)
|
||||||
if err != nil {
|
|
||||||
ri.Fail()
|
|
||||||
ri.Addf("Unable to apply JSON patch to resource, err %s.", err)
|
|
||||||
} else {
|
|
||||||
ri.Addf("Rule %s: Patches succesfully applied.", rule.Name)
|
|
||||||
allPatches = append(allPatches, rulePatches...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ris = append(ris, ri)
|
ris = append(ris, ri)
|
||||||
|
|
|
@ -41,7 +41,7 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
|
||||||
err := validateResourceWithPattern(resource, rule.Validation.Pattern)
|
err := validateResourceWithPattern(resource, rule.Validation.Pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ri.Fail()
|
ri.Fail()
|
||||||
ri.Addf("Rule %s: Validation has failed, err %s.", rule.Name, err)
|
ri.Addf("validation has failed, err %v.", err)
|
||||||
} else {
|
} else {
|
||||||
ri.Addf("Rule %s: Validation succesfully.", rule.Name)
|
ri.Addf("Rule %s: Validation succesfully.", rule.Name)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package event
|
package event
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
|
@ -42,13 +41,12 @@ type Controller interface {
|
||||||
func NewEventController(client *client.Client,
|
func NewEventController(client *client.Client,
|
||||||
shareInformer sharedinformer.PolicyInformer) Controller {
|
shareInformer sharedinformer.PolicyInformer) Controller {
|
||||||
|
|
||||||
controller := &controller{
|
return &controller{
|
||||||
client: client,
|
client: client,
|
||||||
policyLister: shareInformer.GetLister(),
|
policyLister: shareInformer.GetLister(),
|
||||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), eventWorkQueueName),
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), eventWorkQueueName),
|
||||||
recorder: initRecorder(client),
|
recorder: initRecorder(client),
|
||||||
}
|
}
|
||||||
return controller
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRecorder(client *client.Client) record.EventRecorder {
|
func initRecorder(client *client.Client) record.EventRecorder {
|
||||||
|
@ -93,40 +91,58 @@ func (c *controller) Stop() {
|
||||||
defer c.queue.ShutDown()
|
defer c.queue.ShutDown()
|
||||||
glog.Info("Shutting down eventbuilder controller workers")
|
glog.Info("Shutting down eventbuilder controller workers")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) runWorker() {
|
func (c *controller) runWorker() {
|
||||||
for c.processNextWorkItem() {
|
for c.processNextWorkItem() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *controller) handleErr(err error, key interface{}) {
|
||||||
|
if err == nil {
|
||||||
|
c.queue.Forget(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// This controller retries if something goes wrong. After that, it stops trying.
|
||||||
|
if c.queue.NumRequeues(key) < workQueueRetryLimit {
|
||||||
|
glog.Warningf("Error syncing events %v: %v", key, err)
|
||||||
|
// Re-enqueue the key rate limited. Based on the rate limiter on the
|
||||||
|
// queue and the re-enqueue history, the key will be processed later again.
|
||||||
|
c.queue.AddRateLimited(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.queue.Forget(key)
|
||||||
|
glog.Error(err)
|
||||||
|
glog.Warningf("Dropping the key out of the queue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *controller) processNextWorkItem() bool {
|
func (c *controller) processNextWorkItem() bool {
|
||||||
obj, shutdown := c.queue.Get()
|
obj, shutdown := c.queue.Get()
|
||||||
if shutdown {
|
if shutdown {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err := func(obj interface{}) error {
|
err := func(obj interface{}) error {
|
||||||
defer c.queue.Done(obj)
|
defer c.queue.Done(obj)
|
||||||
var key Info
|
var key Info
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
if key, ok = obj.(Info); !ok {
|
if key, ok = obj.(Info); !ok {
|
||||||
c.queue.Forget(obj)
|
c.queue.Forget(obj)
|
||||||
glog.Warningf("Expecting type info by got %v\n", obj)
|
glog.Warningf("Expecting type info by got %v\n", obj)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Run the syncHandler, passing the resource and the policy
|
err := c.syncHandler(key)
|
||||||
if err := c.SyncHandler(key); err != nil {
|
c.handleErr(err, obj)
|
||||||
c.queue.AddRateLimited(key)
|
|
||||||
return fmt.Errorf("error syncing '%s' : %s, requeuing event creation request", key.Namespace+"/"+key.Name, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}(obj)
|
}(obj)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Warning(err)
|
glog.Error(err)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) SyncHandler(key Info) error {
|
func (c *controller) syncHandler(key Info) error {
|
||||||
var robj runtime.Object
|
var robj runtime.Object
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ const eventWorkQueueName = "policy-controller-events"
|
||||||
|
|
||||||
const eventWorkerThreadCount = 1
|
const eventWorkerThreadCount = 1
|
||||||
|
|
||||||
|
const workQueueRetryLimit = 1
|
||||||
|
|
||||||
//Info defines the event details
|
//Info defines the event details
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Kind string
|
Kind string
|
||||||
|
|
|
@ -59,7 +59,8 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) {
|
||||||
policyInfo := info.NewPolicyInfo(p.Name,
|
policyInfo := info.NewPolicyInfo(p.Name,
|
||||||
"Namespace",
|
"Namespace",
|
||||||
ns.Name,
|
ns.Name,
|
||||||
"") // Namespace has no namespace..WOW
|
"",
|
||||||
|
p.Spec.ValidationFailureAction) // Namespace has no namespace..WOW
|
||||||
|
|
||||||
ruleInfos := engine.GenerateNew(c.client, p, ns)
|
ruleInfos := engine.GenerateNew(c.client, p, ns)
|
||||||
policyInfo.AddRuleInfos(ruleInfos)
|
policyInfo.AddRuleInfos(ruleInfos)
|
||||||
|
@ -77,9 +78,7 @@ func (c *Controller) processPolicy(ns *corev1.Namespace, p *v1alpha1.Policy) {
|
||||||
|
|
||||||
if onViolation {
|
if onViolation {
|
||||||
glog.Infof("Adding violation for generation rule of policy %s\n", policyInfo.Name)
|
glog.Infof("Adding violation for generation rule of policy %s\n", policyInfo.Name)
|
||||||
|
v := violation.BuldNewViolation(policyInfo.Name, policyInfo.RKind, policyInfo.RNamespace, policyInfo.RName, event.PolicyViolation.String(), policyInfo.GetFailedRules())
|
||||||
v := violation.NewViolation(event.PolicyViolation, policyInfo.Name, policyInfo.RKind, policyInfo.RName,
|
|
||||||
policyInfo.RNamespace, msg)
|
|
||||||
c.violationBuilder.Add(v)
|
c.violationBuilder.Add(v)
|
||||||
} else {
|
} else {
|
||||||
eventInfo = event.NewEvent(policyKind, "", policyInfo.Name, event.RequestBlocked,
|
eventInfo = event.NewEvent(policyKind, "", policyInfo.Name, event.RequestBlocked,
|
||||||
|
|
|
@ -3,6 +3,8 @@ package info
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
//PolicyInfo defines policy information
|
//PolicyInfo defines policy information
|
||||||
|
@ -16,25 +18,68 @@ type PolicyInfo struct {
|
||||||
// Namespace is the ns of resource
|
// Namespace is the ns of resource
|
||||||
// empty on non-namespaced resources
|
// empty on non-namespaced resources
|
||||||
RNamespace string
|
RNamespace string
|
||||||
Rules []*RuleInfo
|
//TODO: add check/enum for types
|
||||||
success bool
|
ValidationFailureAction string // BlockChanges, ReportViolation
|
||||||
|
Rules []*RuleInfo
|
||||||
|
success bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewPolicyInfo returns a new policy info
|
//NewPolicyInfo returns a new policy info
|
||||||
func NewPolicyInfo(policyName string, rKind string, rName string, rNamespace string) *PolicyInfo {
|
func NewPolicyInfo(policyName, rKind, rName, rNamespace, validationFailureAction string) *PolicyInfo {
|
||||||
return &PolicyInfo{
|
return &PolicyInfo{
|
||||||
Name: policyName,
|
Name: policyName,
|
||||||
RKind: rKind,
|
RKind: rKind,
|
||||||
RName: rName,
|
RName: rName,
|
||||||
RNamespace: rNamespace,
|
RNamespace: rNamespace,
|
||||||
success: true, // fail to be set explicity
|
success: true, // fail to be set explicity
|
||||||
|
ValidationFailureAction: validationFailureAction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//IsSuccessful checks if policy is succesful
|
//IsSuccessful checks if policy is succesful
|
||||||
// the policy is set to fail, if any of the rules have failed
|
// the policy is set to fail, if any of the rules have failed
|
||||||
func (pi *PolicyInfo) IsSuccessful() bool {
|
func (pi *PolicyInfo) IsSuccessful() bool {
|
||||||
return pi.success
|
for _, r := range pi.Rules {
|
||||||
|
if !r.success {
|
||||||
|
pi.success = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pi.success = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuccessfulRules returns list of successful rule names
|
||||||
|
func (pi *PolicyInfo) SuccessfulRules() []string {
|
||||||
|
var rules []string
|
||||||
|
for _, r := range pi.Rules {
|
||||||
|
if r.IsSuccessful() {
|
||||||
|
rules = append(rules, r.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailedRules returns list of failed rule names
|
||||||
|
func (pi *PolicyInfo) FailedRules() []string {
|
||||||
|
var rules []string
|
||||||
|
for _, r := range pi.Rules {
|
||||||
|
if !r.IsSuccessful() {
|
||||||
|
rules = append(rules, r.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetFailedRules returns the failed rules with rule type
|
||||||
|
func (pi *PolicyInfo) GetFailedRules() []v1alpha1.FailedRule {
|
||||||
|
var rules []v1alpha1.FailedRule
|
||||||
|
for _, r := range pi.Rules {
|
||||||
|
if !r.IsSuccessful() {
|
||||||
|
rules = append(rules, v1alpha1.FailedRule{Name: r.Name, Type: r.RuleType.String(), Error: r.GetErrorString()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules
|
||||||
}
|
}
|
||||||
|
|
||||||
//ErrorRules returns error msgs from all rule
|
//ErrorRules returns error msgs from all rule
|
||||||
|
@ -73,12 +118,18 @@ type RuleInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
//ToString reule information
|
//ToString reule information
|
||||||
|
//TODO: check if this is needed
|
||||||
func (ri *RuleInfo) ToString() string {
|
func (ri *RuleInfo) ToString() string {
|
||||||
str := "rulename: " + ri.Name
|
str := "rulename: " + ri.Name
|
||||||
msgs := strings.Join(ri.Msgs, ";")
|
msgs := strings.Join(ri.Msgs, ";")
|
||||||
return strings.Join([]string{str, msgs}, ";")
|
return strings.Join([]string{str, msgs}, ";")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//GetErrorString returns the error message for a rule
|
||||||
|
func (ri *RuleInfo) GetErrorString() string {
|
||||||
|
return strings.Join(ri.Msgs, ";")
|
||||||
|
}
|
||||||
|
|
||||||
//NewRuleInfo creates a new RuleInfo
|
//NewRuleInfo creates a new RuleInfo
|
||||||
func NewRuleInfo(ruleName string, ruleType RuleType) *RuleInfo {
|
func NewRuleInfo(ruleName string, ruleType RuleType) *RuleInfo {
|
||||||
return &RuleInfo{
|
return &RuleInfo{
|
||||||
|
@ -121,6 +172,9 @@ func RulesSuccesfuly(rules []*RuleInfo) bool {
|
||||||
|
|
||||||
//AddRuleInfos sets the rule information
|
//AddRuleInfos sets the rule information
|
||||||
func (pi *PolicyInfo) AddRuleInfos(rules []*RuleInfo) {
|
func (pi *PolicyInfo) AddRuleInfos(rules []*RuleInfo) {
|
||||||
|
if rules == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if !RulesSuccesfuly(rules) {
|
if !RulesSuccesfuly(rules) {
|
||||||
pi.success = false
|
pi.success = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,8 @@ func applyPolicyOnRaw(policy *kubepolicy.Policy, rawResource []byte, gvk *metav1
|
||||||
policyInfo := info.NewPolicyInfo(policy.Name,
|
policyInfo := info.NewPolicyInfo(policy.Name,
|
||||||
gvk.Kind,
|
gvk.Kind,
|
||||||
rname,
|
rname,
|
||||||
rns)
|
rns,
|
||||||
|
policy.Spec.ValidationFailureAction)
|
||||||
|
|
||||||
// Process Mutation
|
// Process Mutation
|
||||||
patches, ruleInfos := engine.Mutate(*policy, rawResource, *gvk)
|
patches, ruleInfos := engine.Mutate(*policy, rawResource, *gvk)
|
||||||
|
|
|
@ -80,7 +80,7 @@ func scanDir(dir string) ([]string, error) {
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v\n", dir, err)
|
return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, path)
|
res = append(res, path)
|
||||||
|
|
|
@ -173,7 +173,8 @@ func (t *test) applyPolicy(policy *pt.Policy,
|
||||||
policyInfo := info.NewPolicyInfo(policy.Name,
|
policyInfo := info.NewPolicyInfo(policy.Name,
|
||||||
rkind,
|
rkind,
|
||||||
rname,
|
rname,
|
||||||
rns)
|
rns,
|
||||||
|
policy.Spec.ValidationFailureAction)
|
||||||
// Apply Mutation Rules
|
// Apply Mutation Rules
|
||||||
patches, ruleInfos := engine.Mutate(*policy, rawResource, *tresource.gvk)
|
patches, ruleInfos := engine.Mutate(*policy, rawResource, *tresource.gvk)
|
||||||
policyInfo.AddRuleInfos(ruleInfos)
|
policyInfo.AddRuleInfos(ruleInfos)
|
||||||
|
|
|
@ -2,24 +2,26 @@ package violation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/golang/glog"
|
v1alpha1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||||
types "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
lister "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||||
v1alpha1 "github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
|
||||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||||
event "github.com/nirmata/kyverno/pkg/event"
|
event "github.com/nirmata/kyverno/pkg/event"
|
||||||
|
"github.com/nirmata/kyverno/pkg/info"
|
||||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Generator to generate policy violation
|
//Generator to generate policy violation
|
||||||
type Generator interface {
|
type Generator interface {
|
||||||
Add(infos ...*Info) error
|
Add(infos ...*Info) error
|
||||||
|
RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error
|
||||||
|
ResourceRemoval(policy, rKind, rNs, rName string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type builder struct {
|
type builder struct {
|
||||||
client *client.Client
|
client *client.Client
|
||||||
policyLister v1alpha1.PolicyLister
|
policyLister lister.PolicyLister
|
||||||
eventBuilder event.Generator
|
eventBuilder event.Generator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +29,6 @@ type builder struct {
|
||||||
type Builder interface {
|
type Builder interface {
|
||||||
Generator
|
Generator
|
||||||
processViolation(info *Info) error
|
processViolation(info *Info) error
|
||||||
isActive(kind string, rname string, rnamespace string) (bool, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewPolicyViolationBuilder returns new violation builder
|
//NewPolicyViolationBuilder returns new violation builder
|
||||||
|
@ -43,7 +44,24 @@ func NewPolicyViolationBuilder(client *client.Client,
|
||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//BuldNewViolation returns a new violation
|
||||||
|
func BuldNewViolation(pName string, rKind string, rNs string, rName string, reason string, frules []v1alpha1.FailedRule) *Info {
|
||||||
|
return &Info{
|
||||||
|
Policy: pName,
|
||||||
|
Violation: v1alpha1.Violation{
|
||||||
|
Kind: rKind,
|
||||||
|
Namespace: rNs,
|
||||||
|
Name: rName,
|
||||||
|
Reason: reason,
|
||||||
|
Rules: frules,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *builder) Add(infos ...*Info) error {
|
func (b *builder) Add(infos ...*Info) error {
|
||||||
|
if infos == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
for _, info := range infos {
|
for _, info := range infos {
|
||||||
return b.processViolation(info)
|
return b.processViolation(info)
|
||||||
}
|
}
|
||||||
|
@ -51,96 +69,203 @@ func (b *builder) Add(infos ...*Info) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) processViolation(info *Info) error {
|
func (b *builder) processViolation(info *Info) error {
|
||||||
currentViolations := []interface{}{}
|
|
||||||
statusMap := map[string]interface{}{}
|
statusMap := map[string]interface{}{}
|
||||||
var ok bool
|
violationsMap := map[string]interface{}{}
|
||||||
//TODO: hack get from client
|
violationMap := map[string]interface{}{}
|
||||||
p1, err := b.client.GetResource("Policy", "", info.Policy, "status")
|
var violations interface{}
|
||||||
|
var violation interface{}
|
||||||
|
// Get Policy
|
||||||
|
obj, err := b.client.GetResource("Policy", "", info.Policy, "status")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
unstr := p1.UnstructuredContent()
|
unstr := obj.UnstructuredContent()
|
||||||
// check if "status" field exists
|
// get "status" subresource
|
||||||
status, ok := unstr["status"]
|
status, ok := unstr["status"]
|
||||||
if ok {
|
if ok {
|
||||||
|
// status exists
|
||||||
// status is already present then we append violations
|
// status is already present then we append violations
|
||||||
if statusMap, ok = status.(map[string]interface{}); !ok {
|
if statusMap, ok = status.(map[string]interface{}); !ok {
|
||||||
return errors.New("Unable to parse status subresource")
|
return errors.New("Unable to parse status subresource")
|
||||||
}
|
}
|
||||||
violations, ok := statusMap["violations"]
|
// get policy violations
|
||||||
|
violations, ok = statusMap["violations"]
|
||||||
if !ok {
|
if !ok {
|
||||||
glog.Info("violation not present")
|
return nil
|
||||||
}
|
}
|
||||||
glog.Info(reflect.TypeOf(violations))
|
violationsMap, ok = violations.(map[string]interface{})
|
||||||
if currentViolations, ok = violations.([]interface{}); !ok {
|
if !ok {
|
||||||
return errors.New("Unable to parse violations")
|
return errors.New("Unable to get status.violations subresource")
|
||||||
}
|
}
|
||||||
}
|
// check if the resource has a violation
|
||||||
newViolation := info.Violation
|
violation, ok = violationsMap[info.getKey()]
|
||||||
for _, violation := range currentViolations {
|
if !ok {
|
||||||
glog.Info(reflect.TypeOf(violation))
|
// add resource violation
|
||||||
if v, ok := violation.(map[string]interface{}); ok {
|
violationsMap[info.getKey()] = info.Violation
|
||||||
if name, ok := v["name"].(string); ok {
|
statusMap["violations"] = violationsMap
|
||||||
if namespace, ok := v["namespace"].(string); ok {
|
unstr["status"] = statusMap
|
||||||
ok, err := b.isActive(info.Kind, name, namespace)
|
} else {
|
||||||
if err != nil {
|
violationMap, ok = violation.(map[string]interface{})
|
||||||
glog.Error(err)
|
if !ok {
|
||||||
continue
|
return errors.New("Unable to get status.violations.violation subresource")
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
//TODO remove the violation as it corresponds to resource that does not exist
|
|
||||||
glog.Info("removed violation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// we check if the new violation updates are different from stored violation info
|
||||||
|
v := v1alpha1.Violation{}
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(violationMap, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// compare v & info.Violation
|
||||||
|
if v.IsEqual(info.Violation) {
|
||||||
|
// no updates to violation
|
||||||
|
// do nothing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// update the violation
|
||||||
|
violationsMap[info.getKey()] = info.Violation
|
||||||
|
statusMap["violations"] = violationsMap
|
||||||
|
unstr["status"] = statusMap
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
violationsMap[info.getKey()] = info.Violation
|
||||||
|
statusMap["violations"] = violationsMap
|
||||||
|
unstr["status"] = statusMap
|
||||||
}
|
}
|
||||||
currentViolations = append(currentViolations, newViolation)
|
|
||||||
// update violations
|
obj.SetUnstructuredContent(unstr)
|
||||||
// set the updated status
|
// update the status sub-resource for policy
|
||||||
statusMap["violations"] = currentViolations
|
_, err = b.client.UpdateStatusResource("Policy", "", obj, false)
|
||||||
unstr["status"] = statusMap
|
|
||||||
p1.SetUnstructuredContent(unstr)
|
|
||||||
_, err = b.client.UpdateStatusResource("policies", "", p1, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *builder) isActive(kind, rname, rnamespace string) (bool, error) {
|
//RemoveInactiveViolation
|
||||||
// Generate Merge Patch
|
func (b *builder) RemoveInactiveViolation(policy, rKind, rNs, rName string, ruleType info.RuleType) error {
|
||||||
_, err := b.client.GetResource(b.client.DiscoveryClient.GetGVRFromKind(kind).Resource, rnamespace, rname)
|
statusMap := map[string]interface{}{}
|
||||||
|
violationsMap := map[string]interface{}{}
|
||||||
|
violationMap := map[string]interface{}{}
|
||||||
|
var violations interface{}
|
||||||
|
var violation interface{}
|
||||||
|
// Get Policy
|
||||||
|
obj, err := b.client.GetResource("Policy", "", policy, "status")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("unable to get resource %s/%s ", rnamespace, rname)
|
return err
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
return true, nil
|
unstr := obj.UnstructuredContent()
|
||||||
|
// get "status" subresource
|
||||||
|
status, ok := unstr["status"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// status exists
|
||||||
|
// status is already present then we append violations
|
||||||
|
if statusMap, ok = status.(map[string]interface{}); !ok {
|
||||||
|
return errors.New("Unable to parse status subresource")
|
||||||
|
}
|
||||||
|
// get policy violations
|
||||||
|
violations, ok = statusMap["violations"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
violationsMap, ok = violations.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unable to get status.violations subresource")
|
||||||
|
}
|
||||||
|
// check if the resource has a violation
|
||||||
|
violation, ok = violationsMap[BuildKey(rKind, rNs, rName)]
|
||||||
|
if !ok {
|
||||||
|
// no violation for this resource
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
violationMap, ok = violation.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unable to get status.violations.violation subresource")
|
||||||
|
}
|
||||||
|
// check remove the rules of the given type
|
||||||
|
// this is called when the policy is applied succesfully, so we can remove the previous failed rules
|
||||||
|
// if all rules are to be removed, the deleted the violation
|
||||||
|
v := v1alpha1.Violation{}
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(violationMap, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !v.RemoveRulesOfType(ruleType.String()) {
|
||||||
|
// no rule of given type found,
|
||||||
|
// no need to remove rule
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// if there are no faile rules remove the violation
|
||||||
|
if len(v.Rules) == 0 {
|
||||||
|
delete(violationsMap, BuildKey(rKind, rNs, rName))
|
||||||
|
} else {
|
||||||
|
// update the rules
|
||||||
|
violationsMap[BuildKey(rKind, rNs, rName)] = v
|
||||||
|
}
|
||||||
|
statusMap["violations"] = violationsMap
|
||||||
|
unstr["status"] = statusMap
|
||||||
|
|
||||||
|
obj.SetUnstructuredContent(unstr)
|
||||||
|
// update the status sub-resource for policy
|
||||||
|
_, err = b.client.UpdateStatusResource("Policy", "", obj, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewViolation return new policy violation
|
// ResourceRemoval on resources reoval we remove the policy violation in the policy
|
||||||
func NewViolation(reason event.Reason, policyName, kind, rname, rnamespace, msg string) *Info {
|
func (b *builder) ResourceRemoval(policy, rKind, rNs, rName string) error {
|
||||||
return &Info{Policy: policyName,
|
statusMap := map[string]interface{}{}
|
||||||
Violation: types.Violation{
|
violationsMap := map[string]interface{}{}
|
||||||
Kind: kind,
|
var violations interface{}
|
||||||
Name: rname,
|
// Get Policy
|
||||||
Namespace: rnamespace,
|
obj, err := b.client.GetResource("Policy", "", policy, "status")
|
||||||
Reason: reason.String(),
|
if err != nil {
|
||||||
Message: msg,
|
return err
|
||||||
},
|
}
|
||||||
|
unstr := obj.UnstructuredContent()
|
||||||
|
// get "status" subresource
|
||||||
|
status, ok := unstr["status"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// status exists
|
||||||
|
// status is already present then we append violations
|
||||||
|
if statusMap, ok = status.(map[string]interface{}); !ok {
|
||||||
|
return errors.New("Unable to parse status subresource")
|
||||||
|
}
|
||||||
|
// get policy violations
|
||||||
|
violations, ok = statusMap["violations"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
violationsMap, ok = violations.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unable to get status.violations subresource")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//NewViolationFromEvent returns violation info from event
|
// check if the resource has a violation
|
||||||
func NewViolationFromEvent(e *event.Info, pName, rKind, rName, rnamespace string) *Info {
|
_, ok = violationsMap[BuildKey(rKind, rNs, rName)]
|
||||||
return &Info{
|
if !ok {
|
||||||
Policy: pName,
|
// no violation for this resource
|
||||||
Violation: types.Violation{
|
return nil
|
||||||
Kind: rKind,
|
|
||||||
Name: rName,
|
|
||||||
Namespace: rnamespace,
|
|
||||||
Reason: e.Reason,
|
|
||||||
Message: e.Message,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
// remove the pair from the map
|
||||||
|
delete(violationsMap, BuildKey(rKind, rNs, rName))
|
||||||
|
if len(violationsMap) == 0 {
|
||||||
|
delete(statusMap, "violations")
|
||||||
|
} else {
|
||||||
|
statusMap["violations"] = violationsMap
|
||||||
|
}
|
||||||
|
unstr["status"] = statusMap
|
||||||
|
|
||||||
|
obj.SetUnstructuredContent(unstr)
|
||||||
|
// update the status sub-resource for policy
|
||||||
|
_, err = b.client.UpdateStatusResource("Policy", "", obj, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,17 @@ const workqueueViolationName = "Policy-Violations"
|
||||||
// Event Reason
|
// Event Reason
|
||||||
const violationEventResrouce = "Violation"
|
const violationEventResrouce = "Violation"
|
||||||
|
|
||||||
//ViolationInfo describes the policyviolation details
|
//Info describes the policyviolation details
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Policy string
|
Policy string
|
||||||
policytype.Violation
|
policytype.Violation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i Info) getKey() string {
|
||||||
|
return i.Kind + "/" + i.Namespace + "/" + i.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
//BuildKey returns the key format
|
||||||
|
func BuildKey(rKind, rNs, rName string) string {
|
||||||
|
return rKind + "/" + rNs + "/" + rName
|
||||||
|
}
|
||||||
|
|
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(
|
constructWebhook(
|
||||||
config.MutatingWebhookName,
|
config.MutatingWebhookName,
|
||||||
config.MutatingWebhookServicePath,
|
config.MutatingWebhookServicePath,
|
||||||
caData),
|
caData,
|
||||||
|
false),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -157,7 +158,8 @@ func (wrc *WebhookRegistrationClient) contructDebugMutatingWebhookConfig(caData
|
||||||
constructDebugWebhook(
|
constructDebugWebhook(
|
||||||
config.MutatingWebhookName,
|
config.MutatingWebhookName,
|
||||||
url,
|
url,
|
||||||
caData),
|
caData,
|
||||||
|
false),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +192,8 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura
|
||||||
constructWebhook(
|
constructWebhook(
|
||||||
config.ValidatingWebhookName,
|
config.ValidatingWebhookName,
|
||||||
config.ValidatingWebhookServicePath,
|
config.ValidatingWebhookServicePath,
|
||||||
caData),
|
caData,
|
||||||
|
true),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -208,7 +211,8 @@ func (wrc *WebhookRegistrationClient) contructDebugValidatingWebhookConfig(caDat
|
||||||
constructDebugWebhook(
|
constructDebugWebhook(
|
||||||
config.ValidatingWebhookName,
|
config.ValidatingWebhookName,
|
||||||
url,
|
url,
|
||||||
caData),
|
caData,
|
||||||
|
true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,7 +245,8 @@ func (wrc *WebhookRegistrationClient) contructPolicyValidatingWebhookConfig() (*
|
||||||
constructWebhook(
|
constructWebhook(
|
||||||
config.PolicyValidatingWebhookName,
|
config.PolicyValidatingWebhookName,
|
||||||
config.PolicyValidatingWebhookServicePath,
|
config.PolicyValidatingWebhookServicePath,
|
||||||
caData),
|
caData,
|
||||||
|
true),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -259,12 +264,13 @@ func (wrc *WebhookRegistrationClient) contructDebugPolicyValidatingWebhookConfig
|
||||||
constructDebugWebhook(
|
constructDebugWebhook(
|
||||||
config.PolicyValidatingWebhookName,
|
config.PolicyValidatingWebhookName,
|
||||||
url,
|
url,
|
||||||
caData),
|
caData,
|
||||||
|
true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook {
|
func constructWebhook(name, servicePath string, caData []byte, validation bool) admregapi.Webhook {
|
||||||
resource := "*/*"
|
resource := "*/*"
|
||||||
apiGroups := "*"
|
apiGroups := "*"
|
||||||
apiversions := "*"
|
apiversions := "*"
|
||||||
|
@ -273,6 +279,15 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
|
||||||
apiGroups = "kyverno.io"
|
apiGroups = "kyverno.io"
|
||||||
apiversions = "v1alpha1"
|
apiversions = "v1alpha1"
|
||||||
}
|
}
|
||||||
|
operationtypes := []admregapi.OperationType{
|
||||||
|
admregapi.Create,
|
||||||
|
admregapi.Update,
|
||||||
|
}
|
||||||
|
// Add operation DELETE for validation
|
||||||
|
if validation {
|
||||||
|
operationtypes = append(operationtypes, admregapi.Delete)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return admregapi.Webhook{
|
return admregapi.Webhook{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -286,10 +301,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
|
||||||
},
|
},
|
||||||
Rules: []admregapi.RuleWithOperations{
|
Rules: []admregapi.RuleWithOperations{
|
||||||
admregapi.RuleWithOperations{
|
admregapi.RuleWithOperations{
|
||||||
Operations: []admregapi.OperationType{
|
Operations: operationtypes,
|
||||||
admregapi.Create,
|
|
||||||
admregapi.Update,
|
|
||||||
},
|
|
||||||
Rule: admregapi.Rule{
|
Rule: admregapi.Rule{
|
||||||
APIGroups: []string{
|
APIGroups: []string{
|
||||||
apiGroups,
|
apiGroups,
|
||||||
|
@ -306,7 +318,7 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
|
func constructDebugWebhook(name, url string, caData []byte, validation bool) admregapi.Webhook {
|
||||||
resource := "*/*"
|
resource := "*/*"
|
||||||
apiGroups := "*"
|
apiGroups := "*"
|
||||||
apiversions := "*"
|
apiversions := "*"
|
||||||
|
@ -316,6 +328,14 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
|
||||||
apiGroups = "kyverno.io"
|
apiGroups = "kyverno.io"
|
||||||
apiversions = "v1alpha1"
|
apiversions = "v1alpha1"
|
||||||
}
|
}
|
||||||
|
operationtypes := []admregapi.OperationType{
|
||||||
|
admregapi.Create,
|
||||||
|
admregapi.Update,
|
||||||
|
}
|
||||||
|
// Add operation DELETE for validation
|
||||||
|
if validation {
|
||||||
|
operationtypes = append(operationtypes, admregapi.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
return admregapi.Webhook{
|
return admregapi.Webhook{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -325,10 +345,7 @@ func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
|
||||||
},
|
},
|
||||||
Rules: []admregapi.RuleWithOperations{
|
Rules: []admregapi.RuleWithOperations{
|
||||||
admregapi.RuleWithOperations{
|
admregapi.RuleWithOperations{
|
||||||
Operations: []admregapi.OperationType{
|
Operations: operationtypes,
|
||||||
admregapi.Create,
|
|
||||||
admregapi.Update,
|
|
||||||
},
|
|
||||||
Rule: admregapi.Rule{
|
Rule: admregapi.Rule{
|
||||||
APIGroups: []string{
|
APIGroups: []string{
|
||||||
apiGroups,
|
apiGroups,
|
||||||
|
|
77
pkg/webhooks/report.go
Normal file
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"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
policyv1 "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
"github.com/nirmata/kyverno/pkg/annotations"
|
||||||
"github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
"github.com/nirmata/kyverno/pkg/client/listers/policy/v1alpha1"
|
||||||
"github.com/nirmata/kyverno/pkg/config"
|
"github.com/nirmata/kyverno/pkg/config"
|
||||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||||
engine "github.com/nirmata/kyverno/pkg/engine"
|
|
||||||
"github.com/nirmata/kyverno/pkg/event"
|
"github.com/nirmata/kyverno/pkg/event"
|
||||||
"github.com/nirmata/kyverno/pkg/info"
|
|
||||||
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
"github.com/nirmata/kyverno/pkg/sharedinformer"
|
||||||
tlsutils "github.com/nirmata/kyverno/pkg/tls"
|
tlsutils "github.com/nirmata/kyverno/pkg/tls"
|
||||||
"github.com/nirmata/kyverno/pkg/utils"
|
"github.com/nirmata/kyverno/pkg/violation"
|
||||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const policyKind = "Policy"
|
|
||||||
|
|
||||||
// WebhookServer contains configured TLS server with MutationWebhook.
|
// WebhookServer contains configured TLS server with MutationWebhook.
|
||||||
// MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient.
|
// MutationWebhook gets policies from policyController and takes control of the cluster with kubeclient.
|
||||||
type WebhookServer struct {
|
type WebhookServer struct {
|
||||||
server http.Server
|
server http.Server
|
||||||
client *client.Client
|
client *client.Client
|
||||||
policyLister v1alpha1.PolicyLister
|
policyLister v1alpha1.PolicyLister
|
||||||
eventController event.Generator
|
eventController event.Generator
|
||||||
filterKinds []string
|
violationBuilder violation.Generator
|
||||||
|
annotationsController annotations.Controller
|
||||||
|
filterKinds []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
|
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
|
||||||
|
@ -46,6 +41,8 @@ func NewWebhookServer(
|
||||||
tlsPair *tlsutils.TlsPemPair,
|
tlsPair *tlsutils.TlsPemPair,
|
||||||
shareInformer sharedinformer.PolicyInformer,
|
shareInformer sharedinformer.PolicyInformer,
|
||||||
eventController event.Generator,
|
eventController event.Generator,
|
||||||
|
violationBuilder violation.Generator,
|
||||||
|
annotationsController annotations.Controller,
|
||||||
filterKinds []string) (*WebhookServer, error) {
|
filterKinds []string) (*WebhookServer, error) {
|
||||||
|
|
||||||
if tlsPair == nil {
|
if tlsPair == nil {
|
||||||
|
@ -60,10 +57,12 @@ func NewWebhookServer(
|
||||||
tlsConfig.Certificates = []tls.Certificate{pair}
|
tlsConfig.Certificates = []tls.Certificate{pair}
|
||||||
|
|
||||||
ws := &WebhookServer{
|
ws := &WebhookServer{
|
||||||
client: client,
|
client: client,
|
||||||
policyLister: shareInformer.GetLister(),
|
policyLister: shareInformer.GetLister(),
|
||||||
eventController: eventController,
|
eventController: eventController,
|
||||||
filterKinds: parseKinds(filterKinds),
|
violationBuilder: violationBuilder,
|
||||||
|
annotationsController: annotationsController,
|
||||||
|
filterKinds: parseKinds(filterKinds),
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve)
|
mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve)
|
||||||
|
@ -94,14 +93,30 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Do not process the admission requests for kinds that are in filterKinds for filtering
|
// Do not process the admission requests for kinds that are in filterKinds for filtering
|
||||||
if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) {
|
if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) {
|
||||||
|
// if the resource is being deleted we need to clear any existing Policy Violations
|
||||||
|
// TODO: can report to the user that we clear the violation corresponding to this resource
|
||||||
|
if admissionReview.Request.Operation == v1beta1.Delete {
|
||||||
|
// Resource DELETE
|
||||||
|
err := ws.removePolicyViolation(admissionReview.Request)
|
||||||
|
if err != nil {
|
||||||
|
glog.Info(err)
|
||||||
|
}
|
||||||
|
admissionReview.Response = &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
}
|
||||||
|
admissionReview.Response.UID = admissionReview.Request.UID
|
||||||
|
} else {
|
||||||
|
// Resource CREATE
|
||||||
|
// Resource UPDATE
|
||||||
|
switch r.URL.Path {
|
||||||
|
case config.MutatingWebhookServicePath:
|
||||||
|
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
|
||||||
|
case config.ValidatingWebhookServicePath:
|
||||||
|
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
|
||||||
|
case config.PolicyValidatingWebhookServicePath:
|
||||||
|
admissionReview.Response = ws.HandlePolicyValidation(admissionReview.Request)
|
||||||
|
}
|
||||||
|
|
||||||
switch r.URL.Path {
|
|
||||||
case config.MutatingWebhookServicePath:
|
|
||||||
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
|
|
||||||
case config.ValidatingWebhookServicePath:
|
|
||||||
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
|
|
||||||
case config.PolicyValidatingWebhookServicePath:
|
|
||||||
admissionReview.Response = ws.HandlePolicyValidation(admissionReview.Request)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,169 +155,6 @@ func (ws *WebhookServer) Stop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleMutation handles mutating webhook admission request
|
|
||||||
func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
|
||||||
|
|
||||||
policies, err := ws.policyLister.List(labels.NewSelector())
|
|
||||||
if err != nil {
|
|
||||||
// Unable to connect to policy Lister to access policies
|
|
||||||
glog.Error("Unable to connect to policy controller to access policies. Mutation Rules are NOT being applied")
|
|
||||||
glog.Warning(err)
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var allPatches [][]byte
|
|
||||||
policyInfos := []*info.PolicyInfo{}
|
|
||||||
for _, policy := range policies {
|
|
||||||
// check if policy has a rule for the admission request kind
|
|
||||||
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rname := engine.ParseNameFromObject(request.Object.Raw)
|
|
||||||
rns := engine.ParseNamespaceFromObject(request.Object.Raw)
|
|
||||||
rkind := engine.ParseKindFromObject(request.Object.Raw)
|
|
||||||
policyInfo := info.NewPolicyInfo(policy.Name,
|
|
||||||
rkind,
|
|
||||||
rname,
|
|
||||||
rns)
|
|
||||||
|
|
||||||
glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
|
|
||||||
request.Kind.Kind, rns, rname, request.UID, request.Operation)
|
|
||||||
|
|
||||||
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
|
||||||
|
|
||||||
policyPatches, ruleInfos := engine.Mutate(*policy, request.Object.Raw, request.Kind)
|
|
||||||
|
|
||||||
policyInfo.AddRuleInfos(ruleInfos)
|
|
||||||
|
|
||||||
if !policyInfo.IsSuccessful() {
|
|
||||||
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
|
|
||||||
for _, r := range ruleInfos {
|
|
||||||
glog.Warning(r.Msgs)
|
|
||||||
}
|
|
||||||
} else if len(policyPatches) > 0 {
|
|
||||||
allPatches = append(allPatches, policyPatches...)
|
|
||||||
glog.Infof("Mutation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns)
|
|
||||||
}
|
|
||||||
policyInfos = append(policyInfos, policyInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allPatches) > 0 {
|
|
||||||
eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update))
|
|
||||||
ws.eventController.Add(eventsInfo...)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, msg := isAdmSuccesful(policyInfos)
|
|
||||||
if ok {
|
|
||||||
patchType := v1beta1.PatchTypeJSONPatch
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: true,
|
|
||||||
Patch: engine.JoinPatches(allPatches),
|
|
||||||
PatchType: &patchType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: false,
|
|
||||||
Result: &metav1.Status{
|
|
||||||
Message: msg,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAdmSuccesful(policyInfos []*info.PolicyInfo) (bool, string) {
|
|
||||||
var admSuccess = true
|
|
||||||
var errMsgs []string
|
|
||||||
for _, pi := range policyInfos {
|
|
||||||
if !pi.IsSuccessful() {
|
|
||||||
admSuccess = false
|
|
||||||
errMsgs = append(errMsgs, fmt.Sprintf("\nPolicy %s failed with following rules", pi.Name))
|
|
||||||
// Get the error rules
|
|
||||||
errorRules := pi.ErrorRules()
|
|
||||||
errMsgs = append(errMsgs, errorRules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return admSuccess, strings.Join(errMsgs, ";")
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleValidation handles validating webhook admission request
|
|
||||||
// If there are no errors in validating rule we apply generation rules
|
|
||||||
func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
|
||||||
policyInfos := []*info.PolicyInfo{}
|
|
||||||
|
|
||||||
policies, err := ws.policyLister.List(labels.NewSelector())
|
|
||||||
if err != nil {
|
|
||||||
// Unable to connect to policy Lister to access policies
|
|
||||||
glog.Error("Unable to connect to policy controller to access policies. Validation Rules are NOT being applied")
|
|
||||||
glog.Warning(err)
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, policy := range policies {
|
|
||||||
|
|
||||||
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rname := engine.ParseNameFromObject(request.Object.Raw)
|
|
||||||
rns := engine.ParseNamespaceFromObject(request.Object.Raw)
|
|
||||||
rkind := engine.ParseKindFromObject(request.Object.Raw)
|
|
||||||
|
|
||||||
policyInfo := info.NewPolicyInfo(policy.Name,
|
|
||||||
rkind,
|
|
||||||
rname,
|
|
||||||
rns)
|
|
||||||
|
|
||||||
glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
|
|
||||||
request.Kind.Kind, rns, rname, request.UID, request.Operation)
|
|
||||||
|
|
||||||
glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
|
|
||||||
ruleInfos, err := engine.Validate(*policy, request.Object.Raw, request.Kind)
|
|
||||||
if err != nil {
|
|
||||||
// This is not policy error
|
|
||||||
// but if unable to parse request raw resource
|
|
||||||
// TODO : create event ? dont think so
|
|
||||||
glog.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
policyInfo.AddRuleInfos(ruleInfos)
|
|
||||||
|
|
||||||
if !policyInfo.IsSuccessful() {
|
|
||||||
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
|
|
||||||
for _, r := range ruleInfos {
|
|
||||||
glog.Warning(r.Msgs)
|
|
||||||
}
|
|
||||||
} else if len(ruleInfos) > 0 {
|
|
||||||
glog.Infof("Validation from policy %s has applied succesfully to %s %s/%s", policy.Name, request.Kind.Kind, rname, rns)
|
|
||||||
}
|
|
||||||
policyInfos = append(policyInfos, policyInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(policyInfos) > 0 && len(policyInfos[0].Rules) != 0 {
|
|
||||||
eventsInfo := newEventInfoFromPolicyInfo(policyInfos, (request.Operation == v1beta1.Update))
|
|
||||||
ws.eventController.Add(eventsInfo...)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Validation fails then reject the request
|
|
||||||
ok, msg := isAdmSuccesful(policyInfos)
|
|
||||||
if !ok {
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: false,
|
|
||||||
Result: &metav1.Status{
|
|
||||||
Message: msg,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: true,
|
|
||||||
}
|
|
||||||
// Generation rules applied via generation controller
|
|
||||||
}
|
|
||||||
|
|
||||||
// bodyToAdmissionReview creates AdmissionReview object from request body
|
// bodyToAdmissionReview creates AdmissionReview object from request body
|
||||||
// Answers to the http.ResponseWriter if request is not valid
|
// Answers to the http.ResponseWriter if request is not valid
|
||||||
func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview {
|
func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer http.ResponseWriter) *v1beta1.AdmissionReview {
|
||||||
|
@ -334,96 +186,3 @@ func (ws *WebhookServer) bodyToAdmissionReview(request *http.Request, writer htt
|
||||||
|
|
||||||
return admissionReview
|
return admissionReview
|
||||||
}
|
}
|
||||||
|
|
||||||
//HandlePolicyValidation performs the validation check on policy resource
|
|
||||||
func (ws *WebhookServer) HandlePolicyValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
|
|
||||||
return ws.validateUniqueRuleName(request.Object.Raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws *WebhookServer) validateUniqueRuleName(rawPolicy []byte) *v1beta1.AdmissionResponse {
|
|
||||||
var policy *policyv1.Policy
|
|
||||||
var ruleNames []string
|
|
||||||
|
|
||||||
json.Unmarshal(rawPolicy, &policy)
|
|
||||||
|
|
||||||
for _, rule := range policy.Spec.Rules {
|
|
||||||
if utils.Contains(ruleNames, rule.Name) {
|
|
||||||
msg := fmt.Sprintf(`The policy "%s" is invalid: duplicate rule name: "%s"`, policy.Name, rule.Name)
|
|
||||||
glog.Errorln(msg)
|
|
||||||
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: false,
|
|
||||||
Result: &metav1.Status{
|
|
||||||
Message: msg,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ruleNames = append(ruleNames, rule.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
glog.V(3).Infof("Policy validation passed")
|
|
||||||
return &v1beta1.AdmissionResponse{
|
|
||||||
Allowed: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEventInfoFromPolicyInfo(policyInfoList []*info.PolicyInfo, onUpdate bool) []*event.Info {
|
|
||||||
var eventsInfo []*event.Info
|
|
||||||
|
|
||||||
ok, msg := isAdmSuccesful(policyInfoList)
|
|
||||||
// create events on operation UPDATE
|
|
||||||
if onUpdate {
|
|
||||||
if !ok {
|
|
||||||
for _, pi := range policyInfoList {
|
|
||||||
ruleNames := getRuleNames(*pi, false)
|
|
||||||
eventsInfo = append(eventsInfo,
|
|
||||||
event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.RequestBlocked, event.FPolicyApplyBlockUpdate, ruleNames, pi.Name))
|
|
||||||
|
|
||||||
eventsInfo = append(eventsInfo,
|
|
||||||
event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyBlockResourceUpdate, pi.RName, ruleNames))
|
|
||||||
|
|
||||||
glog.V(3).Infof("Request blocked events info has prepared for %s/%s and %s/%s\n", policyKind, pi.Name, pi.RKind, pi.RName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return eventsInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// create events on operation CREATE
|
|
||||||
if ok {
|
|
||||||
for _, pi := range policyInfoList {
|
|
||||||
ruleNames := getRuleNames(*pi, true)
|
|
||||||
|
|
||||||
eventsInfo = append(eventsInfo,
|
|
||||||
event.NewEvent(pi.RKind, pi.RNamespace, pi.RName, event.PolicyApplied, event.SRulesApply, ruleNames, pi.Name))
|
|
||||||
|
|
||||||
glog.V(3).Infof("Success event info has prepared for %s/%s\n", pi.RKind, pi.RName)
|
|
||||||
}
|
|
||||||
return eventsInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pi := range policyInfoList {
|
|
||||||
ruleNames := getRuleNames(*pi, false)
|
|
||||||
|
|
||||||
eventsInfo = append(eventsInfo,
|
|
||||||
event.NewEvent(policyKind, "", pi.Name, event.RequestBlocked, event.FPolicyApplyBlockCreate, pi.RName, ruleNames))
|
|
||||||
|
|
||||||
glog.V(3).Infof("Rule(s) %s of policy %s blocked resource creation, error: %s\n", ruleNames, pi.Name, msg)
|
|
||||||
}
|
|
||||||
return eventsInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRuleNames(policyInfo info.PolicyInfo, onSuccess bool) string {
|
|
||||||
var ruleNames []string
|
|
||||||
for _, rule := range policyInfo.Rules {
|
|
||||||
if onSuccess {
|
|
||||||
if rule.IsSuccessful() {
|
|
||||||
ruleNames = append(ruleNames, rule.Name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !rule.IsSuccessful() {
|
|
||||||
ruleNames = append(ruleNames, rule.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(ruleNames, ",")
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,34 @@
|
||||||
package webhooks
|
package webhooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
"github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
"github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||||
|
"github.com/nirmata/kyverno/pkg/info"
|
||||||
|
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const policyKind = "Policy"
|
||||||
|
|
||||||
|
func isAdmSuccesful(policyInfos []*info.PolicyInfo) (bool, string) {
|
||||||
|
var admSuccess = true
|
||||||
|
var errMsgs []string
|
||||||
|
for _, pi := range policyInfos {
|
||||||
|
if !pi.IsSuccessful() {
|
||||||
|
admSuccess = false
|
||||||
|
errMsgs = append(errMsgs, fmt.Sprintf("\nPolicy %s failed with following rules", pi.Name))
|
||||||
|
// Get the error rules
|
||||||
|
errorRules := pi.ErrorRules()
|
||||||
|
errMsgs = append(errMsgs, errorRules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return admSuccess, strings.Join(errMsgs, ";")
|
||||||
|
}
|
||||||
|
|
||||||
//StringInSlice checks if string is present in slice of strings
|
//StringInSlice checks if string is present in slice of strings
|
||||||
func StringInSlice(kind string, list []string) bool {
|
func StringInSlice(kind string, list []string) bool {
|
||||||
for _, b := range list {
|
for _, b := range list {
|
||||||
|
@ -63,3 +86,50 @@ func getApplicableKindsForPolicy(p *v1alpha1.Policy) []string {
|
||||||
}
|
}
|
||||||
return kinds
|
return kinds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Policy Reporting Modes
|
||||||
|
const (
|
||||||
|
BlockChanges = "block"
|
||||||
|
ReportViolation = "report"
|
||||||
|
)
|
||||||
|
|
||||||
|
// returns true -> if there is even one policy that blocks resource requst
|
||||||
|
// returns false -> if all the policies are meant to report only, we dont block resource request
|
||||||
|
func toBlock(pis []*info.PolicyInfo) bool {
|
||||||
|
for _, pi := range pis {
|
||||||
|
if pi.ValidationFailureAction != ReportViolation {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIfOnlyAnnotationsUpdate(request *v1beta1.AdmissionRequest) bool {
|
||||||
|
// process only if its for existing resources
|
||||||
|
if request.Operation != v1beta1.Update {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// updated resoruce
|
||||||
|
obj := request.Object
|
||||||
|
objUnstr := unstructured.Unstructured{}
|
||||||
|
err := objUnstr.UnmarshalJSON(obj.Raw)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
objUnstr.SetAnnotations(nil)
|
||||||
|
objUnstr.SetGeneration(0)
|
||||||
|
oldobj := request.OldObject
|
||||||
|
oldobjUnstr := unstructured.Unstructured{}
|
||||||
|
err = oldobjUnstr.UnmarshalJSON(oldobj.Raw)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
oldobjUnstr.SetAnnotations(nil)
|
||||||
|
oldobjUnstr.SetGeneration(0)
|
||||||
|
if reflect.DeepEqual(objUnstr, oldobjUnstr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
126
pkg/webhooks/validation.go
Normal file
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"
|
message: "This SS is broken"
|
||||||
pattern:
|
pattern:
|
||||||
spec:
|
spec:
|
||||||
replicas: ">20"
|
replicas: ">2"
|
||||||
volumeClaimTemplates:
|
volumeClaimTemplates:
|
||||||
- metadata:
|
- metadata:
|
||||||
name: www
|
name: www
|
||||||
|
|
Loading…
Add table
Reference in a new issue