1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-05 15:37:19 +00:00
kyverno/pkg/webhooks/policymutation.go
Shuting Zhao ad4f06f22d Merge branch 'master' into 744_deny_requests
# Conflicts:
#	pkg/webhooks/mutation.go
#	pkg/webhooks/server.go
#	pkg/webhooks/validation.go
2020-05-18 12:32:42 -07:00

425 lines
13 KiB
Go

package webhooks
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/go-logr/logr"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/utils"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (ws *WebhookServer) policyMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
logger := ws.log.WithValues("action", "policymutation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation)
var policy *kyverno.ClusterPolicy
raw := request.Object.Raw
//TODO: can this happen? wont this be picked by OpenAPI spec schema ?
if err := json.Unmarshal(raw, &policy); err != nil {
logger.Error(err, "faield to unmarshall policy admission request")
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Message: fmt.Sprintf("failed to default value, check kyverno controller logs for details: %v", err),
},
}
}
// Generate JSON Patches for defaults
patches, updateMsgs := generateJSONPatchesForDefaults(policy, logger)
if patches != nil {
patchType := v1beta1.PatchTypeJSONPatch
return &v1beta1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Message: strings.Join(updateMsgs, "'"),
},
Patch: patches,
PatchType: &patchType,
}
}
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
func generateJSONPatchesForDefaults(policy *kyverno.ClusterPolicy, log logr.Logger) ([]byte, []string) {
var patches [][]byte
var updateMsgs []string
// default 'ValidationFailureAction'
if patch, updateMsg := defaultvalidationFailureAction(policy, log); patch != nil {
patches = append(patches, patch)
updateMsgs = append(updateMsgs, updateMsg)
}
// default 'Background'
if patch, updateMsg := defaultBackgroundFlag(policy, log); patch != nil {
patches = append(patches, patch)
updateMsgs = append(updateMsgs, updateMsg)
}
patch, errs := generatePodControllerRule(*policy, log)
if len(errs) > 0 {
var errMsgs []string
for _, err := range errs {
errMsgs = append(errMsgs, err.Error())
log.Error(err, "failed to generate pod controller rule")
}
updateMsgs = append(updateMsgs, strings.Join(errMsgs, ";"))
}
patches = append(patches, patch...)
return utils.JoinPatches(patches), updateMsgs
}
func defaultBackgroundFlag(policy *kyverno.ClusterPolicy, log logr.Logger) ([]byte, string) {
// set 'Background' flag to 'true' if not specified
defaultVal := true
if policy.Spec.Background == nil {
log.V(4).Info("setting default value", "spec.background", true)
jsonPatch := struct {
Path string `json:"path"`
Op string `json:"op"`
Value *bool `json:"value"`
}{
"/spec/background",
"add",
&defaultVal,
}
patchByte, err := json.Marshal(jsonPatch)
if err != nil {
log.Error(err, "failed to set default value", "spec.background", true)
return nil, ""
}
log.V(3).Info("generated JSON Patch to set default", "spec.background", true)
return patchByte, fmt.Sprintf("default 'Background' to '%s'", strconv.FormatBool(true))
}
return nil, ""
}
func defaultvalidationFailureAction(policy *kyverno.ClusterPolicy, log logr.Logger) ([]byte, string) {
// set ValidationFailureAction to "audit" if not specified
if policy.Spec.ValidationFailureAction == "" {
log.V(4).Info("setting defautl value", "spec.validationFailureAction", Audit)
jsonPatch := struct {
Path string `json:"path"`
Op string `json:"op"`
Value string `json:"value"`
}{
"/spec/validationFailureAction",
"add",
Audit,
}
patchByte, err := json.Marshal(jsonPatch)
if err != nil {
log.Error(err, "failed to default value", "spec.validationFailureAction", Audit)
return nil, ""
}
log.V(3).Info("generated JSON Patch to set default", "spec.validationFailureAction", Audit)
return patchByte, fmt.Sprintf("default 'ValidationFailureAction' to '%s'", Audit)
}
return nil, ""
}
// podControllersKey annotation could be:
// scenario A: not exist, set default to "all", which generates on all pod controllers
// - if name / selector exist in resource description -> skip
// as these fields may not be applicable to pod controllers
// scenario B: "none", user explicitly disable this feature -> skip
// scenario C: some certain controllers that user set -> generate on defined controllers
// copy entrie match / exclude block, it's users' responsibility to
// make sure all fields are applicable to pod cotrollers
// generatePodControllerRule returns two patches: rulePatches and annotation patch(if necessary)
func generatePodControllerRule(policy kyverno.ClusterPolicy, log logr.Logger) (patches [][]byte, errs []error) {
ann := policy.GetAnnotations()
controllers, ok := ann[engine.PodControllersAnnotation]
// scenario A
if !ok {
controllers = "DaemonSet,Deployment,Job,StatefulSet"
annPatch, err := defaultPodControllerAnnotation(ann)
if err != nil {
errs = append(errs, fmt.Errorf("failed to generate pod controller annotation for policy '%s': %v", policy.Name, err))
} else {
patches = append(patches, annPatch)
}
}
// scenario B
if controllers == "none" {
return nil, nil
}
log.V(3).Info("auto generating rule for pod controllers", "controlers", controllers)
p, err := generateRulePatches(policy, controllers, log)
patches = append(patches, p...)
errs = append(errs, err...)
return
}
func createRuleMap(rules []kyverno.Rule) map[string]kyvernoRule {
var ruleMap = make(map[string]kyvernoRule)
for _, rule := range rules {
var jsonFriendlyStruct kyvernoRule
jsonFriendlyStruct.Name = rule.Name
if !reflect.DeepEqual(rule.MatchResources, kyverno.MatchResources{}) {
jsonFriendlyStruct.MatchResources = rule.MatchResources.DeepCopy()
}
if !reflect.DeepEqual(rule.ExcludeResources, kyverno.ExcludeResources{}) {
jsonFriendlyStruct.ExcludeResources = rule.ExcludeResources.DeepCopy()
}
if !reflect.DeepEqual(rule.Mutation, kyverno.Mutation{}) {
jsonFriendlyStruct.Mutation = rule.Mutation.DeepCopy()
}
if !reflect.DeepEqual(rule.Validation, kyverno.Validation{}) {
jsonFriendlyStruct.Validation = rule.Validation.DeepCopy()
}
ruleMap[rule.Name] = jsonFriendlyStruct
}
return ruleMap
}
// generateRulePatches generates rule for podControllers based on scenario A and C
func generateRulePatches(policy kyverno.ClusterPolicy, controllers string, log logr.Logger) (rulePatches [][]byte, errs []error) {
var genRule kyvernoRule
insertIdx := len(policy.Spec.Rules)
ruleMap := createRuleMap(policy.Spec.Rules)
var ruleIndex = make(map[string]int)
for index, rule := range policy.Spec.Rules {
ruleIndex[rule.Name] = index
}
for _, rule := range policy.Spec.Rules {
patchPostion := insertIdx
genRule = generateRuleForControllers(rule, controllers, log)
if reflect.DeepEqual(genRule, kyvernoRule{}) {
continue
}
operation := "add"
if existingAutoGenRule, alreadyExists := ruleMap[genRule.Name]; alreadyExists {
existingAutoGenRuleRaw, _ := json.Marshal(existingAutoGenRule)
genRuleRaw, _ := json.Marshal(genRule)
if string(existingAutoGenRuleRaw) == string(genRuleRaw) {
continue
}
operation = "replace"
patchPostion = ruleIndex[genRule.Name]
}
// generate patch bytes
jsonPatch := struct {
Path string `json:"path"`
Op string `json:"op"`
Value interface{} `json:"value"`
}{
fmt.Sprintf("/spec/rules/%s", strconv.Itoa(patchPostion)),
operation,
genRule,
}
pbytes, err := json.Marshal(jsonPatch)
if err != nil {
errs = append(errs, err)
continue
}
// check the patch
if _, err := jsonpatch.DecodePatch([]byte("[" + string(pbytes) + "]")); err != nil {
errs = append(errs, err)
continue
}
rulePatches = append(rulePatches, pbytes)
insertIdx++
}
return
}
// the kyvernoRule holds the temporary kyverno rule struct
// each field is a pointer to the the actual object
// when serilizing data, we would expect to drop the omitempty key
// otherwise (without the pointer), it will be set to empty value
// - an empty struct in this case, some may fail the schema validation
// TODO(shuting) may related to:
// https://github.com/nirmata/kyverno/pull/549#discussion_r360088556
// https://github.com/nirmata/kyverno/issues/568
type kyvernoRule struct {
Name string `json:"name"`
MatchResources *kyverno.MatchResources `json:"match"`
ExcludeResources *kyverno.ExcludeResources `json:"exclude,omitempty"`
Mutation *kyverno.Mutation `json:"mutate,omitempty"`
Validation *kyverno.Validation `json:"validate,omitempty"`
}
func generateRuleForControllers(rule kyverno.Rule, controllers string, log logr.Logger) kyvernoRule {
if strings.HasPrefix(rule.Name, "autogen-") {
return kyvernoRule{}
}
match := rule.MatchResources
exclude := rule.ExcludeResources
if !utils.ContainsString(match.ResourceDescription.Kinds, "Pod") ||
(len(exclude.ResourceDescription.Kinds) != 0 && !utils.ContainsString(exclude.ResourceDescription.Kinds, "Pod")) {
return kyvernoRule{}
}
if rule.Mutation.Overlay == nil && !rule.HasValidate() {
return kyvernoRule{}
}
// Support backword compatibility
skipAutoGeneration := false
var controllersValidated []string
if controllers == "all" {
skipAutoGeneration = true
} else if controllers != "none" && controllers != "all" {
controllersList := map[string]int{"DaemonSet": 1, "Deployment": 1, "Job": 1, "StatefulSet": 1}
for _, value := range strings.Split(controllers, ",") {
if _, ok := controllersList[value]; ok {
controllersValidated = append(controllersValidated, value)
}
}
if len(controllersValidated) > 0 {
skipAutoGeneration = true
}
}
if skipAutoGeneration {
if match.ResourceDescription.Name != "" || match.ResourceDescription.Selector != nil ||
exclude.ResourceDescription.Name != "" || exclude.ResourceDescription.Selector != nil {
log.Info("skip generating rule on pod controllers: Name / Selector in resource decription may not be applicable.", "rule", rule.Name)
return kyvernoRule{}
}
if controllers == "all" {
controllers = engine.PodControllers
} else {
controllers = strings.Join(controllersValidated, ",")
}
}
controllerRule := &kyvernoRule{
Name: fmt.Sprintf("autogen-%s", rule.Name),
MatchResources: match.DeepCopy(),
}
// overwrite Kinds by pod controllers defined in the annotation
controllerRule.MatchResources.Kinds = strings.Split(controllers, ",")
if len(exclude.Kinds) != 0 {
controllerRule.ExcludeResources = exclude.DeepCopy()
controllerRule.ExcludeResources.Kinds = strings.Split(controllers, ",")
}
if rule.Mutation.Overlay != nil {
newMutation := &kyverno.Mutation{
Overlay: map[string]interface{}{
"spec": map[string]interface{}{
"template": rule.Mutation.Overlay,
},
},
}
controllerRule.Mutation = newMutation.DeepCopy()
return *controllerRule
}
if rule.Validation.Pattern != nil {
newValidate := &kyverno.Validation{
Message: rule.Validation.Message,
Pattern: map[string]interface{}{
"spec": map[string]interface{}{
"template": rule.Validation.Pattern,
},
},
}
controllerRule.Validation = newValidate.DeepCopy()
return *controllerRule
}
if len(rule.Validation.AnyPattern) != 0 {
var patterns []interface{}
for _, pattern := range rule.Validation.AnyPattern {
newPattern := map[string]interface{}{
"spec": map[string]interface{}{
"template": pattern,
},
}
patterns = append(patterns, newPattern)
}
controllerRule.Validation = &kyverno.Validation{
Message: rule.Validation.Message,
AnyPattern: patterns,
}
return *controllerRule
}
return kyvernoRule{}
}
// defaultPodControllerAnnotation generates annotation "pod-policies.kyverno.io/autogen-controllers=all"
// ann passes in the annotation of the policy
func defaultPodControllerAnnotation(ann map[string]string) ([]byte, error) {
if ann == nil {
ann = make(map[string]string)
ann[engine.PodControllersAnnotation] = "DaemonSet,Deployment,Job,StatefulSet"
jsonPatch := struct {
Path string `json:"path"`
Op string `json:"op"`
Value interface{} `json:"value"`
}{
"/metadata/annotations",
"add",
ann,
}
patchByte, err := json.Marshal(jsonPatch)
if err != nil {
return nil, err
}
return patchByte, nil
}
jsonPatch := struct {
Path string `json:"path"`
Op string `json:"op"`
Value interface{} `json:"value"`
}{
"/metadata/annotations/pod-policies.kyverno.io~1autogen-controllers",
"add",
"DaemonSet,Deployment,Job,StatefulSet",
}
patchByte, err := json.Marshal(jsonPatch)
if err != nil {
return nil, err
}
return patchByte, nil
}