diff --git a/.vscode/launch.json b/.vscode/launch.json
index 1846d6c901..f8c9f34ef3 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,6 +11,17 @@
"--kubeconfig=${userHome}/.kube/config",
"--serverIP=:9443",
],
+ },
+ {
+ "name": "Launch CLI",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/cli/kubectl-kyverno",
+ "args": [
+ "test",
+ "${workspaceFolder}/test/cli/",
+ ],
}
]
}
\ No newline at end of file
diff --git a/api/kyverno/v1/common_types.go b/api/kyverno/v1/common_types.go
index d3bdc6136b..be59059ca5 100644
--- a/api/kyverno/v1/common_types.go
+++ b/api/kyverno/v1/common_types.go
@@ -283,6 +283,10 @@ type ForEachMutation struct {
// See https://tools.ietf.org/html/rfc6902 and https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/.
// +optional
PatchesJSON6902 string `json:"patchesJson6902,omitempty" yaml:"patchesJson6902,omitempty"`
+
+ // Foreach declares a nested foreach iterator
+ // +optional
+ ForEachMutation *apiextv1.JSON `json:"foreach,omitempty" yaml:"foreach,omitempty"`
}
func (m *ForEachMutation) GetPatchStrategicMerge() apiextensions.JSON {
@@ -398,6 +402,14 @@ func (v *Validation) SetAnyPattern(in apiextensions.JSON) {
v.RawAnyPattern = ToJSON(in)
}
+func (v *Validation) GetForeach() apiextensions.JSON {
+ return FromJSON(v.RawPattern)
+}
+
+func (v *Validation) SetForeach(in apiextensions.JSON) {
+ v.RawPattern = ToJSON(in)
+}
+
// Deny specifies a list of conditions used to pass or fail a validation rule.
type Deny struct {
// Multiple conditions can be declared under an `any` or `all` statement. A direct list
@@ -450,6 +462,10 @@ type ForEachValidation struct {
// Deny defines conditions used to pass or fail a validation rule.
// +optional
Deny *Deny `json:"deny,omitempty" yaml:"deny,omitempty"`
+
+ // Foreach declares a nested foreach iterator
+ // +optional
+ ForEachValidation *apiextv1.JSON `json:"foreach,omitempty" yaml:"foreach,omitempty"`
}
func (v *ForEachValidation) GetPattern() apiextensions.JSON {
diff --git a/api/kyverno/v1/zz_generated.deepcopy.go b/api/kyverno/v1/zz_generated.deepcopy.go
index 351f8b21e7..267dd93e1a 100755
--- a/api/kyverno/v1/zz_generated.deepcopy.go
+++ b/api/kyverno/v1/zz_generated.deepcopy.go
@@ -472,6 +472,11 @@ func (in *ForEachMutation) DeepCopyInto(out *ForEachMutation) {
*out = new(apiextensionsv1.JSON)
(*in).DeepCopyInto(*out)
}
+ if in.ForEachMutation != nil {
+ in, out := &in.ForEachMutation, &out.ForEachMutation
+ *out = new(apiextensionsv1.JSON)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForEachMutation.
@@ -519,6 +524,11 @@ func (in *ForEachValidation) DeepCopyInto(out *ForEachValidation) {
*out = new(Deny)
(*in).DeepCopyInto(*out)
}
+ if in.ForEachValidation != nil {
+ in, out := &in.ForEachValidation, &out.ForEachValidation
+ *out = new(apiextensionsv1.JSON)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForEachValidation.
diff --git a/charts/kyverno/templates/crds.yaml b/charts/kyverno/templates/crds.yaml
index dc36f8dbc3..dcc809dfa5 100644
--- a/charts/kyverno/templates/crds.yaml
+++ b/charts/kyverno/templates/crds.yaml
@@ -3489,6 +3489,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -3693,6 +3696,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -5377,6 +5383,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -5581,6 +5590,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -7132,6 +7144,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -7458,6 +7473,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -9117,6 +9135,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -9321,6 +9342,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -11596,6 +11620,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -11800,6 +11827,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -13484,6 +13514,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -13688,6 +13721,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -15239,6 +15275,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -15565,6 +15604,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -17224,6 +17266,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
@@ -17428,6 +17473,9 @@ spec:
elementScope:
description: ElementScope specifies whether to use the current list element as the scope for validation. Defaults to "true" if not specified. When set to "false", "request.object" is used as the validation scope within the foreach block to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string
diff --git a/cmd/cli/kubectl-kyverno/apply/apply_command_test.go b/cmd/cli/kubectl-kyverno/apply/apply_command_test.go
index a3050818d9..488e901a39 100644
--- a/cmd/cli/kubectl-kyverno/apply/apply_command_test.go
+++ b/cmd/cli/kubectl-kyverno/apply/apply_command_test.go
@@ -82,7 +82,7 @@ func Test_Apply(t *testing.T) {
expectedPolicyReports: []preport.PolicyReport{
{
Summary: preport.PolicyReportSummary{
- Pass: 9,
+ Pass: 6,
Fail: 0,
Skip: 0,
Error: 0,
diff --git a/config/crds/kyverno.io_clusterpolicies.yaml b/config/crds/kyverno.io_clusterpolicies.yaml
index d63ed3a431..c7dfb10fc7 100644
--- a/config/crds/kyverno.io_clusterpolicies.yaml
+++ b/config/crds/kyverno.io_clusterpolicies.yaml
@@ -1729,6 +1729,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -2042,6 +2045,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -4815,6 +4821,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -5140,6 +5150,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -7631,6 +7645,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -8112,6 +8129,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -10845,6 +10865,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -11170,6 +11194,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
diff --git a/config/crds/kyverno.io_policies.yaml b/config/crds/kyverno.io_policies.yaml
index 0c56a36924..882461f737 100644
--- a/config/crds/kyverno.io_policies.yaml
+++ b/config/crds/kyverno.io_policies.yaml
@@ -1731,6 +1731,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -2044,6 +2047,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -4818,6 +4824,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -5143,6 +5153,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -7635,6 +7649,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -8116,6 +8133,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -10849,6 +10869,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -11174,6 +11198,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
diff --git a/config/install.yaml b/config/install.yaml
index 7cbcf8e2f3..2424ddbe78 100644
--- a/config/install.yaml
+++ b/config/install.yaml
@@ -5155,6 +5155,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -5468,6 +5471,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -8241,6 +8247,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -8566,6 +8576,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -11057,6 +11071,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -11538,6 +11555,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -14271,6 +14291,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -14596,6 +14620,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -18072,6 +18100,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -18385,6 +18416,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -21159,6 +21193,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -21484,6 +21522,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -23976,6 +24018,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -24457,6 +24502,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -27190,6 +27238,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -27515,6 +27567,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
diff --git a/config/install_debug.yaml b/config/install_debug.yaml
index e244ecc27c..6f8f47020d 100644
--- a/config/install_debug.yaml
+++ b/config/install_debug.yaml
@@ -5147,6 +5147,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -5460,6 +5463,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -8233,6 +8239,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -8558,6 +8568,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -11049,6 +11063,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -11530,6 +11547,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -14263,6 +14283,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -14588,6 +14612,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -18061,6 +18089,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -18374,6 +18405,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -21148,6 +21182,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -21473,6 +21511,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -23965,6 +24007,9 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -24446,6 +24491,9 @@ spec:
scope within the foreach block to allow referencing
other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which the
@@ -27179,6 +27227,10 @@ spec:
type: object
type: object
type: array
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
@@ -27504,6 +27556,10 @@ spec:
as the validation scope within the foreach block
to allow referencing other elements in the subtree.
type: boolean
+ foreach:
+ description: Foreach declares a nested foreach
+ iterator
+ x-kubernetes-preserve-unknown-fields: true
list:
description: List specifies a JMESPath expression
that results in one or more elements to which
diff --git a/docs/user/crd/index.html b/docs/user/crd/index.html
index 34ec4243ff..5b71d1bd2c 100644
--- a/docs/user/crd/index.html
+++ b/docs/user/crd/index.html
@@ -1533,6 +1533,20 @@ string
See https://tools.ietf.org/html/rfc6902 and https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/.
+
+
+foreach
+
+
+Kubernetes apiextensions/v1.JSON
+
+
+ |
+
+(Optional)
+ Foreach declares a nested foreach iterator
+ |
+
@@ -1653,6 +1667,20 @@ Deny
Deny defines conditions used to pass or fail a validation rule.
+
+
+foreach
+
+
+Kubernetes apiextensions/v1.JSON
+
+
+ |
+
+(Optional)
+ Foreach declares a nested foreach iterator
+ |
+
diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go
index 338b1adcfb..aa582cce9b 100644
--- a/pkg/client/clientset/versioned/fake/register.go
+++ b/pkg/client/clientset/versioned/fake/register.go
@@ -45,14 +45,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
-// import (
-// "k8s.io/client-go/kubernetes"
-// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-// )
+// import (
+// "k8s.io/client-go/kubernetes"
+// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+// )
//
-// kclientset, _ := kubernetes.NewForConfig(c)
-// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+// kclientset, _ := kubernetes.NewForConfig(c)
+// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go
index 98e833e72a..93fb9480eb 100644
--- a/pkg/client/clientset/versioned/scheme/register.go
+++ b/pkg/client/clientset/versioned/scheme/register.go
@@ -45,14 +45,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
-// import (
-// "k8s.io/client-go/kubernetes"
-// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
-// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
-// )
+// import (
+// "k8s.io/client-go/kubernetes"
+// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+// )
//
-// kclientset, _ := kubernetes.NewForConfig(c)
-// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+// kclientset, _ := kubernetes.NewForConfig(c)
+// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
diff --git a/pkg/engine/attestation_test.go b/pkg/engine/attestation_test.go
index 0da75b2a5e..2c716d48f2 100644
--- a/pkg/engine/attestation_test.go
+++ b/pkg/engine/attestation_test.go
@@ -227,7 +227,6 @@ var scanPredicate = `
`
func Test_Conditions(t *testing.T) {
-
conditions := []v1.AnyAllConditions{
{
AnyConditions: []v1.Condition{
diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go
index 90d2bac72f..fbf5db210c 100644
--- a/pkg/engine/context/context.go
+++ b/pkg/engine/context/context.go
@@ -2,6 +2,7 @@ package context
import (
"encoding/json"
+ "fmt"
"strings"
"sync"
@@ -65,7 +66,7 @@ type Interface interface {
AddNamespace(namespace string) error
// AddElement adds element info to the context
- AddElement(data interface{}, index int) error
+ AddElement(data interface{}, index, nesting int) error
// AddImageInfo adds image info to the context
AddImageInfo(info apiutils.ImageInfo) error
@@ -239,10 +240,14 @@ func (ctx *context) AddNamespace(namespace string) error {
return addToContext(ctx, namespace, "request", "namespace")
}
-func (ctx *context) AddElement(data interface{}, index int) error {
+func (ctx *context) AddElement(data interface{}, index, nesting int) error {
+ nestedElement := fmt.Sprintf("element%d", nesting)
+ nestedElementIndex := fmt.Sprintf("elementIndex%d", nesting)
data = map[string]interface{}{
- "element": data,
- "elementIndex": index,
+ "element": data,
+ nestedElement: data,
+ "elementIndex": index,
+ nestedElementIndex: index,
}
return addToContext(ctx, data)
}
diff --git a/pkg/engine/context/context_test.go b/pkg/engine/context/context_test.go
index d4c3ca0715..f098cf5068 100644
--- a/pkg/engine/context/context_test.go
+++ b/pkg/engine/context/context_test.go
@@ -51,7 +51,8 @@ func Test_addResourceAndUserContext(t *testing.T) {
userRequestInfo := urkyverno.RequestInfo{
Roles: nil,
ClusterRoles: nil,
- AdmissionUserInfo: userInfo}
+ AdmissionUserInfo: userInfo,
+ }
var expectedResult string
ctx := NewContext()
diff --git a/pkg/engine/forceMutate.go b/pkg/engine/forceMutate.go
index a8f8f1019d..61522cd325 100644
--- a/pkg/engine/forceMutate.go
+++ b/pkg/engine/forceMutate.go
@@ -3,12 +3,16 @@ package engine
import (
"fmt"
+ "github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/mutate"
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/logging"
+ "github.com/kyverno/kyverno/pkg/utils/api"
+ "github.com/pkg/errors"
+ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@@ -34,30 +38,53 @@ func ForceMutate(ctx context.Interface, policy kyvernov1.PolicyInterface, resour
}
if r.Mutation.ForEachMutation != nil {
- for i, foreach := range r.Mutation.ForEachMutation {
- patcher := mutate.NewPatcher(r.Name, foreach.GetPatchStrategicMerge(), foreach.PatchesJSON6902, patchedResource, ctx, logger)
- resp, mutatedResource := patcher.Patch()
- if resp.Status != response.RuleStatusPass {
- return patchedResource, fmt.Errorf("foreach mutate result %q at index %d: %s", resp.Status.String(), i, resp.Message)
- }
-
- patchedResource = mutatedResource
+ patchedResource, err = applyForeachMutate(r.Name, r.Mutation.ForEachMutation, patchedResource, ctx, logger)
+ if err != nil {
+ return patchedResource, err
}
} else {
m := r.Mutation
- patcher := mutate.NewPatcher(r.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, patchedResource, ctx, logger)
- resp, mutatedResource := patcher.Patch()
- if resp.Status != response.RuleStatusPass {
- return patchedResource, fmt.Errorf("mutate result %q: %s", resp.Status.String(), resp.Message)
+ patchedResource, err = applyPatches(r.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, patchedResource, ctx, logger)
+ if err != nil {
+ return patchedResource, err
}
-
- patchedResource = mutatedResource
}
}
return patchedResource, nil
}
+func applyForeachMutate(name string, foreach []kyvernov1.ForEachMutation, resource unstructured.Unstructured, ctx context.Interface, logger logr.Logger) (patchedResource unstructured.Unstructured, err error) {
+ patchedResource = resource
+ for _, fe := range foreach {
+ if fe.ForEachMutation != nil {
+ nestedForeach, err := api.DeserializeJSONArray[kyvernov1.ForEachMutation](fe.ForEachMutation)
+ if err != nil {
+ return patchedResource, errors.Wrapf(err, "failed to deserialize foreach")
+ }
+
+ return applyForeachMutate(name, nestedForeach, patchedResource, ctx, logger)
+ }
+
+ patchedResource, err = applyPatches(name, fe.GetPatchStrategicMerge(), fe.PatchesJSON6902, patchedResource, ctx, logger)
+ if err != nil {
+ return resource, err
+ }
+ }
+
+ return patchedResource, nil
+}
+
+func applyPatches(name string, mergePatch apiextensions.JSON, jsonPatch string, resource unstructured.Unstructured, ctx context.Interface, logger logr.Logger) (unstructured.Unstructured, error) {
+ patcher := mutate.NewPatcher(name, mergePatch, jsonPatch, resource, ctx, logger)
+ resp, mutatedResource := patcher.Patch()
+ if resp.Status != response.RuleStatusPass {
+ return mutatedResource, fmt.Errorf("mutate status %q: %s", resp.Status.String(), resp.Message)
+ }
+
+ return mutatedResource, nil
+}
+
// removeConditions mutates the rule to remove AnyAllConditions
func removeConditions(rule *kyvernov1.Rule) {
if rule.GetAnyAllConditions() != nil {
diff --git a/pkg/engine/imageVerify_test.go b/pkg/engine/imageVerify_test.go
index 183c553d75..b1f4823e78 100644
--- a/pkg/engine/imageVerify_test.go
+++ b/pkg/engine/imageVerify_test.go
@@ -171,7 +171,6 @@ func Test_CosignMockAttest_fail(t *testing.T) {
}
func buildContext(t *testing.T, policy, resource string, oldResource string) *PolicyContext {
-
var cpol kyverno.ClusterPolicy
err := json.Unmarshal([]byte(policy), &cpol)
assert.NilError(t, err)
@@ -407,8 +406,10 @@ var testConfigMapMissingResource = `{
}
}`
-var testVerifyImageKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nXRh950IZbRj8Ra/N9sbqOPZrfM5/KAQN0/KjHcorm/J5yctVd7iEcnessRQjU917hmKO6JWVGHpDguIyakZA==\n-----END PUBLIC KEY-----\n`
-var testOtherKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpNlOGZ323zMlhs4bcKSpAKQvbcWi5ZLRmijm6SqXDy0Fp0z0Eal+BekFnLzs8rUXUaXlhZ3hNudlgFJH+nFNMw==\n-----END PUBLIC KEY-----\n`
+var (
+ testVerifyImageKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nXRh950IZbRj8Ra/N9sbqOPZrfM5/KAQN0/KjHcorm/J5yctVd7iEcnessRQjU917hmKO6JWVGHpDguIyakZA==\n-----END PUBLIC KEY-----\n`
+ testOtherKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpNlOGZ323zMlhs4bcKSpAKQvbcWi5ZLRmijm6SqXDy0Fp0z0Eal+BekFnLzs8rUXUaXlhZ3hNudlgFJH+nFNMw==\n-----END PUBLIC KEY-----\n`
+)
func Test_ConfigMapMissingSuccess(t *testing.T) {
policyContext := buildContext(t, testConfigMapMissing, testConfigMapMissingResource, "")
@@ -744,7 +745,7 @@ func Test_MarkImageVerified(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, len(patches), 2)
- resource := applyPatches(t, patches)
+ resource := testApplyPatches(t, patches)
patchedAnnotations := resource.GetAnnotations()
assert.Equal(t, len(patchedAnnotations), 1)
@@ -756,7 +757,7 @@ func Test_MarkImageVerified(t *testing.T) {
assert.Equal(t, verified, true)
}
-func applyPatches(t *testing.T, patches [][]byte) unstructured.Unstructured {
+func testApplyPatches(t *testing.T, patches [][]byte) unstructured.Unstructured {
patchedResource, err := utils.ApplyPatches([]byte(testResource), patches)
assert.NilError(t, err)
assert.Assert(t, patchedResource != nil)
diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go
index f92159ff88..2c70977bf9 100644
--- a/pkg/engine/jmespath/functions_test.go
+++ b/pkg/engine/jmespath/functions_test.go
@@ -1382,7 +1382,6 @@ func Test_Items(t *testing.T) {
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
-
query, err := New("items(`" + tc.object + "`,`" + tc.keyName + "`,`" + tc.valName + "`)")
assert.NilError(t, err)
diff --git a/pkg/engine/mutate/mutation.go b/pkg/engine/mutate/mutation.go
index 761b53cca1..0b2ff077ff 100644
--- a/pkg/engine/mutate/mutation.go
+++ b/pkg/engine/mutate/mutation.go
@@ -22,11 +22,11 @@ type Response struct {
Message string
}
-func newErrorResponse(msg string, err error) *Response {
- return newResponse(response.RuleStatusError, unstructured.Unstructured{}, nil, fmt.Sprintf("%s: %v", msg, err))
+func NewErrorResponse(msg string, err error) *Response {
+ return NewResponse(response.RuleStatusError, unstructured.Unstructured{}, nil, fmt.Sprintf("%s: %v", msg, err))
}
-func newResponse(status response.RuleStatus, resource unstructured.Unstructured, patches [][]byte, msg string) *Response {
+func NewResponse(status response.RuleStatus, resource unstructured.Unstructured, patches [][]byte, msg string) *Response {
return &Response{
Status: status,
PatchedResource: resource,
@@ -38,56 +38,56 @@ func newResponse(status response.RuleStatus, resource unstructured.Unstructured,
func Mutate(rule *kyvernov1.Rule, ctx context.Interface, resource unstructured.Unstructured, logger logr.Logger) *Response {
updatedRule, err := variables.SubstituteAllInRule(logger, ctx, *rule)
if err != nil {
- return newErrorResponse("variable substitution failed", err)
+ return NewErrorResponse("variable substitution failed", err)
}
m := updatedRule.Mutation
patcher := NewPatcher(updatedRule.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, resource, ctx, logger)
if patcher == nil {
- return newResponse(response.RuleStatusError, resource, nil, "empty mutate rule")
+ return NewResponse(response.RuleStatusError, resource, nil, "empty mutate rule")
}
resp, patchedResource := patcher.Patch()
if resp.Status != response.RuleStatusPass {
- return newResponse(resp.Status, resource, nil, resp.Message)
+ return NewResponse(resp.Status, resource, nil, resp.Message)
}
if resp.Patches == nil {
- return newResponse(response.RuleStatusSkip, resource, nil, "no patches applied")
+ return NewResponse(response.RuleStatusSkip, resource, nil, "no patches applied")
}
if err := ctx.AddResource(patchedResource.Object); err != nil {
- return newErrorResponse("failed to update patched resource in the JSON context", err)
+ return NewErrorResponse("failed to update patched resource in the JSON context", err)
}
- return newResponse(response.RuleStatusPass, patchedResource, resp.Patches, resp.Message)
+ return NewResponse(response.RuleStatusPass, patchedResource, resp.Patches, resp.Message)
}
func ForEach(name string, foreach kyvernov1.ForEachMutation, ctx context.Interface, resource unstructured.Unstructured, logger logr.Logger) *Response {
fe, err := substituteAllInForEach(foreach, ctx, logger)
if err != nil {
- return newErrorResponse("variable substitution failed", err)
+ return NewErrorResponse("variable substitution failed", err)
}
patcher := NewPatcher(name, fe.GetPatchStrategicMerge(), fe.PatchesJSON6902, resource, ctx, logger)
if patcher == nil {
- return newResponse(response.RuleStatusError, unstructured.Unstructured{}, nil, "no patches found")
+ return NewResponse(response.RuleStatusError, unstructured.Unstructured{}, nil, "no patches found")
}
resp, patchedResource := patcher.Patch()
if resp.Status != response.RuleStatusPass {
- return newResponse(resp.Status, unstructured.Unstructured{}, nil, resp.Message)
+ return NewResponse(resp.Status, unstructured.Unstructured{}, nil, resp.Message)
}
if resp.Patches == nil {
- return newResponse(response.RuleStatusSkip, unstructured.Unstructured{}, nil, "no patches applied")
+ return NewResponse(response.RuleStatusSkip, unstructured.Unstructured{}, nil, "no patches applied")
}
if err := ctx.AddResource(patchedResource.Object); err != nil {
- return newErrorResponse("failed to update patched resource in the JSON context", err)
+ return NewErrorResponse("failed to update patched resource in the JSON context", err)
}
- return newResponse(response.RuleStatusPass, patchedResource, resp.Patches, resp.Message)
+ return NewResponse(response.RuleStatusPass, patchedResource, resp.Patches, resp.Message)
}
func substituteAllInForEach(fe kyvernov1.ForEachMutation, ctx context.Interface, logger logr.Logger) (*kyvernov1.ForEachMutation, error) {
diff --git a/pkg/engine/mutate/mutation_test.go b/pkg/engine/mutate/mutation_test.go
index ff55835325..12dba5dd90 100644
--- a/pkg/engine/mutate/mutation_test.go
+++ b/pkg/engine/mutate/mutation_test.go
@@ -68,7 +68,7 @@ func applyPatches(rule *types.Rule, resource unstructured.Unstructured) (*respon
}
func TestProcessPatches_EmptyPatches(t *testing.T) {
- var emptyRule = &types.Rule{Name: "emptyRule"}
+ emptyRule := &types.Rule{Name: "emptyRule"}
resourceUnstructured, err := utils.ConvertToUnstructured([]byte(endpointsDocument))
if err != nil {
t.Error(err)
diff --git a/pkg/engine/mutate/patch/patchJSON6902_test.go b/pkg/engine/mutate/patch/patchJSON6902_test.go
index c8fb4f563b..a701a27012 100644
--- a/pkg/engine/mutate/patch/patchJSON6902_test.go
+++ b/pkg/engine/mutate/patch/patchJSON6902_test.go
@@ -182,7 +182,6 @@ spec:
for _, testCase := range testCases {
t.Run(testCase.testName, func(t *testing.T) {
-
expectedBytes, err := yaml.YAMLToJSON(testCase.expected)
assert.Nil(t, err)
diff --git a/pkg/engine/mutate/patch/patchesUtils_test.go b/pkg/engine/mutate/patch/patchesUtils_test.go
index d03ff6a96e..7ab5963b06 100644
--- a/pkg/engine/mutate/patch/patchesUtils_test.go
+++ b/pkg/engine/mutate/patch/patchesUtils_test.go
@@ -11,7 +11,6 @@ import (
)
func Test_GeneratePatches(t *testing.T) {
-
out, err := strategicMergePatch(logging.GlobalLogger(), string(baseBytes), string(overlayBytes))
assert.NilError(t, err)
@@ -225,7 +224,6 @@ func Test_GeneratePatches_sortRemovalPatches(t *testing.T) {
fmt.Println(patches)
assertnew.Nil(t, err)
assertnew.Equal(t, expectedPatches, patches)
-
}
func Test_sortRemovalPatches(t *testing.T) {
diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go
index 144c9271bb..97626f31b4 100644
--- a/pkg/engine/mutation.go
+++ b/pkg/engine/mutation.go
@@ -15,6 +15,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/registryclient"
+ "github.com/kyverno/kyverno/pkg/utils/api"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@@ -99,7 +100,9 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
parentResourceGVR = policyContext.requestResource
}
patchedResources = append(patchedResources, resourceInfo{
- unstructured: matchedResource, subresource: policyContext.subresource, parentResourceGVR: parentResourceGVR,
+ unstructured: matchedResource,
+ subresource: policyContext.subresource,
+ parentResourceGVR: parentResourceGVR,
})
}
@@ -117,18 +120,29 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
}
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
+ var mutateResp *mutate.Response
if rule.Mutation.ForEachMutation != nil {
- ruleResp, patchedResource.unstructured = mutateForEach(ctx, rclient, ruleCopy, policyContext, patchedResource.unstructured, patchedResource.subresource, patchedResource.parentResourceGVR, logger)
+ m := &forEachMutator{
+ rule: ruleCopy,
+ foreach: rule.Mutation.ForEachMutation,
+ policyContext: policyContext,
+ resource: patchedResource,
+ log: logger,
+ rclient: rclient,
+ nesting: 0,
+ }
+
+ mutateResp = m.mutateForEach(ctx)
} else {
- ruleResp, patchedResource.unstructured = mutateResource(ruleCopy, policyContext, patchedResource.unstructured, patchedResource.subresource, patchedResource.parentResourceGVR, logger)
+ mutateResp = mutateResource(ruleCopy, policyContext, patchedResource.unstructured, logger)
}
- matchedResource = patchedResource.unstructured
+ matchedResource = mutateResp.PatchedResource
+ ruleResponse := buildRuleResponse(ruleCopy, mutateResp, patchedResource)
- if ruleResp != nil {
- resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
- if ruleResp.Status == response.RuleStatusError {
+ if ruleResponse != nil {
+ resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResponse)
+ if ruleResponse.Status == response.RuleStatusError {
incrementErrorCount(resp)
} else {
incrementAppliedCount(resp)
@@ -154,81 +168,81 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
return resp
}
-func mutateResource(rule *kyvernov1.Rule, ctx *PolicyContext, resource unstructured.Unstructured, subresourceName string, parentResourceGVR metav1.GroupVersionResource, logger logr.Logger) (*response.RuleResponse, unstructured.Unstructured) {
+func mutateResource(rule *kyvernov1.Rule, ctx *PolicyContext, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response {
preconditionsPassed, err := checkPreconditions(logger, ctx, rule.GetAnyAllConditions())
if err != nil {
- return ruleError(rule, response.Mutation, "failed to evaluate preconditions", err), resource
+ return mutate.NewErrorResponse("failed to evaluate preconditions", err)
}
if !preconditionsPassed {
- return ruleResponseWithPatchedTarget(*rule, response.Mutation, "preconditions not met", response.RuleStatusSkip, &resource, subresourceName, parentResourceGVR), resource
+ return mutate.NewResponse(response.RuleStatusSkip, resource, nil, "preconditions not met")
}
- mutateResp := mutate.Mutate(rule, ctx.jsonContext, resource, logger)
- ruleResp := buildRuleResponse(rule, mutateResp, &mutateResp.PatchedResource, subresourceName, parentResourceGVR)
- return ruleResp, mutateResp.PatchedResource
+ return mutate.Mutate(rule, ctx.JSONContext(), resource, logger)
}
-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
- }
+type forEachMutator struct {
+ rule *kyvernov1.Rule
+ policyContext *PolicyContext
+ foreach []kyvernov1.ForEachMutation
+ resource resourceInfo
+ nesting int
+ rclient registryclient.Client
+ log logr.Logger
+}
- patchedResource := resource
+func (f *forEachMutator) mutateForEach(ctx context.Context) *mutate.Response {
var applyCount int
allPatches := make([][]byte, 0)
- for _, foreach := range foreachList {
- if err := LoadContext(ctx, logger, rclient, rule.Context, enginectx, rule.Name); err != nil {
- logger.Error(err, "failed to load context")
- return ruleError(rule, response.Mutation, "failed to load context", err), resource
+ for _, foreach := range f.foreach {
+ if err := LoadContext(ctx, f.log, f.rclient, f.rule.Context, f.policyContext, f.rule.Name); err != nil {
+ f.log.Error(err, "failed to load context")
+ return mutate.NewErrorResponse("failed to load context", err)
}
- preconditionsPassed, err := checkPreconditions(logger, enginectx, rule.GetAnyAllConditions())
+ preconditionsPassed, err := checkPreconditions(f.log, f.policyContext, f.rule.GetAnyAllConditions())
if err != nil {
- return ruleError(rule, response.Mutation, "failed to evaluate preconditions", err), resource
+ return mutate.NewErrorResponse("failed to evaluate preconditions", err)
}
if !preconditionsPassed {
- return ruleResponseWithPatchedTarget(*rule, response.Mutation, "preconditions not met", response.RuleStatusSkip, &patchedResource, subresourceName, parentResourceGVR), resource
+ return mutate.NewResponse(response.RuleStatusSkip, f.resource.unstructured, nil, "preconditions not met")
}
- elements, err := evaluateList(foreach.List, enginectx.jsonContext)
+ elements, err := evaluateList(foreach.List, f.policyContext.JSONContext())
if err != nil {
- msg := fmt.Sprintf("failed to evaluate list %s", foreach.List)
- return ruleError(rule, response.Mutation, msg, err), resource
+ msg := fmt.Sprintf("failed to evaluate list %s: %v", foreach.List, err)
+ return mutate.NewErrorResponse(msg, err)
}
- mutateResp := mutateElements(ctx, rclient, rule.Name, foreach, enginectx, elements, patchedResource, logger)
+ mutateResp := f.mutateElements(ctx, foreach, elements)
if mutateResp.Status == response.RuleStatusError {
- logger.Error(err, "failed to mutate elements")
- return buildRuleResponse(rule, mutateResp, nil, "", metav1.GroupVersionResource{}), resource
+ return mutate.NewErrorResponse("failed to mutate elements", err)
}
if mutateResp.Status != response.RuleStatusSkip {
applyCount++
if len(mutateResp.Patches) > 0 {
- patchedResource = mutateResp.PatchedResource
+ f.resource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...)
}
}
}
+ msg := fmt.Sprintf("%d elements processed", applyCount)
if applyCount == 0 {
- return ruleResponseWithPatchedTarget(*rule, response.Mutation, "0 elements processed", response.RuleStatusSkip, &resource, subresourceName, parentResourceGVR), resource
+ return mutate.NewResponse(response.RuleStatusSkip, f.resource.unstructured, allPatches, msg)
}
- r := ruleResponseWithPatchedTarget(*rule, response.Mutation, fmt.Sprintf("%d elements processed", applyCount), response.RuleStatusPass, &patchedResource, subresourceName, parentResourceGVR)
- r.Patches = allPatches
- return r, patchedResource
+ return mutate.NewResponse(response.RuleStatusPass, f.resource.unstructured, allPatches, msg)
}
-func mutateElements(ctx context.Context, rclient registryclient.Client, name string, foreach kyvernov1.ForEachMutation, enginectx *PolicyContext, elements []interface{}, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response {
- enginectx.jsonContext.Checkpoint()
- defer enginectx.jsonContext.Restore()
+func (f *forEachMutator) mutateElements(ctx context.Context, foreach kyvernov1.ForEachMutation, elements []interface{}) *mutate.Response {
+ f.policyContext.JSONContext().Checkpoint()
+ defer f.policyContext.JSONContext().Restore()
- patchedResource := resource
+ patchedResource := f.resource
var allPatches [][]byte
if foreach.RawPatchStrategicMerge != nil {
invertedElement(elements)
@@ -238,63 +252,79 @@ func mutateElements(ctx context.Context, rclient registryclient.Client, name str
if e == nil {
continue
}
- enginectx.jsonContext.Reset()
- enginectx := enginectx.Copy()
+
+ f.policyContext.JSONContext().Reset()
+ policyContext := f.policyContext.Copy()
+
+ // TODO - this needs to be refactored. The engine should not have a dependency to the CLI code
store.SetForeachElement(i)
+
falseVar := false
- if err := addElementToContext(enginectx, e, i, &falseVar); err != nil {
- return mutateError(err, fmt.Sprintf("failed to add element to mutate.foreach[%d].context", i))
+ if err := addElementToContext(policyContext, e, i, f.nesting, &falseVar); err != nil {
+ return mutate.NewErrorResponse(fmt.Sprintf("failed to add element to mutate.foreach[%d].context", i), err)
}
- if err := LoadContext(ctx, logger, rclient, foreach.Context, enginectx, name); err != nil {
- return mutateError(err, fmt.Sprintf("failed to load to mutate.foreach[%d].context", i))
+ if err := LoadContext(ctx, f.log, f.rclient, foreach.Context, policyContext, f.rule.Name); err != nil {
+ return mutate.NewErrorResponse(fmt.Sprintf("failed to load to mutate.foreach[%d].context", i), err)
}
- preconditionsPassed, err := checkPreconditions(logger, enginectx, foreach.AnyAllConditions)
+ preconditionsPassed, err := checkPreconditions(f.log, policyContext, foreach.AnyAllConditions)
if err != nil {
- return mutateError(err, fmt.Sprintf("failed to evaluate mutate.foreach[%d].preconditions", i))
+ return mutate.NewErrorResponse(fmt.Sprintf("failed to evaluate mutate.foreach[%d].preconditions", i), err)
}
if !preconditionsPassed {
- logger.Info("mutate.foreach.preconditions not met", "elementIndex", i)
+ f.log.Info("mutate.foreach.preconditions not met", "elementIndex", i)
continue
}
- mutateResp := mutate.ForEach(name, foreach, enginectx.jsonContext, patchedResource, logger)
+ var mutateResp *mutate.Response
+ if foreach.ForEachMutation != nil {
+ nestedForeach, err := api.DeserializeJSONArray[kyvernov1.ForEachMutation](foreach.ForEachMutation)
+ if err != nil {
+ return mutate.NewErrorResponse("failed to deserialize foreach", err)
+ }
+
+ m := &forEachMutator{
+ rule: f.rule,
+ policyContext: f.policyContext,
+ resource: patchedResource,
+ log: f.log,
+ foreach: nestedForeach,
+ nesting: f.nesting + 1,
+ }
+
+ mutateResp = m.mutateForEach(ctx)
+ } else {
+ mutateResp = mutate.ForEach(f.rule.Name, foreach, policyContext.JSONContext(), patchedResource.unstructured, f.log)
+ }
+
if mutateResp.Status == response.RuleStatusFail || mutateResp.Status == response.RuleStatusError {
return mutateResp
}
if len(mutateResp.Patches) > 0 {
- patchedResource = mutateResp.PatchedResource
+ patchedResource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...)
}
}
- return &mutate.Response{
- Status: response.RuleStatusPass,
- PatchedResource: patchedResource,
- Patches: allPatches,
- Message: "foreach mutation applied",
- }
+ return mutate.NewResponse(response.RuleStatusPass, patchedResource.unstructured, allPatches, "")
}
-func mutateError(err error, message string) *mutate.Response {
- return &mutate.Response{
- Status: response.RuleStatusFail,
- PatchedResource: unstructured.Unstructured{},
- Patches: nil,
- Message: fmt.Sprintf("failed to add element to context: %v", err),
- }
-}
-
-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)
+func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, info resourceInfo) *response.RuleResponse {
+ resp := ruleResponse(*rule, response.Mutation, mutateResp.Message, mutateResp.Status)
if resp.Status == response.RuleStatusPass {
resp.Patches = mutateResp.Patches
resp.Message = buildSuccessMessage(mutateResp.PatchedResource)
}
+ if len(rule.Mutation.Targets) != 0 {
+ resp.PatchedTarget = &mutateResp.PatchedResource
+ resp.PatchedTargetSubresourceName = info.subresource
+ resp.PatchedTargetParentResourceGVR = info.parentResourceGVR
+ }
+
return resp
}
diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go
index 454b0d34c0..7eb5748c00 100644
--- a/pkg/engine/mutation_test.go
+++ b/pkg/engine/mutation_test.go
@@ -93,7 +93,8 @@ func Test_VariableSubstitutionPatchStrategicMerge(t *testing.T) {
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext)
t.Log(string(expectedPatch))
@@ -166,7 +167,8 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) {
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1)
assert.Assert(t, strings.Contains(er.PolicyResponse.Rules[0].Message, "Unknown key \"name1\" in path"))
@@ -989,6 +991,29 @@ func Test_foreach_order_mutation_(t *testing.T) {
]
}
}`)
+
+ er := testApplyPolicyToResource(t, policyRaw, resourceRaw)
+
+ assert.Equal(t, len(er.PolicyResponse.Rules), 1)
+ assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
+
+ containers, _, err := unstructured.NestedSlice(er.PatchedResource.Object, "spec", "containers")
+ assert.NilError(t, err)
+
+ for i, c := range containers {
+ ctnr := c.(map[string]interface{})
+ switch i {
+ case 0:
+ assert.Equal(t, ctnr["name"], "mongod")
+ case 1:
+ assert.Equal(t, ctnr["name"], "nginx")
+ case 3:
+ assert.Equal(t, ctnr["name"], "mongodb-agent")
+ }
+ }
+}
+
+func testApplyPolicyToResource(t *testing.T, policyRaw, resourceRaw []byte) *response.EngineResponse {
var policy kyverno.ClusterPolicy
err := json.Unmarshal(policyRaw, &policy)
assert.NilError(t, err)
@@ -1013,22 +1038,127 @@ func Test_foreach_order_mutation_(t *testing.T) {
assert.NilError(t, err)
er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext)
+ return er
+}
+func Test_mutate_nested_foreach(t *testing.T) {
+ policyRaw := []byte(`{
+ "apiVersion": "kyverno.io/v1",
+ "kind": "ClusterPolicy",
+ "metadata": {
+ "name": "replace-image-registry"
+ },
+ "spec": {
+ "background": false,
+ "rules": [
+ {
+ "name": "replace-dns-suffix",
+ "match": {
+ "any": [
+ {
+ "resources": {
+ "kinds": [
+ "Ingress"
+ ]
+ }
+ }
+ ]
+ },
+ "mutate": {
+ "foreach": [
+ {
+ "list": "request.object.spec.tls",
+ "foreach": [
+ {
+ "list": "element.hosts",
+ "patchesJson6902": "- path: /spec/tls/{{elementIndex0}}/hosts/{{elementIndex1}}\n op: replace\n value: {{replace_all('{{element}}', '.foo.com', '.newfoo.com')}}"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }`)
+
+ resourceRaw := []byte(`{
+ "apiVersion": "networking.k8s.io/v1",
+ "kind": "Ingress",
+ "metadata": {
+ "name": "tls-example-ingress"
+ },
+ "spec": {
+ "tls": [
+ {
+ "hosts": [
+ "https-example.foo.com"
+ ],
+ "secretName": "testsecret-tls"
+ },
+ {
+ "hosts": [
+ "https-example2.foo.com"
+ ],
+ "secretName": "testsecret-tls-2"
+ }
+ ],
+ "rules": [
+ {
+ "host": "https-example.foo.com",
+ "http": {
+ "paths": [
+ {
+ "path": "/",
+ "pathType": "Prefix",
+ "backend": {
+ "service": {
+ "name": "service1",
+ "port": {
+ "number": 80
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ {
+ "host": "https-example2.foo.com",
+ "http": {
+ "paths": [
+ {
+ "path": "/",
+ "pathType": "Prefix",
+ "backend": {
+ "service": {
+ "name": "service2",
+ "port": {
+ "number": 80
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }`)
+
+ er := testApplyPolicyToResource(t, policyRaw, resourceRaw)
assert.Equal(t, len(er.PolicyResponse.Rules), 1)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusPass)
+ assert.Equal(t, len(er.PolicyResponse.Rules[0].Patches), 2)
- containers, _, err := unstructured.NestedSlice(er.PatchedResource.Object, "spec", "containers")
+ tlsArr, _, err := unstructured.NestedSlice(er.PatchedResource.Object, "spec", "tls")
assert.NilError(t, err)
-
- for i, c := range containers {
- ctnr := c.(map[string]interface{})
- switch i {
- case 0:
- assert.Equal(t, ctnr["name"], "mongod")
- case 1:
- assert.Equal(t, ctnr["name"], "nginx")
- case 3:
- assert.Equal(t, ctnr["name"], "mongodb-agent")
+ for _, e := range tlsArr {
+ tls := e.(map[string]interface{})
+ hosts := tls["hosts"].([]interface{})
+ for _, h := range hosts {
+ s := h.(string)
+ assert.Assert(t, strings.HasSuffix(s, ".newfoo.com"))
}
}
}
diff --git a/pkg/engine/policyContext.go b/pkg/engine/policyContext.go
index eb97f12f5e..170a22f498 100644
--- a/pkg/engine/policyContext.go
+++ b/pkg/engine/policyContext.go
@@ -5,7 +5,6 @@ import (
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
- "github.com/kyverno/kyverno/pkg/engine/context"
enginectx "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/context/resolvers"
"github.com/kyverno/kyverno/pkg/utils"
@@ -54,7 +53,7 @@ type PolicyContext struct {
excludeResourceFunc ExcludeFunc
// jsonContext is the variable context
- jsonContext context.Interface
+ jsonContext enginectx.Interface
// namespaceLabels stores the label of namespace to be processed by namespace selector
namespaceLabels map[string]string
@@ -95,7 +94,7 @@ func (c *PolicyContext) AdmissionInfo() kyvernov1beta1.RequestInfo {
return c.admissionInfo
}
-func (c *PolicyContext) JSONContext() context.Interface {
+func (c *PolicyContext) JSONContext() enginectx.Interface {
return c.jsonContext
}
@@ -193,7 +192,7 @@ func (c *PolicyContext) WithSubresourcesInPolicy(subresourcesInPolicy []struct {
// Constructors
-func NewPolicyContextWithJsonContext(jsonContext context.Interface) *PolicyContext {
+func NewPolicyContextWithJsonContext(jsonContext enginectx.Interface) *PolicyContext {
return &PolicyContext{
jsonContext: jsonContext,
excludeGroupRole: []string{},
@@ -204,7 +203,7 @@ func NewPolicyContextWithJsonContext(jsonContext context.Interface) *PolicyConte
}
func NewPolicyContext() *PolicyContext {
- return NewPolicyContextWithJsonContext(context.NewContext())
+ return NewPolicyContextWithJsonContext(enginectx.NewContext())
}
func NewPolicyContextFromAdmissionRequest(
diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go
index 1238deae91..f39f4b785f 100644
--- a/pkg/engine/utils.go
+++ b/pkg/engine/utils.go
@@ -468,22 +468,6 @@ func ruleResponse(rule kyvernov1.Rule, ruleType response.RuleType, msg string, s
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,
- Message: msg,
- Status: status,
- }
-
- if rule.Mutation.Targets != nil {
- resp.PatchedTarget = patchedResource
- resp.PatchedTargetSubresourceName = patchedSubresourceName
- resp.PatchedTargetParentResourceGVR = parentResourceGVR
- }
- return resp
-}
-
func incrementAppliedCount(resp *response.EngineResponse) {
resp.PolicyResponse.RulesAppliedCount++
}
diff --git a/pkg/engine/utils_test.go b/pkg/engine/utils_test.go
index 4a7472324a..20206b0558 100644
--- a/pkg/engine/utils_test.go
+++ b/pkg/engine/utils_test.go
@@ -1867,7 +1867,6 @@ func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment", "Pods"},
@@ -1881,7 +1880,6 @@ func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
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)
}
-
}
// Match resource name
@@ -1927,7 +1925,6 @@ func TestResourceDescriptionMatch_Name(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -1986,7 +1983,6 @@ func TestResourceDescriptionMatch_GenerateName(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -2046,7 +2042,6 @@ func TestResourceDescriptionMatch_Name_Regex(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -2105,7 +2100,6 @@ func TestResourceDescriptionMatch_GenerateName_Regex(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -2165,7 +2159,6 @@ func TestResourceDescriptionMatch_Label_Expression_NotMatch(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -2233,7 +2226,6 @@ func TestResourceDescriptionMatch_Label_Expression_Match(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -2303,7 +2295,6 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err)
-
}
resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"},
@@ -2331,8 +2322,10 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
},
}
- rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription},
- ExcludeResources: v1.MatchResources{ResourceDescription: resourceDescriptionExclude}}
+ rule := v1.Rule{
+ MatchResources: v1.MatchResources{ResourceDescription: resourceDescription},
+ ExcludeResources: v1.MatchResources{ResourceDescription: resourceDescriptionExclude},
+ }
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")
@@ -2340,7 +2333,6 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
}
func TestWildCardLabels(t *testing.T) {
-
testSelector(t, &metav1.LabelSelector{}, map[string]string{}, true)
testSelector(t, &metav1.LabelSelector{}, map[string]string{"foo": "bar"}, true)
@@ -2386,7 +2378,6 @@ func testSelector(t *testing.T, s *metav1.LabelSelector, l map[string]string, ma
}
func TestWildCardAnnotation(t *testing.T) {
-
// test single annotation values
testAnnotationMatch(t, map[string]string{}, map[string]string{}, true)
testAnnotationMatch(t, map[string]string{"test/*": "*"}, map[string]string{}, false)
diff --git a/pkg/engine/validate/validate_test.go b/pkg/engine/validate/validate_test.go
index c7558d31c5..d2f6bbedfd 100644
--- a/pkg/engine/validate/validate_test.go
+++ b/pkg/engine/validate/validate_test.go
@@ -1667,7 +1667,8 @@ func testMatchPattern(t *testing.T, testCase struct {
pattern []byte
resource []byte
status response.RuleStatus
-}) {
+},
+) {
var pattern, resource interface{}
err := json.Unmarshal(testCase.pattern, &pattern)
assert.NilError(t, err)
@@ -1688,6 +1689,5 @@ func testMatchPattern(t *testing.T, testCase struct {
assert.Assert(t, pe.Skip, fmt.Sprintf("\nexpected skip == true - test: %s\npattern: %s\nresource: %s\n", testCase.name, pattern, resource))
} else if testCase.status == response.RuleStatusError {
assert.Assert(t, err == nil, fmt.Sprintf("\nexpected error - test: %s\npattern: %s\nresource: %s\n", testCase.name, pattern, resource))
-
}
}
diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go
index c202eaf3f7..982e67756d 100644
--- a/pkg/engine/validation.go
+++ b/pkg/engine/validation.go
@@ -21,6 +21,7 @@ import (
"github.com/kyverno/kyverno/pkg/pss"
"github.com/kyverno/kyverno/pkg/registryclient"
"github.com/kyverno/kyverno/pkg/utils"
+ "github.com/kyverno/kyverno/pkg/utils/api"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
@@ -147,12 +148,8 @@ func validateResource(ctx context.Context, log logr.Logger, rclient registryclie
return resp
}
-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 {
- return v.validateForEach(ctx)
- }
-
+func processValidationRule(ctx context.Context, log logr.Logger, rclient registryclient.Client, policyContext *PolicyContext, rule *kyvernov1.Rule) *response.RuleResponse {
+ v := newValidator(log, rclient, policyContext, rule)
return v.validate(ctx)
}
@@ -172,7 +169,7 @@ func addRuleResponse(log logr.Logger, resp *response.EngineResponse, ruleResp *r
type validator struct {
log logr.Logger
- ctx *PolicyContext
+ policyContext *PolicyContext
rule *kyvernov1.Rule
contextEntries []kyvernov1.ContextEntry
anyAllConditions apiextensions.JSON
@@ -180,7 +177,9 @@ type validator struct {
anyPattern apiextensions.JSON
deny *kyvernov1.Deny
podSecurity *kyvernov1.PodSecurity
+ foreach []kyvernov1.ForEachValidation
rclient registryclient.Client
+ nesting int
}
func newValidator(log logr.Logger, rclient registryclient.Client, ctx *PolicyContext, rule *kyvernov1.Rule) *validator {
@@ -188,35 +187,43 @@ func newValidator(log logr.Logger, rclient registryclient.Client, ctx *PolicyCon
return &validator{
log: log,
rule: ruleCopy,
- ctx: ctx,
+ policyContext: ctx,
+ rclient: rclient,
contextEntries: ruleCopy.Context,
anyAllConditions: ruleCopy.GetAnyAllConditions(),
pattern: ruleCopy.Validation.GetPattern(),
anyPattern: ruleCopy.Validation.GetAnyPattern(),
deny: ruleCopy.Validation.Deny,
podSecurity: ruleCopy.Validation.PodSecurity,
- rclient: rclient,
+ foreach: ruleCopy.Validation.ForEachValidation,
}
}
-func newForeachValidator(log logr.Logger, rclient registryclient.Client, foreach kyvernov1.ForEachValidation, rule *kyvernov1.Rule, ctx *PolicyContext) *validator {
+func newForeachValidator(foreach kyvernov1.ForEachValidation, rclient registryclient.Client, nesting int, rule *kyvernov1.Rule, ctx *PolicyContext, log logr.Logger) (*validator, error) {
ruleCopy := rule.DeepCopy()
anyAllConditions, err := utils.ToMap(foreach.AnyAllConditions)
if err != nil {
- log.Error(err, "failed to convert ruleCopy.Validation.ForEachValidation.AnyAllConditions")
+ return nil, errors.Wrap(err, "failed to convert ruleCopy.Validation.ForEachValidation.AnyAllConditions")
+ }
+
+ nestedForeach, err := api.DeserializeJSONArray[kyvernov1.ForEachValidation](foreach.ForEachValidation)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to convert ruleCopy.Validation.ForEachValidation.AnyAllConditions")
}
return &validator{
log: log,
- ctx: ctx,
+ policyContext: ctx,
rule: ruleCopy,
+ rclient: rclient,
contextEntries: foreach.Context,
anyAllConditions: anyAllConditions,
pattern: foreach.GetPattern(),
anyPattern: foreach.GetAnyPattern(),
deny: foreach.Deny,
- rclient: rclient,
- }
+ foreach: nestedForeach,
+ nesting: nesting,
+ }, nil
}
func (v *validator) validate(ctx context.Context) *response.RuleResponse {
@@ -224,7 +231,7 @@ func (v *validator) validate(ctx context.Context) *response.RuleResponse {
return ruleError(v.rule, response.Validation, "failed to load context", err)
}
- preconditionsPassed, err := checkPreconditions(v.log, v.ctx, v.anyAllConditions)
+ preconditionsPassed, err := checkPreconditions(v.log, v.policyContext, v.anyAllConditions)
if err != nil {
return ruleError(v.rule, response.Validation, "failed to evaluate preconditions", err)
}
@@ -243,46 +250,35 @@ func (v *validator) validate(ctx context.Context) *response.RuleResponse {
}
ruleResponse := v.validateResourceWithRule()
-
return ruleResponse
}
if v.podSecurity != nil {
- if !isDeleteRequest(v.ctx) {
+ if !isDeleteRequest(v.policyContext) {
ruleResponse := v.validatePodSecurity()
return ruleResponse
}
}
+ if v.foreach != nil {
+ ruleResponse := v.validateForEach(ctx)
+ return ruleResponse
+ }
+
v.log.V(2).Info("invalid validation rule: podSecurity, patterns, or deny expected")
return nil
}
func (v *validator) validateForEach(ctx context.Context) *response.RuleResponse {
- if err := v.loadContext(ctx); err != nil {
- return ruleError(v.rule, response.Validation, "failed to load context", err)
- }
-
- preconditionsPassed, err := checkPreconditions(v.log, v.ctx, v.anyAllConditions)
- 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)
- }
-
- foreachList := v.rule.Validation.ForEachValidation
applyCount := 0
- if foreachList == nil {
- return nil
- }
-
- for _, foreach := range foreachList {
- elements, err := evaluateList(foreach.List, v.ctx.jsonContext)
+ for _, foreach := range v.foreach {
+ elements, err := evaluateList(foreach.List, (v.policyContext.JSONContext()))
if err != nil {
v.log.V(2).Info("failed to evaluate list", "list", foreach.List, "error", err.Error())
continue
}
- resp, count := v.validateElements(ctx, foreach, elements, foreach.ElementScope)
+
+ resp, count := v.validateElements(ctx, v.rclient, foreach, elements, foreach.ElementScope)
if resp.Status != response.RuleStatusPass {
return resp
}
@@ -291,31 +287,42 @@ func (v *validator) validateForEach(ctx context.Context) *response.RuleResponse
}
if applyCount == 0 {
+ if v.foreach == nil {
+ return nil
+ }
+
return ruleResponse(*v.rule, response.Validation, "rule skipped", response.RuleStatusSkip)
}
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) {
- v.ctx.jsonContext.Checkpoint()
- defer v.ctx.jsonContext.Restore()
+func (v *validator) validateElements(ctx context.Context, rclient registryclient.Client, foreach kyvernov1.ForEachValidation, elements []interface{}, elementScope *bool) (*response.RuleResponse, int) {
+ v.policyContext.jsonContext.Checkpoint()
+ defer v.policyContext.jsonContext.Restore()
applyCount := 0
for i, e := range elements {
if e == nil {
continue
}
- store.SetForeachElement(i)
- v.ctx.jsonContext.Reset()
- enginectx := v.ctx.Copy()
- if err := addElementToContext(enginectx, e, i, elementScope); err != nil {
+ // TODO - this needs to be refactored. The engine should not have a dependency to the CLI code
+ store.SetForeachElement(i)
+
+ v.policyContext.JSONContext().Reset()
+ policyContext := v.policyContext.Copy()
+ if err := addElementToContext(policyContext, e, i, v.nesting, elementScope); err != nil {
v.log.Error(err, "failed to add element to context")
return ruleError(v.rule, response.Validation, "failed to process foreach", err), applyCount
}
- foreachValidator := newForeachValidator(v.log, v.rclient, foreach, v.rule, enginectx)
+ foreachValidator, err := newForeachValidator(foreach, rclient, v.nesting+1, v.rule, policyContext, v.log)
+ if err != nil {
+ v.log.Error(err, "failed to create foreach validator")
+ return ruleError(v.rule, response.Validation, "failed to create foreach validator", err), applyCount
+ }
+
r := foreachValidator.validate(ctx)
if r == nil {
v.log.V(2).Info("skip rule due to empty result")
@@ -341,12 +348,12 @@ func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForE
return ruleResponse(*v.rule, response.Validation, "", response.RuleStatusPass), applyCount
}
-func addElementToContext(ctx *PolicyContext, e interface{}, elementIndex int, elementScope *bool) error {
+func addElementToContext(ctx *PolicyContext, e interface{}, elementIndex, nesting int, elementScope *bool) error {
data, err := variables.DocumentToUntyped(e)
if err != nil {
return err
}
- if err := ctx.jsonContext.AddElement(data, elementIndex); err != nil {
+ if err := ctx.JSONContext().AddElement(data, elementIndex, nesting); err != nil {
return errors.Wrapf(err, "failed to add element (%v) to JSON context", e)
}
dataMap, ok := data.(map[string]interface{})
@@ -375,7 +382,7 @@ func addElementToContext(ctx *PolicyContext, e interface{}, elementIndex int, el
}
func (v *validator) loadContext(ctx context.Context) error {
- if err := LoadContext(ctx, v.log, v.rclient, v.contextEntries, v.ctx, v.rule.Name); err != nil {
+ if err := LoadContext(ctx, v.log, v.rclient, v.contextEntries, v.policyContext, v.rule.Name); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
v.log.V(3).Info("failed to load context", "reason", err.Error())
} else {
@@ -390,7 +397,7 @@ func (v *validator) loadContext(ctx context.Context) error {
func (v *validator) validateDeny() *response.RuleResponse {
anyAllCond := v.deny.GetAnyAllConditions()
- anyAllCond, err := variables.SubstituteAll(v.log, v.ctx.jsonContext, anyAllCond)
+ anyAllCond, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, anyAllCond)
if err != nil {
return ruleError(v.rule, response.Validation, "failed to substitute variables in deny conditions", err)
}
@@ -404,7 +411,7 @@ func (v *validator) validateDeny() *response.RuleResponse {
return ruleError(v.rule, response.Validation, "invalid deny conditions", err)
}
- deny := variables.EvaluateConditions(v.log, v.ctx.jsonContext, denyConditions)
+ deny := variables.EvaluateConditions(v.log, v.policyContext.jsonContext, denyConditions)
if deny {
return ruleResponse(*v.rule, response.Validation, v.getDenyMessage(deny), response.RuleStatusFail)
}
@@ -422,7 +429,7 @@ func (v *validator) getDenyMessage(deny bool) string {
return fmt.Sprintf("validation error: rule %s failed", v.rule.Name)
}
- raw, err := variables.SubstituteAll(v.log, v.ctx.jsonContext, msg)
+ raw, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, msg)
if err != nil {
return msg
}
@@ -431,12 +438,12 @@ func (v *validator) getDenyMessage(deny bool) string {
}
func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta, err error) {
- kind := v.ctx.newResource.GetKind()
+ kind := v.policyContext.newResource.GetKind()
if kind == "DaemonSet" || kind == "Deployment" || kind == "Job" || kind == "StatefulSet" || kind == "ReplicaSet" || kind == "ReplicationController" {
var deployment appsv1.Deployment
- resourceBytes, err := v.ctx.newResource.MarshalJSON()
+ resourceBytes, err := v.policyContext.newResource.MarshalJSON()
if err != nil {
return nil, nil, err
}
@@ -450,7 +457,7 @@ func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta
} else if kind == "CronJob" {
var cronJob batchv1.CronJob
- resourceBytes, err := v.ctx.newResource.MarshalJSON()
+ resourceBytes, err := v.policyContext.newResource.MarshalJSON()
if err != nil {
return nil, nil, err
}
@@ -463,7 +470,7 @@ func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta
} else if kind == "Pod" {
var pod corev1.Pod
- resourceBytes, err := v.ctx.newResource.MarshalJSON()
+ resourceBytes, err := v.policyContext.newResource.MarshalJSON()
if err != nil {
return nil, nil, err
}
@@ -508,16 +515,16 @@ func (v *validator) validatePodSecurity() *response.RuleResponse {
}
func (v *validator) validateResourceWithRule() *response.RuleResponse {
- if !isEmptyUnstructured(&v.ctx.element) {
- return v.validatePatterns(v.ctx.element)
+ if !isEmptyUnstructured(&v.policyContext.element) {
+ return v.validatePatterns(v.policyContext.element)
}
- if isDeleteRequest(v.ctx) {
+ if isDeleteRequest(v.policyContext) {
v.log.V(3).Info("skipping validation on deleted resource")
return nil
}
- resp := v.validatePatterns(v.ctx.newResource)
+ resp := v.validatePatterns(v.policyContext.newResource)
return resp
}
@@ -673,7 +680,7 @@ func (v *validator) buildErrorMessage(err error, path string) string {
return fmt.Sprintf("validation error: rule %s execution error: %s", v.rule.Name, err.Error())
}
- msgRaw, sErr := variables.SubstituteAll(v.log, v.ctx.jsonContext, v.rule.Validation.Message)
+ msgRaw, sErr := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.rule.Validation.Message)
if sErr != nil {
v.log.V(2).Info("failed to substitute variables in message", "error", sErr)
return fmt.Sprintf("validation error: variables substitution error in rule %s execution error: %s", v.rule.Name, err.Error())
@@ -704,7 +711,7 @@ func buildAnyPatternErrorMessage(rule *kyvernov1.Rule, errors []string) string {
func (v *validator) substitutePatterns() error {
if v.pattern != nil {
- i, err := variables.SubstituteAll(v.log, v.ctx.jsonContext, v.pattern)
+ i, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.pattern)
if err != nil {
return err
}
@@ -714,7 +721,7 @@ func (v *validator) substitutePatterns() error {
}
if v.anyPattern != nil {
- i, err := variables.SubstituteAll(v.log, v.ctx.jsonContext, v.anyPattern)
+ i, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.anyPattern)
if err != nil {
return err
}
@@ -731,7 +738,7 @@ func (v *validator) substituteDeny() error {
return nil
}
- i, err := variables.SubstituteAll(v.log, v.ctx.jsonContext, v.deny)
+ i, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.deny)
if err != nil {
return err
}
diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go
index 99c4dacf4a..802ac6a1a0 100644
--- a/pkg/engine/validation_test.go
+++ b/pkg/engine/validation_test.go
@@ -1479,7 +1479,8 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1)
@@ -1572,7 +1573,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSu
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1)
@@ -1633,7 +1635,8 @@ func Test_VariableSubstitution_NotOperatorWithStringVariable(t *testing.T) {
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "validation error: rule not-operator-with-variable-should-alway-fail-validation failed at path /spec/content/")
@@ -1724,7 +1727,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *test
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1)
@@ -1817,7 +1821,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatter
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
@@ -1922,7 +1927,8 @@ func Test_VariableSubstitutionValidate_VariablesInMessageAreResolved(t *testing.
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "The animal cow is not in the allowed list of animals.")
@@ -1975,7 +1981,8 @@ func Test_Flux_Kustomization_PathNotPresent(t *testing.T) {
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
for i, rule := range er.PolicyResponse.Rules {
@@ -2658,7 +2665,6 @@ func Test_foreach_container_deny_error(t *testing.T) {
}
func Test_foreach_context_preconditions(t *testing.T) {
-
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
@@ -2752,7 +2758,6 @@ func Test_foreach_context_preconditions(t *testing.T) {
}
func Test_foreach_context_preconditions_fail(t *testing.T) {
-
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
@@ -2847,7 +2852,6 @@ func Test_foreach_context_preconditions_fail(t *testing.T) {
}
func Test_foreach_element_validation(t *testing.T) {
-
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
@@ -2895,7 +2899,6 @@ func Test_foreach_element_validation(t *testing.T) {
}
func Test_outof_foreach_element_validation(t *testing.T) {
-
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
@@ -2938,7 +2941,6 @@ func Test_outof_foreach_element_validation(t *testing.T) {
}
func Test_foreach_skip_initContainer_pass(t *testing.T) {
-
resourceRaw := []byte(`{"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
@@ -2977,7 +2979,7 @@ func Test_foreach_skip_initContainer_pass(t *testing.T) {
}
},
{
- "list": "request.object.spec.template.spec..initContainers",
+ "list": "request.object.spec.template.spec.initContainers",
"pattern": {
"image": "trusted-registry.io/*"
}
@@ -2992,6 +2994,102 @@ func Test_foreach_skip_initContainer_pass(t *testing.T) {
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass)
}
+func Test_foreach_validate_nested(t *testing.T) {
+ resourceRaw := []byte(`{
+ "apiVersion": "networking.k8s.io/v1",
+ "kind": "Ingress",
+ "metadata": {
+ "name": "name-virtual-host-ingress"
+ },
+ "spec": {
+ "rules": [
+ {
+ "host": "foo.bar.com",
+ "http": {
+ "paths": [
+ {
+ "pathType": "Prefix",
+ "path": "/",
+ "backend": {
+ "service": {
+ "name": "service1",
+ "port": {
+ "number": 80
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ {
+ "host": "bar.foo.com",
+ "http": {
+ "paths": [
+ {
+ "pathType": "Prefix",
+ "path": "/",
+ "backend": {
+ "service": {
+ "name": "service2",
+ "port": {
+ "number": 80
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }`)
+
+ policyraw := []byte(`{
+ "apiVersion": "kyverno.io/v1",
+ "kind": "ClusterPolicy",
+ "metadata": {
+ "name": "replace-image-registry"
+ },
+ "spec": {
+ "background": false,
+ "rules": [
+ {
+ "name": "replace-dns-suffix",
+ "match": {
+ "any": [
+ {
+ "resources": {
+ "kinds": [
+ "Ingress"
+ ]
+ }
+ }
+ ]
+ },
+ "validate": {
+ "foreach": [
+ {
+ "list": "request.object.spec.rules",
+ "foreach": [
+ {
+ "list": "element.http.paths",
+ "pattern": {
+ "path": "/"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }`)
+
+ testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass)
+}
+
func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string, status response.RuleStatus) {
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
@@ -3005,7 +3103,8 @@ func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string,
policyContext := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, status)
@@ -3015,7 +3114,6 @@ func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string,
}
func Test_delete_ignore_pattern(t *testing.T) {
-
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Pod",
@@ -3069,7 +3167,8 @@ func Test_delete_ignore_pattern(t *testing.T) {
policyContextCreate := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- newResource: *resourceUnstructured}
+ newResource: *resourceUnstructured,
+ }
engineResponseCreate := Validate(context.TODO(), registryclient.NewOrDie(), policyContextCreate)
assert.Equal(t, len(engineResponseCreate.PolicyResponse.Rules), 1)
assert.Equal(t, engineResponseCreate.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
@@ -3077,7 +3176,8 @@ func Test_delete_ignore_pattern(t *testing.T) {
policyContextDelete := &PolicyContext{
policy: &policy,
jsonContext: ctx,
- oldResource: *resourceUnstructured}
+ oldResource: *resourceUnstructured,
+ }
engineResponseDelete := Validate(context.TODO(), registryclient.NewOrDie(), policyContextDelete)
assert.Equal(t, len(engineResponseDelete.PolicyResponse.Rules), 0)
}
diff --git a/pkg/engine/variables/variables_test.go b/pkg/engine/variables/variables_test.go
index ce5c23440e..9b82093e80 100644
--- a/pkg/engine/variables/variables_test.go
+++ b/pkg/engine/variables/variables_test.go
@@ -189,6 +189,7 @@ func Test_variablesub_multiple(t *testing.T) {
t.Error("result does not match")
}
}
+
func Test_variablesubstitution(t *testing.T) {
patternMap := []byte(`
{
@@ -277,7 +278,6 @@ func Test_variablesubstitution(t *testing.T) {
}
func Test_variableSubstitutionValue(t *testing.T) {
-
resourceRaw := []byte(`
{
"metadata": {
@@ -336,7 +336,6 @@ func Test_variableSubstitutionValue(t *testing.T) {
}
func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
-
resourceRaw := []byte(`
{
"metadata": {
@@ -396,7 +395,6 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
}
func Test_variableSubstitutionValueFail(t *testing.T) {
-
resourceRaw := []byte(`
{
"metadata": {
@@ -444,7 +442,6 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
t.Log("expected to fails")
t.Fail()
}
-
}
func Test_variableSubstitutionObject(t *testing.T) {
diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go
index aa0a8e6491..0a293471a2 100644
--- a/pkg/engine/variables/vars.go
+++ b/pkg/engine/variables/vars.go
@@ -15,10 +15,11 @@ import (
"github.com/kyverno/kyverno/pkg/engine/context"
jsonUtils "github.com/kyverno/kyverno/pkg/engine/jsonutils"
"github.com/kyverno/kyverno/pkg/engine/operator"
+ "github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/utils/jsonpointer"
)
-var RegexVariables = regexp.MustCompile(`(?:^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`)
+var RegexVariables = regexp.MustCompile(`(^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`)
var RegexEscpVariables = regexp.MustCompile(`\\\{\{(\{[^{}]*\}|[^{}])*\}\}`)
@@ -30,7 +31,7 @@ var RegexEscpReferences = regexp.MustCompile(`\\\$\(.[^\ ]*\)`)
var regexVariableInit = regexp.MustCompile(`^\{\{(\{[^{}]*\}|[^{}])*\}\}`)
-var regexElementIndex = regexp.MustCompile(`{{\s*elementIndex\s*}}`)
+var regexElementIndex = regexp.MustCompile(`{{\s*elementIndex\d*\s*}}`)
// IsVariable returns true if the element contains a 'valid' variable {{}}
func IsVariable(value string) bool {
@@ -569,12 +570,13 @@ func replaceSubstituteVariables(document interface{}) interface{} {
break
}
- rawDocument = RegexVariables.ReplaceAll(rawDocument, []byte(`placeholderValue`))
+ rawDocument = RegexVariables.ReplaceAll(rawDocument, []byte(`${1}placeholderValue`))
}
var output interface{}
err = json.Unmarshal(rawDocument, &output)
if err != nil {
+ logging.Error(err, "failed to unmarshall JSON: %s", string(rawDocument))
return document
}
diff --git a/pkg/engine/variables/vars_test.go b/pkg/engine/variables/vars_test.go
index 7fd5b8da50..de6af64bc7 100644
--- a/pkg/engine/variables/vars_test.go
+++ b/pkg/engine/variables/vars_test.go
@@ -365,7 +365,7 @@ func Test_subVars_withRegexReplaceAll(t *testing.T) {
func Test_ReplacingPathWhenDeleting(t *testing.T) {
patternRaw := []byte(`"{{request.object.metadata.annotations.target}}"`)
- var resourceRaw = []byte(`
+ resourceRaw := []byte(`
{
"request": {
"operation": "DELETE",
@@ -408,7 +408,7 @@ func Test_ReplacingPathWhenDeleting(t *testing.T) {
func Test_ReplacingNestedVariableWhenDeleting(t *testing.T) {
patternRaw := []byte(`"{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`)
- var resourceRaw = []byte(`
+ resourceRaw := []byte(`
{
"request":{
"operation":"DELETE",
@@ -468,8 +468,8 @@ func Test_SubstituteSuccess(t *testing.T) {
results, err := action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
- Path: "/"})
-
+ Path: "/",
+ })
if err != nil {
t.Errorf("substitution failed: %v", err.Error())
return
@@ -492,7 +492,8 @@ func Test_SubstituteRecursiveErrors(t *testing.T) {
results, err := action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
- Path: "/"})
+ Path: "/",
+ })
if err == nil {
t.Errorf("expected error but received: %v", results)
@@ -505,7 +506,8 @@ func Test_SubstituteRecursiveErrors(t *testing.T) {
results, err = action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
- Path: "/"})
+ Path: "/",
+ })
if err == nil {
t.Errorf("expected error but received: %v", results)
@@ -524,8 +526,8 @@ func Test_SubstituteRecursive(t *testing.T) {
results, err := action(&ju.ActionData{
Document: nil,
Element: string(patternRaw),
- Path: "/"})
-
+ Path: "/",
+ })
if err != nil {
t.Errorf("substitution failed: %v", err.Error())
return
@@ -1146,7 +1148,7 @@ func Test_EscpReferenceSubstitution(t *testing.T) {
func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
patternRaw := []byte(`"\\{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`)
- var resourceRaw = []byte(`
+ resourceRaw := []byte(`
{
"request":{
"operation":"DELETE",
@@ -1177,3 +1179,38 @@ func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%v", pattern), "{{request.object.metadata.annotations.target}}")
}
+
+func Test_RegexVariables(t *testing.T) {
+ vars := RegexVariables.FindAllString("tag: {{ value }}", -1)
+ assert.Equal(t, len(vars), 1)
+ assert.Equal(t, vars[0], " {{ value }}")
+
+ res := RegexVariables.ReplaceAllString("tag: {{ value }}", "${1}test")
+ assert.Equal(t, res, "tag: test")
+}
+
+func Test_IsVariable(t *testing.T) {
+ assert.Equal(t, IsVariable("{{ foo }}"), true)
+ assert.Equal(t, IsVariable("{{ foo {{foo2}} }}"), true)
+ assert.Equal(t, IsVariable("\\{{ foo }}"), false)
+}
+
+func Test_ReplaceAllVars(t *testing.T) {
+ result := ReplaceAllVars("{{ foo }}", func(s string) string { return "test" })
+ assert.Equal(t, result, "test")
+
+ result = ReplaceAllVars("\"{{ foo }}\"", func(s string) string { return "test" })
+ assert.Equal(t, result, "\"test\"")
+
+ result = ReplaceAllVars("/s/{{elementIndex}}/r", func(s string) string { return "test" })
+ assert.Equal(t, result, "/s/test/r")
+
+ result = ReplaceAllVars("{{ foo }} {{foo}} {{foo}}", func(s string) string { return "test" })
+ assert.Equal(t, result, "test test test")
+
+ result = ReplaceAllVars("{{ foo }} \\{{foo}} {{foo}}", func(s string) string { return "test" })
+ assert.Equal(t, result, "test \\{{foo}} test")
+
+ result = ReplaceAllVars("{{ foo {{foo}} }}", func(s string) string { return "test" })
+ assert.Equal(t, result, "{{ foo test }}")
+}
diff --git a/pkg/engine/wildcards/wildcards_test.go b/pkg/engine/wildcards/wildcards_test.go
index bdb51d2993..52dcddc43b 100644
--- a/pkg/engine/wildcards/wildcards_test.go
+++ b/pkg/engine/wildcards/wildcards_test.go
@@ -6,7 +6,7 @@ import (
)
func TestExpandInMetadata(t *testing.T) {
- //testExpand(t, map[string]string{"test/*": "*"}, map[string]string{},
+ // testExpand(t, map[string]string{"test/*": "*"}, map[string]string{},
// map[string]string{"test/0": "0"})
testExpand(t, map[string]string{"test/*": "*"}, map[string]string{"test/test": "test"},
diff --git a/pkg/openapi/manager_test.go b/pkg/openapi/manager_test.go
index fa9640ae5b..02bf540490 100644
--- a/pkg/openapi/manager_test.go
+++ b/pkg/openapi/manager_test.go
@@ -65,6 +65,11 @@ func Test_ValidateMutationPolicy(t *testing.T) {
policy: []byte(`{"apiVersion":"kyverno.io\/v1","kind":"ClusterPolicy","metadata":{"name":"set-image-pull-policy"},"spec":{"rules":[{"name":"set-image-pull-policy","match":{"all":[{"resources":{"kinds":["Pod"]}}]},"mutate":{"patchStrategicMerge":{"spec":{"containers":[{"(image)":"*:latest","imagePullPolicy":"IfNotPresent"}]}}}}]}}`),
mustSucceed: true,
},
+ {
+ description: "Policy with nested foreach and patchesJson6902",
+ policy: []byte(`{"apiVersion":"kyverno.io/v2beta1","kind":"ClusterPolicy","metadata":{"name":"replace-image-registry"},"spec":{"background":false,"validationFailureAction":"Enforce","rules":[{"name":"replace-dns-suffix","match":{"any":[{"resources":{"kinds":["Ingress"]}}]},"mutate":{"foreach":[{"list":"request.object.spec.tls","foreach":[{"list":"element.hosts","patchesJson6902":"- path: \"/spec/tls/{{elementIndex0}}/hosts/{{elementIndex1}}\"\n op: replace\n value: \"{{replace_all('{{element}}', '.foo.com', '.newfoo.com')}}\""}]}]}}]}}`),
+ mustSucceed: true,
+ },
}
o, _ := NewManager()
diff --git a/pkg/policy/background.go b/pkg/policy/background.go
index 50a574a37e..484e85b44b 100644
--- a/pkg/policy/background.go
+++ b/pkg/policy/background.go
@@ -31,8 +31,8 @@ func containsUserVariables(policy kyvernov1.PolicyInterface, vars [][]string) er
}
for _, s := range vars {
for _, banned := range forbidden {
- if banned.Match([]byte(s[1])) {
- return fmt.Errorf("variable %s is not allowed", s[1])
+ if banned.Match([]byte(s[2])) {
+ return fmt.Errorf("variable %s is not allowed", s[2])
}
}
}
diff --git a/pkg/policy/mutate/validate.go b/pkg/policy/mutate/validate.go
index 7f0dce1d05..63fb512cd2 100644
--- a/pkg/policy/mutate/validate.go
+++ b/pkg/policy/mutate/validate.go
@@ -4,6 +4,9 @@ import (
"fmt"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
+ "github.com/kyverno/kyverno/pkg/utils/api"
+ "github.com/pkg/errors"
+ v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
// Mutate provides implementation to validate 'mutate' rule
@@ -21,7 +24,11 @@ func NewMutateFactory(m kyvernov1.Mutation) *Mutate {
// Validate validates the 'mutate' rule
func (m *Mutate) Validate() (string, error) {
if m.hasForEach() {
- return m.validateForEach()
+ if m.hasPatchStrategicMerge() || m.hasPatchesJSON6902() {
+ return "foreach", fmt.Errorf("only one of `foreach`, `patchStrategicMerge`, or `patchesJson6902` is allowed")
+ }
+
+ return m.validateForEach("", m.mutation.ForEachMutation)
}
if m.hasPatchesJSON6902() && m.hasPatchStrategicMerge() {
@@ -31,21 +38,35 @@ func (m *Mutate) Validate() (string, error) {
return "", nil
}
-func (m *Mutate) validateForEach() (string, error) {
- if m.hasPatchStrategicMerge() || m.hasPatchesJSON6902() {
- return "foreach", fmt.Errorf("only one of `foreach`, `patchStrategicMerge`, or `patchesJson6902` is allowed")
- }
+func (m *Mutate) validateForEach(tag string, foreach []kyvernov1.ForEachMutation) (string, error) {
+ for i, fe := range foreach {
+ tag = tag + fmt.Sprintf("foreach[%d]", i)
+ if fe.ForEachMutation != nil {
+ if fe.Context != nil || fe.AnyAllConditions != nil || fe.PatchesJSON6902 != "" || fe.RawPatchStrategicMerge != nil {
+ return tag, fmt.Errorf("a nested foreach cannot contain other declarations")
+ }
+
+ return m.validateNestedForEach(tag, fe.ForEachMutation)
+ }
- for i, fe := range m.mutation.ForEachMutation {
psm := fe.GetPatchStrategicMerge()
if (fe.PatchesJSON6902 == "" && psm == nil) || (fe.PatchesJSON6902 != "" && psm != nil) {
- return fmt.Sprintf("foreach[%d]", i), fmt.Errorf("only one of `patchStrategicMerge` or `patchesJson6902` is allowed")
+ return tag, fmt.Errorf("only one of `patchStrategicMerge` or `patchesJson6902` is allowed")
}
}
return "", nil
}
+func (m *Mutate) validateNestedForEach(tag string, j *v1.JSON) (string, error) {
+ nestedForeach, err := api.DeserializeJSONArray[kyvernov1.ForEachMutation](j)
+ if err != nil {
+ return tag, errors.Wrapf(err, "invalid foreach syntax")
+ }
+
+ return m.validateForEach(tag, nestedForeach)
+}
+
func (m *Mutate) hasForEach() bool {
return len(m.mutation.ForEachMutation) > 0
}
diff --git a/pkg/utils/api/json.go b/pkg/utils/api/json.go
new file mode 100644
index 0000000000..c1d9ab3acb
--- /dev/null
+++ b/pkg/utils/api/json.go
@@ -0,0 +1,26 @@
+package api
+
+import (
+ "encoding/json"
+
+ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
+)
+
+// Deserialize "apiextensions.JSON" to a typed array
+func DeserializeJSONArray[T any](j apiextensions.JSON) ([]T, error) {
+ if j == nil {
+ return nil, nil
+ }
+
+ data, err := json.Marshal(j)
+ if err != nil {
+ return nil, err
+ }
+
+ var res []T
+ if err := json.Unmarshal(data, &res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}