1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

refactor validating admission policies ()

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>
This commit is contained in:
Mariam Fahmy 2023-07-28 03:32:30 +03:00 committed by GitHub
parent 3ed7303efe
commit 34a6119cc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 483 additions and 264 deletions

View file

@ -345,6 +345,7 @@ func (c *ApplyCommandConfig) applyCommandHelper() (*common.ResultCounts, []*unst
for _, policy := range policies {
policyRulesCount += len(autogen.ComputeRules(policy))
}
policyRulesCount += len(validatingAdmissionPolicies)
fmt.Printf("\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources))
}

View file

@ -176,8 +176,8 @@ func Test_Apply(t *testing.T) {
},
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../test/cli/test-validating-admission-policy/validate-deployment/policy.yaml"},
ResourcePaths: []string{"../../../../test/cli/test-validating-admission-policy/validate-deployment/deployment1.yaml"},
PolicyPaths: []string{"../../../../test/cli/test-vap/check-deployments-replica/policy.yaml"},
ResourcePaths: []string{"../../../../test/cli/test-vap/check-deployments-replica/deployment1.yaml"},
PolicyReport: true,
},
expectedPolicyReports: []preport.PolicyReport{
@ -194,8 +194,44 @@ func Test_Apply(t *testing.T) {
},
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../test/cli/test-validating-admission-policy/validate-deployment/policy.yaml"},
ResourcePaths: []string{"../../../../test/cli/test-validating-admission-policy/validate-deployment/deployment2.yaml"},
PolicyPaths: []string{"../../../../test/cli/test-vap/check-deployments-replica/policy.yaml"},
ResourcePaths: []string{"../../../../test/cli/test-vap/check-deployments-replica/deployment2.yaml"},
PolicyReport: true,
},
expectedPolicyReports: []preport.PolicyReport{
{
Summary: preport.PolicyReportSummary{
Pass: 0,
Fail: 1,
Skip: 0,
Error: 0,
Warn: 0,
},
},
},
},
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../test/cli/test-vap/disallow-host-path/policy.yaml"},
ResourcePaths: []string{"../../../../test/cli/test-vap/disallow-host-path/pod1.yaml"},
PolicyReport: true,
},
expectedPolicyReports: []preport.PolicyReport{
{
Summary: preport.PolicyReportSummary{
Pass: 1,
Fail: 0,
Skip: 0,
Error: 0,
Warn: 0,
},
},
},
},
{
config: ApplyCommandConfig{
PolicyPaths: []string{"../../../../test/cli/test-vap/disallow-host-path/policy.yaml"},
ResourcePaths: []string{"../../../../test/cli/test-vap/disallow-host-path/pod2.yaml"},
PolicyReport: true,
},
expectedPolicyReports: []preport.PolicyReport{

View file

@ -64,10 +64,10 @@ func buildPolicyResults(auditWarn bool, engineResponses ...engineapi.EngineRespo
isVAP := engineResponse.IsValidatingAdmissionPolicy()
if isVAP {
validatingAdmissionPolicy := engineResponse.ValidatingAdmissionPolicy()
ns = validatingAdmissionPolicy.GetNamespace()
policyName = validatingAdmissionPolicy.GetName()
ann = validatingAdmissionPolicy.GetAnnotations()
vap := engineResponse.ValidatingAdmissionPolicy()
ns = vap.GetNamespace()
policyName = vap.GetName()
ann = vap.GetAnnotations()
} else {
kyvernoPolicy := engineResponse.Policy()
ns = kyvernoPolicy.GetNamespace()

View file

@ -21,10 +21,10 @@ type TestResults struct {
// It's required in case policy is a kyverno policy.
// +optional
Rule string `json:"rule,omitempty"`
// IsVap indicates if the policy is a validating admission policy.
// IsValidatingAdmissionPolicy indicates if the policy is a validating admission policy.
// It's required in case policy is a validating admission policy.
// +optional
IsVap bool `json:"isVap"`
IsValidatingAdmissionPolicy bool `json:"isValidatingAdmissionPolicy"`
// Result mentions the result that the user is expecting.
// Possible values are pass, fail and skip.
Result policyreportv1alpha2.PolicyResult `json:"result"`

View file

@ -136,7 +136,7 @@ func applyPoliciesFromPath(
for _, rule := range autogen.ComputeRules(p) {
for _, res := range values.Results {
if res.IsVap {
if res.IsValidatingAdmissionPolicy {
continue
}
@ -340,10 +340,10 @@ func buildPolicyResults(
isVAP := resp.IsValidatingAdmissionPolicy()
if isVAP {
validatingAdmissionPolicy := resp.ValidatingAdmissionPolicy()
ns = validatingAdmissionPolicy.GetNamespace()
name = validatingAdmissionPolicy.GetName()
ann = validatingAdmissionPolicy.GetAnnotations()
vap := resp.ValidatingAdmissionPolicy()
ns = vap.GetNamespace()
name = vap.GetName()
ann = vap.GetAnnotations()
} else {
kyvernoPolicy := resp.Policy()
ns = kyvernoPolicy.GetNamespace()
@ -398,8 +398,8 @@ func buildPolicyResults(
for _, resource := range test.Resources {
if resource == resourceName {
var resultsKey string
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource, test.IsVap)
if !test.IsVap {
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource, test.IsValidatingAdmissionPolicy)
if !test.IsValidatingAdmissionPolicy {
if !slices.Contains(rules, test.Rule) {
if !slices.Contains(rules, "autogen-"+test.Rule) {
if !slices.Contains(rules, "autogen-cronjob-"+test.Rule) {
@ -407,12 +407,12 @@ func buildPolicyResults(
} else {
testResults[i].AutoGeneratedRule = "autogen-cronjob"
test.Rule = "autogen-cronjob-" + test.Rule
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource, test.IsVap)
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource, test.IsValidatingAdmissionPolicy)
}
} else {
testResults[i].AutoGeneratedRule = "autogen"
test.Rule = "autogen-" + test.Rule
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource, test.IsVap)
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource, test.IsValidatingAdmissionPolicy)
}
if results[resultsKey].Result == "" {
@ -434,8 +434,8 @@ func buildPolicyResults(
if test.Resource != "" {
if test.Policy == policyName && test.Resource == resourceName {
var resultsKey string
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource, test.IsVap)
if !test.IsVap {
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource, test.IsValidatingAdmissionPolicy)
if !test.IsValidatingAdmissionPolicy {
if !slices.Contains(rules, test.Rule) {
if !slices.Contains(rules, "autogen-"+test.Rule) {
if !slices.Contains(rules, "autogen-cronjob-"+test.Rule) {
@ -443,12 +443,12 @@ func buildPolicyResults(
} else {
testResults[i].AutoGeneratedRule = "autogen-cronjob"
test.Rule = "autogen-cronjob-" + test.Rule
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource, test.IsVap)
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource, test.IsValidatingAdmissionPolicy)
}
} else {
testResults[i].AutoGeneratedRule = "autogen"
test.Rule = "autogen-" + test.Rule
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource, test.IsVap)
resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource, test.IsValidatingAdmissionPolicy)
}
if results[resultsKey].Result == "" {
@ -474,7 +474,7 @@ func buildPolicyResults(
var resultsKey []string
var resultKey string
var result policyreportv1alpha2.PolicyReportResult
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName, test.IsVap)
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName, test.IsValidatingAdmissionPolicy)
for _, key := range resultsKey {
if val, ok := results[key]; ok {
result = val
@ -507,7 +507,7 @@ func buildPolicyResults(
var resultsKey []string
var resultKey string
var result policyreportv1alpha2.PolicyReportResult
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName, test.IsVap)
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName, test.IsValidatingAdmissionPolicy)
for _, key := range resultsKey {
if val, ok := results[key]; ok {
result = val
@ -537,14 +537,14 @@ func buildPolicyResults(
}
for _, rule := range resp.PolicyResponse.Rules {
if rule.RuleType() != engineapi.Validation && rule.RuleType() != engineapi.ImageVerify || test.Rule != rule.Name() && !test.IsVap {
if rule.RuleType() != engineapi.Validation && rule.RuleType() != engineapi.ImageVerify || test.Rule != rule.Name() && !test.IsValidatingAdmissionPolicy {
continue
}
var resultsKey []string
var resultKey string
var result policyreportv1alpha2.PolicyReportResult
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName, test.IsVap)
resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName, test.IsValidatingAdmissionPolicy)
for _, key := range resultsKey {
if val, ok := results[key]; ok {
result = val
@ -579,11 +579,11 @@ func buildPolicyResults(
return results, testResults
}
func GetAllPossibleResultsKey(policyNamespace, policy, rule, resourceNamespace, kind, resource string, isVap bool) []string {
func GetAllPossibleResultsKey(policyNamespace, policy, rule, resourceNamespace, kind, resource string, isVAP bool) []string {
var resultsKey []string
var resultKey1, resultKey2, resultKey3, resultKey4 string
if isVap {
if isVAP {
resultKey1 = fmt.Sprintf("%s-%s-%s", policy, kind, resource)
resultKey2 = fmt.Sprintf("%s-%s-%s-%s", policy, resourceNamespace, kind, resource)
resultKey3 = fmt.Sprintf("%s-%s-%s-%s", policyNamespace, policy, kind, resource)
@ -599,9 +599,9 @@ func GetAllPossibleResultsKey(policyNamespace, policy, rule, resourceNamespace,
return resultsKey
}
func GetResultKeyAccordingToTestResults(policyNs, policy, rule, resourceNs, kind, resource string, isVap bool) string {
func GetResultKeyAccordingToTestResults(policyNs, policy, rule, resourceNs, kind, resource string, isVAP bool) string {
var resultKey string
if isVap {
if isVAP {
resultKey = fmt.Sprintf("%s-%s-%s", policy, kind, resource)
if policyNs != "" && resourceNs != "" {

View file

@ -158,7 +158,7 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
testCount++
row.Resource = color.Resource(v.Kind, v.Namespace, resource)
var ruleNameInResultKey string
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
if v.AutoGeneratedRule != "" {
ruleNameInResultKey = fmt.Sprintf("%s-%s", v.AutoGeneratedRule, v.Rule)
} else {
@ -167,7 +167,7 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
}
var resultKey string
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s", v.Policy, ruleNameInResultKey, v.Kind, resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s", v.Policy, v.Kind, resource)
@ -177,13 +177,13 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
var ns string
ns, v.Policy = getUserDefinedPolicyNameAndNamespace(v.Policy)
if found && v.Namespace != "" {
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", ns, v.Policy, v.Namespace, v.Kind, resource)
}
} else if found {
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Kind, resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s-%s", ns, v.Policy, v.Kind, resource)
@ -193,7 +193,7 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
} else if v.Namespace != "" {
row.Resource = color.Resource(v.Kind, v.Namespace, resource)
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s-%s", v.Policy, v.Namespace, v.Kind, resource)
@ -242,7 +242,7 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
countDeprecatedResource++
row.Resource = color.Resource(v.Kind, v.Namespace, v.Resource)
var ruleNameInResultKey string
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
if v.AutoGeneratedRule != "" {
ruleNameInResultKey = fmt.Sprintf("%s-%s", v.AutoGeneratedRule, v.Rule)
} else {
@ -251,7 +251,7 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
}
var resultKey string
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s", v.Policy, ruleNameInResultKey, v.Kind, v.Resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s", v.Policy, v.Kind, v.Resource)
@ -261,13 +261,13 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
var ns string
ns, v.Policy = getUserDefinedPolicyNameAndNamespace(v.Policy)
if found && v.Namespace != "" {
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, v.Resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", ns, v.Policy, v.Namespace, v.Kind, v.Resource)
}
} else if found {
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Kind, v.Resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s-%s", ns, v.Policy, v.Kind, v.Resource)
@ -278,7 +278,7 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t
} else if v.Namespace != "" {
row.Resource = color.Resource(v.Kind, v.Namespace, v.Resource)
if !v.IsVap {
if !v.IsValidatingAdmissionPolicy {
resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, v.Resource)
} else {
resultKey = fmt.Sprintf("%s-%s-%s-%s", v.Policy, v.Namespace, v.Kind, v.Resource)

View file

@ -17,9 +17,7 @@ import (
"github.com/kyverno/kyverno/pkg/clients/dclient"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml"
"golang.org/x/text/cases"
"golang.org/x/text/language"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"github.com/kyverno/kyverno/pkg/validatingadmissionpolicy"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -347,62 +345,16 @@ func GetKindsFromRule(rule kyvernov1.Rule, client dclient.Interface) (map[schema
return resourceTypesMap, subresourceMap
}
func getKindsFromValidatingAdmissionRule(rule admissionregistrationv1.Rule, client dclient.Interface) (map[schema.GroupVersionKind]bool, map[schema.GroupVersionKind]values.Subresource, error) {
func getKindsFromValidatingAdmissionPolicy(policy v1alpha1.ValidatingAdmissionPolicy, client dclient.Interface) (map[schema.GroupVersionKind]bool, map[schema.GroupVersionKind]values.Subresource) {
resourceTypesMap := make(map[schema.GroupVersionKind]bool)
subresourceMap := make(map[schema.GroupVersionKind]values.Subresource)
group := rule.APIGroups[0]
if group == "" {
group = "*"
}
version := rule.APIVersions[0]
for _, resource := range rule.Resources {
var kind, subresource string
isSubresource := kubeutils.IsSubresource(resource)
if isSubresource {
parts := strings.Split(resource, "/")
kind = cases.Title(language.English, cases.NoLower).String(parts[0])
kind, _ = strings.CutSuffix(kind, "s")
subresource = parts[1]
} else {
resource = cases.Title(language.English, cases.NoLower).String(resource)
resource, _ = strings.CutSuffix(resource, "s")
kind = resource
subresource = ""
}
gvrss, err := client.Discovery().FindResources(group, version, kind, subresource)
if err != nil {
log.Info("failed to find resource", "kind", kind, "error", err)
return resourceTypesMap, subresourceMap, err
}
for parent, child := range gvrss {
// The resource is not a subresource
if parent.SubResource == "" {
resourceTypesMap[parent.GroupVersionKind()] = true
} else {
gvk := schema.GroupVersionKind{
Group: child.Group, Version: child.Version, Kind: child.Kind,
}
subresourceMap[gvk] = values.Subresource{
APIResource: child,
ParentResource: metav1.APIResource{
Group: parent.Group,
Version: parent.Version,
Kind: parent.Kind,
Name: parent.Resource,
},
}
}
}
kinds := validatingadmissionpolicy.GetKinds(policy)
for _, kind := range kinds {
addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
}
return resourceTypesMap, subresourceMap, nil
return resourceTypesMap, subresourceMap
}
func addGVKToResourceTypesMap(kind string, resourceTypesMap map[schema.GroupVersionKind]bool, subresourceMap map[schema.GroupVersionKind]values.Subresource, client dclient.Interface) {

View file

@ -1,146 +1,23 @@
package common
import (
"context"
"fmt"
"time"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"github.com/kyverno/kyverno/pkg/validatingadmissionpolicy"
)
type ValidatingAdmissionPolicies struct{}
func (p *ValidatingAdmissionPolicies) ApplyPolicyOnResource(c ApplyPolicyConfig) ([]engineapi.EngineResponse, error) {
var engineResponses []engineapi.EngineResponse
engineResp := validatingadmissionpolicy.Validate(c.ValidatingAdmissionPolicy, *c.Resource)
ruleResp := engineResp.PolicyResponse.Rules[0]
resPath := fmt.Sprintf("%s/%s/%s", c.Resource.GetNamespace(), c.Resource.GetKind(), c.Resource.GetName())
log.V(3).Info("applying policy on resource", "policy", c.ValidatingAdmissionPolicy.GetName(), "resource", resPath)
startTime := time.Now()
validations := c.ValidatingAdmissionPolicy.Spec.Validations
var expressions, messageExpressions []cel.ExpressionAccessor
for _, validation := range validations {
var reason metav1.StatusReason
if validation.Reason == nil {
reason = metav1.StatusReasonInvalid
} else {
reason = *validation.Reason
}
var message string
if validation.Message != "" && validation.MessageExpression != "" {
message = validation.MessageExpression
} else if validation.Message != "" {
message = validation.Message
} else if validation.MessageExpression != "" {
message = validation.MessageExpression
} else {
message = fmt.Sprintf("error: failed to create %s: %s \"%s\" is forbidden: ValidatingAdmissionPolicy '%s' denied request: failed expression: %s", c.Resource.GetKind(), c.Resource.GetAPIVersion(), c.Resource.GetName(), c.ValidatingAdmissionPolicy.Name, validation.Expression)
}
condition := &validatingadmissionpolicy.ValidationCondition{
Expression: validation.Expression,
Message: message,
Reason: &reason,
}
messageCondition := &validatingadmissionpolicy.MessageExpressionCondition{
MessageExpression: validation.MessageExpression,
}
expressions = append(expressions, condition)
messageExpressions = append(messageExpressions, messageCondition)
}
hasParams := c.ValidatingAdmissionPolicy.Spec.ParamKind != nil
filterCompiler := cel.NewFilterCompiler()
filter := filterCompiler.Compile(expressions, cel.OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: false}, celconfig.PerCallLimit)
messageExpressionfilter := filterCompiler.Compile(messageExpressions, cel.OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: false}, celconfig.PerCallLimit)
admissionAttributes := admission.NewAttributesRecord(c.Resource.DeepCopyObject(), nil, c.Resource.GroupVersionKind(), c.Resource.GetNamespace(), c.Resource.GetName(), schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
versionedAttr, _ := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), nil)
ctx := context.TODO()
failPolicy := admissionregistrationv1.FailurePolicyType(*c.ValidatingAdmissionPolicy.Spec.FailurePolicy)
matchConditions := c.ValidatingAdmissionPolicy.Spec.MatchConditions
var matchExpressions []cel.ExpressionAccessor
for _, expression := range matchConditions {
condition := &matchconditions.MatchCondition{
Name: expression.Name,
Expression: expression.Expression,
}
matchExpressions = append(matchExpressions, condition)
}
matchFilter := filterCompiler.Compile(matchExpressions, cel.OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: false}, celconfig.PerCallLimit)
var matchPolicy *v1alpha1.MatchPolicyType
if c.ValidatingAdmissionPolicy.Spec.MatchConstraints.MatchPolicy == nil {
equivalent := v1alpha1.Equivalent
matchPolicy = &equivalent
} else {
matchPolicy = c.ValidatingAdmissionPolicy.Spec.MatchConstraints.MatchPolicy
}
newMatcher := matchconditions.NewMatcher(matchFilter, nil, &failPolicy, string(*matchPolicy), "")
auditAnnotations := c.ValidatingAdmissionPolicy.Spec.AuditAnnotations
var auditExpressions []cel.ExpressionAccessor
for _, expression := range auditAnnotations {
condition := &validatingadmissionpolicy.AuditAnnotationCondition{
Key: expression.Key,
ValueExpression: expression.ValueExpression,
}
auditExpressions = append(auditExpressions, condition)
}
auditAnnotationFilter := filterCompiler.Compile(auditExpressions, cel.OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: false}, celconfig.PerCallLimit)
validator := validatingadmissionpolicy.NewValidator(filter, newMatcher, auditAnnotationFilter, messageExpressionfilter, &failPolicy, nil)
validateResult := validator.Validate(ctx, versionedAttr, nil, celconfig.RuntimeCELCostBudget)
engineResponse := engineapi.NewEngineResponseWithValidatingAdmissionPolicy(*c.Resource, c.ValidatingAdmissionPolicy, nil)
policyResp := engineapi.NewPolicyResponse()
var ruleResp *engineapi.RuleResponse
isPass := true
for _, policyDecision := range validateResult.Decisions {
if policyDecision.Evaluation == validatingadmissionpolicy.EvalError {
isPass = false
c.Rc.Error++
ruleResp = engineapi.RuleError(c.ValidatingAdmissionPolicy.GetName(), engineapi.Validation, policyDecision.Message, nil)
break
} else if policyDecision.Action == validatingadmissionpolicy.ActionDeny {
isPass = false
c.Rc.Fail++
ruleResp = engineapi.RuleFail(c.ValidatingAdmissionPolicy.GetName(), engineapi.Validation, policyDecision.Message)
break
}
}
if isPass {
if ruleResp.Status() == engineapi.RuleStatusPass {
c.Rc.Pass++
ruleResp = engineapi.RulePass(c.ValidatingAdmissionPolicy.GetName(), engineapi.Validation, "")
} else if ruleResp.Status() == engineapi.RuleStatusFail {
c.Rc.Fail++
} else if ruleResp.Status() == engineapi.RuleStatusError {
c.Rc.Error++
}
policyResp.Add(engineapi.NewExecutionStats(startTime, time.Now()), *ruleResp)
engineResponse = engineResponse.WithPolicyResponse(policyResp)
engineResponses = append(engineResponses, engineResponse)
return engineResponses, nil
return []engineapi.EngineResponse{engineResp}, nil
}

View file

@ -21,15 +21,13 @@ func (r *ValidatingAdmissionResources) FetchResourcesFromPolicy(resourcePaths []
var subresourceMap map[schema.GroupVersionKind]values.Subresource
for _, policy := range r.policies {
for _, rule := range policy.Spec.MatchConstraints.ResourceRules {
var resourceTypesInRule map[schema.GroupVersionKind]bool
resourceTypesInRule, subresourceMap, err = getKindsFromValidatingAdmissionRule(rule.RuleWithOperations.Rule, dClient)
if err != nil {
return resources, err
}
for resourceKind := range resourceTypesInRule {
resourceTypesMap[resourceKind] = true
}
var resourceTypesInRule map[schema.GroupVersionKind]bool
resourceTypesInRule, subresourceMap = getKindsFromValidatingAdmissionPolicy(policy, dClient)
if err != nil {
return resources, err
}
for resourceKind := range resourceTypesInRule {
resourceTypesMap[resourceKind] = true
}
}

View file

@ -0,0 +1,5 @@
package validatingadmissionpolicy
import "github.com/kyverno/kyverno/pkg/logging"
var logger = logging.WithName("validatingadmissionpolicy")

View file

@ -0,0 +1,182 @@
package validatingadmissionpolicy
import (
"context"
"fmt"
"strings"
"time"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"golang.org/x/text/cases"
"golang.org/x/text/language"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
func GetKinds(policy v1alpha1.ValidatingAdmissionPolicy) []string {
var kindList []string
matchResources := policy.Spec.MatchConstraints
for _, rule := range matchResources.ResourceRules {
group := rule.APIGroups[0]
version := rule.APIVersions[0]
for _, resource := range rule.Resources {
isSubresource := kubeutils.IsSubresource(resource)
if isSubresource {
parts := strings.Split(resource, "/")
kind := cases.Title(language.English, cases.NoLower).String(parts[0])
kind, _ = strings.CutSuffix(kind, "s")
subresource := parts[1]
if group == "" {
kindList = append(kindList, strings.Join([]string{version, kind, subresource}, "/"))
} else {
kindList = append(kindList, strings.Join([]string{group, version, kind, subresource}, "/"))
}
} else {
resource = cases.Title(language.English, cases.NoLower).String(resource)
resource, _ = strings.CutSuffix(resource, "s")
kind := resource
if group == "" {
kindList = append(kindList, strings.Join([]string{version, kind}, "/"))
} else {
kindList = append(kindList, strings.Join([]string{group, version, kind}, "/"))
}
}
}
}
return kindList
}
func Validate(policy v1alpha1.ValidatingAdmissionPolicy, resource unstructured.Unstructured) engineapi.EngineResponse {
resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName())
logger.V(3).Info("applying policy on resource", "policy", policy.GetName(), "resource", resPath)
startTime := time.Now()
var expressions, messageExpressions, matchExpressions, auditExpressions []cel.ExpressionAccessor
validations := policy.Spec.Validations
matchConditions := policy.Spec.MatchConditions
auditAnnotations := policy.Spec.AuditAnnotations
hasParam := policy.Spec.ParamKind != nil
var failPolicy admissionregistrationv1.FailurePolicyType
if policy.Spec.FailurePolicy == nil {
failPolicy = admissionregistrationv1.Fail
} else {
failPolicy = admissionregistrationv1.FailurePolicyType(*policy.Spec.FailurePolicy)
}
var matchPolicy v1alpha1.MatchPolicyType
if policy.Spec.MatchConstraints.MatchPolicy == nil {
matchPolicy = v1alpha1.Equivalent
} else {
matchPolicy = *policy.Spec.MatchConstraints.MatchPolicy
}
for _, cel := range validations {
condition := &validatingadmissionpolicy.ValidationCondition{
Expression: cel.Expression,
Message: cel.Message,
}
messageCondition := &validatingadmissionpolicy.MessageExpressionCondition{
MessageExpression: cel.MessageExpression,
}
expressions = append(expressions, condition)
messageExpressions = append(messageExpressions, messageCondition)
}
for _, expression := range matchConditions {
condition := &matchconditions.MatchCondition{
Name: expression.Name,
Expression: expression.Expression,
}
matchExpressions = append(matchExpressions, condition)
}
for _, auditAnnotation := range auditAnnotations {
auditCondition := &validatingadmissionpolicy.AuditAnnotationCondition{
Key: auditAnnotation.Key,
ValueExpression: auditAnnotation.ValueExpression,
}
auditExpressions = append(auditExpressions, auditCondition)
}
filterCompiler := cel.NewFilterCompiler()
filter := filterCompiler.Compile(
expressions,
cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false},
celconfig.PerCallLimit,
)
messageExpressionfilter := filterCompiler.Compile(
messageExpressions,
cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false},
celconfig.PerCallLimit,
)
auditAnnotationFilter := filterCompiler.Compile(
auditExpressions,
cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false},
celconfig.PerCallLimit,
)
matchConditionFilter := filterCompiler.Compile(
matchExpressions,
cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false},
celconfig.PerCallLimit,
)
newMatcher := matchconditions.NewMatcher(matchConditionFilter, nil, &failPolicy, string(matchPolicy), "")
validator := validatingadmissionpolicy.NewValidator(filter, newMatcher, auditAnnotationFilter, messageExpressionfilter, nil, nil)
admissionAttributes := admission.NewAttributesRecord(
resource.DeepCopyObject(),
nil, resource.GroupVersionKind(),
resource.GetNamespace(),
resource.GetName(),
schema.GroupVersionResource{},
"",
admission.Create,
nil,
false,
nil,
)
versionedAttr, _ := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), nil)
validateResult := validator.Validate(context.TODO(), versionedAttr, nil, celconfig.RuntimeCELCostBudget)
engineResponse := engineapi.NewEngineResponseWithValidatingAdmissionPolicy(resource, policy, nil)
policyResp := engineapi.NewPolicyResponse()
var ruleResp *engineapi.RuleResponse
isPass := true
for _, policyDecision := range validateResult.Decisions {
if policyDecision.Evaluation == validatingadmissionpolicy.EvalError {
isPass = false
ruleResp = engineapi.RuleError(policy.GetName(), engineapi.Validation, policyDecision.Message, nil)
break
} else if policyDecision.Action == validatingadmissionpolicy.ActionDeny {
isPass = false
ruleResp = engineapi.RuleFail(policy.GetName(), engineapi.Validation, policyDecision.Message)
break
}
}
if isPass {
ruleResp = engineapi.RulePass(policy.GetName(), engineapi.Validation, "")
}
policyResp.Add(engineapi.NewExecutionStats(startTime, time.Now()), *ruleResp)
engineResponse = engineResponse.WithPolicyResponse(policyResp)
return engineResponse
}

View file

@ -0,0 +1,137 @@
package validatingadmissionpolicy
import (
"reflect"
"testing"
yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml"
)
func TestGetKinds(t *testing.T) {
type test struct {
name string
policy []byte
wantKinds []string
}
tests := []test{
{
name: "Matching pods",
policy: []byte(`
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "policy-1"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "object.metadata.name.matches('nginx')"
`),
wantKinds: []string{"v1/Pod"},
},
{
name: "Matching deployments, replicasets, daemonsets and statefulsets",
policy: []byte(`
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "policy-2"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments", "replicasets", "daemonsets", "statefulsets"]
validations:
- expression: "object.spec.replicas <= 5"
`),
wantKinds: []string{"apps/v1/Deployment", "apps/v1/Replicaset", "apps/v1/Daemonset", "apps/v1/Statefulset"},
},
{
name: "Matching deployments/scale",
policy: []byte(`
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "policy-3"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments/scale"]
validations:
- expression: "object.spec.replicas <= 5"
`),
wantKinds: []string{"apps/v1/Deployment/scale"},
},
{
name: "Matching jobs and cronjobs",
policy: []byte(`
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "policy-4"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["batch"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["jobs", "cronjobs"]
validations:
- expression: "object.spec.jobTemplate.spec.template.spec.containers.all(container, has(container.securityContext) && has(container.securityContext.readOnlyRootFilesystem) && container.securityContext.readOnlyRootFilesystem == true)"
`),
wantKinds: []string{"batch/v1/Job", "batch/v1/Cronjob"},
},
{
name: "Multiple resource rules",
policy: []byte(`
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "policy-5"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments", "replicasets", "daemonsets", "statefulsets"]
- apiGroups: ["batch"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["jobs", "cronjobs"]
validations:
- expression: "object.spec.replicas <= 5"
`),
wantKinds: []string{"v1/Pod", "apps/v1/Deployment", "apps/v1/Replicaset", "apps/v1/Daemonset", "apps/v1/Statefulset", "batch/v1/Job", "batch/v1/Cronjob"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, policy, _ := yamlutils.GetPolicy(tt.policy)
kinds := GetKinds(policy[0])
if !reflect.DeepEqual(kinds, tt.wantKinds) {
t.Errorf("Expected %v, got %v", tt.wantKinds, kinds)
}
})
}
}

View file

@ -1,20 +0,0 @@
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.example.com"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.template.spec.containers.all(container, !container.image.contains('latest'))"
message: "Using a mutable image tag e.g. 'latest' is not allowed."
- expression: "object.spec.replicas <= 5"
message: "Replicas must be less than or equal 5"
matchConditions:
- name: "deployment name"
expression: "object.metadata.name == 'nginx-deployment'"

View file

@ -5,7 +5,7 @@ metadata:
labels:
app: nginx
spec:
replicas: 3
replicas: 2
selector:
matchLabels:
app: nginx
@ -16,7 +16,4 @@ spec:
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
image: nginx:latest

View file

@ -17,6 +17,3 @@ spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80

View file

@ -0,0 +1,14 @@
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "chech-deployment-replicas"
spec:
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 2"
message: "Deployment replicas must be less than or equal 2"

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: Pod
metadata:
name: good-pod
spec:
containers:
- name:
image: nginx
volumeMounts:
- name: udev
mountPath: /data
volumes:
- name: udev
emptyDir: {}

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
spec:
containers:
- name:
image: nginx
volumeMounts:
- name: udev
mountPath: /data
volumes:
- name: udev
hostPath:
path: /etc/udev

View file

@ -0,0 +1,14 @@
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: "disallow-host-path"
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "!has(object.spec.volumes) || object.spec.volumes.all(volume, !has(volume.hostPath))"
message: "HostPath volumes are forbidden. The field spec.volumes[*].hostPath must be unset."