1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00
kyverno/pkg/validatingadmissionpolicy/builder.go
2024-12-12 07:13:09 +00:00

223 lines
8 KiB
Go

package validatingadmissionpolicy
import (
"fmt"
"slices"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BuildValidatingAdmissionPolicy is used to build a Kubernetes ValidatingAdmissionPolicy from a Kyverno policy
func BuildValidatingAdmissionPolicy(discoveryClient dclient.IDiscovery, vap *admissionregistrationv1alpha1.ValidatingAdmissionPolicy, cpol kyvernov1.PolicyInterface) error {
// set owner reference
vap.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "kyverno.io/v1",
Kind: cpol.GetKind(),
Name: cpol.GetName(),
UID: cpol.GetUID(),
},
}
// construct validating admission policy resource rules
var matchResources admissionregistrationv1alpha1.MatchResources
var matchRules []admissionregistrationv1alpha1.NamedRuleWithOperations
rule := cpol.GetSpec().Rules[0]
match := rule.MatchResources
if !match.ResourceDescription.IsEmpty() {
if err := translateResource(discoveryClient, &matchResources, &matchRules, match.ResourceDescription); err != nil {
return err
}
}
if match.Any != nil {
if err := translateResourceFilters(discoveryClient, &matchResources, &matchRules, match.Any); err != nil {
return err
}
}
if match.All != nil {
if err := translateResourceFilters(discoveryClient, &matchResources, &matchRules, match.All); err != nil {
return err
}
}
// set validating admission policy spec
vap.Spec = admissionregistrationv1alpha1.ValidatingAdmissionPolicySpec{
MatchConstraints: &matchResources,
ParamKind: rule.Validation.CEL.ParamKind,
Variables: rule.Validation.CEL.Variables,
Validations: rule.Validation.CEL.Expressions,
AuditAnnotations: rule.Validation.CEL.AuditAnnotations,
MatchConditions: rule.CELPreconditions,
}
// set labels
controllerutils.SetManagedByKyvernoLabel(vap)
return nil
}
// BuildValidatingAdmissionPolicyBinding is used to build a Kubernetes ValidatingAdmissionPolicyBinding from a Kyverno policy
func BuildValidatingAdmissionPolicyBinding(vapbinding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, cpol kyvernov1.PolicyInterface) error {
// set owner reference
vapbinding.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "kyverno.io/v1",
Kind: cpol.GetKind(),
Name: cpol.GetName(),
UID: cpol.GetUID(),
},
}
// set validation action for vap binding
var validationActions []admissionregistrationv1alpha1.ValidationAction
action := cpol.GetSpec().ValidationFailureAction
if action.Enforce() {
validationActions = append(validationActions, admissionregistrationv1alpha1.Deny)
} else if action.Audit() {
validationActions = append(validationActions, admissionregistrationv1alpha1.Audit)
validationActions = append(validationActions, admissionregistrationv1alpha1.Warn)
}
// set validating admission policy binding spec
rule := cpol.GetSpec().Rules[0]
vapbinding.Spec = admissionregistrationv1alpha1.ValidatingAdmissionPolicyBindingSpec{
PolicyName: cpol.GetName(),
ParamRef: rule.Validation.CEL.ParamRef,
ValidationActions: validationActions,
}
// set labels
controllerutils.SetManagedByKyvernoLabel(vapbinding)
return nil
}
func translateResourceFilters(discoveryClient dclient.IDiscovery, matchResources *admissionregistrationv1alpha1.MatchResources, rules *[]admissionregistrationv1alpha1.NamedRuleWithOperations, resFilters kyvernov1.ResourceFilters) error {
for _, filter := range resFilters {
err := translateResource(discoveryClient, matchResources, rules, filter.ResourceDescription)
if err != nil {
return err
}
}
return nil
}
func translateResource(discoveryClient dclient.IDiscovery, matchResources *admissionregistrationv1alpha1.MatchResources, rules *[]admissionregistrationv1alpha1.NamedRuleWithOperations, res kyvernov1.ResourceDescription) error {
err := constructValidatingAdmissionPolicyRules(discoveryClient, rules, res)
if err != nil {
return err
}
matchResources.ResourceRules = *rules
matchResources.NamespaceSelector = res.NamespaceSelector
matchResources.ObjectSelector = res.Selector
return nil
}
func constructValidatingAdmissionPolicyRules(discoveryClient dclient.IDiscovery, rules *[]admissionregistrationv1alpha1.NamedRuleWithOperations, res kyvernov1.ResourceDescription) error {
// translate operations to their corresponding values in validating admission policy.
ops := translateOperations(res.GetOperations())
resourceNames := res.Names
if res.Name != "" {
resourceNames = append(resourceNames, res.Name)
}
// get kinds from kyverno policies and translate them to rules in validating admission policies.
// matched resources in kyverno policies are written in the following format:
// group/version/kind/subresource
// whereas matched resources in validating admission policies are written in the following format:
// apiGroups: ["group"]
// apiVersions: ["version"]
// resources: ["resource"]
for _, kind := range res.Kinds {
var r admissionregistrationv1alpha1.NamedRuleWithOperations
if kind == "*" {
r = buildNamedRuleWithOperations(resourceNames, "*", "*", ops, "*")
*rules = append(*rules, r)
} else {
group, version, kind, subresource := kubeutils.ParseKindSelector(kind)
gvrss, err := discoveryClient.FindResources(group, version, kind, subresource)
if err != nil {
return err
}
if len(gvrss) != 1 {
return fmt.Errorf("no unique match for kind %s", kind)
}
for topLevelApi, apiResource := range gvrss {
resources := []string{apiResource.Name}
// Add pods/ephemeralcontainers if pods resource.
if apiResource.Name == "pods" {
resources = append(resources, "pods/ephemeralcontainers")
}
// Check if there's an existing rule for the same group and version.
var isNewRule bool = true
for i := range *rules {
if slices.Contains((*rules)[i].APIGroups, topLevelApi.Group) && slices.Contains((*rules)[i].APIVersions, topLevelApi.Version) {
(*rules)[i].Resources = append((*rules)[i].Resources, resources...)
isNewRule = false
break
}
}
// If no existing rule found, create a new one.
if isNewRule {
r = buildNamedRuleWithOperations(resourceNames, topLevelApi.Group, topLevelApi.Version, ops, resources...)
*rules = append(*rules, r)
}
}
}
}
return nil
}
func buildNamedRuleWithOperations(
resourceNames []string,
group, version string,
operations []admissionregistrationv1.OperationType,
resources ...string,
) admissionregistrationv1alpha1.NamedRuleWithOperations {
return admissionregistrationv1alpha1.NamedRuleWithOperations{
ResourceNames: resourceNames,
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
Rule: admissionregistrationv1.Rule{
Resources: resources,
APIGroups: []string{group},
APIVersions: []string{version},
},
Operations: operations,
},
}
}
func translateOperations(operations []string) []admissionregistrationv1.OperationType {
var vapOperations []admissionregistrationv1.OperationType
for _, op := range operations {
if op == string(kyvernov1.Create) {
vapOperations = append(vapOperations, admissionregistrationv1.Create)
} else if op == string(kyvernov1.Update) {
vapOperations = append(vapOperations, admissionregistrationv1.Update)
} else if op == string(kyvernov1.Connect) {
vapOperations = append(vapOperations, admissionregistrationv1.Connect)
} else if op == string(kyvernov1.Delete) {
vapOperations = append(vapOperations, admissionregistrationv1.Delete)
}
}
// set default values for operations since it's a required field in validating admission policies
if len(vapOperations) == 0 {
vapOperations = append(vapOperations, admissionregistrationv1.Create)
vapOperations = append(vapOperations, admissionregistrationv1.Update)
}
return vapOperations
}