mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-05 07:26:55 +00:00
NK-10: Refactored policy types, used patch operation struct from there instead of internal struct.
Implemented checking of incoming request to correspond the policy rule, added tests. Implemented generation of JSON patches according to patches in policy object, added tests. Implemented base version of Mutate function as a wrapper for all mutation functions.
This commit is contained in:
parent
ea9491a105
commit
eb5c486ae1
7 changed files with 299 additions and 52 deletions
|
@ -1,8 +1,7 @@
|
|||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +genclient
|
||||
|
@ -10,62 +9,63 @@ import (
|
|||
|
||||
// Policy is a specification for a Policy resource
|
||||
type Policy struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Spec PolicySpec `json:"spec"`
|
||||
Status PolicyStatus `json:"status"`
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Spec PolicySpec `json:"spec"`
|
||||
Status PolicyStatus `json:"status"`
|
||||
}
|
||||
|
||||
// PolicySpec is the spec for a Policy resource
|
||||
type PolicySpec struct {
|
||||
FailurePolicy *string `json:"failurePolicy"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
FailurePolicy *string `json:"failurePolicy"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// PolicyRule is policy rule that will be applied to resource
|
||||
type PolicyRule struct {
|
||||
Resource PolicyResource `json:"resource"`
|
||||
Patches []PolicyPatch `json:"patches"`
|
||||
Generators []PolicyConfigGenerator `json:"generator"`
|
||||
Resource PolicyResource `json:"resource"`
|
||||
Patches *[]PolicyPatch `json:"patches"`
|
||||
ConfigMapGenerator *PolicyConfigGenerator `json:"configMapGenerator"`
|
||||
SecretGenerator *PolicyConfigGenerator `json:"secretGenerator"`
|
||||
}
|
||||
|
||||
// PolicyResource describes the resource rule applied to
|
||||
type PolicyResource struct {
|
||||
Kind string `json:"kind"`
|
||||
Name *string `json:"name"`
|
||||
Selector *metav1.LabelSelector `json:"selector"`
|
||||
Kind string `json:"kind"`
|
||||
Name *string `json:"name"`
|
||||
Selector *metav1.LabelSelector `json:"selector"`
|
||||
}
|
||||
|
||||
// PolicyPatch is TODO
|
||||
type PolicyPatch struct {
|
||||
Path string `json:"path"`
|
||||
Operation string `json:"operation"`
|
||||
Value int `json:"value"`
|
||||
Path string `json:"path"`
|
||||
Operation string `json:"op"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// PolicyConfigGenerator is TODO
|
||||
type PolicyConfigGenerator struct {
|
||||
Name string `json:"name"`
|
||||
CopyFrom *PolicyCopyFrom `json:"copyFrom"`
|
||||
Data map[string]string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
CopyFrom *PolicyCopyFrom `json:"copyFrom"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
// PolicyCopyFrom is TODO
|
||||
type PolicyCopyFrom struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// PolicyStatus is the status for a Policy resource
|
||||
type PolicyStatus struct {
|
||||
Logs []string `json:"log"`
|
||||
Logs []string `json:"log"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// PolicyList is a list of Policy resources
|
||||
type PolicyList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
Items []Policy `json:"items"`
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
Items []Policy `json:"items"`
|
||||
}
|
||||
|
|
|
@ -25,16 +25,6 @@ type WebhookServer struct {
|
|||
mutationWebhook *webhooks.MutationWebhook
|
||||
}
|
||||
|
||||
type patchOperations struct {
|
||||
patches []patchOperation
|
||||
}
|
||||
|
||||
type patchOperation struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// NewWebhookServer creates new instance of WebhookServer and configures it
|
||||
func NewWebhookServer(certFile string, keyFile string, controller *controller.PolicyController, logger *log.Logger) *WebhookServer {
|
||||
if logger == nil {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package webhooks
|
||||
|
||||
import "k8s.io/api/admission/v1beta1"
|
||||
import (
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
var supportedKinds = [...]string{
|
||||
"ConfigMap",
|
||||
|
@ -36,3 +39,17 @@ func AdmissionIsRequired(request *v1beta1.AdmissionRequest) bool {
|
|||
// Here you can make additional hardcoded checks
|
||||
return kindIsSupported(request.Kind.Kind)
|
||||
}
|
||||
|
||||
func IsRuleApplicableToRequest(rule types.PolicyRule, request *v1beta1.AdmissionRequest) bool {
|
||||
return IsRuleResourceFitsRequest(rule.Resource, request)
|
||||
}
|
||||
|
||||
func IsRuleResourceFitsRequest(resource types.PolicyResource, request *v1beta1.AdmissionRequest) bool {
|
||||
if resource.Kind != request.Kind.Kind {
|
||||
return false
|
||||
}
|
||||
if resource.Name != nil && *resource.Name != request.Name {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
87
webhooks/admission_test.go
Normal file
87
webhooks/admission_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAdmissionIsRequired(t *testing.T) {
|
||||
var request v1beta1.AdmissionRequest
|
||||
request.Kind.Kind = "ConfigMap"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "CronJob"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "DaemonSet"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Deployment"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Endpoint"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "HorizontalPodAutoscaler"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Ingress"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Job"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "LimitRange"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Namespace"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "NetworkPolicy"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PersistentVolumeClaim"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PodDisruptionBudget"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "PodTemplate"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "ResourceQuota"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Secret"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "Service"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
request.Kind.Kind = "StatefulSet"
|
||||
assertEq(t, true, webhooks.AdmissionIsRequired(&request))
|
||||
}
|
||||
|
||||
func TestIsRuleResourceFitsRequest_Kind(t *testing.T) {
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
}
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
}
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleResourceFitsRequest(resource, &request))
|
||||
resource.Kind = "Deployment"
|
||||
assertEq(t, false, webhooks.IsRuleResourceFitsRequest(resource, &request))
|
||||
}
|
||||
|
||||
func TestIsRuleResourceFitsRequest_Name(t *testing.T) {
|
||||
resourceName := "test-config-map"
|
||||
resource := types.PolicyResource{
|
||||
Kind: "ConfigMap",
|
||||
Name: &resourceName,
|
||||
}
|
||||
request := v1beta1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Kind: "ConfigMap"},
|
||||
Name: "test-config-map",
|
||||
}
|
||||
|
||||
assertEq(t, true, webhooks.IsRuleResourceFitsRequest(resource, &request))
|
||||
resourceName = "test-config-map-new"
|
||||
assertEq(t, false, webhooks.IsRuleResourceFitsRequest(resource, &request))
|
||||
request.Name = "test-config-map-new"
|
||||
assertEq(t, true, webhooks.IsRuleResourceFitsRequest(resource, &request))
|
||||
request.Name = ""
|
||||
assertEq(t, false, webhooks.IsRuleResourceFitsRequest(resource, &request))
|
||||
}
|
||||
|
||||
func TestIsRuleApplicableToRequest(t *testing.T) {
|
||||
// TODO
|
||||
}
|
|
@ -8,7 +8,6 @@ import (
|
|||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
coreTypes "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
type MutationWebhook struct {
|
||||
|
@ -30,23 +29,44 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies []
|
|||
return nil
|
||||
}
|
||||
|
||||
var configMap coreTypes.ConfigMap
|
||||
if err := json.Unmarshal(request.Object.Raw, &configMap); err != nil {
|
||||
mw.logger.Printf("Could not unmarshal raw object: %v", err)
|
||||
return errorToResponse(err)
|
||||
var allPatches []types.PolicyPatch
|
||||
for _, policy := range policies {
|
||||
var stopOnError bool = true
|
||||
if policy.Spec.FailurePolicy != nil && *policy.Spec.FailurePolicy == "continueOnError" {
|
||||
stopOnError = false
|
||||
}
|
||||
|
||||
for ruleIdx, rule := range policy.Spec.Rules {
|
||||
if IsRuleApplicableToRequest(rule, request) {
|
||||
mw.logger.Printf("Applying policy %v, rule index = %v", policy.ObjectMeta.Name, ruleIdx)
|
||||
rulePatches, err := mw.applyPolicyRule(request, rule)
|
||||
/*
|
||||
* If at least one error is detected in the rule, the entire rule will not be applied.
|
||||
* This may be changed in the future by varying the policy.Spec.FailurePolicy values.
|
||||
*/
|
||||
if err != nil {
|
||||
mw.logger.Printf("Error occurred while applying the policy: %v", err)
|
||||
if stopOnError {
|
||||
mw.logger.Printf("/!\\ Denying the request according to FailurePolicy spec /!\\")
|
||||
return errorToResponse(err, false)
|
||||
}
|
||||
} else {
|
||||
mw.logger.Printf("Prepared %v patches", len(rulePatches))
|
||||
allPatches = append(allPatches, rulePatches...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patchesBytes, err := SerializePatches(allPatches)
|
||||
if err != nil {
|
||||
mw.logger.Printf("Error occerred while serializing JSONPathch: %v", err)
|
||||
return errorToResponse(err, true)
|
||||
}
|
||||
/*patch := patchOperation{
|
||||
Path: "/labels",
|
||||
Op: "add",
|
||||
Value: map[string]string{
|
||||
"is-mutated": "true",
|
||||
},
|
||||
}*/
|
||||
patch := `[ {"op":"add","path":"/metadata/labels","value":{"is-mutated":"true"}} ]`
|
||||
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: []byte(patch),
|
||||
Patch: patchesBytes,
|
||||
PatchType: func() *v1beta1.PatchType {
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
return &pt
|
||||
|
@ -54,10 +74,57 @@ func (mw *MutationWebhook) Mutate(request *v1beta1.AdmissionRequest, policies []
|
|||
}
|
||||
}
|
||||
|
||||
func errorToResponse(err error) *v1beta1.AdmissionResponse {
|
||||
// Applies all possible patches in a rule
|
||||
func (mw *MutationWebhook) applyPolicyRule(request *v1beta1.AdmissionRequest, rule types.PolicyRule) ([]types.PolicyPatch, error) {
|
||||
var allPatches []types.PolicyPatch
|
||||
if rule.Patches == nil && rule.ConfigMapGenerator == nil && rule.SecretGenerator == nil {
|
||||
return nil, errors.New("The rule is empty!")
|
||||
}
|
||||
|
||||
if rule.Patches != nil {
|
||||
for _, patch := range *rule.Patches {
|
||||
allPatches = append(allPatches, patch)
|
||||
}
|
||||
}
|
||||
|
||||
if rule.ConfigMapGenerator != nil {
|
||||
// TODO: Make patches from configMapGenerator and add them to returned array
|
||||
}
|
||||
|
||||
if rule.SecretGenerator != nil {
|
||||
// TODO: Make patches from secretGenerator and add them to returned array
|
||||
}
|
||||
|
||||
return allPatches, nil
|
||||
}
|
||||
|
||||
func SerializePatches(patches []types.PolicyPatch) ([]byte, error) {
|
||||
var result []byte
|
||||
result = append(result, []byte("[\n")...)
|
||||
for index, patch := range patches {
|
||||
if patch.Operation == "" || patch.Path == "" {
|
||||
return nil, errors.New("JSONPatch doesn't contain mandatory fields 'path' or 'op'")
|
||||
}
|
||||
|
||||
patchBytes, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, patchBytes...)
|
||||
if index != (len(patches) - 1) {
|
||||
result = append(result, []byte(",\n")...)
|
||||
}
|
||||
}
|
||||
result = append(result, []byte("\n]")...)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func errorToResponse(err error, allowed bool) *v1beta1.AdmissionResponse {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
Allowed: allowed,
|
||||
}
|
||||
}
|
||||
|
|
48
webhooks/mutation_test.go
Normal file
48
webhooks/mutation_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nirmata/kube-policy/webhooks"
|
||||
|
||||
//v1beta1 "k8s.io/api/admission/v1beta1"
|
||||
//metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1"
|
||||
)
|
||||
|
||||
func TestSerializePatches_Empty(t *testing.T) {
|
||||
var patches []types.PolicyPatch
|
||||
bytes, err := webhooks.SerializePatches(patches)
|
||||
assertEq(t, nil, err)
|
||||
assertEqStringAndData(t, "[\n\n]", bytes)
|
||||
}
|
||||
|
||||
func TestSerializePatches_SingleValid(t *testing.T) {
|
||||
patch := types.PolicyPatch{
|
||||
Path: "/metadata/labels/is-mutated",
|
||||
Operation: "add",
|
||||
Value: "true",
|
||||
}
|
||||
patches := []types.PolicyPatch{patch}
|
||||
bytes, err := webhooks.SerializePatches(patches)
|
||||
assertEq(t, nil, err)
|
||||
assertEqStringAndData(t, `[
|
||||
{"path":"/metadata/labels/is-mutated","op":"add","value":"true"}
|
||||
]`, bytes)
|
||||
}
|
||||
|
||||
func TestSerializePatches_SingleInvalid(t *testing.T) {
|
||||
patch := types.PolicyPatch{
|
||||
Path: "/metadata/labels/is-mutated",
|
||||
Value: "true",
|
||||
}
|
||||
patches := []types.PolicyPatch{patch}
|
||||
_, err := webhooks.SerializePatches(patches)
|
||||
assertNe(t, nil, err)
|
||||
patches[0].Path = ""
|
||||
patches[0].Operation = "delete"
|
||||
_, err = webhooks.SerializePatches(patches)
|
||||
assertNe(t, nil, err)
|
||||
}
|
||||
|
||||
// patch := `[ {"op":"add","path":"/metadata/labels","value":{"is-mutated":"true"}} ]`
|
38
webhooks/utils_test.go
Normal file
38
webhooks/utils_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package webhooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertEq(t *testing.T, expected interface{}, actual interface{}) {
|
||||
if expected != actual {
|
||||
t.Errorf("%s != %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNe(t *testing.T, expected interface{}, actual interface{}) {
|
||||
if expected == actual {
|
||||
t.Errorf("%s != %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqDataImpl(t *testing.T, expected, actual []byte, formatModifier string) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("len(expected) != len(actual): %d != %d\n1:"+formatModifier+"\n2:"+formatModifier, len(expected), len(actual), expected, actual)
|
||||
return
|
||||
}
|
||||
|
||||
for idx, val := range actual {
|
||||
if val != expected[idx] {
|
||||
t.Errorf("Slices not equal at index %d:\n1:"+formatModifier+"\n2:"+formatModifier, idx, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqData(t *testing.T, expected, actual []byte) {
|
||||
assertEqDataImpl(t, expected, actual, "%x")
|
||||
}
|
||||
|
||||
func assertEqStringAndData(t *testing.T, str string, data []byte) {
|
||||
assertEqDataImpl(t, []byte(str), data, "%s")
|
||||
}
|
Loading…
Add table
Reference in a new issue