1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-04-15 00:36:28 +00:00

feat: add support for subresources to validating and mutating policies (#4916)

* feat: add support for subresources to validating and mutating policies

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

* Add CLI test cases with subresources for validating policies

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

* Fix existing e2e tests for validating policies and remove tests migrated to kuttl

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

* Add kuttl e2e tests for validating policies with subresources

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

* Add kuttl e2e tests for mutating policies with subresources

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

* Add kuttl e2e tests for validating policy by-pass by manipulating preconditions

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>

Signed-off-by: Vyom-Yadav <jackhammervyom@gmail.com>
This commit is contained in:
Vyom Yadav 2022-12-09 22:15:23 +05:30 committed by GitHub
parent 4ae8b4b0ac
commit 99d988e98c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2451 additions and 1044 deletions

View file

@ -1,3 +1,10 @@
## v1.9.0-rc.2
### Note
- Webhook is no longer updated to match `pods/ephemeralcontainers` when policy only specifies `pods`. If users want to match on `pods/ephemeralcontainers`, they must specify `pods/ephemeralcontainers` in the policy.
- Webhook is no longer updated to match `services/status` when policy only specifies `services`. If users want to match on `services/status`, they must specify `services/status` in the policy.
## v1.9.0-rc.1
### Note

View file

@ -614,6 +614,7 @@ test-e2e:
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/metrics -v
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/mutate -v
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/generate -v
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/validate -v
test-e2e-local:
kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno/main/config/github/rbac.yaml
@ -622,6 +623,7 @@ test-e2e-local:
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/metrics -v
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/mutate -v
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/generate -v
E2E=ok K8S_VERSION=$(K8S_VERSION) go test ./test/e2e/validate -v
kill $!
helm-test-values:

View file

@ -25,7 +25,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
log "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log"
yaml1 "sigs.k8s.io/yaml"
)
@ -131,6 +131,16 @@ To apply policy with variables:
- name: <namespace2 name>
labels:
<label key>: <label value>
# If policy is matching on Kind/Subresource, then this is required
subresources:
- subresource:
name: <name of subresource>
kind: <kind of subresource>
version: <version of subresource>
parentResource:
name: <name of parent resource>
kind: <kind of parent resource>
version: <version of parent resource>
More info: https://kyverno.io/docs/kyverno-cli/
`
@ -192,7 +202,7 @@ func (c *ApplyCommandConfig) applyCommandHelper() (rc *common.ResultCounts, reso
return rc, resources, skipInvalidPolicies, pvInfos, sanitizederror.NewWithError("pass the values either using set flag or values_file flag", err)
}
variables, globalValMap, valuesMap, namespaceSelectorMap, err := common.GetVariable(c.VariablesString, c.ValuesFile, fs, false, "")
variables, globalValMap, valuesMap, namespaceSelectorMap, subresources, err := common.GetVariable(c.VariablesString, c.ValuesFile, fs, false, "")
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return rc, resources, skipInvalidPolicies, pvInfos, sanitizederror.NewWithError("failed to decode yaml", err)
@ -391,7 +401,7 @@ func (c *ApplyCommandConfig) applyCommandHelper() (rc *common.ResultCounts, reso
}
}
kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(policy)
kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(policy, subresources, dClient)
for _, resource := range resources {
thisPolicyResourceValues, err := common.CheckVariableForPolicy(valuesMap, globalValMap, policy.GetName(), resource.GetName(), resource.GetKind(), variables, kindOnwhichPolicyIsApplied, variable)
@ -412,6 +422,7 @@ func (c *ApplyCommandConfig) applyCommandHelper() (rc *common.ResultCounts, reso
PrintPatchResource: true,
Client: dClient,
AuditWarn: c.AuditWarn,
Subresources: subresources,
}
_, info, err := common.ApplyPolicyOnResource(applyPolicyConfig)
if err != nil {

View file

@ -36,7 +36,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
log "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log"
)
var longHelp = `
@ -140,6 +140,16 @@ policies:
- name: <resource_name_2>
values:
foo: bin
# If policy is matching on Kind/Subresource, then this is required
subresources:
- subresource:
name: <name of subresource>
kind: <kind of subresource>
version: <version of subresource>
parentResource:
name: <name of parent resource>
kind: <kind of parent resource>
version: <version of parent resource>
**RESULT DESCRIPTIONS**:
@ -748,7 +758,7 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
valuesFile := values.Variables
userInfoFile := values.UserInfo
variables, globalValMap, valuesMap, namespaceSelectorMap, err := common.GetVariable(variablesString, values.Variables, fs, isGit, policyResourcePath)
variables, globalValMap, valuesMap, namespaceSelectorMap, subresources, err := common.GetVariable(variablesString, values.Variables, fs, isGit, policyResourcePath)
if err != nil {
if !sanitizederror.IsErrorSanitized(err) {
return sanitizederror.NewWithError("failed to decode yaml", err)
@ -911,7 +921,7 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
}
}
kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(policy)
kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(policy, subresources, dClient)
for _, resource := range noDuplicateResources {
thisPolicyResourceValues, err := common.CheckVariableForPolicy(valuesMap, globalValMap, policy.GetName(), resource.GetName(), resource.GetKind(), variables, kindOnwhichPolicyIsApplied, variable)
@ -929,6 +939,7 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, isGit bool,
Rc: &resultCounts,
RuleToCloneSourceResource: ruleToCloneSourceResource,
Client: dClient,
Subresources: subresources,
}
ers, info, err := common.ApplyPolicyOnResource(applyPolicyConfig)
if err != nil {

View file

@ -27,8 +27,10 @@ import (
ut "github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/registryclient"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml"
yamlv2 "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
@ -58,6 +60,7 @@ type Values struct {
Policies []Policy `json:"policies"`
GlobalValues map[string]string `json:"globalValues"`
NamespaceSelectors []NamespaceSelector `json:"namespaceSelector"`
Subresources []Subresource `json:"subresources"`
}
type Resource struct {
@ -65,6 +68,11 @@ type Resource struct {
Values map[string]interface{} `json:"values"`
}
type Subresource struct {
APIResource metav1.APIResource `json:"subresource"`
ParentResource metav1.APIResource `json:"parentResource"`
}
type NamespaceSelector struct {
Name string `json:"name"`
Labels map[string]string `json:"labels"`
@ -85,6 +93,7 @@ type ApplyPolicyConfig struct {
RuleToCloneSourceResource map[string]string
Client dclient.Interface
AuditWarn bool
Subresources []Subresource
}
// HasVariables - check for variables in the policy
@ -215,11 +224,12 @@ func RemoveDuplicateAndObjectVariables(matches [][]string) string {
return variableStr
}
func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit bool, policyResourcePath string) (map[string]string, map[string]string, map[string]map[string]Resource, map[string]map[string]string, error) {
func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit bool, policyResourcePath string) (map[string]string, map[string]string, map[string]map[string]Resource, map[string]map[string]string, []Subresource, error) {
valuesMapResource := make(map[string]map[string]Resource)
valuesMapRule := make(map[string]map[string]Rule)
namespaceSelectorMap := make(map[string]map[string]string)
variables := make(map[string]string)
subresources := make([]Subresource, 0)
globalValMap := make(map[string]string)
reqObjVars := ""
@ -259,17 +269,17 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit
}
if err != nil {
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, sanitizederror.NewWithError("unable to read yaml", err)
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, subresources, sanitizederror.NewWithError("unable to read yaml", err)
}
valuesBytes, err := yaml.ToJSON(yamlFile)
if err != nil {
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, sanitizederror.NewWithError("failed to convert json", err)
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, subresources, sanitizederror.NewWithError("failed to convert json", err)
}
values := &Values{}
if err := json.Unmarshal(valuesBytes, values); err != nil {
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, sanitizederror.NewWithError("failed to decode yaml", err)
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, subresources, sanitizederror.NewWithError("failed to decode yaml", err)
}
if values.GlobalValues == nil {
@ -321,15 +331,16 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit
for _, n := range values.NamespaceSelectors {
namespaceSelectorMap[n.Name] = n.Labels
}
subresources = values.Subresources
}
if reqObjVars != "" {
fmt.Printf(("\nNOTICE: request.object.* variables are automatically parsed from the supplied resource. Ignoring value of variables `%v`.\n"), reqObjVars)
fmt.Printf("\nNOTICE: request.object.* variables are automatically parsed from the supplied resource. Ignoring value of variables `%v`.\n", reqObjVars)
}
if globalValMap != nil {
_, exist := globalValMap["request.operation"]
if !exist {
if _, ok := globalValMap["request.operation"]; !ok {
globalValMap["request.operation"] = "CREATE"
log.Log.V(3).Info("Defaulting request.operation to CREATE")
}
@ -355,7 +366,7 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit
Policies: storePolicies,
})
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, nil
return variables, globalValMap, valuesMapResource, namespaceSelectorMap, subresources, nil
}
// ApplyPolicyOnResource - function to apply policy on resource
@ -451,12 +462,30 @@ OuterLoop:
log.Log.Error(err, "failed to add image variables to context")
}
subresources := make([]struct {
APIResource metav1.APIResource
ParentResource metav1.APIResource
}, 0)
// If --cluster flag is not set, then we need to add subresources to the context
if c.Client == nil {
for _, subresource := range c.Subresources {
subresources = append(subresources, struct {
APIResource metav1.APIResource
ParentResource metav1.APIResource
}{
APIResource: subresource.APIResource, ParentResource: subresource.ParentResource,
})
}
}
policyContext := engine.NewPolicyContextWithJsonContext(ctx).
WithPolicy(c.Policy).
WithNewResource(*updatedResource).
WithNamespaceLabels(namespaceLabels).
WithAdmissionInfo(c.UserInfo).
WithClient(c.Client)
WithClient(c.Client).
WithSubresourcesInPolicy(subresources)
mutateResponse := engine.Mutate(context.Background(), registryclient.NewOrDie(), policyContext)
if mutateResponse != nil {
@ -942,19 +971,64 @@ func CheckVariableForPolicy(valuesMap map[string]map[string]Resource, globalValM
return thisPolicyResourceValues, nil
}
func GetKindsFromPolicy(policy kyvernov1.PolicyInterface) map[string]struct{} {
func GetKindsFromPolicy(policy kyvernov1.PolicyInterface, subresources []Subresource, dClient dclient.Interface) map[string]struct{} {
kindOnwhichPolicyIsApplied := make(map[string]struct{})
for _, rule := range autogen.ComputeRules(policy) {
for _, kind := range rule.MatchResources.ResourceDescription.Kinds {
kindOnwhichPolicyIsApplied[kind] = struct{}{}
k, err := getKind(kind, subresources, dClient)
if err != nil {
fmt.Printf("Error: %s", err.Error())
continue
}
kindOnwhichPolicyIsApplied[k] = struct{}{}
}
for _, kind := range rule.ExcludeResources.ResourceDescription.Kinds {
kindOnwhichPolicyIsApplied[kind] = struct{}{}
k, err := getKind(kind, subresources, dClient)
if err != nil {
fmt.Printf("Error: %s", err.Error())
continue
}
kindOnwhichPolicyIsApplied[k] = struct{}{}
}
}
return kindOnwhichPolicyIsApplied
}
func getKind(kind string, subresources []Subresource, dClient dclient.Interface) (string, error) {
gv, k := kubeutils.GetKindFromGVK(kind)
parentKind, subresource := kubeutils.SplitSubresource(k)
var err error
if subresource != "" {
if dClient != nil {
var apiResource *metav1.APIResource
apiResource, _, _, err = dClient.Discovery().FindResource(gv, k)
if err == nil {
k = apiResource.Kind
}
} else {
k, err = getSubresourceKind(gv, parentKind, subresource, subresources)
}
}
return k, err
}
func getSubresourceKind(groupVersion, parentKind, subresourceName string, subresources []Subresource) (string, error) {
for _, subresource := range subresources {
parentResourceGroupVersion := metav1.GroupVersion{
Group: subresource.ParentResource.Group,
Version: subresource.ParentResource.Version,
}.String()
if groupVersion == "" || kubeutils.GroupVersionMatches(groupVersion, parentResourceGroupVersion) {
if parentKind == subresource.ParentResource.Kind {
if strings.ToLower(subresourceName) == strings.Split(subresource.APIResource.Name, "/")[1] {
return subresource.APIResource.Kind, nil
}
}
}
}
return "", sanitizederror.NewWithError(fmt.Sprintf("subresource %s not found for parent resource %s", subresourceName, parentKind), nil)
}
// GetResourceFromPath - get patchedResource and generatedResource from given path
func GetResourceFromPath(fs billy.Filesystem, path string, isGit bool, policyResourcePath string, resourceType string) (unstructured.Unstructured, error) {
var resourceBytes []byte

View file

@ -3,9 +3,10 @@ package common
import (
"testing"
v1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/api/kyverno/v1beta1"
yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var policyNamespaceSelector = []byte(`{
@ -187,3 +188,19 @@ func Test_GetGitBranchOrPolicyPaths(t *testing.T) {
}
}
}
func Test_getSubresourceKind(t *testing.T) {
podAPIResource := metav1.APIResource{Name: "pods", SingularName: "", Namespaced: true, Kind: "Pod"}
podEvictionAPIResource := metav1.APIResource{Name: "pods/eviction", SingularName: "", Namespaced: true, Group: "policy", Version: "v1", Kind: "Eviction"}
subresources := []Subresource{
{
APIResource: podEvictionAPIResource,
ParentResource: podAPIResource,
},
}
subresourceKind, err := getSubresourceKind("", "Pod", "eviction", subresources)
assert.NilError(t, err)
assert.Equal(t, subresourceKind, "Eviction")
}

View file

@ -15,9 +15,8 @@ import (
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/clients/dclient"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
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"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
@ -29,27 +28,33 @@ import (
// the resources are fetched from
// - local paths to resources, if given
// - the k8s cluster, if given
func GetResources(policies []kyvernov1.PolicyInterface, resourcePaths []string, dClient dclient.Interface, cluster bool, namespace string, policyReport bool) ([]*unstructured.Unstructured, error) {
func GetResources(
policies []kyvernov1.PolicyInterface, resourcePaths []string, dClient dclient.Interface, cluster bool,
namespace string, policyReport bool,
) ([]*unstructured.Unstructured, error) {
resources := make([]*unstructured.Unstructured, 0)
var err error
resourceTypesMap := make(map[string]bool)
var resourceTypes []string
for _, policy := range policies {
for _, rule := range autogen.ComputeRules(policy) {
resourceTypesInRule := GetKindsFromRule(rule)
for resourceKind := range resourceTypesInRule {
resourceTypesMap[resourceKind] = true
}
}
}
for kind := range resourceTypesMap {
resourceTypes = append(resourceTypes, kind)
}
if cluster && dClient != nil {
resources, err = whenClusterIsTrue(resourceTypes, dClient, namespace, resourcePaths, policyReport)
resourceTypesMap := make(map[schema.GroupVersionKind]bool)
var resourceTypes []schema.GroupVersionKind
var subresourceMap map[schema.GroupVersionKind]Subresource
for _, policy := range policies {
for _, rule := range autogen.ComputeRules(policy) {
var resourceTypesInRule map[schema.GroupVersionKind]bool
resourceTypesInRule, subresourceMap = GetKindsFromRule(rule, dClient)
for resourceKind := range resourceTypesInRule {
resourceTypesMap[resourceKind] = true
}
}
}
for kind := range resourceTypesMap {
resourceTypes = append(resourceTypes, kind)
}
resources, err = whenClusterIsTrue(resourceTypes, subresourceMap, dClient, namespace, resourcePaths, policyReport)
if err != nil {
return resources, err
}
@ -62,9 +67,9 @@ func GetResources(policies []kyvernov1.PolicyInterface, resourcePaths []string,
return resources, err
}
func whenClusterIsTrue(resourceTypes []string, dClient dclient.Interface, namespace string, resourcePaths []string, policyReport bool) ([]*unstructured.Unstructured, error) {
func whenClusterIsTrue(resourceTypes []schema.GroupVersionKind, subresourceMap map[schema.GroupVersionKind]Subresource, dClient dclient.Interface, namespace string, resourcePaths []string, policyReport bool) ([]*unstructured.Unstructured, error) {
resources := make([]*unstructured.Unstructured, 0)
resourceMap, err := getResourcesOfTypeFromCluster(resourceTypes, dClient, namespace)
resourceMap, err := getResourcesOfTypeFromCluster(resourceTypes, subresourceMap, dClient, namespace)
if err != nil {
return nil, err
}
@ -189,24 +194,53 @@ func GetResource(resourceBytes []byte) ([]*unstructured.Unstructured, error) {
return resources, nil
}
func getResourcesOfTypeFromCluster(resourceTypes []string, dClient dclient.Interface, namespace string) (map[string]*unstructured.Unstructured, error) {
func getResourcesOfTypeFromCluster(resourceTypes []schema.GroupVersionKind, subresourceMap map[schema.GroupVersionKind]Subresource, dClient dclient.Interface, namespace string) (map[string]*unstructured.Unstructured, error) {
r := make(map[string]*unstructured.Unstructured)
for _, kind := range resourceTypes {
resourceList, err := dClient.ListResource(context.TODO(), "", kind, namespace, nil)
resourceList, err := dClient.ListResource(context.TODO(), kind.GroupVersion().String(), kind.Kind, namespace, nil)
if err != nil {
continue
}
version := resourceList.GetAPIVersion()
gvk := resourceList.GroupVersionKind()
for _, resource := range resourceList.Items {
key := kind + "-" + resource.GetNamespace() + "-" + resource.GetName()
r[key] = resource.DeepCopy()
key := kind.Kind + "-" + resource.GetNamespace() + "-" + resource.GetName()
resource.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: version,
Kind: kind,
Group: gvk.Group,
Version: gvk.Version,
Kind: kind.Kind,
})
r[key] = resource.DeepCopy()
}
}
for _, subresource := range subresourceMap {
parentGV := schema.GroupVersion{Group: subresource.ParentResource.Group, Version: subresource.ParentResource.Version}
resourceList, err := dClient.ListResource(context.TODO(), parentGV.String(), subresource.ParentResource.Kind, namespace, nil)
if err != nil {
continue
}
parentResourceNames := make([]string, 0)
for _, resource := range resourceList.Items {
parentResourceNames = append(parentResourceNames, resource.GetName())
}
for _, parentResourceName := range parentResourceNames {
subresourceName := strings.Split(subresource.APIResource.Name, "/")[1]
resource, err := dClient.GetResource(context.TODO(), parentGV.String(), subresource.ParentResource.Kind, namespace, parentResourceName, subresourceName)
if err != nil {
fmt.Printf("Error: %s", err.Error())
continue
}
key := subresource.APIResource.Kind + "-" + resource.GetNamespace() + "-" + resource.GetName()
resource.SetGroupVersionKind(schema.GroupVersionKind{
Group: subresource.APIResource.Group,
Version: subresource.APIResource.Version,
Kind: subresource.APIResource.Kind,
})
r[key] = resource.DeepCopy()
}
}
return r, nil
@ -280,7 +314,7 @@ func convertResourceToUnstructured(resourceYaml []byte) (*unstructured.Unstructu
return resource, nil
}
// GetPatchedResource converts raw bytes to unstructured object
// GetPatchedAndGeneratedResource converts raw bytes to unstructured object
func GetPatchedAndGeneratedResource(resourceBytes []byte) (unstructured.Unstructured, error) {
getResource, err := GetResource(resourceBytes)
if err != nil {
@ -291,24 +325,17 @@ func GetPatchedAndGeneratedResource(resourceBytes []byte) (unstructured.Unstruct
}
// GetKindsFromRule will return the kinds from policy match block
func GetKindsFromRule(rule kyvernov1.Rule) map[string]bool {
resourceTypesMap := make(map[string]bool)
func GetKindsFromRule(rule kyvernov1.Rule, client dclient.Interface) (map[schema.GroupVersionKind]bool, map[schema.GroupVersionKind]Subresource) {
resourceTypesMap := make(map[schema.GroupVersionKind]bool)
subresourceMap := make(map[schema.GroupVersionKind]Subresource)
for _, kind := range rule.MatchResources.Kinds {
if strings.Contains(kind, "/") {
lastElement := kind[strings.LastIndex(kind, "/")+1:]
resourceTypesMap[cases.Title(language.Und, cases.NoLower).String(lastElement)] = true
}
resourceTypesMap[cases.Title(language.Und, cases.NoLower).String(kind)] = true
addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
}
if rule.MatchResources.Any != nil {
for _, resFilter := range rule.MatchResources.Any {
for _, kind := range resFilter.ResourceDescription.Kinds {
if strings.Contains(kind, "/") {
lastElement := kind[strings.LastIndex(kind, "/")+1:]
resourceTypesMap[cases.Title(language.Und, cases.NoLower).String(lastElement)] = true
}
resourceTypesMap[kind] = true
addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
}
}
}
@ -316,13 +343,36 @@ func GetKindsFromRule(rule kyvernov1.Rule) map[string]bool {
if rule.MatchResources.All != nil {
for _, resFilter := range rule.MatchResources.All {
for _, kind := range resFilter.ResourceDescription.Kinds {
if strings.Contains(kind, "/") {
lastElement := kind[strings.LastIndex(kind, "/")+1:]
resourceTypesMap[cases.Title(language.Und, cases.NoLower).String(lastElement)] = true
}
resourceTypesMap[cases.Title(language.Und, cases.NoLower).String(kind)] = true
addGVKToResourceTypesMap(kind, resourceTypesMap, subresourceMap, client)
}
}
}
return resourceTypesMap
return resourceTypesMap, subresourceMap
}
func addGVKToResourceTypesMap(kind string, resourceTypesMap map[schema.GroupVersionKind]bool, subresourceMap map[schema.GroupVersionKind]Subresource, client dclient.Interface) {
gvString, k := kubeutils.GetKindFromGVK(kind)
apiResource, parentApiResource, _, err := client.Discovery().FindResource(gvString, k)
if err != nil {
log.Log.Info("failed to find resource", "kind", kind, "error", err)
return
}
// The resource is not a subresource
if parentApiResource == nil {
gvk := schema.GroupVersionKind{
Group: apiResource.Group,
Version: apiResource.Version,
Kind: apiResource.Kind,
}
resourceTypesMap[gvk] = true
} else {
gvk := schema.GroupVersionKind{
Group: apiResource.Group, Version: apiResource.Version, Kind: apiResource.Kind,
}
subresourceMap[gvk] = Subresource{
APIResource: *apiResource,
ParentResource: *parentApiResource,
}
}
}

View file

@ -34,7 +34,7 @@ import (
webhookcontroller "github.com/kyverno/kyverno/pkg/controllers/webhook"
"github.com/kyverno/kyverno/pkg/cosign"
"github.com/kyverno/kyverno/pkg/engine/context/resolvers"
event "github.com/kyverno/kyverno/pkg/event"
"github.com/kyverno/kyverno/pkg/event"
"github.com/kyverno/kyverno/pkg/leaderelection"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/metrics"
@ -110,7 +110,6 @@ func createNonLeaderControllers(
kubeInformer kubeinformers.SharedInformerFactory,
kubeKyvernoInformer kubeinformers.SharedInformerFactory,
kyvernoInformer kyvernoinformer.SharedInformerFactory,
kubeClient kubernetes.Interface,
kyvernoClient versioned.Interface,
dynamicClient dclient.Interface,
rclient registryclient.Client,
@ -120,6 +119,7 @@ func createNonLeaderControllers(
manager openapi.Manager,
) ([]internal.Controller, func() error) {
policyCacheController := policycachecontroller.NewController(
dynamicClient,
policyCache,
kyvernoInformer.Kyverno().V1().ClusterPolicies(),
kyvernoInformer.Kyverno().V1().Policies(),
@ -479,7 +479,6 @@ func main() {
kubeInformer,
kubeKyvernoInformer,
kyvernoInformer,
kubeClient,
kyvernoClient,
dClient,
rclient,

View file

@ -845,7 +845,7 @@ func (c *GenerateController) GetUnstrResource(genResourceSpec kyvernov1.Resource
func deleteGeneratedResources(log logr.Logger, client dclient.Interface, ur kyvernov1beta1.UpdateRequest) error {
for _, genResource := range ur.Status.GeneratedResources {
err := client.DeleteResource(context.TODO(), "", genResource.Kind, genResource.Namespace, genResource.Name, false)
err := client.DeleteResource(context.TODO(), genResource.APIVersion, genResource.Kind, genResource.Namespace, genResource.Name, false)
if err != nil && !apierrors.IsNotFound(err) {
return err
}

View file

@ -20,7 +20,8 @@ import (
"go.uber.org/multierr"
yamlv2 "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
cache "k8s.io/client-go/tools/cache"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/cache"
)
var ErrEmptyPatch error = fmt.Errorf("empty resource to patch")
@ -96,6 +97,7 @@ func (c *MutateExistingController) ProcessUR(ur *kyvernov1beta1.UpdateRequest) e
er := engine.Mutate(context.TODO(), c.rclient, policyContext)
for _, r := range er.PolicyResponse.Rules {
patched := r.PatchedTarget
patchedTargetSubresourceName := r.PatchedTargetSubresourceName
switch r.Status {
case response.RuleStatusFail, response.RuleStatusError, response.RuleStatusWarn:
err := fmt.Errorf("failed to mutate existing resource, rule response%v: %s", r.Status, r.Message)
@ -123,7 +125,22 @@ func (c *MutateExistingController) ProcessUR(ur *kyvernov1beta1.UpdateRequest) e
if r.Status == response.RuleStatusPass {
patchedNew.SetResourceVersion("")
_, updateErr := c.client.UpdateResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
var updateErr error
if patchedTargetSubresourceName == "status" {
_, updateErr = c.client.UpdateStatusResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
} else if patchedTargetSubresourceName != "" {
parentResourceGVR := r.PatchedTargetParentResourceGVR
parentResourceGV := schema.GroupVersion{Group: parentResourceGVR.Group, Version: parentResourceGVR.Version}
parentResourceGVK, err := c.client.Discovery().GetGVKFromGVR(parentResourceGV.String(), parentResourceGVR.Resource)
if err != nil {
logger.Error(err, "failed to get GVK from GVR", "GVR", parentResourceGVR)
errs = append(errs, err)
continue
}
_, updateErr = c.client.UpdateResource(context.TODO(), parentResourceGV.String(), parentResourceGVK.Kind, patchedNew.GetNamespace(), patchedNew.Object, false, patchedTargetSubresourceName)
} else {
_, updateErr = c.client.UpdateResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
}
if updateErr != nil {
errs = append(errs, updateErr)
logger.WithName(rule.Name).Error(updateErr, "failed to update target resource", "namespace", patchedNew.GetNamespace(), "name", patchedNew.GetName())

View file

@ -42,7 +42,7 @@ type Interface interface {
// CreateResource creates object for the specified resource/namespace
CreateResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error)
// UpdateResource updates object for the specified resource/namespace
UpdateResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error)
UpdateResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool, subresources ...string) (*unstructured.Unstructured, error)
// UpdateStatusResource updates the resource "status" subresource
UpdateStatusResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error)
}
@ -69,7 +69,7 @@ func NewClient(
rest: disco.RESTClient(),
}
// Set discovery client
discoveryClient := &serverPreferredResources{
discoveryClient := &serverResources{
cachedClient: memory.NewMemCacheClient(disco),
}
// client will invalidate registered resources cache every x seconds,
@ -175,14 +175,14 @@ func (c *client) CreateResource(ctx context.Context, apiVersion string, kind str
}
// UpdateResource updates object for the specified resource/namespace
func (c *client) UpdateResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) {
func (c *client) UpdateResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool, subresources ...string) (*unstructured.Unstructured, error) {
options := metav1.UpdateOptions{}
if dryRun {
options = metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}
}
// convert typed to unstructured obj
if unstructuredObj, err := kubeutils.ConvertToUnstructured(obj); err == nil && unstructuredObj != nil {
return c.getResourceInterface(apiVersion, kind, namespace).Update(ctx, unstructuredObj, options)
return c.getResourceInterface(apiVersion, kind, namespace).Update(ctx, unstructuredObj, options, subresources...)
}
return nil, fmt.Errorf("unable to update resource ")
}

View file

@ -16,37 +16,44 @@ import (
// IDiscovery provides interface to mange Kind and GVR mapping
type IDiscovery interface {
FindResource(apiVersion string, kind string) (*metav1.APIResource, schema.GroupVersionResource, error)
FindResource(groupVersion string, kind string) (apiResource, parentAPIResource *metav1.APIResource, gvr schema.GroupVersionResource, err error)
GetGVRFromKind(kind string) (schema.GroupVersionResource, error)
GetGVRFromAPIVersionKind(apiVersion string, kind string) schema.GroupVersionResource
GetGVRFromAPIVersionKind(groupVersion string, kind string) schema.GroupVersionResource
GetGVKFromGVR(apiVersion, resourceName string) (schema.GroupVersionKind, error)
GetServerVersion() (*version.Info, error)
OpenAPISchema() (*openapiv2.Document, error)
DiscoveryCache() discovery.CachedDiscoveryInterface
DiscoveryInterface() discovery.DiscoveryInterface
}
// serverPreferredResources stores the cachedClient instance for discovery client
type serverPreferredResources struct {
// apiResourceWithListGV is a wrapper for metav1.APIResource with the group-version of its metav1.APIResourceList
type apiResourceWithListGV struct {
apiResource metav1.APIResource
listGV string
}
// serverResources stores the cachedClient instance for discovery client
type serverResources struct {
cachedClient discovery.CachedDiscoveryInterface
}
// DiscoveryCache gets the discovery client cache
func (c serverPreferredResources) DiscoveryCache() discovery.CachedDiscoveryInterface {
func (c serverResources) DiscoveryCache() discovery.CachedDiscoveryInterface {
return c.cachedClient
}
// DiscoveryInterface gets the discovery client
func (c serverPreferredResources) DiscoveryInterface() discovery.DiscoveryInterface {
func (c serverResources) DiscoveryInterface() discovery.DiscoveryInterface {
return c.cachedClient
}
// Poll will keep invalidate the local cache
func (c serverPreferredResources) Poll(ctx context.Context, resync time.Duration) {
func (c serverResources) Poll(ctx context.Context, resync time.Duration) {
logger := logger.WithName("Poll")
// start a ticker
ticker := time.NewTicker(resync)
defer func() { ticker.Stop() }()
logger.V(4).Info("starting registered resources sync", "period", resync)
logger.V(6).Info("starting registered resources sync", "period", resync)
for {
select {
case <-ctx.Done():
@ -61,17 +68,17 @@ func (c serverPreferredResources) Poll(ctx context.Context, resync time.Duration
}
// OpenAPISchema returns the API server OpenAPI schema document
func (c serverPreferredResources) OpenAPISchema() (*openapiv2.Document, error) {
func (c serverResources) OpenAPISchema() (*openapiv2.Document, error) {
return c.cachedClient.OpenAPISchema()
}
// GetGVRFromKind get the Group Version Resource from kind
func (c serverPreferredResources) GetGVRFromKind(kind string) (schema.GroupVersionResource, error) {
func (c serverResources) GetGVRFromKind(kind string) (schema.GroupVersionResource, error) {
if kind == "" {
return schema.GroupVersionResource{}, nil
}
gv, k := kubeutils.GetKindFromGVK(kind)
_, gvr, err := c.FindResource(gv, k)
_, _, gvr, err := c.FindResource(gv, k)
if err != nil {
logger.Info("schema not found", "kind", k)
return schema.GroupVersionResource{}, err
@ -81,8 +88,8 @@ func (c serverPreferredResources) GetGVRFromKind(kind string) (schema.GroupVersi
}
// GetGVRFromAPIVersionKind get the Group Version Resource from APIVersion and kind
func (c serverPreferredResources) GetGVRFromAPIVersionKind(apiVersion string, kind string) schema.GroupVersionResource {
_, gvr, err := c.FindResource(apiVersion, kind)
func (c serverResources) GetGVRFromAPIVersionKind(apiVersion string, kind string) schema.GroupVersionResource {
_, _, gvr, err := c.FindResource(apiVersion, kind)
if err != nil {
logger.Info("schema not found", "kind", kind, "apiVersion", apiVersion, "error : ", err)
return schema.GroupVersionResource{}
@ -92,88 +99,244 @@ func (c serverPreferredResources) GetGVRFromAPIVersionKind(apiVersion string, ki
}
// GetServerVersion returns the server version of the cluster
func (c serverPreferredResources) GetServerVersion() (*version.Info, error) {
func (c serverResources) GetServerVersion() (*version.Info, error) {
return c.cachedClient.ServerVersion()
}
// FindResource finds an API resource that matches 'kind'. If the resource is not
// found and the Cache is not fresh, the cache is invalidated and a retry is attempted
func (c serverPreferredResources) FindResource(apiVersion string, kind string) (*metav1.APIResource, schema.GroupVersionResource, error) {
r, gvr, err := c.findResource(apiVersion, kind)
// GetGVKFromGVR returns the Group Version Kind from Group Version Resource. The groupVersion has to be specified properly
// for example, for corev1.Pod, the groupVersion has to be specified as `v1`, specifying empty groupVersion won't work.
func (c serverResources) GetGVKFromGVR(groupVersion, resourceName string) (schema.GroupVersionKind, error) {
gvk, err := c.findResourceFromResourceName(groupVersion, resourceName)
if err == nil {
return r, gvr, nil
return gvk, nil
}
if !c.cachedClient.Fresh() {
c.cachedClient.Invalidate()
if r, gvr, err = c.findResource(apiVersion, kind); err == nil {
return r, gvr, nil
if gvk, err := c.findResourceFromResourceName(groupVersion, resourceName); err == nil {
return gvk, nil
}
}
return nil, schema.GroupVersionResource{}, err
return schema.GroupVersionKind{}, err
}
func (c serverPreferredResources) findResource(apiVersion string, kind string) (*metav1.APIResource, schema.GroupVersionResource, error) {
var serverResources []*metav1.APIResourceList
var err error
if apiVersion == "" {
serverResources, err = c.cachedClient.ServerPreferredResources()
} else {
_, serverResources, err = c.cachedClient.ServerGroupsAndResources()
}
// findResourceFromResourceName returns the GVK for the a particular resourceName and groupVersion
func (c serverResources) findResourceFromResourceName(groupVersion, resourceName string) (schema.GroupVersionKind, error) {
_, serverGroupsAndResources, err := c.cachedClient.ServerGroupsAndResources()
if err != nil && !strings.Contains(err.Error(), "Got empty response for") {
if discovery.IsGroupDiscoveryFailedError(err) {
logDiscoveryErrors(err, c)
} else if isMetricsServerUnavailable(kind, err) {
logDiscoveryErrors(err)
} else if isMetricsServerUnavailable(groupVersion, err) {
logger.V(3).Info("failed to find preferred resource version", "error", err.Error())
} else {
logger.Error(err, "failed to find preferred resource version")
return nil, schema.GroupVersionResource{}, err
return schema.GroupVersionKind{}, err
}
}
apiResource, err := findResourceFromResourceName(groupVersion, resourceName, serverGroupsAndResources)
if err != nil {
return schema.GroupVersionKind{}, err
}
return schema.GroupVersionKind{Group: apiResource.Group, Version: apiResource.Version, Kind: apiResource.Kind}, err
}
// FindResource finds an API resource that matches 'kind'. For finding subresources that have the same kind as the parent
// resource, kind has to be specified as 'ParentKind/SubresourceName'. For matching status subresource of Pod, kind has
// to be specified as `Pod/status`. If the resource is not found and the Cache is not fresh, the cache is invalidated
// and a retry is attempted
func (c serverResources) FindResource(groupVersion string, kind string) (apiResource, parentAPIResource *metav1.APIResource, gvr schema.GroupVersionResource, err error) {
r, pr, gvr, err := c.findResource(groupVersion, kind)
if err == nil {
return r, pr, gvr, nil
}
if !c.cachedClient.Fresh() {
c.cachedClient.Invalidate()
if r, pr, gvr, err = c.findResource(groupVersion, kind); err == nil {
return r, pr, gvr, nil
}
}
k, subresource := kubeutils.SplitSubresource(kind)
return nil, nil, schema.GroupVersionResource{}, err
}
func (c serverResources) findResource(groupVersion string, kind string) (apiResource, parentAPIResource *metav1.APIResource,
gvr schema.GroupVersionResource, err error,
) {
serverPreferredResources, _ := c.cachedClient.ServerPreferredResources()
_, serverGroupsAndResources, err := c.cachedClient.ServerGroupsAndResources()
if err != nil && !strings.Contains(err.Error(), "Got empty response for") {
if discovery.IsGroupDiscoveryFailedError(err) {
logDiscoveryErrors(err)
} else if isMetricsServerUnavailable(groupVersion, err) {
logger.V(3).Info("failed to find preferred resource version", "error", err.Error())
} else {
logger.Error(err, "failed to find preferred resource version")
return nil, nil, schema.GroupVersionResource{}, err
}
}
kindWithoutSubresource, subresource := kubeutils.SplitSubresource(kind)
if subresource != "" {
kind = k
parentApiResource, _, _, err := c.findResource(groupVersion, kindWithoutSubresource)
if err != nil {
logger.Error(err, "Unable to find parent resource", "kind", kind)
return nil, nil, schema.GroupVersionResource{}, err
}
parentResourceName := parentApiResource.Name
resource, gvr, err := findSubresource(groupVersion, parentResourceName, subresource, kind, serverGroupsAndResources)
return resource, parentApiResource, gvr, err
}
for _, serverResource := range serverResources {
if apiVersion != "" && serverResource.GroupVersion != apiVersion {
continue
}
return findResource(groupVersion, kind, serverPreferredResources, serverGroupsAndResources)
}
for _, resource := range serverResource.APIResources {
if resourceMatches(resource, kind, subresource) {
logger.V(6).Info("matched API resource to kind", "apiResource", resource, "kind", kind)
gv, err := schema.ParseGroupVersion(serverResource.GroupVersion)
if err != nil {
logger.Error(err, "failed to parse GV", "groupVersion", serverResource.GroupVersion)
return nil, schema.GroupVersionResource{}, err
// findSubresource finds the subresource for the given parent resource, groupVersion and serverResourcesList
func findSubresource(groupVersion, parentResourceName, subresource, kind string, serverResourcesList []*metav1.APIResourceList) (
apiResource *metav1.APIResource, gvr schema.GroupVersionResource, err error,
) {
for _, serverResourceList := range serverResourcesList {
if groupVersion == "" || kubeutils.GroupVersionMatches(groupVersion, serverResourceList.GroupVersion) {
for _, serverResource := range serverResourceList.APIResources {
if serverResource.Name == parentResourceName+"/"+strings.ToLower(subresource) {
logger.V(6).Info("matched API resource to kind", "apiResource", serverResource, "kind", kind)
serverResourceGv := getServerResourceGroupVersion(serverResourceList.GroupVersion, serverResource.Group, serverResource.Version)
gv, _ := schema.ParseGroupVersion(serverResourceGv)
serverResource.Group = gv.Group
serverResource.Version = gv.Version
groupVersionResource := gv.WithResource(serverResource.Name)
logger.V(6).Info("gv with resource", "gvWithResource", groupVersionResource)
return &serverResource, groupVersionResource, nil
}
// We potentially need to fix Group and Version with what the list is for
if resource.Group == "" {
resource.Group = gv.Group
}
if resource.Version == "" {
resource.Version = gv.Version
}
return &resource, gv.WithResource(resource.Name), nil
}
}
}
return nil, schema.GroupVersionResource{}, fmt.Errorf("kind '%s' not found in apiVersion '%s'", kind, apiVersion)
return nil, schema.GroupVersionResource{}, fmt.Errorf("resource not found for kind %s", kind)
}
// resourceMatches checks the resource Kind, Name, SingularName and a subresource if specified
// e.g. &apiResource{Name: "taskruns/status", Kind: "TaskRun"} will match "kind=TaskRun, subresource=Status"
func resourceMatches(resource metav1.APIResource, kind, subresource string) bool {
if resource.Kind == kind || resource.Name == kind || resource.SingularName == kind {
_, s := kubeutils.SplitSubresource(resource.Name)
return strings.EqualFold(s, subresource)
// findResource finds an API resource that matches 'groupVersion', 'kind', in the given serverResourcesList
func findResource(groupVersion string, kind string, serverPreferredResources, serverGroupsAndResources []*metav1.APIResourceList) (
apiResource, parentAPIResource *metav1.APIResource, gvr schema.GroupVersionResource, err error,
) {
matchingServerResources := getMatchingServerResources(groupVersion, kind, serverGroupsAndResources)
onlySubresourcePresentInMatchingResources := len(matchingServerResources) > 0
for _, matchingServerResource := range matchingServerResources {
if !kubeutils.IsSubresource(matchingServerResource.apiResource.Name) {
onlySubresourcePresentInMatchingResources = false
break
}
}
return false
if onlySubresourcePresentInMatchingResources {
apiResourceWithListGV := matchingServerResources[0]
matchingServerResource := apiResourceWithListGV.apiResource
logger.V(6).Info("matched API resource to kind", "apiResource", matchingServerResource, "kind", kind)
groupVersionResource := schema.GroupVersionResource{
Resource: matchingServerResource.Name,
Group: matchingServerResource.Group,
Version: matchingServerResource.Version,
}
logger.V(6).Info("gv with resource", "gvWithResource", groupVersionResource)
parentAPIResource, err := findResourceFromResourceName(apiResourceWithListGV.listGV, strings.Split(matchingServerResource.Name, "/")[0], serverPreferredResources)
if err != nil {
return nil, nil, schema.GroupVersionResource{}, fmt.Errorf("failed to find parent resource for subresource %s: %v", matchingServerResource.Name, err)
}
logger.V(6).Info("parent API resource", "parentAPIResource", parentAPIResource)
return &matchingServerResource, parentAPIResource, groupVersionResource, nil
}
if groupVersion == "" && len(matchingServerResources) > 0 {
for _, serverResourceList := range serverPreferredResources {
for _, serverResource := range serverResourceList.APIResources {
serverResourceGv := getServerResourceGroupVersion(serverResourceList.GroupVersion, serverResource.Group, serverResource.Version)
if serverResource.Kind == kind || serverResource.SingularName == kind {
gv, _ := schema.ParseGroupVersion(serverResourceGv)
serverResource.Group = gv.Group
serverResource.Version = gv.Version
groupVersionResource := gv.WithResource(serverResource.Name)
logger.V(6).Info("matched API resource to kind", "apiResource", serverResource, "kind", kind)
return &serverResource, nil, groupVersionResource, nil
}
}
}
} else {
for _, apiResourceWithListGV := range matchingServerResources {
matchingServerResource := apiResourceWithListGV.apiResource
if !kubeutils.IsSubresource(matchingServerResource.Name) {
logger.V(6).Info("matched API resource to kind", "apiResource", matchingServerResource, "kind", kind)
groupVersionResource := schema.GroupVersionResource{
Resource: matchingServerResource.Name,
Group: matchingServerResource.Group,
Version: matchingServerResource.Version,
}
logger.V(6).Info("gv with resource", "groupVersionResource", groupVersionResource)
return &matchingServerResource, nil, groupVersionResource, nil
}
}
}
return nil, nil, schema.GroupVersionResource{}, fmt.Errorf("kind '%s' not found in groupVersion '%s'", kind, groupVersion)
}
// getMatchingServerResources returns a list of API resources that match the given groupVersion and kind
func getMatchingServerResources(groupVersion string, kind string, serverGroupsAndResources []*metav1.APIResourceList) []apiResourceWithListGV {
matchingServerResources := make([]apiResourceWithListGV, 0)
for _, serverResourceList := range serverGroupsAndResources {
for _, serverResource := range serverResourceList.APIResources {
serverResourceGv := getServerResourceGroupVersion(serverResourceList.GroupVersion, serverResource.Group, serverResource.Version)
if groupVersion == "" || kubeutils.GroupVersionMatches(groupVersion, serverResourceGv) {
if serverResource.Kind == kind || serverResource.SingularName == kind {
gv, _ := schema.ParseGroupVersion(serverResourceGv)
serverResource.Group = gv.Group
serverResource.Version = gv.Version
matchingServerResources = append(matchingServerResources, apiResourceWithListGV{apiResource: serverResource, listGV: serverResourceList.GroupVersion})
}
}
}
}
return matchingServerResources
}
// findResourceFromResourceName finds an API resource that matches 'resourceName', in the given serverResourcesList
func findResourceFromResourceName(groupVersion string, resourceName string, serverGroupsAndResources []*metav1.APIResourceList) (*metav1.APIResource, error) {
for _, serverResourceList := range serverGroupsAndResources {
for _, apiResource := range serverResourceList.APIResources {
serverResourceGroupVersion := getServerResourceGroupVersion(serverResourceList.GroupVersion, apiResource.Group, apiResource.Version)
if serverResourceGroupVersion == groupVersion && apiResource.Name == resourceName {
logger.V(6).Info("found preferred resource", "groupVersion", groupVersion, "resourceName", resourceName)
groupVersion, _ := schema.ParseGroupVersion(serverResourceGroupVersion)
apiResource.Group = groupVersion.Group
apiResource.Version = groupVersion.Version
return &apiResource, nil
}
}
}
return nil, fmt.Errorf("resource %s not found in group %s", resourceName, groupVersion)
}
// getServerResourceGroupVersion returns the groupVersion of the serverResource from the apiResourceMetadata
func getServerResourceGroupVersion(apiResourceListGroupVersion, apiResourceGroup, apiResourceVersion string) string {
var serverResourceGroupVersion string
if apiResourceGroup == "" && apiResourceVersion == "" {
serverResourceGroupVersion = apiResourceListGroupVersion
} else {
serverResourceGroupVersion = schema.GroupVersion{
Group: apiResourceGroup,
Version: apiResourceVersion,
}.String()
}
return serverResourceGroupVersion
}

View file

@ -3,20 +3,203 @@ package dclient
import (
"testing"
"k8s.io/apimachinery/pkg/runtime/schema"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func Test_resourceMatches(t *testing.T) {
ar := metav1.APIResource{Name: "taskruns/status", Kind: "TaskRun"}
assert.Equal(t, resourceMatches(ar, "TaskRun", "Status"), true)
var (
networkPolicyAPIResource = metav1.APIResource{Name: "networkpolicies", SingularName: "", Namespaced: true, Kind: "NetworkPolicy"}
networkPolicyStatusAPIResource = metav1.APIResource{Name: "networkpolicies/status", SingularName: "", Namespaced: true, Kind: "NetworkPolicy"}
ar = metav1.APIResource{Name: "taskruns/status", Kind: "TaskRun"}
assert.Equal(t, resourceMatches(ar, "TaskRun", ""), false)
podAPIResource = metav1.APIResource{Name: "pods", SingularName: "", Namespaced: true, Kind: "Pod"}
podEvictionAPIResource = metav1.APIResource{Name: "pods/eviction", SingularName: "", Namespaced: true, Group: "policy", Version: "v1", Kind: "Eviction"}
podLogAPIResource = metav1.APIResource{Name: "pods/log", SingularName: "", Namespaced: true, Kind: "Pod"}
ar = metav1.APIResource{Name: "taskruns", Kind: "TaskRun"}
assert.Equal(t, resourceMatches(ar, "TaskRun", ""), true)
cronJobAPIResource = metav1.APIResource{Name: "cronjobs", SingularName: "", Namespaced: true, Kind: "CronJob"}
)
ar = metav1.APIResource{Name: "tasks/status", Kind: "Task"}
assert.Equal(t, resourceMatches(ar, "TaskRun", "Status"), false)
func Test_findSubresource(t *testing.T) {
serverGroupsAndResources := []*metav1.APIResourceList{
{
GroupVersion: "networking.k8s.io/v1",
APIResources: []metav1.APIResource{
networkPolicyAPIResource,
networkPolicyStatusAPIResource,
},
},
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
podAPIResource,
podEvictionAPIResource,
},
},
}
apiResource, gvr, err := findSubresource("", "pods", "eviction", "Pod/eviction", serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "pods/eviction", Group: "policy", Version: "v1"})
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, podEvictionAPIResource.Name)
assert.Equal(t, apiResource.Kind, podEvictionAPIResource.Kind)
assert.Equal(t, apiResource.Group, podEvictionAPIResource.Group)
assert.Equal(t, apiResource.Version, podEvictionAPIResource.Version)
apiResource, gvr, err = findSubresource("v1", "pods", "eviction", "Pod/eviction", serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "pods/eviction", Group: "policy", Version: "v1"})
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, podEvictionAPIResource.Name)
assert.Equal(t, apiResource.Kind, podEvictionAPIResource.Kind)
assert.Equal(t, apiResource.Group, podEvictionAPIResource.Group)
assert.Equal(t, apiResource.Version, podEvictionAPIResource.Version)
apiResource, gvr, err = findSubresource("networking.k8s.io/*", "networkpolicies", "status", "NetworkPolicy/status", serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "networkpolicies/status", Group: "networking.k8s.io", Version: "v1"})
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, networkPolicyStatusAPIResource.Name)
assert.Equal(t, apiResource.Kind, networkPolicyStatusAPIResource.Kind)
// Resources with empty GV use the GV of APIResourceList
assert.Equal(t, apiResource.Group, "networking.k8s.io")
assert.Equal(t, apiResource.Version, "v1")
}
func Test_findResource(t *testing.T) {
serverGroupsAndResources := []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
podEvictionAPIResource,
podLogAPIResource,
podAPIResource,
},
},
{
GroupVersion: "batch/v1beta1",
APIResources: []metav1.APIResource{
cronJobAPIResource,
},
},
{
GroupVersion: "batch/v1",
APIResources: []metav1.APIResource{
cronJobAPIResource,
},
},
}
serverPreferredResourcesList := []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
podAPIResource,
},
},
{
GroupVersion: "batch/v1",
APIResources: []metav1.APIResource{
cronJobAPIResource,
},
},
}
apiResource, parentAPIResource, gvr, err := findResource("", "Pod", serverPreferredResourcesList, serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "pods", Group: "", Version: "v1"})
assert.Equal(t, parentAPIResource, (*metav1.APIResource)(nil))
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, podAPIResource.Name)
assert.Equal(t, apiResource.Kind, podAPIResource.Kind)
// Resources with empty GV use the GV of APIResourceList
assert.Equal(t, apiResource.Group, "")
assert.Equal(t, apiResource.Version, "v1")
apiResource, parentAPIResource, gvr, err = findResource("policy/v1", "Eviction", serverPreferredResourcesList, serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "pods/eviction", Group: "policy", Version: "v1"})
assert.Equal(t, parentAPIResource.Name, podAPIResource.Name)
assert.Equal(t, parentAPIResource.Kind, podAPIResource.Kind)
assert.Equal(t, parentAPIResource.Group, "")
assert.Equal(t, parentAPIResource.Version, "v1")
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, podEvictionAPIResource.Name)
assert.Equal(t, apiResource.Kind, podEvictionAPIResource.Kind)
assert.Equal(t, apiResource.Group, podEvictionAPIResource.Group)
assert.Equal(t, apiResource.Version, podEvictionAPIResource.Version)
apiResource, parentAPIResource, gvr, err = findResource("", "CronJob", serverPreferredResourcesList, serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "cronjobs", Group: "batch", Version: "v1"})
assert.Equal(t, parentAPIResource, (*metav1.APIResource)(nil))
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, cronJobAPIResource.Name)
assert.Equal(t, apiResource.Kind, cronJobAPIResource.Kind)
assert.Equal(t, apiResource.Group, "batch")
assert.Equal(t, apiResource.Version, "v1")
apiResource, parentAPIResource, gvr, err = findResource("batch/v1beta1", "CronJob", serverPreferredResourcesList, serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, gvr, schema.GroupVersionResource{Resource: "cronjobs", Group: "batch", Version: "v1beta1"})
assert.Equal(t, parentAPIResource, (*metav1.APIResource)(nil))
// Not comparing directly because actual apiResource also contains fields like 'ShortNames' which are not set in the expected apiResource
assert.Equal(t, apiResource.Name, cronJobAPIResource.Name)
assert.Equal(t, apiResource.Kind, cronJobAPIResource.Kind)
assert.Equal(t, apiResource.Group, "batch")
assert.Equal(t, apiResource.Version, "v1beta1")
}
func Test_getServerResourceGroupVersion(t *testing.T) {
apiResource := &metav1.APIResource{Name: "pods", SingularName: "", Namespaced: true, Kind: "Pod"}
apiResourceListGV := "v1"
assert.Equal(t, getServerResourceGroupVersion(apiResourceListGV, apiResource.Group, apiResource.Version), "v1")
apiResource = &metav1.APIResource{Name: "horizontalpodautoscalers", SingularName: "", Namespaced: true, Kind: "HorizontalPodAutoscaler"}
apiResourceListGV = "autoscaling/v2beta1"
assert.Equal(t, getServerResourceGroupVersion(apiResourceListGV, apiResource.Group, apiResource.Version), "autoscaling/v2beta1")
apiResource = &metav1.APIResource{Name: "deployments/scale", SingularName: "", Namespaced: true, Group: "autoscaling", Version: "v1", Kind: "Scale"}
apiResourceListGV = "apps/v1"
assert.Equal(t, getServerResourceGroupVersion(apiResourceListGV, apiResource.Group, apiResource.Version), "autoscaling/v1")
}
func Test_findResourceFromResourceName(t *testing.T) {
serverGroupsAndResources := []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
podAPIResource,
podEvictionAPIResource,
},
},
}
apiResource, err := findResourceFromResourceName("v1", "pods", serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, apiResource.Name, podAPIResource.Name)
assert.Equal(t, apiResource.Kind, podAPIResource.Kind)
assert.Equal(t, apiResource.Group, "")
assert.Equal(t, apiResource.Version, "v1")
apiResource, err = findResourceFromResourceName("policy/v1", "pods/eviction", serverGroupsAndResources)
assert.NilError(t, err)
assert.Equal(t, apiResource.Name, podEvictionAPIResource.Name)
assert.Equal(t, apiResource.Kind, podEvictionAPIResource.Kind)
assert.Equal(t, apiResource.Group, podEvictionAPIResource.Group)
assert.Equal(t, apiResource.Version, podEvictionAPIResource.Version)
}

View file

@ -77,13 +77,17 @@ func (c *fakeDiscoveryClient) GetGVRFromKind(kind string) (schema.GroupVersionRe
return c.getGVR(resource), nil
}
func (c *fakeDiscoveryClient) GetGVKFromGVR(apiVersion, resourceName string) (schema.GroupVersionKind, error) {
return schema.GroupVersionKind{}, nil
}
func (c *fakeDiscoveryClient) GetGVRFromAPIVersionKind(apiVersion string, kind string) schema.GroupVersionResource {
resource := strings.ToLower(kind) + "s"
return c.getGVR(resource)
}
func (c *fakeDiscoveryClient) FindResource(apiVersion string, kind string) (*metav1.APIResource, schema.GroupVersionResource, error) {
return nil, schema.GroupVersionResource{}, fmt.Errorf("not implemented")
func (c *fakeDiscoveryClient) FindResource(groupVersion string, kind string) (apiResource, parentAPIResource *metav1.APIResource, gvr schema.GroupVersionResource, err error) {
return nil, nil, schema.GroupVersionResource{}, fmt.Errorf("not implemented")
}
func (c *fakeDiscoveryClient) OpenAPISchema() (*openapiv2.Document, error) {

View file

@ -6,7 +6,7 @@ import (
"k8s.io/client-go/discovery"
)
func logDiscoveryErrors(err error, c serverPreferredResources) {
func logDiscoveryErrors(err error) {
discoveryError := err.(*discovery.ErrGroupDiscoveryFailed)
for gv, e := range discoveryError.Groups {
if gv.Group == "custom.metrics.k8s.io" || gv.Group == "metrics.k8s.io" || gv.Group == "external.metrics.k8s.io" {
@ -19,9 +19,9 @@ func logDiscoveryErrors(err error, c serverPreferredResources) {
}
}
func isMetricsServerUnavailable(kind string, err error) bool {
func isMetricsServerUnavailable(groupVersion string, err error) bool {
// error message is defined at:
// https://github.com/kubernetes/apimachinery/blob/2456ebdaba229616fab2161a615148884b46644b/pkg/api/errors/errors.go#L432
return strings.HasPrefix(kind, "metrics.k8s.io/") &&
return strings.HasPrefix(groupVersion, "metrics.k8s.io/") &&
strings.Contains(err.Error(), "the server is currently unable to handle the request")
}

View file

@ -6,11 +6,14 @@ import (
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/autogen"
kyvernov1informers "github.com/kyverno/kyverno/pkg/client/informers/externalversions/kyverno/v1"
kyvernov1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/controllers"
pcache "github.com/kyverno/kyverno/pkg/policycache"
controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
@ -39,14 +42,18 @@ type controller struct {
// queue
queue workqueue.RateLimitingInterface
// client
client dclient.Interface
}
func NewController(pcache pcache.Cache, cpolInformer kyvernov1informers.ClusterPolicyInformer, polInformer kyvernov1informers.PolicyInformer) Controller {
func NewController(client dclient.Interface, pcache pcache.Cache, cpolInformer kyvernov1informers.ClusterPolicyInformer, polInformer kyvernov1informers.PolicyInformer) Controller {
c := controller{
cache: pcache,
cpolLister: cpolInformer.Lister(),
polLister: polInformer.Lister(),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), ControllerName),
client: client,
}
controllerutils.AddDefaultEventHandlers(logger, cpolInformer.Informer(), c.queue)
controllerutils.AddDefaultEventHandlers(logger, polInformer.Informer(), c.queue)
@ -65,7 +72,8 @@ func (c *controller) WarmUp() error {
if key, err := cache.MetaNamespaceKeyFunc(policy); err != nil {
return err
} else {
c.cache.Set(key, policy)
subresourceGVKToKind := getSubresourceGVKToKindMap(policy, c.client)
c.cache.Set(key, policy, subresourceGVKToKind)
}
}
cpols, err := c.cpolLister.List(labels.Everything())
@ -76,7 +84,8 @@ func (c *controller) WarmUp() error {
if key, err := cache.MetaNamespaceKeyFunc(policy); err != nil {
return err
} else {
c.cache.Set(key, policy)
subresourceGVKToKind := getSubresourceGVKToKindMap(policy, c.client)
c.cache.Set(key, policy, subresourceGVKToKind)
}
}
return nil
@ -95,7 +104,8 @@ func (c *controller) reconcile(ctx context.Context, logger logr.Logger, key, nam
return err
}
// TODO: check resource version ?
c.cache.Set(key, policy)
subresourceGVKToKind := getSubresourceGVKToKindMap(policy, c.client)
c.cache.Set(key, policy, subresourceGVKToKind)
return nil
}
@ -106,3 +116,18 @@ func (c *controller) loadPolicy(namespace, name string) (kyvernov1.PolicyInterfa
return c.polLister.Policies(namespace).Get(name)
}
}
func getSubresourceGVKToKindMap(policy kyvernov1.PolicyInterface, client dclient.Interface) map[string]string {
subresourceGVKToKind := make(map[string]string)
for _, rule := range autogen.ComputeRules(policy) {
for _, gvk := range rule.MatchResources.GetKinds() {
gv, k := kubeutils.GetKindFromGVK(gvk)
_, subresource := kubeutils.SplitSubresource(k)
if subresource != "" {
apiResource, _, _, _ := client.Discovery().FindResource(gv, k)
subresourceGVKToKind[gvk] = apiResource.Kind
}
}
}
return subresourceGVKToKind
}

View file

@ -206,7 +206,7 @@ func (c *controller) updateDynamicWatchers(ctx context.Context) error {
gvrs := map[schema.GroupVersionKind]schema.GroupVersionResource{}
for _, kind := range kinds.List() {
apiVersion, kind := kubeutils.GetKindFromGVK(kind)
apiResource, gvr, err := c.client.Discovery().FindResource(apiVersion, kind)
apiResource, _, gvr, err := c.client.Discovery().FindResource(apiVersion, kind)
if err != nil {
logger.Error(err, "failed to get gvr from kind", "kind", kind)
} else {

View file

@ -839,35 +839,24 @@ func (c *controller) mergeWebhook(dst *webhook, policy kyvernov1.PolicyInterface
gvkMap[gvk] = 1
// NOTE: webhook stores GVR in its rules while policy stores GVK in its rules definition
gv, k := kubeutils.GetKindFromGVK(gvk)
switch k {
case "Binding":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods/binding"})
case "NodeProxyOptions":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "nodes/proxy"})
case "PodAttachOptions":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods/attach"})
case "PodExecOptions":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods/exec"})
case "PodPortForwardOptions":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods/portforward"})
case "PodProxyOptions":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods/proxy"})
case "ServiceProxyOptions":
gvrList = append(gvrList, schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services/proxy"})
default:
_, gvr, err := c.discoveryClient.FindResource(gv, k)
if err != nil {
logger.Error(err, "unable to convert GVK to GVR", "GVK", gvk)
continue
}
if strings.Contains(gvk, "*") {
group := kubeutils.GetGroupFromGVK(gvk)
gvrList = append(gvrList, schema.GroupVersionResource{Group: group, Version: "*", Resource: gvr.Resource})
} else {
logger.V(4).Info("configuring webhook", "GVK", gvk, "GVR", gvr)
gvrList = append(gvrList, gvr)
_, parentAPIResource, gvr, err := c.discoveryClient.FindResource(gv, k)
if err != nil {
logger.Error(err, "unable to convert GVK to GVR", "GVK", gvk)
continue
}
if parentAPIResource != nil {
gvr = schema.GroupVersionResource{
Group: parentAPIResource.Group,
Version: parentAPIResource.Version,
Resource: gvr.Resource,
}
}
if strings.Contains(gvk, "*") {
gvrList = append(gvrList, schema.GroupVersionResource{Group: gvr.Group, Version: "*", Resource: gvr.Resource})
} else {
logger.V(4).Info("configuring webhook", "GVK", gvk, "GVR", gvr)
gvrList = append(gvrList, gvr)
}
}
}
for _, gvr := range gvrList {

View file

@ -31,12 +31,6 @@ func (wh *webhook) buildRulesWithOperations(ops ...admissionregistrationv1.Opera
var rules []admissionregistrationv1.RuleWithOperations
for gvr := range wh.rules {
resources := sets.NewString(gvr.Resource)
if resources.Has("pods") {
resources.Insert("pods/ephemeralcontainers")
}
if resources.Has("services") {
resources.Insert("services/status")
}
rules = append(rules, admissionregistrationv1.RuleWithOperations{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{gvr.Group},

View file

@ -88,10 +88,13 @@ func filterRule(rclient registryclient.Client, rule kyvernov1.Rule, policyContex
logger := logging.WithName(string(ruleType)).WithValues("policy", policy.GetName(),
"kind", newResource.GetKind(), "namespace", newResource.GetNamespace(), "name", newResource.GetName())
if err = MatchesResourceDescription(newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, ""); err != nil {
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext)
if err = MatchesResourceDescription(subresourceGVKToAPIResource, newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.subresource); err != nil {
if ruleType == response.Generation {
// if the oldResource matched, return "false" to delete GR for it
if err = MatchesResourceDescription(oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, ""); err == nil {
if err = MatchesResourceDescription(subresourceGVKToAPIResource, oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.subresource); err == nil {
return &response.RuleResponse{
Name: rule.Name,
Type: ruleType,

62
pkg/engine/common.go Normal file
View file

@ -0,0 +1,62 @@
package engine
import (
"strings"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GetSubresourceGVKToAPIResourceMap returns a map of subresource GVK to APIResource. This is used to determine if a resource is a subresource.
func GetSubresourceGVKToAPIResourceMap(kindsInPolicy []string, ctx *PolicyContext) map[string]*metav1.APIResource {
subresourceGVKToAPIResource := make(map[string]*metav1.APIResource)
for _, gvk := range kindsInPolicy {
gv, k := kubeutils.GetKindFromGVK(gvk)
parentKind, subresource := kubeutils.SplitSubresource(k)
// Len of subresources is non zero only when validation request was sent from CLI without connecting to the cluster.
if len(ctx.subresourcesInPolicy) != 0 {
if subresource != "" {
for _, subresourceInPolicy := range ctx.subresourcesInPolicy {
parentResourceGroupVersion := metav1.GroupVersion{
Group: subresourceInPolicy.ParentResource.Group,
Version: subresourceInPolicy.ParentResource.Version,
}.String()
if gv == "" || kubeutils.GroupVersionMatches(gv, parentResourceGroupVersion) {
if parentKind == subresourceInPolicy.ParentResource.Kind {
if strings.ToLower(subresource) == strings.Split(subresourceInPolicy.APIResource.Name, "/")[1] {
subresourceGVKToAPIResource[gvk] = &(subresourceInPolicy.APIResource)
break
}
}
}
}
} else { // Complete kind may be a subresource, for eg- 'PodExecOptions'
for _, subresourceInPolicy := range ctx.subresourcesInPolicy {
// Subresources which can be just specified by kind, for eg- 'PodExecOptions'
// have different kind than their parent resource. Otherwise for subresources which
// have same kind as parent resource, need to be specified as Kind/Subresource, eg - 'Pod/status'
if k == subresourceInPolicy.APIResource.Kind &&
k != subresourceInPolicy.ParentResource.Kind {
subresourceGroupVersion := metav1.GroupVersion{
Group: subresourceInPolicy.APIResource.Group,
Version: subresourceInPolicy.APIResource.Version,
}.String()
if gv == "" || kubeutils.GroupVersionMatches(gv, subresourceGroupVersion) {
subresourceGVKToAPIResource[gvk] = subresourceInPolicy.APIResource.DeepCopy()
break
}
}
}
}
} else if ctx.client != nil {
// find the resource from API client
apiResource, _, _, err := ctx.client.Discovery().FindResource(gv, k)
if err == nil {
if kubeutils.IsSubresource(apiResource.Name) {
subresourceGVKToAPIResource[gvk] = apiResource
}
}
}
}
return subresourceGVKToAPIResource
}

78
pkg/engine/common_test.go Normal file
View file

@ -0,0 +1,78 @@
package engine
import (
"testing"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func Test_GetSubresourceGVKToAPIResourceMap(t *testing.T) {
podAPIResource := metav1.APIResource{
Name: "pods",
SingularName: "",
Namespaced: true,
Kind: "Pod",
Group: "",
Version: "v1",
}
podStatusAPIResource := metav1.APIResource{
Name: "pods/status",
SingularName: "",
Namespaced: true,
Kind: "Pod",
Group: "",
Version: "v1",
}
podEvictAPIResource := metav1.APIResource{
Name: "pods/eviction",
SingularName: "",
Namespaced: true,
Kind: "Eviction",
Group: "policy",
Version: "v1",
}
policyContext := NewPolicyContext().
WithSubresourcesInPolicy([]struct {
APIResource metav1.APIResource
ParentResource metav1.APIResource
}{
{
APIResource: podStatusAPIResource,
ParentResource: podAPIResource,
},
{
APIResource: podEvictAPIResource,
ParentResource: podAPIResource,
},
})
kindsInPolicy := []string{"Pod", "Eviction", "Pod/status", "Pod/eviction"}
subresourceGVKToAPIResourceMap := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext)
podStatusResourceFromMap := subresourceGVKToAPIResourceMap["Pod/status"]
assert.Equal(t, podStatusResourceFromMap.Name, podStatusAPIResource.Name)
assert.Equal(t, podStatusResourceFromMap.Kind, podStatusAPIResource.Kind)
assert.Equal(t, podStatusResourceFromMap.Group, podStatusAPIResource.Group)
assert.Equal(t, podStatusResourceFromMap.Version, podStatusAPIResource.Version)
podEvictResourceFromMap := subresourceGVKToAPIResourceMap["Pod/eviction"]
assert.Equal(t, podEvictResourceFromMap.Name, podEvictAPIResource.Name)
assert.Equal(t, podEvictResourceFromMap.Kind, podEvictAPIResource.Kind)
assert.Equal(t, podEvictResourceFromMap.Group, podEvictAPIResource.Group)
assert.Equal(t, podEvictResourceFromMap.Version, podEvictAPIResource.Version)
podEvictResourceFromMap = subresourceGVKToAPIResourceMap["Eviction"]
assert.Equal(t, podEvictResourceFromMap.Name, podEvictAPIResource.Name)
assert.Equal(t, podEvictResourceFromMap.Kind, podEvictAPIResource.Kind)
assert.Equal(t, podEvictResourceFromMap.Group, podEvictAPIResource.Group)
assert.Equal(t, podEvictResourceFromMap.Version, podEvictAPIResource.Version)
_, ok := subresourceGVKToAPIResourceMap["Pod"]
assert.Equal(t, ok, false)
}

View file

@ -149,7 +149,7 @@ func VerifyAndPatchImages(
}
func appendResponse(resp *response.EngineResponse, rule *kyvernov1.Rule, msg string, status response.RuleStatus) {
rr := ruleResponse(*rule, response.ImageVerify, msg, status, nil)
rr := ruleResponse(*rule, response.ImageVerify, msg, status)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *rr)
incrementErrorCount(resp)
}
@ -196,7 +196,7 @@ func (iv *imageVerifier) verify(ctx context.Context, imageVerify kyvernov1.Image
if hasImageVerifiedAnnotationChanged(iv.policyContext, iv.logger) {
msg := imageVerifyAnnotationKey + " annotation cannot be changed"
iv.logger.Info("image verification error", "reason", msg)
ruleResp := ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil)
ruleResp := ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail)
iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
incrementAppliedCount(iv.resp)
continue
@ -223,7 +223,7 @@ func (iv *imageVerifier) verify(ctx context.Context, imageVerify kyvernov1.Image
ruleResp = ruleError(iv.rule, response.ImageVerify, "failed to update digest", err)
} else if patch != nil {
if ruleResp == nil {
ruleResp = ruleResponse(*iv.rule, response.ImageVerify, "mutated image digest", response.RuleStatusPass, nil)
ruleResp = ruleResponse(*iv.rule, response.ImageVerify, "mutated image digest", response.RuleStatusPass)
}
ruleResp.Patches = append(ruleResp.Patches, patch)
@ -310,7 +310,7 @@ func (iv *imageVerifier) verifyImage(
if err := iv.policyContext.jsonContext.AddImageInfo(imageInfo); err != nil {
iv.logger.Error(err, "failed to add image to context")
msg := fmt.Sprintf("failed to add image to context %s: %s", image, err.Error())
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusError, nil), ""
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusError), ""
}
if len(imageVerify.Attestors) > 0 {
@ -346,10 +346,10 @@ func (iv *imageVerifier) verifyAttestors(
// handle registry network errors as a rule error (instead of a policy failure)
var netErr *net.OpError
if errors.As(err, &netErr) {
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusError, nil), nil, nil
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusError), nil, nil
}
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil), nil, nil
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail), nil, nil
}
newAttestors = append(newAttestors, attestors[i])
}
@ -359,7 +359,7 @@ func (iv *imageVerifier) verifyAttestors(
}
msg := fmt.Sprintf("verified image signatures for %s", image)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), cosignResponse, newAttestors
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass), cosignResponse, newAttestors
}
func (iv *imageVerifier) verifyAttestations(
@ -398,23 +398,23 @@ func (iv *imageVerifier) verifyAttestations(
// handle registry network errors as a rule error (instead of a policy failure)
var netErr *net.OpError
if errors.As(err, &netErr) {
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusError, nil), ""
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusError), ""
}
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail, nil), ""
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusFail), ""
}
verifiedCount++
attestationError = iv.verifyAttestation(cosignResp.Statements, attestation, imageInfo)
if attestationError != nil {
attestationError = errors.Wrapf(attestationError, entryPath+subPath)
return ruleResponse(*iv.rule, response.ImageVerify, attestationError.Error(), response.RuleStatusFail, nil), ""
return ruleResponse(*iv.rule, response.ImageVerify, attestationError.Error(), response.RuleStatusFail), ""
}
if verifiedCount >= requiredCount {
msg := fmt.Sprintf("image attestations verification succeeded, verifiedCount: %v, requiredCount: %v", verifiedCount, requiredCount)
iv.logger.V(2).Info(msg)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), ""
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass), ""
}
}
}
@ -423,7 +423,7 @@ func (iv *imageVerifier) verifyAttestations(
msg := fmt.Sprintf("verified image attestations for %s", image)
iv.logger.V(2).Info(msg)
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass, nil), ""
return ruleResponse(*iv.rule, response.ImageVerify, msg, response.RuleStatusPass), ""
}
func (iv *imageVerifier) verifyAttestorSet(

View file

@ -23,10 +23,10 @@ func processImageValidationRule(ctx context.Context, log logr.Logger, rclient re
log = log.WithValues("rule", rule.Name)
matchingImages, _, err := extractMatchingImages(enginectx, rule)
if err != nil {
return ruleResponse(*rule, response.Validation, err.Error(), response.RuleStatusError, nil)
return ruleResponse(*rule, response.Validation, err.Error(), response.RuleStatusError)
}
if len(matchingImages) == 0 {
return ruleResponse(*rule, response.Validation, "image verified", response.RuleStatusSkip, nil)
return ruleResponse(*rule, response.Validation, "image verified", response.RuleStatusSkip)
}
if err := LoadContext(ctx, log, rclient, rule.Context, enginectx, rule.Name); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
@ -48,7 +48,7 @@ func processImageValidationRule(ctx context.Context, log logr.Logger, rclient re
return nil
}
return ruleResponse(*rule, response.Validation, "preconditions not met", response.RuleStatusSkip, nil)
return ruleResponse(*rule, response.Validation, "preconditions not met", response.RuleStatusSkip)
}
for _, v := range rule.VerifyImages {
@ -65,14 +65,14 @@ func processImageValidationRule(ctx context.Context, log logr.Logger, rclient re
log.V(4).Info("validating image", "image", image)
if err := validateImage(enginectx, imageVerify, name, imageInfo, log); err != nil {
return ruleResponse(*rule, response.ImageVerify, err.Error(), response.RuleStatusFail, nil)
return ruleResponse(*rule, response.ImageVerify, err.Error(), response.RuleStatusFail)
}
}
}
}
log.V(4).Info("validated image", "rule", rule.Name)
return ruleResponse(*rule, response.Validation, "image verified", response.RuleStatusPass, nil)
return ruleResponse(*rule, response.Validation, "image verified", response.RuleStatusPass)
}
func validateImage(ctx *PolicyContext, imageVerify *kyvernov1.ImageVerification, name string, imageInfo apiutils.ImageInfo, log logr.Logger) error {

View file

@ -51,9 +51,9 @@ func handleVerifyManifest(ctx *PolicyContext, rule *kyvernov1.Rule, logger logr.
}
logger.V(3).Info("verifyManifest result", "verified", strconv.FormatBool(verified), "reason", reason)
if !verified {
return ruleResponse(*rule, response.Validation, reason, response.RuleStatusFail, nil)
return ruleResponse(*rule, response.Validation, reason, response.RuleStatusFail)
}
return ruleResponse(*rule, response.Validation, reason, response.RuleStatusPass, nil)
return ruleResponse(*rule, response.Validation, reason, response.RuleStatusPass)
}
func verifyManifest(policyContext *PolicyContext, verifyRule kyvernov1.Manifests, logger logr.Logger) (bool, string, error) {

View file

@ -3,17 +3,28 @@ package engine
import (
"context"
"fmt"
"strings"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/variables"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"go.uber.org/multierr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func loadTargets(targets []kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.Logger) ([]unstructured.Unstructured, error) {
targetObjects := []unstructured.Unstructured{}
// resourceInfo contains the Unstructured resource, and if the resource is a subresource, it contains its name and its
// parentResource's group-version-resource
type resourceInfo struct {
unstructured unstructured.Unstructured
subresource string
parentResourceGVR metav1.GroupVersionResource
}
func loadTargets(targets []kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.Logger) ([]resourceInfo, error) {
var targetObjects []resourceInfo
var errors []error
for i := range targets {
@ -23,7 +34,7 @@ func loadTargets(targets []kyvernov1.ResourceSpec, ctx *PolicyContext, logger lo
continue
}
objs, err := getTargets(spec, ctx, logger)
objs, err := getTargets(spec, ctx)
if err != nil {
errors = append(errors, err)
continue
@ -64,8 +75,8 @@ func resolveSpec(i int, target kyvernov1.ResourceSpec, ctx *PolicyContext, logge
}, nil
}
func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.Logger) ([]unstructured.Unstructured, error) {
var targetObjects []unstructured.Unstructured
func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext) ([]resourceInfo, error) {
var targetObjects []resourceInfo
namespace := target.Namespace
name := target.Name
@ -74,26 +85,82 @@ func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.L
namespace = ctx.policy.GetNamespace()
}
if namespace != "" && name != "" &&
!wildcard.ContainsWildcard(namespace) && !wildcard.ContainsWildcard(name) {
obj, err := ctx.client.GetResource(context.TODO(), target.APIVersion, target.Kind, namespace, name)
if err != nil {
return nil, fmt.Errorf("failed to get target %s/%s %s/%s : %v", target.APIVersion, target.Kind, namespace, name, err)
}
return []unstructured.Unstructured{*obj}, nil
}
// list all targets if wildcard is specified
objList, err := ctx.client.ListResource(context.TODO(), target.APIVersion, target.Kind, "", nil)
apiResource, parentAPIResource, _, err := ctx.client.Discovery().FindResource(target.APIVersion, target.Kind)
if err != nil {
return nil, err
}
for i := range objList.Items {
obj := objList.Items[i].DeepCopy()
if match(namespace, name, obj.GetNamespace(), obj.GetName()) {
targetObjects = append(targetObjects, *obj)
if namespace != "" && name != "" &&
!wildcard.ContainsWildcard(namespace) && !wildcard.ContainsWildcard(name) {
// If the target resource is a subresource
var obj *unstructured.Unstructured
var parentResourceGVR metav1.GroupVersionResource
subresourceName := ""
if kubeutils.IsSubresource(apiResource.Name) {
apiVersion := metav1.GroupVersion{
Group: parentAPIResource.Group,
Version: parentAPIResource.Version,
}.String()
subresourceName = strings.Split(apiResource.Name, "/")[1]
obj, err = ctx.client.GetResource(context.TODO(), apiVersion, parentAPIResource.Kind, namespace, name, subresourceName)
parentResourceGVR = metav1.GroupVersionResource{
Group: parentAPIResource.Group,
Version: parentAPIResource.Version,
Resource: parentAPIResource.Name,
}
} else {
obj, err = ctx.client.GetResource(context.TODO(), target.APIVersion, target.Kind, namespace, name)
}
if err != nil {
return nil, fmt.Errorf("failed to get target %s/%s %s/%s : %v", target.APIVersion, target.Kind, namespace, name, err)
}
return []resourceInfo{{unstructured: *obj, subresource: subresourceName, parentResourceGVR: parentResourceGVR}}, nil
}
if kubeutils.IsSubresource(apiResource.Name) {
apiVersion := metav1.GroupVersion{
Group: parentAPIResource.Group,
Version: parentAPIResource.Version,
}.String()
objList, err := ctx.client.ListResource(context.TODO(), apiVersion, parentAPIResource.Kind, "", nil)
if err != nil {
return nil, err
}
var parentObjects []unstructured.Unstructured
for i := range objList.Items {
obj := objList.Items[i].DeepCopy()
if match(namespace, name, obj.GetNamespace(), obj.GetName()) {
parentObjects = append(parentObjects, *obj)
}
}
for i := range parentObjects {
parentObj := parentObjects[i]
subresourceName := strings.Split(apiResource.Name, "/")[1]
obj, err := ctx.client.GetResource(context.TODO(), parentObj.GetAPIVersion(), parentAPIResource.Kind, parentObj.GetNamespace(), parentObj.GetName(), subresourceName)
if err != nil {
return nil, err
}
parentResourceGVR := metav1.GroupVersionResource{
Group: parentAPIResource.Group,
Version: parentAPIResource.Version,
Resource: parentAPIResource.Name,
}
targetObjects = append(targetObjects, resourceInfo{unstructured: *obj, subresource: subresourceName, parentResourceGVR: parentResourceGVR})
}
} else {
// list all targets if wildcard is specified
objList, err := ctx.client.ListResource(context.TODO(), target.APIVersion, target.Kind, "", nil)
if err != nil {
return nil, err
}
for i := range objList.Items {
obj := objList.Items[i].DeepCopy()
if match(namespace, name, obj.GetNamespace(), obj.GetName()) {
targetObjects = append(targetObjects, resourceInfo{unstructured: *obj})
}
}
}

View file

@ -5,7 +5,7 @@ import (
"regexp"
"strings"
wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/mattbaird/jsonpatch"
)
@ -126,10 +126,6 @@ func filterInvalidPatches(patches []jsonpatch.JsonPatchOperation) []jsonpatch.Js
}
func ignorePatch(path string) bool {
if strings.Contains(path, "/status") {
return true
}
if wildcard.Match("/spec/triggers/*/metadata/*", path) {
return false
}

View file

@ -195,7 +195,7 @@ func Test_ignorePath(t *testing.T) {
},
{
path: "/status",
ignore: true,
ignore: false,
},
{
path: "/spec",

View file

@ -15,6 +15,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/registryclient"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -54,7 +55,9 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
excludeResource = policyContext.excludeGroupRole
}
if err = MatchesResourceDescription(matchedResource, rule, policyContext.admissionInfo, excludeResource, policyContext.namespaceLabels, policyContext.policy.GetNamespace()); err != nil {
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext)
if err = MatchesResourceDescription(subresourceGVKToAPIResource, matchedResource, rule, policyContext.admissionInfo, excludeResource, policyContext.namespaceLabels, policyContext.policy.GetNamespace(), policyContext.subresource); err != nil {
logger.V(4).Info("rule not matched", "reason", err.Error())
skippedRules = append(skippedRules, rule.Name)
continue
@ -81,17 +84,23 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
}
ruleCopy := rule.DeepCopy()
var patchedResources []unstructured.Unstructured
var patchedResources []resourceInfo
if !policyContext.admissionOperation && rule.IsMutateExisting() {
targets, err := loadTargets(ruleCopy.Mutation.Targets, policyContext, logger)
if err != nil {
rr := ruleResponse(rule, response.Mutation, err.Error(), response.RuleStatusError, nil)
rr := ruleResponse(rule, response.Mutation, err.Error(), response.RuleStatusError)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *rr)
} else {
patchedResources = append(patchedResources, targets...)
}
} else {
patchedResources = append(patchedResources, matchedResource)
var parentResourceGVR metav1.GroupVersionResource
if policyContext.subresource != "" {
parentResourceGVR = policyContext.requestResource
}
patchedResources = append(patchedResources, resourceInfo{
unstructured: matchedResource, subresource: policyContext.subresource, parentResourceGVR: parentResourceGVR,
})
}
for _, patchedResource := range patchedResources {
@ -101,21 +110,21 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
if !policyContext.admissionOperation && rule.IsMutateExisting() {
policyContext := policyContext.Copy()
if err := policyContext.jsonContext.AddTargetResource(patchedResource.Object); err != nil {
if err := policyContext.jsonContext.AddTargetResource(patchedResource.unstructured.Object); err != nil {
logging.Error(err, "failed to add target resource to the context")
continue
}
}
logger.V(4).Info("apply rule to resource", "rule", rule.Name, "resource namespace", patchedResource.GetNamespace(), "resource name", patchedResource.GetName())
logger.V(4).Info("apply rule to resource", "rule", rule.Name, "resource namespace", patchedResource.unstructured.GetNamespace(), "resource name", patchedResource.unstructured.GetName())
var ruleResp *response.RuleResponse
if rule.Mutation.ForEachMutation != nil {
ruleResp, patchedResource = mutateForEach(ctx, rclient, ruleCopy, policyContext, patchedResource, logger)
ruleResp, patchedResource.unstructured = mutateForEach(ctx, rclient, ruleCopy, policyContext, patchedResource.unstructured, patchedResource.subresource, patchedResource.parentResourceGVR, logger)
} else {
ruleResp, patchedResource = mutateResource(ruleCopy, policyContext, patchedResource, logger)
ruleResp, patchedResource.unstructured = mutateResource(ruleCopy, policyContext, patchedResource.unstructured, patchedResource.subresource, patchedResource.parentResourceGVR, logger)
}
matchedResource = patchedResource
matchedResource = patchedResource.unstructured
if ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
@ -145,22 +154,22 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
return resp
}
func mutateResource(rule *kyvernov1.Rule, ctx *PolicyContext, resource unstructured.Unstructured, logger logr.Logger) (*response.RuleResponse, unstructured.Unstructured) {
func mutateResource(rule *kyvernov1.Rule, ctx *PolicyContext, resource unstructured.Unstructured, subresourceName string, parentResourceGVR metav1.GroupVersionResource, logger logr.Logger) (*response.RuleResponse, unstructured.Unstructured) {
preconditionsPassed, err := checkPreconditions(logger, ctx, rule.GetAnyAllConditions())
if err != nil {
return ruleError(rule, response.Mutation, "failed to evaluate preconditions", err), resource
}
if !preconditionsPassed {
return ruleResponse(*rule, response.Mutation, "preconditions not met", response.RuleStatusSkip, &resource), resource
return ruleResponseWithPatchedTarget(*rule, response.Mutation, "preconditions not met", response.RuleStatusSkip, &resource, subresourceName, parentResourceGVR), resource
}
mutateResp := mutate.Mutate(rule, ctx.jsonContext, resource, logger)
ruleResp := buildRuleResponse(rule, mutateResp, &mutateResp.PatchedResource)
ruleResp := buildRuleResponse(rule, mutateResp, &mutateResp.PatchedResource, subresourceName, parentResourceGVR)
return ruleResp, mutateResp.PatchedResource
}
func mutateForEach(ctx context.Context, rclient registryclient.Client, rule *kyvernov1.Rule, enginectx *PolicyContext, resource unstructured.Unstructured, logger logr.Logger) (*response.RuleResponse, unstructured.Unstructured) {
func mutateForEach(ctx context.Context, rclient registryclient.Client, rule *kyvernov1.Rule, enginectx *PolicyContext, resource unstructured.Unstructured, subresourceName string, parentResourceGVR metav1.GroupVersionResource, logger logr.Logger) (*response.RuleResponse, unstructured.Unstructured) {
foreachList := rule.Mutation.ForEachMutation
if foreachList == nil {
return nil, resource
@ -182,7 +191,7 @@ func mutateForEach(ctx context.Context, rclient registryclient.Client, rule *kyv
}
if !preconditionsPassed {
return ruleResponse(*rule, response.Mutation, "preconditions not met", response.RuleStatusSkip, &patchedResource), resource
return ruleResponseWithPatchedTarget(*rule, response.Mutation, "preconditions not met", response.RuleStatusSkip, &patchedResource, subresourceName, parentResourceGVR), resource
}
elements, err := evaluateList(foreach.List, enginectx.jsonContext)
@ -194,7 +203,7 @@ func mutateForEach(ctx context.Context, rclient registryclient.Client, rule *kyv
mutateResp := mutateElements(ctx, rclient, rule.Name, foreach, enginectx, elements, patchedResource, logger)
if mutateResp.Status == response.RuleStatusError {
logger.Error(err, "failed to mutate elements")
return buildRuleResponse(rule, mutateResp, nil), resource
return buildRuleResponse(rule, mutateResp, nil, "", metav1.GroupVersionResource{}), resource
}
if mutateResp.Status != response.RuleStatusSkip {
@ -207,10 +216,10 @@ func mutateForEach(ctx context.Context, rclient registryclient.Client, rule *kyv
}
if applyCount == 0 {
return ruleResponse(*rule, response.Mutation, "0 elements processed", response.RuleStatusSkip, &resource), resource
return ruleResponseWithPatchedTarget(*rule, response.Mutation, "0 elements processed", response.RuleStatusSkip, &resource, subresourceName, parentResourceGVR), resource
}
r := ruleResponse(*rule, response.Mutation, fmt.Sprintf("%d elements processed", applyCount), response.RuleStatusPass, &patchedResource)
r := ruleResponseWithPatchedTarget(*rule, response.Mutation, fmt.Sprintf("%d elements processed", applyCount), response.RuleStatusPass, &patchedResource, subresourceName, parentResourceGVR)
r.Patches = allPatches
return r, patchedResource
}
@ -279,8 +288,8 @@ func mutateError(err error, message string) *mutate.Response {
}
}
func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, patchedResource *unstructured.Unstructured) *response.RuleResponse {
resp := ruleResponse(*rule, response.Mutation, mutateResp.Message, mutateResp.Status, patchedResource)
func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, patchedResource *unstructured.Unstructured, patchedSubresourceName string, parentResourceGVR metav1.GroupVersionResource) *response.RuleResponse {
resp := ruleResponseWithPatchedTarget(*rule, response.Mutation, mutateResp.Message, mutateResp.Status, patchedResource, patchedSubresourceName, parentResourceGVR)
if resp.Status == response.RuleStatusPass {
resp.Patches = mutateResp.Patches
resp.Message = buildSuccessMessage(mutateResp.PatchedResource)

View file

@ -11,6 +11,7 @@ import (
"github.com/kyverno/kyverno/pkg/utils"
"github.com/pkg/errors"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -34,6 +35,16 @@ type PolicyContext struct {
// admissionInfo contains the admission request information
admissionInfo kyvernov1beta1.RequestInfo
// requestResource is the fully-qualified resource of the original API request (for example, v1.pods).
// If this is specified and differs from the value in "resource", an equivalent match and conversion was performed.
//
// For example, if deployments can be modified via apps/v1 and apps/v1beta1, and a webhook registered a rule of
// `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]` and `matchPolicy: Equivalent`,
// an API request to apps/v1beta1 deployments would be converted and sent to the webhook
// with `resource: {group:"apps", version:"v1", resource:"deployments"}` (matching the resource the webhook registered for),
// and `requestResource: {group:"apps", version:"v1beta1", resource:"deployments"}` (indicating the resource of the original API request).
requestResource metav1.GroupVersionResource
// Dynamic client - used for api lookups
client dclient.Interface
@ -53,6 +64,17 @@ type PolicyContext struct {
// informerCacheResolvers - used to get resources from informer cache
informerCacheResolvers resolvers.ConfigmapResolver
// subresource is the subresource being requested, if any (for example, "status" or "scale")
subresource string
// subresourcesInPolicy represents the APIResources that are subresources along with their parent resource.
// This is used to determine if a resource is a subresource. It is only used when the policy context is populated
// by kyverno CLI. In all other cases when connected to a cluster, this is empty.
subresourcesInPolicy []struct {
APIResource metav1.APIResource
ParentResource metav1.APIResource
}
}
// Getters
@ -97,6 +119,12 @@ func (c *PolicyContext) WithAdmissionInfo(admissionInfo kyvernov1beta1.RequestIn
return copy
}
func (c *PolicyContext) WithRequestResource(requestResource metav1.GroupVersionResource) *PolicyContext {
copy := c.Copy()
copy.requestResource = requestResource
return copy
}
func (c *PolicyContext) WithNewResource(resource unstructured.Unstructured) *PolicyContext {
copy := c.Copy()
copy.newResource = resource
@ -147,6 +175,22 @@ func (c *PolicyContext) WithInformerCacheResolver(informerCacheResolver resolver
return copy
}
func (c *PolicyContext) WithSubresource(subresource string) *PolicyContext {
copy := c.Copy()
copy.subresource = subresource
return copy
}
func (c *PolicyContext) WithSubresourcesInPolicy(subresourcesInPolicy []struct {
APIResource metav1.APIResource
ParentResource metav1.APIResource
},
) *PolicyContext {
copy := c.Copy()
copy.subresourcesInPolicy = subresourcesInPolicy
return copy
}
// Constructors
func NewPolicyContextWithJsonContext(jsonContext context.Interface) *PolicyContext {
@ -181,6 +225,7 @@ func NewPolicyContextFromAdmissionRequest(
if err := ctx.AddImageInfos(&newResource); err != nil {
return nil, errors.Wrap(err, "failed to add image information to the policy rule context")
}
requestResource := request.RequestResource.DeepCopy()
policyContext := NewPolicyContextWithJsonContext(ctx).
WithNewResource(newResource).
WithOldResource(oldResource).
@ -188,7 +233,9 @@ func NewPolicyContextFromAdmissionRequest(
WithConfiguration(configuration).
WithClient(client).
WithAdmissionOperation(true).
WithInformerCacheResolver(informerCacheResolver)
WithInformerCacheResolver(informerCacheResolver).
WithRequestResource(*requestResource).
WithSubresource(request.SubResource)
return policyContext, nil
}

View file

@ -7,6 +7,7 @@ import (
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -114,6 +115,14 @@ type RuleResponse struct {
// PatchedTarget is the patched resource for mutate.targets
PatchedTarget *unstructured.Unstructured
// PatchedTargetSubresourceName is the name of the subresource which is patched, empty if the resource patched is
// not a subresource.
PatchedTargetSubresourceName string
// PatchedTargetParentResourceGVR is the GVR of the parent resource of the PatchedTarget. This is only populated
// when PatchedTarget is a subresource.
PatchedTargetParentResourceGVR metav1.GroupVersionResource
}
// ToString ...

View file

@ -17,7 +17,8 @@ import (
"github.com/kyverno/kyverno/pkg/engine/wildcards"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/utils"
wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"golang.org/x/text/cases"
@ -39,32 +40,30 @@ type EngineStats struct {
RulesAppliedCount int
}
func checkKind(kinds []string, resourceKind string, gvk schema.GroupVersionKind) bool {
func checkKind(subresourceGVKToAPIResource map[string]*metav1.APIResource, kinds []string, gvk schema.GroupVersionKind, subresourceInAdmnReview string) bool {
title := cases.Title(language.Und, cases.NoLower)
result := false
for _, k := range kinds {
parts := strings.Split(k, "/")
if len(parts) == 1 {
if k == "*" || resourceKind == title.String(k) {
return true
if k != "*" {
gv, kind := kubeutils.GetKindFromGVK(k)
apiResource, ok := subresourceGVKToAPIResource[k]
if ok {
result = apiResource.Group == gvk.Group && (apiResource.Version == gvk.Version || strings.Contains(gv, "*")) && apiResource.Kind == gvk.Kind
} else { // if the kind is not found in the subresourceGVKToAPIResource, then it is not a subresource
result = title.String(kind) == gvk.Kind && subresourceInAdmnReview == ""
if gv != "" {
result = result && kubeutils.GroupVersionMatches(gv, gvk.GroupVersion().String())
}
}
} else {
result = true
}
if len(parts) == 2 {
kindParts := strings.SplitN(parts[1], ".", 2)
if gvk.Kind == title.String(kindParts[0]) && gvk.Version == parts[0] {
return true
}
}
if len(parts) == 3 || len(parts) == 4 {
kindParts := strings.SplitN(parts[2], ".", 2)
if gvk.Group == parts[0] && (gvk.Version == parts[1] || parts[1] == "*") && gvk.Kind == title.String(kindParts[0]) {
return true
}
if result {
break
}
}
return false
return result
}
func checkName(name, resourceName string) bool {
@ -142,11 +141,11 @@ func checkSelector(labelSelector *metav1.LabelSelector, resourceLabels map[strin
// should be: AND across attributes but an OR inside attributes that of type list
// To filter out the targeted resources with UserInfo, the check
// should be: OR (across & inside) attributes
func doesResourceMatchConditionBlock(conditionBlock kyvernov1.ResourceDescription, userInfo kyvernov1.UserInfo, admissionInfo kyvernov1beta1.RequestInfo, resource unstructured.Unstructured, dynamicConfig []string, namespaceLabels map[string]string) []error {
func doesResourceMatchConditionBlock(subresourceGVKToAPIResource map[string]*metav1.APIResource, conditionBlock kyvernov1.ResourceDescription, userInfo kyvernov1.UserInfo, admissionInfo kyvernov1beta1.RequestInfo, resource unstructured.Unstructured, dynamicConfig []string, namespaceLabels map[string]string, subresourceInAdmnReview string) []error {
var errs []error
if len(conditionBlock.Kinds) > 0 {
if !checkKind(conditionBlock.Kinds, resource.GetKind(), resource.GroupVersionKind()) {
if !checkKind(subresourceGVKToAPIResource, conditionBlock.Kinds, resource.GroupVersionKind(), subresourceInAdmnReview) {
errs = append(errs, fmt.Errorf("kind does not match %v", conditionBlock.Kinds))
}
}
@ -282,7 +281,7 @@ func matchSubjects(ruleSubjects []rbacv1.Subject, userInfo authenticationv1.User
}
// MatchesResourceDescription checks if the resource matches resource description of the rule or not
func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef kyvernov1.Rule, admissionInfoRef kyvernov1beta1.RequestInfo, dynamicConfig []string, namespaceLabels map[string]string, policyNamespace string) error {
func MatchesResourceDescription(subresourceGVKToAPIResource map[string]*metav1.APIResource, resourceRef unstructured.Unstructured, ruleRef kyvernov1.Rule, admissionInfoRef kyvernov1beta1.RequestInfo, dynamicConfig []string, namespaceLabels map[string]string, policyNamespace, subresourceInAdmnReview string) error {
rule := ruleRef.DeepCopy()
resource := *resourceRef.DeepCopy()
admissionInfo := *admissionInfoRef.DeepCopy()
@ -298,7 +297,7 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k
oneMatched := false
for _, rmr := range rule.MatchResources.Any {
// if there are no errors it means it was a match
if len(matchesResourceDescriptionMatchHelper(rmr, admissionInfo, resource, dynamicConfig, namespaceLabels)) == 0 {
if len(matchesResourceDescriptionMatchHelper(subresourceGVKToAPIResource, rmr, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)) == 0 {
oneMatched = true
break
}
@ -309,17 +308,17 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k
} else if len(rule.MatchResources.All) > 0 {
// include object if ALL of the criteria match
for _, rmr := range rule.MatchResources.All {
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionMatchHelper(rmr, admissionInfo, resource, dynamicConfig, namespaceLabels)...)
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionMatchHelper(subresourceGVKToAPIResource, rmr, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)...)
}
} else {
rmr := kyvernov1.ResourceFilter{UserInfo: rule.MatchResources.UserInfo, ResourceDescription: rule.MatchResources.ResourceDescription}
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionMatchHelper(rmr, admissionInfo, resource, dynamicConfig, namespaceLabels)...)
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionMatchHelper(subresourceGVKToAPIResource, rmr, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)...)
}
if len(rule.ExcludeResources.Any) > 0 {
// exclude the object if ANY of the criteria match
for _, rer := range rule.ExcludeResources.Any {
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionExcludeHelper(rer, admissionInfo, resource, dynamicConfig, namespaceLabels)...)
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionExcludeHelper(subresourceGVKToAPIResource, rer, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)...)
}
} else if len(rule.ExcludeResources.All) > 0 {
// exclude the object if ALL the criteria match
@ -327,7 +326,7 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k
for _, rer := range rule.ExcludeResources.All {
// we got no errors inplying a resource did NOT exclude it
// "matchesResourceDescriptionExcludeHelper" returns errors if resource is excluded by a filter
if len(matchesResourceDescriptionExcludeHelper(rer, admissionInfo, resource, dynamicConfig, namespaceLabels)) == 0 {
if len(matchesResourceDescriptionExcludeHelper(subresourceGVKToAPIResource, rer, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)) == 0 {
excludedByAll = false
break
}
@ -337,7 +336,7 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k
}
} else {
rer := kyvernov1.ResourceFilter{UserInfo: rule.ExcludeResources.UserInfo, ResourceDescription: rule.ExcludeResources.ResourceDescription}
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionExcludeHelper(rer, admissionInfo, resource, dynamicConfig, namespaceLabels)...)
reasonsForFailure = append(reasonsForFailure, matchesResourceDescriptionExcludeHelper(subresourceGVKToAPIResource, rer, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)...)
}
// creating final error
@ -355,7 +354,7 @@ func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef k
return nil
}
func matchesResourceDescriptionMatchHelper(rmr kyvernov1.ResourceFilter, admissionInfo kyvernov1beta1.RequestInfo, resource unstructured.Unstructured, dynamicConfig []string, namespaceLabels map[string]string) []error {
func matchesResourceDescriptionMatchHelper(subresourceGVKToAPIResource map[string]*metav1.APIResource, rmr kyvernov1.ResourceFilter, admissionInfo kyvernov1beta1.RequestInfo, resource unstructured.Unstructured, dynamicConfig []string, namespaceLabels map[string]string, subresourceInAdmnReview string) []error {
var errs []error
if reflect.DeepEqual(admissionInfo, kyvernov1.RequestInfo{}) {
rmr.UserInfo = kyvernov1.UserInfo{}
@ -364,7 +363,7 @@ func matchesResourceDescriptionMatchHelper(rmr kyvernov1.ResourceFilter, admissi
// checking if resource matches the rule
if !reflect.DeepEqual(rmr.ResourceDescription, kyvernov1.ResourceDescription{}) ||
!reflect.DeepEqual(rmr.UserInfo, kyvernov1.UserInfo{}) {
matchErrs := doesResourceMatchConditionBlock(rmr.ResourceDescription, rmr.UserInfo, admissionInfo, resource, dynamicConfig, namespaceLabels)
matchErrs := doesResourceMatchConditionBlock(subresourceGVKToAPIResource, rmr.ResourceDescription, rmr.UserInfo, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)
errs = append(errs, matchErrs...)
} else {
errs = append(errs, fmt.Errorf("match cannot be empty"))
@ -372,12 +371,12 @@ func matchesResourceDescriptionMatchHelper(rmr kyvernov1.ResourceFilter, admissi
return errs
}
func matchesResourceDescriptionExcludeHelper(rer kyvernov1.ResourceFilter, admissionInfo kyvernov1beta1.RequestInfo, resource unstructured.Unstructured, dynamicConfig []string, namespaceLabels map[string]string) []error {
func matchesResourceDescriptionExcludeHelper(subresourceGVKToAPIResource map[string]*metav1.APIResource, rer kyvernov1.ResourceFilter, admissionInfo kyvernov1beta1.RequestInfo, resource unstructured.Unstructured, dynamicConfig []string, namespaceLabels map[string]string, subresourceInAdmnReview string) []error {
var errs []error
// checking if resource matches the rule
if !reflect.DeepEqual(rer.ResourceDescription, kyvernov1.ResourceDescription{}) ||
!reflect.DeepEqual(rer.UserInfo, kyvernov1.UserInfo{}) {
excludeErrs := doesResourceMatchConditionBlock(rer.ResourceDescription, rer.UserInfo, admissionInfo, resource, dynamicConfig, namespaceLabels)
excludeErrs := doesResourceMatchConditionBlock(subresourceGVKToAPIResource, rer.ResourceDescription, rer.UserInfo, admissionInfo, resource, dynamicConfig, namespaceLabels, subresourceInAdmnReview)
// it was a match so we want to exclude it
if len(excludeErrs) == 0 {
errs = append(errs, fmt.Errorf("resource excluded since one of the criteria excluded it"))
@ -456,10 +455,20 @@ func evaluateList(jmesPath string, ctx context.EvalInterface) ([]interface{}, er
func ruleError(rule *kyvernov1.Rule, ruleType response.RuleType, msg string, err error) *response.RuleResponse {
msg = fmt.Sprintf("%s: %s", msg, err.Error())
return ruleResponse(*rule, ruleType, msg, response.RuleStatusError, nil)
return ruleResponse(*rule, ruleType, msg, response.RuleStatusError)
}
func ruleResponse(rule kyvernov1.Rule, ruleType response.RuleType, msg string, status response.RuleStatus, patchedResource *unstructured.Unstructured) *response.RuleResponse {
func ruleResponse(rule kyvernov1.Rule, ruleType response.RuleType, msg string, status response.RuleStatus) *response.RuleResponse {
resp := &response.RuleResponse{
Name: rule.Name,
Type: ruleType,
Message: msg,
Status: status,
}
return resp
}
func ruleResponseWithPatchedTarget(rule kyvernov1.Rule, ruleType response.RuleType, msg string, status response.RuleStatus, patchedResource *unstructured.Unstructured, patchedSubresourceName string, parentResourceGVR metav1.GroupVersionResource) *response.RuleResponse {
resp := &response.RuleResponse{
Name: rule.Name,
Type: ruleType,
@ -469,6 +478,8 @@ func ruleResponse(rule kyvernov1.Rule, ruleType response.RuleType, msg string, s
if rule.Mutation.Targets != nil {
resp.PatchedTarget = patchedResource
resp.PatchedTargetSubresourceName = patchedSubresourceName
resp.PatchedTargetParentResourceGVR = parentResourceGVR
}
return resp
}

View file

@ -7,7 +7,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
v1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/engine/utils"
"gotest.tools/assert"
@ -905,7 +905,7 @@ func TestMatchesResourceDescription(t *testing.T) {
resource, _ := utils.ConvertToUnstructured(tc.Resource)
for _, rule := range autogen.ComputeRules(&policy) {
err := MatchesResourceDescription(*resource, rule, tc.AdmissionInfo, []string{}, nil, "")
err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, tc.AdmissionInfo, []string{}, nil, "", "")
if err != nil {
if !tc.areErrorsExpected {
t.Errorf("Testcase %d Unexpected error: %v\nmsg: %s", i+1, err, tc.Description)
@ -1810,7 +1810,7 @@ func TestMatchesResourceDescription_GenerateName(t *testing.T) {
resource, _ := utils.ConvertToUnstructured(tc.Resource)
for _, rule := range autogen.ComputeRules(&policy) {
err := MatchesResourceDescription(*resource, rule, tc.AdmissionInfo, []string{}, nil, "")
err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, tc.AdmissionInfo, []string{}, nil, "", "")
if err != nil {
if !tc.areErrorsExpected {
t.Errorf("Testcase %d Unexpected error: %v\nmsg: %s", i+1, err, tc.Description)
@ -1878,7 +1878,7 @@ func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
@ -1939,7 +1939,7 @@ func TestResourceDescriptionMatch_Name(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -1998,7 +1998,7 @@ func TestResourceDescriptionMatch_GenerateName(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2058,7 +2058,7 @@ func TestResourceDescriptionMatch_Name_Regex(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2117,7 +2117,7 @@ func TestResourceDescriptionMatch_GenerateName_Regex(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2185,7 +2185,7 @@ func TestResourceDescriptionMatch_Label_Expression_NotMatch(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2254,7 +2254,7 @@ func TestResourceDescriptionMatch_Label_Expression_Match(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err != nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2334,7 +2334,7 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription},
ExcludeResources: v1.MatchResources{ResourceDescription: resourceDescriptionExclude}}
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, ""); err == nil {
if err := MatchesResourceDescription(make(map[string]*metav1.APIResource), *resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", ""); err == nil {
t.Errorf("Testcase has failed due to the following:\n Function has returned no error, even though it was supposed to fail")
}
}
@ -2480,21 +2480,66 @@ func TestManagedPodResource(t *testing.T) {
}
func Test_checkKind(t *testing.T) {
match := checkKind([]string{"*"}, "Deployment", schema.GroupVersionKind{Kind: "Deployment", Group: "", Version: "v1"})
subresourceGVKToAPIResource := make(map[string]*metav1.APIResource)
match := checkKind(subresourceGVKToAPIResource, []string{"*"}, schema.GroupVersionKind{Kind: "Deployment", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = checkKind([]string{"Pod"}, "Pod", schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"})
match = checkKind(subresourceGVKToAPIResource, []string{"Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = checkKind([]string{"v1/Pod"}, "Pod", schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"})
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "")
assert.Equal(t, match, true)
match = checkKind([]string{"tekton.dev/v1beta1/TaskRun"}, "TaskRun", schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1beta1"})
match = checkKind(subresourceGVKToAPIResource, []string{"tekton.dev/v1beta1/TaskRun"}, schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1beta1"}, "")
assert.Equal(t, match, true)
match = checkKind([]string{"tekton.dev/v1beta1/TaskRun/status"}, "TaskRun", schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1beta1"})
match = checkKind(subresourceGVKToAPIResource, []string{"tekton.dev/*/TaskRun"}, schema.GroupVersionKind{Kind: "TaskRun", Group: "tekton.dev", Version: "v1alpha1"}, "")
assert.Equal(t, match, true)
match = checkKind([]string{"v1/pod.status"}, "Pod", schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"})
// Though both 'pods', 'pods/status' have same kind i.e. 'Pod' but they are different resources, 'subresourceInAdmnReview' is used in determining that.
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "status")
assert.Equal(t, match, false)
// Though both 'pods', 'pods/status' have same kind i.e. 'Pod' but they are different resources, 'subresourceInAdmnReview' is used in determining that.
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "ephemeralcontainers")
assert.Equal(t, match, false)
subresourceGVKToAPIResource["networking.k8s.io/v1/NetworkPolicy/status"] = &metav1.APIResource{
Name: "networkpolicies/status",
SingularName: "",
Namespaced: true,
Kind: "NetworkPolicy",
Group: "networking.k8s.io",
Version: "v1",
}
subresourceGVKToAPIResource["v1/Pod.status"] = &metav1.APIResource{
Name: "pods/status",
SingularName: "",
Namespaced: true,
Kind: "Pod",
Group: "",
Version: "v1",
}
subresourceGVKToAPIResource["*/Pod.eviction"] = &metav1.APIResource{
Name: "pods/eviction",
SingularName: "",
Namespaced: true,
Kind: "Eviction",
Group: "policy",
Version: "v1",
}
match = checkKind(subresourceGVKToAPIResource, []string{"networking.k8s.io/v1/NetworkPolicy/status"}, schema.GroupVersionKind{Kind: "NetworkPolicy", Group: "networking.k8s.io", Version: "v1"}, "status")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"v1/Pod.status"}, schema.GroupVersionKind{Kind: "Pod", Group: "", Version: "v1"}, "status")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"*/Pod.eviction"}, schema.GroupVersionKind{Kind: "Eviction", Group: "policy", Version: "v1"}, "eviction")
assert.Equal(t, match, true)
match = checkKind(subresourceGVKToAPIResource, []string{"v1alpha1/Pod.eviction"}, schema.GroupVersionKind{Kind: "Eviction", Group: "policy", Version: "v1"}, "eviction")
assert.Equal(t, match, false)
}

View file

@ -14,7 +14,6 @@ import (
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store"
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/engine/common"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/engine/validate"
"github.com/kyverno/kyverno/pkg/engine/variables"
@ -148,22 +147,6 @@ func validateResource(ctx context.Context, log logr.Logger, rclient registryclie
return resp
}
func validateOldObject(ctx context.Context, log logr.Logger, rclient registryclient.Client, enginectx *PolicyContext, rule *kyvernov1.Rule) (*response.RuleResponse, error) {
ctxCopy := enginectx.Copy()
ctxCopy.newResource = *ctxCopy.oldResource.DeepCopy()
ctxCopy.oldResource = unstructured.Unstructured{}
if err := enginecontext.ReplaceResource(ctxCopy.jsonContext, ctxCopy.newResource.Object); err != nil {
return nil, errors.Wrapf(err, "failed to replace object in the JSON context")
}
if err := enginecontext.ReplaceOldResource(ctxCopy.jsonContext, ctxCopy.oldResource.Object); err != nil {
return nil, errors.Wrapf(err, "failed to replace old object in the JSON context")
}
return processValidationRule(ctx, log, rclient, ctxCopy, rule), nil
}
func processValidationRule(ctx context.Context, log logr.Logger, rclient registryclient.Client, enginectx *PolicyContext, rule *kyvernov1.Rule) *response.RuleResponse {
v := newValidator(log, rclient, enginectx, rule)
if rule.Validation.ForEachValidation != nil {
@ -247,7 +230,7 @@ func (v *validator) validate(ctx context.Context) *response.RuleResponse {
}
if !preconditionsPassed {
return ruleResponse(*v.rule, response.Validation, "preconditions not met", response.RuleStatusSkip, nil)
return ruleResponse(*v.rule, response.Validation, "preconditions not met", response.RuleStatusSkip)
}
if v.deny != nil {
@ -260,17 +243,6 @@ func (v *validator) validate(ctx context.Context) *response.RuleResponse {
}
ruleResponse := v.validateResourceWithRule()
if isUpdateRequest(v.ctx) {
priorResp, err := validateOldObject(ctx, v.log, v.rclient, v.ctx, v.rule)
if err != nil {
return ruleError(v.rule, response.Validation, "failed to validate old object", err)
}
if isSameRuleResponse(ruleResponse, priorResp) {
v.log.V(3).Info("skipping modified resource as validation results have not changed")
return nil
}
}
return ruleResponse
}
@ -295,7 +267,7 @@ func (v *validator) validateForEach(ctx context.Context) *response.RuleResponse
if err != nil {
return ruleError(v.rule, response.Validation, "failed to evaluate preconditions", err)
} else if !preconditionsPassed {
return ruleResponse(*v.rule, response.Validation, "preconditions not met", response.RuleStatusSkip, nil)
return ruleResponse(*v.rule, response.Validation, "preconditions not met", response.RuleStatusSkip)
}
foreachList := v.rule.Validation.ForEachValidation
@ -319,10 +291,10 @@ func (v *validator) validateForEach(ctx context.Context) *response.RuleResponse
}
if applyCount == 0 {
return ruleResponse(*v.rule, response.Validation, "rule skipped", response.RuleStatusSkip, nil)
return ruleResponse(*v.rule, response.Validation, "rule skipped", response.RuleStatusSkip)
}
return ruleResponse(*v.rule, response.Validation, "rule passed", response.RuleStatusPass, nil)
return ruleResponse(*v.rule, response.Validation, "rule passed", response.RuleStatusPass)
}
func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForEachValidation, elements []interface{}, elementScope *bool) (*response.RuleResponse, int) {
@ -357,16 +329,16 @@ func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForE
continue
}
msg := fmt.Sprintf("validation failure: %v", r.Message)
return ruleResponse(*v.rule, response.Validation, msg, r.Status, nil), applyCount
return ruleResponse(*v.rule, response.Validation, msg, r.Status), applyCount
}
msg := fmt.Sprintf("validation failure: %v", r.Message)
return ruleResponse(*v.rule, response.Validation, msg, r.Status, nil), applyCount
return ruleResponse(*v.rule, response.Validation, msg, r.Status), applyCount
}
applyCount++
}
return ruleResponse(*v.rule, response.Validation, "", response.RuleStatusPass, nil), applyCount
return ruleResponse(*v.rule, response.Validation, "", response.RuleStatusPass), applyCount
}
func addElementToContext(ctx *PolicyContext, e interface{}, elementIndex int, elementScope *bool) error {
@ -434,10 +406,10 @@ func (v *validator) validateDeny() *response.RuleResponse {
deny := variables.EvaluateConditions(v.log, v.ctx.jsonContext, denyConditions)
if deny {
return ruleResponse(*v.rule, response.Validation, v.getDenyMessage(deny), response.RuleStatusFail, nil)
return ruleResponse(*v.rule, response.Validation, v.getDenyMessage(deny), response.RuleStatusFail)
}
return ruleResponse(*v.rule, response.Validation, v.getDenyMessage(deny), response.RuleStatusPass, nil)
return ruleResponse(*v.rule, response.Validation, v.getDenyMessage(deny), response.RuleStatusPass)
}
func (v *validator) getDenyMessage(deny bool) string {
@ -528,10 +500,10 @@ func (v *validator) validatePodSecurity() *response.RuleResponse {
}
if allowed {
msg := fmt.Sprintf("Validation rule '%s' passed.", v.rule.Name)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusPass, nil)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusPass)
} else {
msg := fmt.Sprintf(`Validation rule '%s' failed. It violates PodSecurity "%s:%s": %s`, v.rule.Name, v.podSecurity.Level, v.podSecurity.Version, pss.FormatChecksPrint(pssChecks))
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusFail, nil)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusFail)
}
}
@ -554,11 +526,6 @@ func isDeleteRequest(ctx *PolicyContext) bool {
return isEmptyUnstructured(&ctx.newResource)
}
func isUpdateRequest(ctx *PolicyContext) bool {
// is the OldObject and NewObject are available, the request is an UPDATE
return !isEmptyUnstructured(&ctx.oldResource) && !isEmptyUnstructured(&ctx.newResource)
}
func isEmptyUnstructured(u *unstructured.Unstructured) bool {
if u == nil {
return true
@ -573,13 +540,16 @@ func isEmptyUnstructured(u *unstructured.Unstructured) bool {
// matches checks if either the new or old resource satisfies the filter conditions defined in the rule
func matches(logger logr.Logger, rule *kyvernov1.Rule, ctx *PolicyContext) bool {
err := MatchesResourceDescription(ctx.newResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "")
kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...)
subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, ctx)
err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.newResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "", ctx.subresource)
if err == nil {
return true
}
if !reflect.DeepEqual(ctx.oldResource, unstructured.Unstructured{}) {
err := MatchesResourceDescription(ctx.oldResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "")
if !reflect.DeepEqual(ctx.OldResource, unstructured.Unstructured{}) {
err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.oldResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "", ctx.subresource)
if err == nil {
return true
}
@ -589,26 +559,6 @@ func matches(logger logr.Logger, rule *kyvernov1.Rule, ctx *PolicyContext) bool
return false
}
func isSameRuleResponse(r1 *response.RuleResponse, r2 *response.RuleResponse) bool {
if r1.Name != r2.Name {
return false
}
if r1.Type != r2.Type {
return false
}
if r1.Message != r2.Message {
return false
}
if r1.Status != r2.Status {
return false
}
return true
}
// validatePatterns validate pattern and anyPattern
func (v *validator) validatePatterns(resource unstructured.Unstructured) *response.RuleResponse {
if v.pattern != nil {
@ -618,22 +568,22 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *respon
v.log.V(3).Info("validation error", "path", pe.Path, "error", err.Error())
if pe.Skip {
return ruleResponse(*v.rule, response.Validation, pe.Error(), response.RuleStatusSkip, nil)
return ruleResponse(*v.rule, response.Validation, pe.Error(), response.RuleStatusSkip)
}
if pe.Path == "" {
return ruleResponse(*v.rule, response.Validation, v.buildErrorMessage(err, ""), response.RuleStatusError, nil)
return ruleResponse(*v.rule, response.Validation, v.buildErrorMessage(err, ""), response.RuleStatusError)
}
return ruleResponse(*v.rule, response.Validation, v.buildErrorMessage(err, pe.Path), response.RuleStatusFail, nil)
return ruleResponse(*v.rule, response.Validation, v.buildErrorMessage(err, pe.Path), response.RuleStatusFail)
}
return ruleResponse(*v.rule, response.Validation, v.buildErrorMessage(err, pe.Path), response.RuleStatusError, nil)
return ruleResponse(*v.rule, response.Validation, v.buildErrorMessage(err, pe.Path), response.RuleStatusError)
}
v.log.V(4).Info("successfully processed rule")
msg := fmt.Sprintf("validation rule '%s' passed.", v.rule.Name)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusPass, nil)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusPass)
}
if v.anyPattern != nil {
@ -644,14 +594,14 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *respon
anyPatterns, err := deserializeAnyPattern(v.anyPattern)
if err != nil {
msg := fmt.Sprintf("failed to deserialize anyPattern, expected type array: %v", err)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusError, nil)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusError)
}
for idx, pattern := range anyPatterns {
err := validate.MatchPattern(v.log, resource.Object, pattern)
if err == nil {
msg := fmt.Sprintf("validation rule '%s' anyPattern[%d] passed.", v.rule.Name, idx)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusPass, nil)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusPass)
}
if pe, ok := err.(*validate.PatternError); ok {
@ -680,7 +630,7 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *respon
}
v.log.V(4).Info(fmt.Sprintf("Validation rule '%s' skipped. %s", v.rule.Name, errorStr))
return ruleResponse(*v.rule, response.Validation, strings.Join(errorStr, " "), response.RuleStatusSkip, nil)
return ruleResponse(*v.rule, response.Validation, strings.Join(errorStr, " "), response.RuleStatusSkip)
} else if len(failedAnyPatternsErrors) > 0 {
var errorStr []string
for _, err := range failedAnyPatternsErrors {
@ -689,11 +639,11 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *respon
v.log.V(4).Info(fmt.Sprintf("Validation rule '%s' failed. %s", v.rule.Name, errorStr))
msg := buildAnyPatternErrorMessage(v.rule, errorStr)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusFail, nil)
return ruleResponse(*v.rule, response.Validation, msg, response.RuleStatusFail)
}
}
return ruleResponse(*v.rule, response.Validation, v.rule.Validation.Message, response.RuleStatusPass, nil)
return ruleResponse(*v.rule, response.Validation, v.rule.Validation.Message, response.RuleStatusPass)
}
func deserializeAnyPattern(anyPattern apiextensions.JSON) ([]interface{}, error) {

View file

@ -11,7 +11,7 @@ import (
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/utils"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"

View file

@ -209,7 +209,7 @@ func (pc *PolicyController) processExistingKinds(kinds []string, policy kyvernov
if err != nil {
gv, k := kubeutils.GetKindFromGVK(kind)
if !strings.Contains(k, "*") {
resourceSchema, _, err := pc.client.Discovery().FindResource(gv, k)
resourceSchema, _, _, err := pc.client.Discovery().FindResource(gv, k)
if err != nil {
logger.Error(err, "failed to find resource", "kind", k)
continue

View file

@ -261,13 +261,23 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
}
}
podOnlyMap := make(map[string]bool) // Validate that Kind is only Pod
podOnlyMap["Pod"] = true
if reflect.DeepEqual(common.GetKindsFromRule(rule), podOnlyMap) && podControllerAutoGenExclusion(policy) {
msg := "Policies that match Pods apply to all Pods including those created and managed by controllers " +
"excluded from autogen. Use preconditions to exclude the Pods managed by controllers which are " +
"excluded from autogen. Refer to https://kyverno.io/docs/writing-policies/autogen/ for details."
warnings = append(warnings, msg)
kindsFromRule := rule.MatchResources.GetKinds()
resourceTypesMap := make(map[string]bool)
for _, kind := range kindsFromRule {
_, k := kubeutils.GetKindFromGVK(kind)
k, _ = kubeutils.SplitSubresource(k)
resourceTypesMap[k] = true
}
if len(resourceTypesMap) == 1 {
for k := range resourceTypesMap {
if k == "Pod" && podControllerAutoGenExclusion(policy) {
msg := "Policies that match Pods apply to all Pods including those created and managed by controllers " +
"excluded from autogen. Use preconditions to exclude the Pods managed by controllers which are " +
"excluded from autogen. Refer to https://kyverno.io/docs/writing-policies/autogen/ for details."
warnings = append(warnings, msg)
}
}
}
// Validate Kind with match resource kinds
@ -279,7 +289,7 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, policy)
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, errors.Wrapf(err, "the kind defined in the any match resource is invalid")
}
@ -291,7 +301,7 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, policy)
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, errors.Wrapf(err, "the kind defined in the all match resource is invalid")
}
@ -303,7 +313,7 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, policy)
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, errors.Wrapf(err, "the kind defined in the any exclude resource is invalid")
}
@ -315,7 +325,7 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
return warnings, wildcardErr
}
if !slices.Contains(value.ResourceDescription.Kinds, "*") {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, policy)
err := validateKinds(value.ResourceDescription.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, errors.Wrapf(err, "the kind defined in the all exclude resource is invalid")
}
@ -323,11 +333,11 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
}
if !slices.Contains(rule.MatchResources.Kinds, "*") {
err := validateKinds(rule.MatchResources.Kinds, mock, client, policy)
err := validateKinds(rule.MatchResources.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, errors.Wrapf(err, "match resource kind is invalid")
}
err = validateKinds(rule.ExcludeResources.Kinds, mock, client, policy)
err = validateKinds(rule.ExcludeResources.Kinds, mock, background, rule.HasValidate(), client)
if err != nil {
return warnings, errors.Wrapf(err, "exclude resource kind is invalid")
}
@ -383,6 +393,35 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
}
}
}
matchKinds := match.GetKinds()
excludeKinds := exclude.GetKinds()
allKinds := make([]string, 0, len(matchKinds)+len(excludeKinds))
allKinds = append(allKinds, matchKinds...)
allKinds = append(allKinds, excludeKinds...)
if rule.HasValidate() {
validationJson, err := json.Marshal(rule.Validation)
if err != nil {
return nil, err
}
checkForScaleSubresource(validationJson, allKinds, &warnings)
checkForStatusSubresource(validationJson, allKinds, &warnings)
checkForEphemeralContainersSubresource(validationJson, allKinds, &warnings)
}
if rule.HasMutate() {
mutationJson, err := json.Marshal(rule.Mutation)
targets := rule.Mutation.Targets
for _, target := range targets {
allKinds = append(allKinds, target.GetKind())
}
if err != nil {
return nil, err
}
checkForScaleSubresource(mutationJson, allKinds, &warnings)
checkForStatusSubresource(mutationJson, allKinds, &warnings)
checkForEphemeralContainersSubresource(mutationJson, allKinds, &warnings)
}
}
if !mock && (spec.SchemaValidation == nil || *spec.SchemaValidation) {
if err := openApiManager.ValidatePolicyMutation(policy); err != nil {
@ -1209,16 +1248,20 @@ func validateWildcard(kinds []string, spec *kyvernov1.Spec, rule kyvernov1.Rule)
}
// validateKinds verifies if an API resource that matches 'kind' is valid kind
// and found in the cache, returns error if not found
func validateKinds(kinds []string, mock bool, client dclient.Interface, p kyvernov1.PolicyInterface) error {
// and found in the cache, returns error if not found. It also returns an error if background scanning
// is enabled for a subresource.
func validateKinds(kinds []string, mock, backgroundScanningEnabled, isValidationPolicy bool, client dclient.Interface) error {
for _, kind := range kinds {
gv, k := kubeutils.GetKindFromGVK(kind)
if !mock && !kubeutils.SkipSubResources(k) && !strings.Contains(kind, "*") {
_, _, err := client.Discovery().FindResource(gv, k)
if !mock && !strings.Contains(kind, "*") {
gv, k := kubeutils.GetKindFromGVK(kind)
_, _, gvr, err := client.Discovery().FindResource(gv, k)
if err != nil {
return fmt.Errorf("unable to convert GVK to GVR for kinds %s, err: %s", k, err)
}
_, subresource := kubeutils.SplitSubresource(gvr.Resource)
if subresource != "" && isValidationPolicy && backgroundScanningEnabled {
return fmt.Errorf("background scan enabled with subresource %s", subresource)
}
}
}
return nil
@ -1280,3 +1323,39 @@ func validateNamespaces(s *kyvernov1.Spec, path *field.Path) error {
return nil
}
func checkForScaleSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) {
if strings.Contains(string(ruleTypeJson), "replicas") {
for _, kind := range allKinds {
if strings.Contains(strings.ToLower(kind), "scale") {
return
}
}
msg := "You are matching on replicas but not including the scale subresource in the policy."
*warnings = append(*warnings, msg)
}
}
func checkForStatusSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) {
if strings.Contains(string(ruleTypeJson), "status") {
for _, kind := range allKinds {
if strings.Contains(strings.ToLower(kind), "status") {
return
}
}
msg := "You are matching on status but not including the status subresource in the policy."
*warnings = append(*warnings, msg)
}
}
func checkForEphemeralContainersSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) {
if strings.Contains(string(ruleTypeJson), "ephemeralcontainers") {
for _, kind := range allKinds {
if strings.Contains(strings.ToLower(kind), "ephemeralcontainers") {
return
}
}
msg := "You are matching on ephemeralcontainers but not including the ephemeralcontainers subresource in the policy."
*warnings = append(*warnings, msg)
}
}

View file

@ -8,7 +8,7 @@ import (
// Cache get method use for to get policy names and mostly use to test cache testcases
type Cache interface {
// Set inserts a policy in the cache
Set(string, kyvernov1.PolicyInterface)
Set(string, kyvernov1.PolicyInterface, map[string]string)
// Unset removes a policy from the cache
Unset(string)
// GetPolicies returns all policies that apply to a namespace, including cluster-wide policies
@ -27,8 +27,8 @@ func NewCache() Cache {
}
}
func (c *cache) Set(key string, policy kyvernov1.PolicyInterface) {
c.store.set(key, policy)
func (c *cache) Set(key string, policy kyvernov1.PolicyInterface, subresourceGVKToKind map[string]string) {
c.store.set(key, policy, subresourceGVKToKind)
}
func (c *cache) Unset(key string) {

View file

@ -12,7 +12,7 @@ import (
func setPolicy(store store, policy kyvernov1.PolicyInterface) {
key, _ := kubecache.MetaNamespaceKeyFunc(policy)
store.set(key, policy)
store.set(key, policy, make(map[string]string))
}
func unsetPolicy(store store, policy kyvernov1.PolicyInterface) {
@ -1164,7 +1164,7 @@ func Test_Get_Policies(t *testing.T) {
cache := NewCache()
policy := newPolicy(t)
key, _ := kubecache.MetaNamespaceKeyFunc(policy)
cache.Set(key, policy)
cache.Set(key, policy, make(map[string]string))
validateAudit := cache.GetPolicies(ValidateAudit, "Namespace", "")
if len(validateAudit) != 0 {
@ -1197,7 +1197,7 @@ func Test_Get_Policies_Ns(t *testing.T) {
cache := NewCache()
policy := newNsPolicy(t)
key, _ := kubecache.MetaNamespaceKeyFunc(policy)
cache.Set(key, policy)
cache.Set(key, policy, make(map[string]string))
nspace := policy.GetNamespace()
validateAudit := cache.GetPolicies(ValidateAudit, "Pod", nspace)
@ -1226,9 +1226,9 @@ func Test_Get_Policies_Validate_Failure_Action_Overrides(t *testing.T) {
policy1 := newValidateAuditPolicy(t)
policy2 := newValidateEnforcePolicy(t)
key1, _ := kubecache.MetaNamespaceKeyFunc(policy1)
cache.Set(key1, policy1)
cache.Set(key1, policy1, make(map[string]string))
key2, _ := kubecache.MetaNamespaceKeyFunc(policy2)
cache.Set(key2, policy2)
cache.Set(key2, policy2, make(map[string]string))
validateAudit := cache.GetPolicies(ValidateAudit, "Pod", "")
if len(validateAudit) != 1 {

View file

@ -12,7 +12,7 @@ import (
type store interface {
// set inserts a policy in the cache
set(string, kyvernov1.PolicyInterface)
set(string, kyvernov1.PolicyInterface, map[string]string)
// unset removes a policy from the cache
unset(string)
// get finds policies that match a given type, gvk and namespace
@ -30,10 +30,10 @@ func newPolicyCache() store {
}
}
func (pc *policyCache) set(key string, policy kyvernov1.PolicyInterface) {
func (pc *policyCache) set(key string, policy kyvernov1.PolicyInterface, subresourceGVKToKind map[string]string) {
pc.lock.Lock()
defer pc.lock.Unlock()
pc.store.set(key, policy)
pc.store.set(key, policy, subresourceGVKToKind)
logger.V(4).Info("policy is added to cache", "key", key)
}
@ -93,7 +93,7 @@ func set(set sets.String, item string, value bool) sets.String {
}
}
func (m *policyMap) set(key string, policy kyvernov1.PolicyInterface) {
func (m *policyMap) set(key string, policy kyvernov1.PolicyInterface, subresourceGVKToKind map[string]string) {
enforcePolicy := computeEnforcePolicy(policy.GetSpec())
m.policies[key] = policy
type state struct {
@ -102,13 +102,16 @@ func (m *policyMap) set(key string, policy kyvernov1.PolicyInterface) {
kindStates := map[string]state{}
for _, rule := range autogen.ComputeRules(policy) {
for _, gvk := range rule.MatchResources.GetKinds() {
kind := computeKind(gvk)
kind, ok := subresourceGVKToKind[gvk]
if !ok {
kind = computeKind(gvk)
}
entry := kindStates[kind]
entry.hasMutate = (entry.hasMutate || rule.HasMutate())
entry.hasValidate = (entry.hasValidate || rule.HasValidate())
entry.hasGenerate = (entry.hasGenerate || rule.HasGenerate())
entry.hasVerifyImages = (entry.hasVerifyImages || rule.HasVerifyImages())
entry.hasImagesValidationChecks = (entry.hasImagesValidationChecks || rule.HasImagesValidationChecks())
entry.hasMutate = entry.hasMutate || rule.HasMutate()
entry.hasValidate = entry.hasValidate || rule.HasValidate()
entry.hasGenerate = entry.hasGenerate || rule.HasGenerate()
entry.hasVerifyImages = entry.hasVerifyImages || rule.HasVerifyImages()
entry.hasImagesValidationChecks = entry.hasImagesValidationChecks || rule.HasImagesValidationChecks()
kindStates[kind] = entry
}
}

View file

@ -1,27 +1,33 @@
package kube
import "strings"
import (
"regexp"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GetKindFromGVK - get kind and APIVersion from GVK
func GetKindFromGVK(str string) (apiVersion string, kind string) {
func GetKindFromGVK(str string) (groupVersion string, kind string) {
parts := strings.Split(str, "/")
count := len(parts)
versionRegex := regexp.MustCompile(`v\d((alpha|beta)\d)?`)
if count == 2 {
return parts[0], formatSubresource(parts[1])
}
if count == 3 {
if parts[1] == "*" {
return "", formatSubresource(parts[2])
if versionRegex.MatchString(parts[0]) || parts[0] == "*" {
return parts[0], formatSubresource(parts[1])
} else {
return "", parts[0] + "/" + parts[1]
}
return parts[0] + "/" + parts[1], formatSubresource(parts[2])
}
if count == 4 {
} else if count == 3 {
if versionRegex.MatchString(parts[0]) || parts[0] == "*" {
return parts[0], parts[1] + "/" + parts[2]
} else {
return parts[0] + "/" + parts[1], formatSubresource(parts[2])
}
} else if count == 4 {
return parts[0] + "/" + parts[1], parts[2] + "/" + parts[3]
}
return "", formatSubresource(str)
}
@ -29,21 +35,9 @@ func formatSubresource(s string) string {
return strings.Replace(s, ".", "/", 1)
}
// GetGroupFromGVK - get group GVK
func GetGroupFromGVK(str string) (group string) {
parts := strings.Split(str, "/")
count := len(parts)
if count == 3 {
if parts[1] == "*" {
return parts[0]
}
}
return ""
}
// SplitSubresource - split subresource from kind
func SplitSubresource(s string) (kind string, subresource string) {
normalized := strings.Replace(s, ".", "/", 1)
parts := strings.Split(normalized, "/")
parts := strings.Split(s, "/")
if len(parts) == 2 {
return parts[0], parts[1]
}
@ -51,17 +45,36 @@ func SplitSubresource(s string) (kind string, subresource string) {
return s, ""
}
// ContainsKind - check if kind is in list
func ContainsKind(list []string, kind string) bool {
for _, e := range list {
if _, k := GetKindFromGVK(e); k == kind {
_, k := GetKindFromGVK(e)
k, _ = SplitSubresource(k)
if k == kind {
return true
}
}
return false
}
// SkipSubResources skip list of resources which don't have an API group.
func SkipSubResources(kind string) bool {
s := []string{"PodExecOptions", "PodAttachOptions", "PodProxyOptions", "ServiceProxyOptions", "NodeProxyOptions"}
return ContainsKind(s, kind)
// GroupVersionMatches - check if the given group version matches the server resource group version.
// If the group version contains a wildcard, it will match any version, but the group must match. Returns false if the
// supplied group version is empty, that condition should be checked before calling this function.
func GroupVersionMatches(groupVersion, serverResourceGroupVersion string) bool {
if strings.Contains(groupVersion, "*") {
return strings.HasPrefix(serverResourceGroupVersion, strings.TrimSuffix(groupVersion, "*"))
}
gv, err := schema.ParseGroupVersion(groupVersion)
if err == nil {
serverResourceGV, _ := schema.ParseGroupVersion(serverResourceGroupVersion)
return gv.Group == serverResourceGV.Group && gv.Version == serverResourceGV.Version
}
return false
}
// IsSubresource returns true if the resource is a subresource
func IsSubresource(resourceName string) bool {
return strings.Contains(resourceName, "/")
}

View file

@ -21,7 +21,7 @@ func Test_GetKindFromGVK(t *testing.T) {
assert.Equal(t, "Pod", kind)
apiVersion, kind = GetKindFromGVK("batch/*/CronJob")
assert.Equal(t, "", apiVersion)
assert.Equal(t, "batch/*", apiVersion)
assert.Equal(t, "CronJob", kind)
apiVersion, kind = GetKindFromGVK("storage.k8s.io/v1/CSIDriver")
@ -39,6 +39,34 @@ func Test_GetKindFromGVK(t *testing.T) {
apiVersion, kind = GetKindFromGVK("Pod.Status")
assert.Equal(t, "", apiVersion)
assert.Equal(t, "Pod/Status", kind)
apiVersion, kind = GetKindFromGVK("apps/v1/Deployment/Scale")
assert.Equal(t, "apps/v1", apiVersion)
assert.Equal(t, "Deployment/Scale", kind)
apiVersion, kind = GetKindFromGVK("v1/ReplicationController/Scale")
assert.Equal(t, "v1", apiVersion)
assert.Equal(t, "ReplicationController/Scale", kind)
apiVersion, kind = GetKindFromGVK("*/ReplicationController/Scale")
assert.Equal(t, "*", apiVersion)
assert.Equal(t, "ReplicationController/Scale", kind)
apiVersion, kind = GetKindFromGVK("*/Deployment/scale")
assert.Equal(t, "*", apiVersion)
assert.Equal(t, "Deployment/scale", kind)
apiVersion, kind = GetKindFromGVK("*/Deployment.scale")
assert.Equal(t, "*", apiVersion)
assert.Equal(t, "Deployment/scale", kind)
apiVersion, kind = GetKindFromGVK("*/Deployment/scale")
assert.Equal(t, "*", apiVersion)
assert.Equal(t, "Deployment/scale", kind)
apiVersion, kind = GetKindFromGVK("apps/v1/Deployment.scale")
assert.Equal(t, "apps/v1", apiVersion)
assert.Equal(t, "Deployment/scale", kind)
}
func Test_SplitSubresource(t *testing.T) {
@ -50,16 +78,25 @@ func Test_SplitSubresource(t *testing.T) {
kind, subresource = SplitSubresource("TaskRun/status")
assert.Equal(t, kind, "TaskRun")
assert.Equal(t, subresource, "status")
kind, subresource = SplitSubresource("Pod.Status")
assert.Equal(t, kind, "Pod")
assert.Equal(t, subresource, "Status")
kind, subresource = SplitSubresource("v1/Pod/Status")
assert.Equal(t, kind, "v1/Pod/Status")
assert.Equal(t, subresource, "")
kind, subresource = SplitSubresource("v1/Pod.Status")
assert.Equal(t, kind, "v1/Pod.Status")
assert.Equal(t, subresource, "")
}
func Test_GroupVersionMatches(t *testing.T) {
groupVersion, serverResourceGroupVersion := "v1", "v1"
assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), true)
// If user does not specify a group, then it is considered as legacy group which is empty
groupVersion, serverResourceGroupVersion = "v1", "networking.k8s.io/v1"
assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), false)
groupVersion, serverResourceGroupVersion = "*", "v1"
assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), true)
groupVersion, serverResourceGroupVersion = "certificates.k8s.io/*", "certificates.k8s.io/v1"
assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), true)
groupVersion, serverResourceGroupVersion = "*", "certificates.k8s.io/v1"
assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), true)
groupVersion, serverResourceGroupVersion = "certificates.k8s.io/*", "networking.k8s.io/v1"
assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), false)
}

View file

@ -271,7 +271,8 @@ func Test_AdmissionResponseValid(t *testing.T) {
assert.NilError(t, err)
key := makeKey(&validPolicy)
policyCache.Set(key, &validPolicy)
subresourceGVKToKind := make(map[string]string)
policyCache.Set(key, &validPolicy, subresourceGVKToKind)
request := &v1.AdmissionRequest{
Operation: v1.Create,
@ -280,6 +281,7 @@ func Test_AdmissionResponseValid(t *testing.T) {
Object: runtime.RawExtension{
Raw: []byte(pod),
},
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
}
response := handlers.Mutate(ctx, logger, request, "", time.Now())
@ -291,7 +293,7 @@ func Test_AdmissionResponseValid(t *testing.T) {
assert.Equal(t, len(response.Warnings), 0)
validPolicy.Spec.ValidationFailureAction = "Enforce"
policyCache.Set(key, &validPolicy)
policyCache.Set(key, &validPolicy, subresourceGVKToKind)
response = handlers.Validate(ctx, logger, request, "", time.Now())
assert.Equal(t, response.Allowed, false)
@ -320,11 +322,13 @@ func Test_AdmissionResponseInvalid(t *testing.T) {
Object: runtime.RawExtension{
Raw: []byte(pod),
},
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
}
keyInvalid := makeKey(&invalidPolicy)
invalidPolicy.Spec.ValidationFailureAction = "Enforce"
policyCache.Set(keyInvalid, &invalidPolicy)
subresourceGVKToKind := make(map[string]string)
policyCache.Set(keyInvalid, &invalidPolicy, subresourceGVKToKind)
response := handlers.Validate(ctx, logger, request, "", time.Now())
assert.Equal(t, response.Allowed, false)
@ -332,7 +336,7 @@ func Test_AdmissionResponseInvalid(t *testing.T) {
var ignore kyverno.FailurePolicyType = kyverno.Ignore
invalidPolicy.Spec.FailurePolicy = &ignore
policyCache.Set(keyInvalid, &invalidPolicy)
policyCache.Set(keyInvalid, &invalidPolicy, subresourceGVKToKind)
response = handlers.Validate(ctx, logger, request, "", time.Now())
assert.Equal(t, response.Allowed, true)
@ -353,7 +357,8 @@ func Test_ImageVerify(t *testing.T) {
assert.NilError(t, err)
key := makeKey(&policy)
policyCache.Set(key, &policy)
subresourceGVKToKind := make(map[string]string)
policyCache.Set(key, &policy, subresourceGVKToKind)
request := &v1.AdmissionRequest{
Operation: v1.Create,
@ -362,10 +367,11 @@ func Test_ImageVerify(t *testing.T) {
Object: runtime.RawExtension{
Raw: []byte(pod),
},
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
}
policy.Spec.ValidationFailureAction = "Enforce"
policyCache.Set(key, &policy)
policyCache.Set(key, &policy, subresourceGVKToKind)
response := handlers.Mutate(ctx, logger, request, "", time.Now())
assert.Equal(t, response.Allowed, false)
@ -373,7 +379,7 @@ func Test_ImageVerify(t *testing.T) {
var ignore kyverno.FailurePolicyType = kyverno.Ignore
policy.Spec.FailurePolicy = &ignore
policyCache.Set(key, &policy)
policyCache.Set(key, &policy, subresourceGVKToKind)
response = handlers.Mutate(ctx, logger, request, "", time.Now())
assert.Equal(t, response.Allowed, false)
@ -394,7 +400,7 @@ func Test_MutateAndVerify(t *testing.T) {
assert.NilError(t, err)
key := makeKey(&policy)
policyCache.Set(key, &policy)
policyCache.Set(key, &policy, make(map[string]string))
request := &v1.AdmissionRequest{
Operation: v1.Create,
@ -403,6 +409,7 @@ func Test_MutateAndVerify(t *testing.T) {
Object: runtime.RawExtension{
Raw: []byte(resourceMutateAndVerify),
},
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
}
response := handlers.Mutate(ctx, logger, request, "", time.Now())

View file

@ -172,8 +172,8 @@ func (v *validationHandler) handleAudit(
if request.DryRun != nil && *request.DryRun {
return
}
// we don't need reports for deletions and when it's about sub resources
if request.Operation == admissionv1.Delete || request.SubResource != "" {
// we don't need reports for deletions
if request.Operation == admissionv1.Delete {
return
}
// check if the resource supports reporting

View file

@ -0,0 +1,41 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-exec-by-pod-label
annotations:
policies.kyverno.io/title: Block Pod Exec by Pod Label
policies.kyverno.io/category: Sample
policies.kyverno.io/minversion: 1.4.2
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
The `exec` command may be used to gain shell access, or run other commands, in a Pod's container. While this can
be useful for troubleshooting purposes, it could represent an attack vector and is discouraged.
This policy blocks Pod exec commands to Pods having the label `exec=false`.
spec:
validationFailureAction: Enforce
background: false
rules:
- name: deny-exec-by-label
match:
all:
- resources:
kinds:
- PodExecOptions
context:
- name: podexeclabel
apiCall:
urlPath: "/api/v1/namespaces/{{request.namespace}}/pods/{{request.name}}"
jmesPath: "metadata.labels.exec"
preconditions:
all:
- key: "{{ request.operation }}"
operator: Equals
value: CONNECT
validate:
message: Exec'ing into Pods protected with the label `exec=false` is forbidden.
deny:
conditions:
all:
- key: "{{ podexeclabel }}"
operator: Equals
value: "false"

View file

@ -0,0 +1,13 @@
name: deny-exec-by-pod-label
policies:
- deny-exec-by-pod-label.yaml
resources:
- resource.yaml
variables: values.yaml
results:
- policy: deny-exec-by-pod-label
rule: deny-exec-by-label
resource: execpod
namespace: default
kind: PodExecOptions
result: fail

View file

@ -0,0 +1,13 @@
kind: PodExecOptions
apiVersion: v1
# PodExecOptions actually does not contain any metadata, but kyverno relies on the name and namespace of object
# to perform CLI execution. So we add them as a hack to get this working.
metadata:
name: execpod
namespace: default
stdin: true
stdout: true
tty: true
container: nginx
command:
- sh

View file

@ -0,0 +1,17 @@
policies:
- name: deny-exec-by-pod-label
rules:
- name: deny-exec-by-label
values:
podexeclabel: "false"
globalValues:
request.operation: CONNECT
subresources:
- subresource:
name: "pods/exec"
kind: "PodExecOptions"
version: "v1"
parentResource:
name: "pods"
kind: "Pod"
version: "v1"

View file

@ -0,0 +1,24 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-replicas-for-scale-subresource
spec:
background: false
failurePolicy: Fail
rules:
- match:
all:
- resources:
kinds:
- "Deployment/scale"
names:
- nginx-test
namespaces:
- default
name: validate-nginx-test
validate:
message: 'nginx-test needs to have 2 replicas'
pattern:
spec:
replicas: 2
validationFailureAction: Enforce

View file

@ -0,0 +1,13 @@
name: enforce-replicas-for-scale-subresource
policies:
- enforce-replicas-for-scale-subresource.yml
resources:
- resource.yaml
variables: values.yaml
results:
- policy: enforce-replicas-for-scale-subresource
rule: validate-nginx-test
resource: nginx-test
namespace: default
kind: Scale
result: fail

View file

@ -0,0 +1,7 @@
kind: Scale
apiVersion: autoscaling/v1
metadata:
name: nginx-test
namespace: default
spec:
replicas: 4

View file

@ -0,0 +1,11 @@
subresources:
- subresource:
name: "deployments/scale"
kind: "Scale"
group: "autoscaling"
version: "v1"
parentResource:
name: "deployments"
kind: "Deployment"
group: "apps"
version: "v1"

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: advertise-resource
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,41 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: advertise-resource
spec:
background: false
rules:
- name: advertise-resource
match:
resources:
kinds:
- Node
mutate:
targets:
- apiVersion: v1
kind: Node/status
name: kind-control-plane
namespace: ""
patchStrategicMerge:
status:
capacity:
example.com/dongle: "41"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/instance: kyverno
app.kubernetes.io/name: kyverno
name: kyverno:modify-nodes
rules:
- apiGroups:
- ""
resources:
- nodes
- nodes/status
verbs:
- create
- update
- patch
- delete

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: Node
metadata:
name: kind-control-plane
status:
capacity:
example.com/dongle: "41"

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: ./modify-resource-filters.sh removeNode
# Send an update request to the node
- command: kubectl label nodes kind-control-plane abc=xyz

View file

@ -0,0 +1,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete cpol advertise-resource --force --wait=true --ignore-not-found=true
- script: ./modify-resource-filters.sh addNode
- script: ./clear-modified-node-status.sh
- command: kubectl label nodes kind-control-plane abc-

View file

@ -0,0 +1,22 @@
## Description
This test validates that an incoming request to `Node` triggers mutating the existing `Node/status` subresource.
## Expected Behavior
The existing `Node/status` subresource is mutated.
## Steps
### Test Steps
1. Create a `ClusterPolicy` that matches on `Node` and mutates `Node/status` object.
2. Create `ClusterRole` for allowing modifications to `Node/status` subresource.
3. Modify kyverno `resourceFilters` to allow mutating requests for `Node` resource.
4. Send a update request to `Node`.
5. Mutate the existing `Node/status` subresource.
6. Verify that the existing `Node/status` object is mutated.
## Reference Issue(s)
https://github.com/kyverno/kyverno/issues/2843

View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -eu
kubectl proxy &
proxy_pid=$!
echo $proxy_pid
function cleanup {
echo "killing kubectl proxy" >&2
kill $proxy_pid
}
attempt_counter=0
max_attempts=5
until curl --output /dev/null -fsSL http://localhost:8001/; do
if [ ${attempt_counter} -eq ${max_attempts} ]; then
echo "Max attempts reached"
exit 1
fi
attempt_counter=$((attempt_counter + 1))
sleep 5
done
curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--output /dev/null \
--data '[{"op": "remove", "path": "/status/capacity/example.com~1dongle"}]' \
http://localhost:8001/api/v1/nodes/kind-control-plane/status
kubectl annotate node kind-control-plane policies.kyverno.io/last-applied-patches-
trap cleanup EXIT

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eu
if [ $# -ne 1 ]; then
echo "Usage: $0 [addNode|removeNode]"
exit 1
fi
if [ "$1" = "removeNode" ]; then
resource_filters=$(kubectl get ConfigMap kyverno -n kyverno -o json | jq .data.resourceFilters)
resource_filters="${resource_filters//\[Node,\*,\*\]/}"
kubectl patch ConfigMap kyverno -n kyverno --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/resourceFilters\", \"value\":""$resource_filters""}]"
fi
if [ "$1" = "addNode" ]; then
resource_filters=$(kubectl get ConfigMap kyverno -n kyverno -o json | jq .data.resourceFilters)
resource_filters="${resource_filters%?}"
resource_filters="${resource_filters}""[Node,*,*]\""
kubectl patch ConfigMap kyverno -n kyverno --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/resourceFilters\", \"value\":""$resource_filters""}]"
fi

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: advertise-resource
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,17 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: advertise-resource
spec:
background: false
rules:
- name: advertise-resource
match:
resources:
kinds:
- Node/status
mutate:
patchesJson6902: |-
- op: add
path: "/status/capacity/example.com~1dongle"
value: "4"

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: Node
metadata:
name: kind-control-plane
status:
capacity:
example.com/dongle: "4"

View file

@ -0,0 +1,5 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: ./modify-resource-filters.sh removeNode
- script: ./send-request-to-status-subresource.sh

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete cpol advertise-resource --force --wait=true --ignore-not-found=true
- script: ./modify-resource-filters.sh addNode
- script: ./clear-modified-node-status.sh

View file

@ -0,0 +1,22 @@
## Description
This test validates that an incoming request to `Node/status` is mutated by the mutation policy matching
on `Node/status`.
## Expected Behavior
The request is mutated.
## Steps
### Test Steps
1. Create a `ClusterPolicy` that matches on `Node/status` and mutates the request.
2. Modify kyverno `resourceFilters` to allow mutating requests for `Node` resource.
3. Send a update request to `Node/status`.
4. Mutate the incoming request (done by kyverno).
5. Verify that the request is mutated.
## Reference Issue(s)
https://github.com/kyverno/kyverno/issues/2843

View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -eu
kubectl proxy &
proxy_pid=$!
echo $proxy_pid
function cleanup {
echo "killing kubectl proxy" >&2
kill $proxy_pid
}
attempt_counter=0
max_attempts=5
until curl --output /dev/null -fsSL http://localhost:8001/; do
if [ ${attempt_counter} -eq ${max_attempts} ]; then
echo "Max attempts reached"
exit 1
fi
attempt_counter=$((attempt_counter + 1))
sleep 5
done
curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--output /dev/null \
--data '[{"op": "remove", "path": "/status/capacity/example.com~1dongle"}]' \
http://localhost:8001/api/v1/nodes/kind-control-plane/status
kubectl annotate node kind-control-plane policies.kyverno.io/last-applied-patches-
trap cleanup EXIT

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eu
if [ $# -ne 1 ]; then
echo "Usage: $0 [addNode|removeNode]"
exit 1
fi
if [ "$1" = "removeNode" ]; then
resource_filters=$(kubectl get ConfigMap kyverno -n kyverno -o json | jq .data.resourceFilters)
resource_filters="${resource_filters//\[Node,\*,\*\]/}"
kubectl patch ConfigMap kyverno -n kyverno --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/resourceFilters\", \"value\":""$resource_filters""}]"
fi
if [ "$1" = "addNode" ]; then
resource_filters=$(kubectl get ConfigMap kyverno -n kyverno -o json | jq .data.resourceFilters)
resource_filters="${resource_filters%?}"
resource_filters="${resource_filters}""[Node,*,*]\""
kubectl patch ConfigMap kyverno -n kyverno --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/resourceFilters\", \"value\":""$resource_filters""}]"
fi

View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -eu
kubectl proxy &
proxy_pid=$!
echo $proxy_pid
function cleanup {
echo "killing kubectl proxy" >&2
kill $proxy_pid
}
attempt_counter=0
max_attempts=5
until curl --output /dev/null -fsSL http://localhost:8001/; do
if [ ${attempt_counter} -eq ${max_attempts} ];then
echo "Max attempts reached"
exit 1
fi
attempt_counter=$((attempt_counter+1))
sleep 5
done
if curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--output /dev/null \
--data '[{"op": "add", "path": "/status/capacity/example.com~1dongle", "value": "1"}]' \
http://localhost:8001/api/v1/nodes/kind-control-plane/status; then
echo "Successfully sent request to status subresource."
trap cleanup EXIT
exit 0
else
echo "Failed to send request to status subresource."
trap cleanup EXIT
exit 1
fi

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-evict-by-pod-label
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,45 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-validate
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-evict-by-pod-label
spec:
validationFailureAction: Enforce
background: false
rules:
- name: deny-evict-by-label
match:
resources:
kinds:
- Pod/eviction
context:
- name: podevictlabel
apiCall:
urlPath: "/api/v1/namespaces/{{request.namespace}}/pods/{{request.name}}"
jmesPath: "metadata.labels.evict"
validate:
message: Evicting Pods protected with the label 'evict=false' is forbidden.
deny:
conditions:
all:
- key: "{{ podevictlabel }}"
operator: Equals
value: "false"
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
tier: frontend
evict: "false"
namespace: test-validate
spec:
containers:
- name: nginx
image: nginx

View file

@ -0,0 +1,4 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: ./api-initiated-eviction.sh

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete pod nginx -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete cpol deny-evict-by-pod-label -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete ns test-validate --force --ignore-not-found=true

View file

@ -0,0 +1,3 @@
# Evicting pod with label 'evict=false' is forbidden
Validate test to check that a pod with label 'evict=false' cannot be evicted. Related issue https://github.com/kyverno/kyverno/issues/4313

View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -eu
kubectl proxy &
proxy_pid=$!
echo $proxy_pid
function cleanup {
echo "killing kubectl proxy" >&2
kill $proxy_pid
}
attempt_counter=0
max_attempts=5
until curl --output /dev/null -fsSL http://localhost:8001/; do
if [ ${attempt_counter} -eq ${max_attempts} ];then
echo "Max attempts reached"
exit 1
fi
attempt_counter=$((attempt_counter+1))
sleep 5
done
if curl -v -H 'Content-type: application/json' \
http://localhost:8001/api/v1/namespaces/test-validate/pods/nginx/eviction -d @eviction.json 2>&1 | grep -q "Evicting Pods protected with the label 'evict=false' is forbidden"; then
echo "Test succeeded. Resource was not evicted."
trap cleanup EXIT
exit 0
else
echo "Tested failed. Resource was evicted."
trap cleanup EXIT
exit 1
fi

View file

@ -0,0 +1,8 @@
{
"apiVersion": "policy/v1",
"kind": "Eviction",
"metadata": {
"name": "nginx",
"namespace": "test-validate"
}
}

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-exec-by-pod-label
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,59 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-validate
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-exec-by-pod-label
annotations:
policies.kyverno.io/title: Block Pod Exec by Pod Label
policies.kyverno.io/category: Sample
policies.kyverno.io/minversion: 1.4.2
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
The 'exec' command may be used to gain shell access, or run other commands, in a Pod's container. While this can
be useful for troubleshooting purposes, it could represent an attack vector and is discouraged.
This policy blocks Pod exec commands to Pods having the label 'exec=false'.
spec:
validationFailureAction: Enforce
background: false
rules:
- name: deny-exec-by-label
match:
resources:
kinds:
- PodExecOptions
context:
- name: podexeclabel
apiCall:
urlPath: "/api/v1/namespaces/{{request.namespace}}/pods/{{request.name}}"
jmesPath: "metadata.labels.exec"
preconditions:
all:
- key: "{{ request.operation }}"
operator: Equals
value: CONNECT
validate:
message: Exec'ing into Pods protected with the label 'exec=false' is forbidden.
deny:
conditions:
all:
- key: "{{ podexeclabel }}"
operator: Equals
value: "false"
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
tier: frontend
exec: "false"
namespace: test-validate
spec:
containers:
- name: nginx
image: nginx

View file

@ -0,0 +1,12 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: |
if kubectl -n test-validate exec nginx -it -- sh 2>&1 | grep -q "Exec'ing into Pods protected with the label 'exec=false' is forbidden"
then
echo "Tested failed. Exec Request was not blocked."
exit 1
else
echo "Test succeeded. Exec Request was blocked."
exit 0
fi

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete pod nginx -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete cpol deny-exec-by-pod-label -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete ns test-validate --force --ignore-not-found=true

View file

@ -0,0 +1,3 @@
# Exec'ing into a pod
Validate test to ensure pods with label `exec=false` cannot be exec'ed into.

View file

@ -0,0 +1,19 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: nginx-test-scaling-policy
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-test
name: nginx-test
namespace: test-validate
status:
replicas: 2

View file

@ -0,0 +1,49 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-validate
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: nginx-test-scaling-policy
spec:
background: false
failurePolicy: Fail
rules:
- match:
resources:
kinds:
- "Deployment/scale"
names:
- nginx-test
namespaces:
- test-validate
name: validate-nginx-test
validate:
message: 'nginx-test needs to have 2 replicas'
pattern:
spec:
replicas: 2
validationFailureAction: Enforce
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-test
name: nginx-test
namespace: test-validate
spec:
replicas: 2
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- image: nginx
name: nginx

View file

@ -0,0 +1,12 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: |
if kubectl scale deployment nginx-test --replicas=1 -n test-validate 2>&1 | grep -q 'validation error: nginx-test needs to have 2 replicas'
then
echo "Test succeeded. Resource was blocked from scaling."
exit 0
else
echo "Tested failed. Resource was allowed to scale."
exit 1
fi

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete deploy nginx-test -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete cpol nginx-test-scaling-policy -n test-validate --force --wait=true --ignore-not-found=true
- command: kubectl delete ns test-validate --force --ignore-not-found=true

View file

@ -0,0 +1,3 @@
# Scaling with kubectl scale
Validate test to check that a resource can't be scaled through the `kubectl scale` command. Related issue https://github.com/kyverno/kyverno/issues/3118

View file

@ -0,0 +1,10 @@
apiVersion: kyverno.io/v1
kind: Policy
metadata:
name: configmap-policy
namespace: test-validate-e2e-adding-key-to-config-map
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,39 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-validate-e2e-adding-key-to-config-map
---
apiVersion: kyverno.io/v1
kind: Policy
metadata:
name: configmap-policy
namespace: test-validate-e2e-adding-key-to-config-map
spec:
background: false
failurePolicy: Fail
validationFailureAction: Enforce
rules:
- match:
all:
- resources:
kinds:
- ConfigMap
name: key-abc
preconditions:
all:
- key: "admin"
operator: Equals
value: "{{ request.object.data.lock || '' }}"
validate:
anyPattern:
- data:
key: "abc"
message: Configmap key must be "abc"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-configmap
namespace: test-validate-e2e-adding-key-to-config-map
data:
key: xyz

View file

@ -0,0 +1,12 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: |
if kubectl patch ConfigMap test-configmap -n test-validate-e2e-adding-key-to-config-map --type='json' -p="[{\"op\": \"add\", \"path\": \"/data/lock\", \"value\":""admin""}]" 2>&1 | grep -q 'validation error: Configmap key must be "abc"'
then
echo "Test succeeded. Resource was blocked from adding key."
exit 0
else
echo "Tested failed. Resource was not blocked from adding key."
exit 1
fi

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: kubectl delete ConfigMap test-configmap -n test-validate-e2e-adding-key-to-config-map --force --wait=true --ignore-not-found=true
- command: kubectl delete pol configmap-policy -n test-validate-e2e-adding-key-to-config-map --force --wait=true --ignore-not-found=true
- command: kubectl delete ns test-validate-e2e-adding-key-to-config-map --force --ignore-not-found=true

View file

@ -0,0 +1,21 @@
## Description
This test validates that an existing ConfigMap can't be updated with a new key that results in violation of a policy.
## Expected Behavior
The existing ConfigMap isn't patched and policy violation is reported.
## Steps
### Test Steps
1. Create a `Policy` that denies only permits combination of two particular keys together.
2. Create a `ConfigMap` that contains one of the keys.
3. Try to patch the `ConfigMap` with a new key that is not permitted by the policy.
4. Verify that the `ConfigMap` is not patched and policy violation is reported.
5. Delete the `Policy` and `ConfigMap`.
## Reference Issue(s)
https://github.com/kyverno/kyverno/issues/3253

View file

@ -5,42 +5,12 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
// FluxValidateTests is E2E Test Config for validation
var FluxValidateTests = []struct {
// TestName - Name of the Test
TestName string
// PolicyRaw - The Yaml file of the ClusterPolicy
PolicyRaw []byte
// ResourceRaw - The Yaml file of the ClusterPolicy
ResourceRaw []byte
// ResourceNamespace - Namespace of the Resource
ResourceNamespace string
// MustSucceed declares if test case must fail on validation
MustSucceed bool
}{
{
TestName: "test-validate-with-flux-and-variable-substitution-2043",
PolicyRaw: kyverno_2043_policy,
ResourceRaw: kyverno_2043_FluxKustomization,
ResourceNamespace: "test-validate",
MustSucceed: false,
},
{
TestName: "test-validate-with-flux-and-variable-substitution-2241",
PolicyRaw: kyverno_2241_policy,
ResourceRaw: kyverno_2241_FluxKustomization,
ResourceNamespace: "test-validate",
MustSucceed: true,
},
}
var (
podGVR = e2e.GetGVR("", "v1", "pods")
deploymentGVR = e2e.GetGVR("apps", "v1", "deployments")
configmapGVR = e2e.GetGVR("", "v1", "configmaps")
podGVR = e2e.GetGVR("", "v1", "pods")
kustomizeGVR = e2e.GetGVR("kustomize.toolkit.fluxcd.io", "v1beta1", "kustomizations")
)
var ValidateTests = []struct {
type ValidationTest struct {
// TestDescription - Description of the Test
TestDescription string
// PolicyName - Name of the Policy
@ -57,126 +27,63 @@ var ValidateTests = []struct {
ResourceRaw []byte
// MustSucceed - indicates if validation must succeed
MustSucceed bool
}{
}
var FluxValidateTests = []ValidationTest{
{
TestDescription: "test-validate-with-flux-and-variable-substitution-2043",
PolicyName: "flux-multi-tenancy",
PolicyRaw: kyverno2043Policy,
ResourceName: "dev-team",
ResourceNamespace: "test-validate",
ResourceGVR: kustomizeGVR,
ResourceRaw: kyverno2043Fluxkustomization,
MustSucceed: false,
},
{
TestDescription: "test-validate-with-flux-and-variable-substitution-2241",
PolicyName: "flux-multi-tenancy-2",
PolicyRaw: kyverno2241Policy,
ResourceName: "tenants",
ResourceNamespace: "test-validate",
ResourceGVR: kustomizeGVR,
ResourceRaw: kyverno2241Fluxkustomization,
MustSucceed: true,
},
}
var ValidateTests = []ValidationTest{
{
// Case for https://github.com/kyverno/kyverno/issues/2345 issue
TestDescription: "checks that contains function works properly with string list",
PolicyName: "drop-cap-net-raw",
PolicyRaw: kyverno_2345_policy,
PolicyRaw: kyverno2345Policy,
ResourceName: "test",
ResourceNamespace: "test-validate1",
ResourceGVR: podGVR,
ResourceRaw: kyverno_2345_resource,
MustSucceed: false,
},
{
// Case for https://github.com/kyverno/kyverno/issues/2390 issue
TestDescription: "checks that policy contains global anchor fields",
PolicyName: "check-image-pull-secret",
PolicyRaw: kyverno_global_anchor_validate_policy,
ResourceName: "pod-with-nginx-allowed-registory",
ResourceNamespace: "test-validate",
ResourceGVR: podGVR,
ResourceRaw: kyverno_global_anchor_validate_resource_1,
MustSucceed: true,
},
{
// Case for https://github.com/kyverno/kyverno/issues/2390 issue
TestDescription: "checks that policy contains global anchor fields",
PolicyName: "check-image-pull-secret",
PolicyRaw: kyverno_global_anchor_validate_policy,
ResourceName: "pod-with-nginx-disallowed-registory",
ResourceNamespace: "test-validate",
ResourceGVR: podGVR,
ResourceRaw: kyverno_global_anchor_validate_resource_2,
MustSucceed: false,
},
{
// Case for image validation
TestDescription: "checks that images are trustable",
PolicyName: "check-trustable-images",
PolicyRaw: kyverno_trustable_image_policy,
ResourceName: "pod-with-trusted-registry",
ResourceNamespace: "test-validate",
ResourceGVR: podGVR,
ResourceRaw: kyverno_trusted_image_pod,
MustSucceed: true,
},
{
// Case for image validation
TestDescription: "checks that images are trustable",
PolicyName: "check-trustable-images",
PolicyRaw: kyverno_trustable_image_policy,
ResourceName: "pod-with-root-user",
ResourceNamespace: "test-validate",
ResourceGVR: podGVR,
ResourceRaw: kyverno_pod_with_root_user,
ResourceRaw: kyverno2345Resource,
MustSucceed: false,
},
{
// Case for small image validation
TestDescription: "checks that images are small",
PolicyName: "check-small-images",
PolicyRaw: kyverno_small_image_policy,
PolicyRaw: kyvernoSmallImagePolicy,
ResourceName: "pod-with-small-image",
ResourceNamespace: "test-validate",
ResourceGVR: podGVR,
ResourceRaw: kyverno_pod_with_small_image,
ResourceRaw: kyvernoPodWithSmallImage,
MustSucceed: true,
},
{
// Case for small image validation
TestDescription: "checks that images are small",
PolicyName: "check-large-images",
PolicyRaw: kyverno_small_image_policy,
PolicyRaw: kyvernoSmallImagePolicy,
ResourceName: "pod-with-large-image",
ResourceNamespace: "test-validate",
ResourceGVR: podGVR,
ResourceRaw: kyverno_pod_with_large_image,
ResourceRaw: kyvernoPodWithLargeImage,
MustSucceed: false,
},
{
// Case for yaml signing validation
TestDescription: "checks that unsigned yaml manifest is blocked",
PolicyName: "check-yaml-signing",
PolicyRaw: kyverno_yaml_signing_validate_policy,
ResourceName: "test-deployment",
ResourceNamespace: "test-validate",
ResourceGVR: deploymentGVR,
ResourceRaw: kyverno_yaml_signing_validate_resource_1,
MustSucceed: false,
},
{
// Case for yaml signing validation
TestDescription: "checks that signed yaml manifest is created",
PolicyName: "check-yaml-signing",
PolicyRaw: kyverno_yaml_signing_validate_policy,
ResourceName: "test-deployment",
ResourceNamespace: "test-validate",
ResourceGVR: deploymentGVR,
ResourceRaw: kyverno_yaml_signing_validate_resource_2,
MustSucceed: true,
},
{
// Case for failing X.509 certificate decoding validation
TestDescription: "checks if the public key modulus of base64 encoded x.509 certificate is same as the pem x.509 certificate",
PolicyName: "check-x509-decode",
PolicyRaw: kyverno_decode_x509_certificate_policy,
ResourceName: "test-configmap",
ResourceNamespace: "test-validate",
ResourceGVR: configmapGVR,
ResourceRaw: kyverno_decode_x509_certificate_resource_fail,
MustSucceed: false,
},
{
// Case for passing X.509 certificate decoding validation
TestDescription: "checks if the public key modulus of base64 encoded x.509 certificate is same as the pem x.509 certificate",
PolicyName: "check-x509-decode",
PolicyRaw: kyverno_decode_x509_certificate_policy,
ResourceName: "test-configmap",
ResourceNamespace: "test-validate",
ResourceGVR: configmapGVR,
ResourceRaw: kyverno_decode_x509_certificate_resource_pass,
MustSucceed: true,
},
}

View file

@ -1,14 +1,8 @@
package validate
import "fmt"
// Namespace Description
var namespaceYaml = []byte(`
apiVersion: v1
kind: Namespace
metadata:
name: test-validate
`)
import (
"fmt"
)
func newNamespaceYaml(name string) []byte {
ns := fmt.Sprintf(`
@ -22,7 +16,7 @@ func newNamespaceYaml(name string) []byte {
// Regression: https://github.com/kyverno/kyverno/issues/2043
// Policy: https://github.com/fluxcd/flux2-multi-tenancy/blob/main/infrastructure/kyverno-policies/flux-multi-tenancy.yaml
var kyverno_2043_policy = []byte(`
var kyverno2043Policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
@ -39,7 +33,6 @@ spec:
resources:
kinds:
- Kustomization
- HelmRelease
validate:
message: ".spec.serviceAccountName is required"
pattern:
@ -54,7 +47,6 @@ spec:
resources:
kinds:
- Kustomization
- HelmRelease
validate:
message: "spec.sourceRef.namespace must be the same as metadata.namespace"
deny:
@ -64,11 +56,11 @@ spec:
value: "{{request.object.metadata.namespace}}"
`)
var kyverno_2241_policy = []byte(`
var kyverno2241Policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: flux-multi-tenancy
name: flux-multi-tenancy-2
spec:
validationFailureAction: enforce
rules:
@ -81,7 +73,6 @@ spec:
resources:
kinds:
- Kustomization
- HelmRelease
validate:
message: ".spec.serviceAccountName is required"
pattern:
@ -96,10 +87,9 @@ spec:
resources:
kinds:
- Kustomization
- HelmRelease
preconditions:
any:
- key: "{{request.object.spec.sourceRef.namespace}}"
- key: "{{request.object.spec.sourceRef.namespace || ''}}"
operator: NotEquals
value: ""
validate:
@ -111,7 +101,7 @@ spec:
value: "{{request.object.metadata.namespace}}"
`)
var kyverno_2043_FluxCRD = []byte(`
var kyverno2043Fluxcrd = []byte(`
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
@ -541,7 +531,7 @@ status:
storedVersions: []
`)
var kyverno_2043_FluxKustomization = []byte(`
var kyverno2043Fluxkustomization = []byte(`
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
@ -557,7 +547,7 @@ spec:
validation: client
`)
var kyverno_2241_FluxKustomization = []byte(`
var kyverno2241Fluxkustomization = []byte(`
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
@ -574,7 +564,7 @@ spec:
validation: client
`)
var kyverno_2345_policy = []byte(`
var kyverno2345Policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
@ -597,7 +587,7 @@ spec:
value: false
`)
var kyverno_2345_resource = []byte(`
var kyverno2345Resource = []byte(`
apiVersion: v1
kind: Pod
metadata:
@ -632,115 +622,7 @@ spec:
- CAP_SOMETHING
`)
var kyverno_trustable_image_policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-trustable-images
spec:
validationFailureAction: enforce
rules:
- name: only-allow-trusted-images
match:
resources:
kinds:
- Pod
preconditions:
- key: "{{request.operation}}"
operator: NotEquals
value: DELETE
validate:
message: "images with root user are not allowed"
foreach:
- list: "request.object.spec.containers"
context:
- name: imageData
imageRegistry:
reference: "{{ element.image }}"
jmesPath: "{user: configData.config.User || '', registry: registry}"
deny:
conditions:
all:
- key: "{{ imageData.user }}"
operator: Equals
value: ""
- key: "{{ imageData.registry }}"
operator: NotEquals
value: "ghcr.io"
`)
var kyverno_global_anchor_validate_policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sample
spec:
validationFailureAction: enforce
rules:
- name: check-container-image
match:
resources:
kinds:
- Pod
validate:
pattern:
spec:
containers:
- name: "*"
<(image): "nginx"
imagePullSecrets:
- name: my-registry-secret
`)
var kyverno_global_anchor_validate_resource_1 = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: pod-with-nginx-allowed-registory
spec:
containers:
- name: nginx
image: nginx
imagePullSecrets:
- name: my-registry-secret
`)
var kyverno_global_anchor_validate_resource_2 = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: pod-with-nginx-disallowed-registory
spec:
containers:
- name: nginx
image: nginx
imagePullSecrets:
- name: other-registory-secret
`)
var kyverno_trusted_image_pod = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: pod-with-trusted-registry
spec:
containers:
- name: kyverno
image: ghcr.io/kyverno/kyverno:latest
`)
var kyverno_pod_with_root_user = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: pod-with-root-user
spec:
containers:
- name: ubuntu
image: ubuntu:bionic
`)
var kyverno_small_image_policy = []byte(`
var kyvernoSmallImagePolicy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
@ -775,7 +657,7 @@ spec:
value: "{{imageSize}}"
`)
var kyverno_pod_with_small_image = []byte(`
var kyvernoPodWithSmallImage = []byte(`
apiVersion: v1
kind: Pod
metadata:
@ -786,7 +668,7 @@ spec:
image: busybox:latest
`)
var kyverno_pod_with_large_image = []byte(`
var kyvernoPodWithLargeImage = []byte(`
apiVersion: v1
kind: Pod
metadata:
@ -796,188 +678,3 @@ spec:
- name: large-image
image: nvidia/cuda:11.6.0-devel-ubi8
`)
var kyverno_yaml_signing_validate_policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: validate-resources
spec:
validationFailureAction: enforce
background: false
webhookTimeoutSeconds: 30
failurePolicy: Fail
rules:
- name: validate-resources
match:
any:
- resources:
kinds:
- Deployment
- Pod
name: test*
exclude:
any:
- resources:
kinds:
- Pod
subjects:
- kind: ServiceAccount
namespace: kube-system
name: replicaset-controller
- resources:
kinds:
- ReplicaSet
subjects:
- kind: ServiceAccount
namespace: kube-system
name: deployment-controller
validate:
manifests:
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyQfmL5YwHbn9xrrgG3vgbU0KJxMY
BibYLJ5L4VSMvGxeMLnBGdM48w5IE//6idUPj3rscigFdHs7GDMH4LLAng==
-----END PUBLIC KEY-----
`)
var kyverno_yaml_signing_validate_resource_1 = []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: test-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.14.2
name: nginx
ports:
- containerPort: 80
`)
var kyverno_yaml_signing_validate_resource_2 = []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
cosign.sigstore.dev/message: H4sIAAAAAAAA/wBaAaX+H4sIAAAAAAAA/+ySz27bMAzGffZT8AUcSf6TpDrvuMMOw64DazOeEP2bxBZtn35wnXhegOW4oYB/F9rg930gQYlnTOIU7EApC/8mlDye7c9xqNk/Stc49902rn1ppZRy9OKr6IOLiXI2fqwYUzW+KXmQDw9tUx8FU+ZqoGjDqyPPu1d0tigm775t3+th371XWc//E12zL1Rbq042XacOhWzquusKkMU/4CkzpkLKdH4awh1dZjyd7vQvuyz1g4DRfKOUTfAaMMYsnlV5Nn7Q8Gk5Y+mIcUBGXQJYfCSbpy+YDBr8aPxLCeDRkYabF1DmSP0kThSt6TFrUCVAJks9hzTHOOT+x+dV7k0yk4sWmS7q1TAT9g/jjRXgOsBEHzyj8ZRW8gqMw5EuFq12qt3VS/e61u+8mRgSr0LmoCX+S0is4SjL/33djY2Njb/zKwAA//+MAMwjAAgAAAEAAP//7NcJ9loBAAA=
cosign.sigstore.dev/signature: MEUCICLCfb3LGKXcdKV3gTXl6qba3T2goZMbVX/54gyNR05UAiEAlvPuWVsCPuBx5wVqvtyT7hr/AfR9Fl7cNLDACaNIbx8=
labels:
app: nginx
name: test-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.14.2
name: nginx
ports:
- containerPort: 80
`)
var kyverno_decode_x509_certificate_policy = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: test-x509-decode
spec:
validationFailureAction: enforce
rules:
- name: test-x509-decode
match:
any:
- resources:
kinds:
- ConfigMap
validate:
message: "public key modulus mismatch: \"{{ x509_decode('{{request.object.data.cert}}').PublicKey.N }}\" != \"{{ x509_decode('{{base64_decode('{{request.object.data.certB64}}')}}').PublicKey.N }}\""
deny:
conditions:
any:
- key: "{{ x509_decode('{{request.object.data.cert}}').PublicKey.N }}"
operator: NotEquals
value: "{{ x509_decode('{{base64_decode('{{request.object.data.certB64}}')}}').PublicKey.N }}"
`)
var kyverno_decode_x509_certificate_resource_fail = []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: test-configmap
namespace: test-validate
data:
cert: |
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIUWxmj40l+TDVJq98Xy7c6Leo3np8wDQYJKoZIhvcNAQEL
BQAwPTELMAkGA1UEBhMCeHgxCjAIBgNVBAgTAXgxCjAIBgNVBAcTAXgxCjAIBgNV
BAoTAXgxCjAIBgNVBAsTAXgwHhcNMTgwMjAyMTIzODAwWhcNMjMwMjAxMTIzODAw
WjA9MQswCQYDVQQGEwJ4eDEKMAgGA1UECBMBeDEKMAgGA1UEBxMBeDEKMAgGA1UE
ChMBeDEKMAgGA1UECxMBeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
ANHkqOmVf23KMXdaZU2eFUx1h4wb09JINBB8x/HL7UE0KFJcnOoVnNQB0gRukUop
iYCzrzMFyGWWmB/pAEKool+ZiI2uMy6mcYBDtOi4pOm7U0TQQMV6L/5Yfi65xRz3
RTMd/tYAoFi4aCZbJAGjxU6UWNYDzTy8E/cP6ZnlNbVHRiA6/wHsoWcXtWTXYP5y
n9cf7EWQi1hOBM4BWmOIyB1f6LEgQipZWMOMPPHO3hsuSBn0rk7jovSt5XTlbgRr
txqAJiNjJUykWzIF+lLnZCioippGv5vkdGvE83JoACXvZTUwzA+MLu49fkw3bweq
kbhrer8kacjfGlw3aJN37eECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFKXcb52bv6oqnD+D9fTNFHZL8IWxMA0GCSqG
SIb3DQEBCwUAA4IBAQADvKvv3ym0XAYwKxPLLl3Lc6sJYHDbTN0donduG7PXeb1d
huukJ2lfufUYp2IGSAxuLecTYeeByOVp1gaMb5LsIGt2BVDmlMMkiH29LUHsvbyi
85CpJo7A5RJG6AWW2VBCiDjz5v8JFM6pMkBRFfXH+pwIge65CE+MTSQcfb1/aIIo
Q226P7E/3uUGX4k4pDXG/O7GNvykF40v1DB5y7DDBTQ4JWiJfyGkT69TmdOGLFAm
jwxUjWyvEey4qJex/EGEm5RQcMv9iy7tba1wK7sykNGn5uDELGPGIIEAa5rIHm1F
UFOZZVoELaasWS559wy8og39Eq21dDMynb8Bndn/
-----END CERTIFICATE-----
certB64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM3VENDQWRXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFZTVJZd0ZBWURWUVFEREEwcUxtdDUKZG1WeWJtOHVjM1pqTUI0WERUSXlNREV4TVRFek1qWTBNMW9YRFRJek1ERXhNVEUwTWpZME0xb3dHREVXTUJRRwpBMVVFQXd3TktpNXJlWFpsY201dkxuTjJZekNDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DCmdnRUJBTXNBejg1K3lpbm8rTW1kS3NWdEh3Tmkzb0FWanVtelhIaUxmVUpLN3hpNUtVOEI3Z29QSEYvVkNlL1YKN1kyYzRhZnlmZ1kyZVB3NEx4U0RrQ1lOZ1l3cWpTd0dJYmNzcXY1WlJhekJkRHhSMDlyaTZQa25OeUJWR0xpNQpSbFBYSXJHUTNwc051ZjU1cXd4SnhMTzMxcUNadXZrdEtZNVl2dUlSNEpQbUJodVNGWE9ubjBaaVF3OHV4TWNRCjBRQTJseitQeFdDVk5rOXErMzFINURIMW9ZWkRMZlUzbWlqSU9BK0FKR1piQmIrWndCbXBWTDArMlRYTHhFNzQKV293ZEtFVitXVHNLb2pOVGQwVndjdVJLUktSLzZ5blhBQWlzMjF5MVg3VWk5RkpFNm1ESXlsVUQ0MFdYT0tHSgoxbFlZNDFrUm5ZaFZodlhZTjlKdE5ZZFkzSHNDQXdFQUFhTkNNRUF3RGdZRFZSMFBBUUgvQkFRREFnS2tNQThHCkExVWRFd0VCL3dRRk1BTUJBZjh3SFFZRFZSME9CQllFRk9ubEFTVkQ5ZnUzVEFqcHRsVy9nQVhBNHFsK01BMEcKQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNJcHlSaUNoeHA5N2NyS2ZRMjRKdDd6OFArQUdwTGYzc1g0ZUw4N0VTYQo3UVJvVkp0WExtYXV0MXBVRW9ZTFFydUttaC8wWUZ0Wkc5V3hWZ1k2aXVLYldudTdiT2VNQi9JcitWL3lyWDNSCitYdlpPc3VYaUpuRWJKaUJXNmxKekxsZG9XNGYvNzFIK2oxV0Q0dEhwcW1kTXhxL3NMcVhmUEl1YzAvbTB5RkMKbitBREJXR0dCOE5uNjZ2eHR2K2NUNnArUklWb3RYUFFXYk1pbFdwNnBkNXdTdUI2OEZxckR3dFlMTkp0UHdGcwo5TVBWa3VhSmRZWjBlV2Qvck1jS0Q5NEhnZjg5Z3ZBMCtxek1WRmYrM0JlbVhza2pRUll5NkNLc3FveUM2alg0Cm5oWWp1bUFQLzdwc2J6SVRzbnBIdGZDRUVVKzJKWndnTTQwNmFpTWNzZ0xiCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
`)
var kyverno_decode_x509_certificate_resource_pass = []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: test-configmap
namespace: test-validate
data:
cert: |
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIUWxmj40l+TDVJq98Xy7c6Leo3np8wDQYJKoZIhvcNAQEL
BQAwPTELMAkGA1UEBhMCeHgxCjAIBgNVBAgTAXgxCjAIBgNVBAcTAXgxCjAIBgNV
BAoTAXgxCjAIBgNVBAsTAXgwHhcNMTgwMjAyMTIzODAwWhcNMjMwMjAxMTIzODAw
WjA9MQswCQYDVQQGEwJ4eDEKMAgGA1UECBMBeDEKMAgGA1UEBxMBeDEKMAgGA1UE
ChMBeDEKMAgGA1UECxMBeDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
ANHkqOmVf23KMXdaZU2eFUx1h4wb09JINBB8x/HL7UE0KFJcnOoVnNQB0gRukUop
iYCzrzMFyGWWmB/pAEKool+ZiI2uMy6mcYBDtOi4pOm7U0TQQMV6L/5Yfi65xRz3
RTMd/tYAoFi4aCZbJAGjxU6UWNYDzTy8E/cP6ZnlNbVHRiA6/wHsoWcXtWTXYP5y
n9cf7EWQi1hOBM4BWmOIyB1f6LEgQipZWMOMPPHO3hsuSBn0rk7jovSt5XTlbgRr
txqAJiNjJUykWzIF+lLnZCioippGv5vkdGvE83JoACXvZTUwzA+MLu49fkw3bweq
kbhrer8kacjfGlw3aJN37eECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFKXcb52bv6oqnD+D9fTNFHZL8IWxMA0GCSqG
SIb3DQEBCwUAA4IBAQADvKvv3ym0XAYwKxPLLl3Lc6sJYHDbTN0donduG7PXeb1d
huukJ2lfufUYp2IGSAxuLecTYeeByOVp1gaMb5LsIGt2BVDmlMMkiH29LUHsvbyi
85CpJo7A5RJG6AWW2VBCiDjz5v8JFM6pMkBRFfXH+pwIge65CE+MTSQcfb1/aIIo
Q226P7E/3uUGX4k4pDXG/O7GNvykF40v1DB5y7DDBTQ4JWiJfyGkT69TmdOGLFAm
jwxUjWyvEey4qJex/EGEm5RQcMv9iy7tba1wK7sykNGn5uDELGPGIIEAa5rIHm1F
UFOZZVoELaasWS559wy8og39Eq21dDMynb8Bndn/
-----END CERTIFICATE-----
certB64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURTakNDQWpLZ0F3SUJBZ0lVV3htajQwbCtURFZKcTk4WHk3YzZMZW8zbnA4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1BURUxNQWtHQTFVRUJoTUNlSGd4Q2pBSUJnTlZCQWdUQVhneENqQUlCZ05WQkFjVEFYZ3hDakFJQmdOVgpCQW9UQVhneENqQUlCZ05WQkFzVEFYZ3dIaGNOTVRnd01qQXlNVEl6T0RBd1doY05Nak13TWpBeE1USXpPREF3CldqQTlNUXN3Q1FZRFZRUUdFd0o0ZURFS01BZ0dBMVVFQ0JNQmVERUtNQWdHQTFVRUJ4TUJlREVLTUFnR0ExVUUKQ2hNQmVERUtNQWdHQTFVRUN4TUJlRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQgpBTkhrcU9tVmYyM0tNWGRhWlUyZUZVeDFoNHdiMDlKSU5CQjh4L0hMN1VFMEtGSmNuT29Wbk5RQjBnUnVrVW9wCmlZQ3pyek1GeUdXV21CL3BBRUtvb2wrWmlJMnVNeTZtY1lCRHRPaTRwT203VTBUUVFNVjZMLzVZZmk2NXhSejMKUlRNZC90WUFvRmk0YUNaYkpBR2p4VTZVV05ZRHpUeThFL2NQNlpubE5iVkhSaUE2L3dIc29XY1h0V1RYWVA1eQpuOWNmN0VXUWkxaE9CTTRCV21PSXlCMWY2TEVnUWlwWldNT01QUEhPM2hzdVNCbjByazdqb3ZTdDVYVGxiZ1JyCnR4cUFKaU5qSlV5a1d6SUYrbExuWkNpb2lwcEd2NXZrZEd2RTgzSm9BQ1h2WlRVd3pBK01MdTQ5Zmt3M2J3ZXEKa2JocmVyOGthY2pmR2x3M2FKTjM3ZUVDQXdFQUFhTkNNRUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQThHQTFVZApFd0VCL3dRRk1BTUJBZjh3SFFZRFZSME9CQllFRktYY2I1MmJ2Nm9xbkQrRDlmVE5GSFpMOElXeE1BMEdDU3FHClNJYjNEUUVCQ3dVQUE0SUJBUUFEdkt2djN5bTBYQVl3S3hQTExsM0xjNnNKWUhEYlROMGRvbmR1RzdQWGViMWQKaHV1a0oybGZ1ZlVZcDJJR1NBeHVMZWNUWWVlQnlPVnAxZ2FNYjVMc0lHdDJCVkRtbE1Na2lIMjlMVUhzdmJ5aQo4NUNwSm83QTVSSkc2QVdXMlZCQ2lEano1djhKRk02cE1rQlJGZlhIK3B3SWdlNjVDRStNVFNRY2ZiMS9hSUlvClEyMjZQN0UvM3VVR1g0azRwRFhHL083R052eWtGNDB2MURCNXk3RERCVFE0SldpSmZ5R2tUNjlUbWRPR0xGQW0Kand4VWpXeXZFZXk0cUpleC9FR0VtNVJRY012OWl5N3RiYTF3SzdzeWtOR241dURFTEdQR0lJRUFhNXJJSG0xRgpVRk9aWlZvRUxhYXNXUzU1OXd5OG9nMzlFcTIxZERNeW5iOEJuZG4vCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
`)

View file

@ -1,7 +1,6 @@
package validate
import (
"errors"
"fmt"
"os"
"testing"
@ -24,9 +23,6 @@ var (
// ClusterPolicy Namespace
policyNamespace = ""
// Namespace Name
// Hardcoded in YAML Definition
nspace = "test-validate"
crdName = "kustomizations.kustomize.toolkit.fluxcd.io"
)
@ -41,58 +37,32 @@ func Test_Validate_Flux_Sets(t *testing.T) {
e2eClient, err := e2e.NewE2EClient()
Expect(err).To(BeNil())
// Create Flux CRD
err = createKustomizationCRD(e2eClient)
Expect(err).NotTo(HaveOccurred())
// Created CRD is not a guarantee that we already can create new resources
time.Sleep(10 * time.Second)
for _, test := range FluxValidateTests {
By(fmt.Sprintf("Test to validate objects: \"%s\"", test.TestName))
By(fmt.Sprintf("Validate Test: %s", test.TestDescription))
// Clean up Resources
By(string("Cleaning Cluster Policies"))
e2eClient.CleanClusterPolicies(policyGVR)
// Clear Namespace
By(fmt.Sprintf("Deleting Namespace: \"%s\"", nspace))
e2eClient.DeleteClusteredResource(namespaceGVR, nspace)
//CleanUp CRDs
e2eClient.DeleteClusteredResource(crdGVR, crdName)
// Wait Till Deletion of Namespace
e2e.GetWithRetry(time.Duration(1*time.Second), 15, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, nspace)
if err != nil {
return nil
}
return errors.New("Deleting Namespace")
})
// Create Namespace
By(fmt.Sprintf("Creating namespace \"%s\"", nspace))
_, err = e2eClient.CreateClusteredResourceYaml(namespaceGVR, namespaceYaml)
err = deleteClusterPolicy(e2eClient)
Expect(err).NotTo(HaveOccurred())
// Create policy
By(fmt.Sprintf("Creating policy in \"%s\"", policyNamespace))
_, err = e2eClient.CreateNamespacedResourceYaml(policyGVR, policyNamespace, "", test.PolicyRaw)
err = deleteResource(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
// Create Flux CRD
By(fmt.Sprintf("Creating Flux CRD in \"%s\"", nspace))
_, err = e2eClient.CreateClusteredResourceYaml(crdGVR, kyverno_2043_FluxCRD)
err = deleteNamespace(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
// Wait till CRD is created
e2e.GetWithRetry(time.Duration(1*time.Second), 15, func() error {
_, err := e2eClient.GetClusteredResource(crdGVR, crdName)
if err == nil {
return nil
}
return errors.New("Waiting for CRD to be created...")
})
err = createNamespace(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
// Created CRD is not a garantee that we already can create new resources
time.Sleep(3 * time.Second)
err = createPolicy(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
// Create Kustomize resource
kustomizeGVR := e2e.GetGVR("kustomize.toolkit.fluxcd.io", "v1beta1", "kustomizations")
By(fmt.Sprintf("Creating Kustomize resource in \"%s\"", nspace))
_, err = e2eClient.CreateNamespacedResourceYaml(kustomizeGVR, nspace, "", test.ResourceRaw)
err = createResource(e2eClient, test)
if test.MustSucceed {
Expect(err).NotTo(HaveOccurred())
@ -100,25 +70,20 @@ func Test_Validate_Flux_Sets(t *testing.T) {
Expect(err).To(HaveOccurred())
}
//CleanUp Resources
e2eClient.CleanClusterPolicies(policyGVR)
err = deleteClusterPolicy(e2eClient)
Expect(err).NotTo(HaveOccurred())
//CleanUp CRDs
e2eClient.DeleteClusteredResource(crdGVR, crdName)
err = deleteResource(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
// Clear Namespace
e2eClient.DeleteClusteredResource(namespaceGVR, nspace)
// Wait Till Deletion of Namespace
e2e.GetWithRetry(time.Duration(1*time.Second), 15, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, nspace)
if err != nil {
return nil
}
return errors.New("Deleting Namespace")
})
err = deleteNamespace(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Test %s Completed \n\n\n", test.TestName))
By("Test passed successfully:" + test.TestDescription)
}
err = deleteKustomizationCRD(e2eClient)
Expect(err).NotTo(HaveOccurred())
}
func TestValidate(t *testing.T) {
@ -131,55 +96,27 @@ func TestValidate(t *testing.T) {
Expect(err).To(BeNil())
for _, test := range ValidateTests {
By(fmt.Sprintf("Mutation Test: %s", test.TestDescription))
By(fmt.Sprintf("Validate Test: %s", test.TestDescription))
By("Deleting Cluster Policies...")
_ = e2eClient.CleanClusterPolicies(policyGVR)
By("Deleting Resource...")
_ = e2eClient.DeleteNamespacedResource(test.ResourceGVR, test.ResourceNamespace, test.ResourceName)
By("Deleting Namespace...")
By(fmt.Sprintf("Deleting Namespace: %s...", test.ResourceNamespace))
_ = e2eClient.DeleteClusteredResource(namespaceGVR, test.ResourceNamespace)
By("Wait Till Deletion of Namespace...")
err = e2e.GetWithRetry(1*time.Second, 15, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, test.ResourceNamespace)
if err != nil {
return nil
}
return fmt.Errorf("failed to delete namespace: %v", err)
})
err = deleteClusterPolicy(e2eClient)
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Creating Namespace: %s...", policyNamespace))
_, err = e2eClient.CreateClusteredResourceYaml(namespaceGVR, newNamespaceYaml(test.ResourceNamespace))
err = deleteResource(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
By("Wait Till Creation of Namespace...")
err = e2e.GetWithRetry(1*time.Second, 15, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, test.ResourceNamespace)
if err != nil {
return err
}
return nil
})
err = deleteNamespace(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
By("Creating Policy...")
_, err = e2eClient.CreateNamespacedResourceYaml(policyGVR, policyNamespace, test.PolicyName, test.PolicyRaw)
err = createNamespace(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
err = commonE2E.PolicyCreated(test.PolicyName)
err = createPolicy(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
By("Creating Resource...")
_, err = e2eClient.CreateNamespacedResourceYaml(test.ResourceGVR, test.ResourceNamespace, test.PolicyName, test.ResourceRaw)
err = createResource(e2eClient, test)
statusErr, ok := err.(*k8sErrors.StatusError)
validationError := (ok && statusErr.ErrStatus.Code == 400) // Validation error is always Bad Request
validationError := ok && statusErr.ErrStatus.Code == 400 // Validation error is always Bad Request
if test.MustSucceed || !validationError {
Expect(err).NotTo(HaveOccurred())
@ -187,28 +124,111 @@ func TestValidate(t *testing.T) {
Expect(err).To(HaveOccurred())
}
By("Deleting Cluster Policies...")
err = e2eClient.CleanClusterPolicies(policyGVR)
err = deleteClusterPolicy(e2eClient)
Expect(err).NotTo(HaveOccurred())
By("Deleting Resource...") // if it is present, so ignore an error
e2eClient.DeleteNamespacedResource(test.ResourceGVR, test.ResourceNamespace, test.ResourceName)
By("Deleting Namespace...")
err = e2eClient.DeleteClusteredResource(namespaceGVR, test.ResourceNamespace)
err = deleteResource(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
By("Wait Till Creation of Namespace...")
e2e.GetWithRetry(1*time.Second, 15, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, test.ResourceNamespace)
if err != nil {
return nil
}
return fmt.Errorf("failed to delete namespace: %v", err)
})
// Do not fail if waiting fails. Sometimes namespace needs time to be deleted.
err = deleteNamespace(e2eClient, test)
Expect(err).NotTo(HaveOccurred())
By("Done")
}
}
func createNamespace(e2eClient *e2e.E2EClient, test ValidationTest) error {
By(fmt.Sprintf("Creating Namespace: %s...", test.ResourceNamespace))
_, err := e2eClient.CreateClusteredResourceYaml(namespaceGVR, newNamespaceYaml(test.ResourceNamespace))
Expect(err).NotTo(HaveOccurred())
By("Wait Till Creation of Namespace...")
err = e2e.GetWithRetry(1*time.Second, 240, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, test.ResourceNamespace)
if err != nil {
return err
}
return nil
})
return err
}
func createPolicy(e2eClient *e2e.E2EClient, test ValidationTest) error {
By("Creating Policy...")
_, err := e2eClient.CreateNamespacedResourceYaml(policyGVR, policyNamespace, test.PolicyName, test.PolicyRaw)
Expect(err).NotTo(HaveOccurred())
err = commonE2E.PolicyCreated(test.PolicyName)
return err
}
func createResource(e2eClient *e2e.E2EClient, test ValidationTest) error {
By("Creating Resource...")
_, err := e2eClient.CreateNamespacedResourceYaml(test.ResourceGVR, test.ResourceNamespace, test.ResourceName, test.ResourceRaw)
return err
}
func createKustomizationCRD(e2eClient *e2e.E2EClient) error {
By("Creating Flux CRD")
_, err := e2eClient.CreateClusteredResourceYaml(crdGVR, kyverno2043Fluxcrd)
Expect(err).NotTo(HaveOccurred())
// Wait till CRD is created
By("Wait Till Creation of CRD...")
err = e2e.GetWithRetry(1*time.Second, 240, func() error {
_, err := e2eClient.GetClusteredResource(crdGVR, crdName)
if err == nil {
return nil
}
return fmt.Errorf("failed to create CRD: %v", err)
})
return err
}
func deleteClusterPolicy(e2eClient *e2e.E2EClient) error {
By("Deleting Cluster Policies...")
err := e2eClient.CleanClusterPolicies(policyGVR)
return err
}
func deleteResource(e2eClient *e2e.E2EClient, test ValidationTest) error {
By("Deleting Resource...")
err := e2eClient.DeleteNamespacedResource(test.ResourceGVR, test.ResourceNamespace, test.ResourceName)
if k8sErrors.IsNotFound(err) {
return nil
}
return err
}
func deleteNamespace(e2eClient *e2e.E2EClient, test ValidationTest) error {
By("Deleting Namespace...")
By(fmt.Sprintf("Deleting Namespace: %s...", test.ResourceNamespace))
_ = e2eClient.DeleteClusteredResource(namespaceGVR, test.ResourceNamespace)
By("Wait Till Deletion of Namespace...")
err := e2e.GetWithRetry(1*time.Second, 240, func() error {
_, err := e2eClient.GetClusteredResource(namespaceGVR, test.ResourceNamespace)
if err != nil {
return nil
}
return fmt.Errorf("failed to delete namespace: %v", err)
})
return err
}
func deleteKustomizationCRD(e2eClient *e2e.E2EClient) error {
By("Deleting Flux CRD")
_ = e2eClient.DeleteClusteredResource(crdGVR, crdName)
// Wait till CRD is deleted
By("Wait Till Deletion of CRD...")
err := e2e.GetWithRetry(1*time.Second, 240, func() error {
_, err := e2eClient.GetClusteredResource(crdGVR, crdName)
if err != nil {
return nil
}
return fmt.Errorf("failed to delete CRD: %v", err)
})
return err
}