1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-15 17:51:20 +00:00

Nested foreach (#5589)

* updated foreach logic and added tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* uncomment tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix vars and unit tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix vars and unit tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix some tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix more tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* format

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* make codegen

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* linter

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* cleanup

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix linter issue

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* revert local launch

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* propagate context

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* uncomment tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix propagation of registry client

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Jim Bugwadia 2022-12-12 07:20:20 -08:00 committed by GitHub
parent d36a42b815
commit 9d3b176def
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 923 additions and 284 deletions

11
.vscode/launch.json vendored
View file

@ -11,6 +11,17 @@
"--kubeconfig=${userHome}/.kube/config", "--kubeconfig=${userHome}/.kube/config",
"--serverIP=<local ip>:9443", "--serverIP=<local ip>:9443",
], ],
},
{
"name": "Launch CLI",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/cli/kubectl-kyverno",
"args": [
"test",
"${workspaceFolder}/test/cli/",
],
} }
] ]
} }

View file

@ -283,6 +283,10 @@ type ForEachMutation struct {
// See https://tools.ietf.org/html/rfc6902 and https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/. // See https://tools.ietf.org/html/rfc6902 and https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/.
// +optional // +optional
PatchesJSON6902 string `json:"patchesJson6902,omitempty" yaml:"patchesJson6902,omitempty"` 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 { func (m *ForEachMutation) GetPatchStrategicMerge() apiextensions.JSON {
@ -398,6 +402,14 @@ func (v *Validation) SetAnyPattern(in apiextensions.JSON) {
v.RawAnyPattern = ToJSON(in) 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. // Deny specifies a list of conditions used to pass or fail a validation rule.
type Deny struct { type Deny struct {
// Multiple conditions can be declared under an `any` or `all` statement. A direct list // 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. // Deny defines conditions used to pass or fail a validation rule.
// +optional // +optional
Deny *Deny `json:"deny,omitempty" yaml:"deny,omitempty"` 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 { func (v *ForEachValidation) GetPattern() apiextensions.JSON {

View file

@ -472,6 +472,11 @@ func (in *ForEachMutation) DeepCopyInto(out *ForEachMutation) {
*out = new(apiextensionsv1.JSON) *out = new(apiextensionsv1.JSON)
(*in).DeepCopyInto(*out) (*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. // 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) *out = new(Deny)
(*in).DeepCopyInto(*out) (*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. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForEachValidation.

View file

@ -3489,6 +3489,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -3693,6 +3696,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -5377,6 +5383,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -5581,6 +5590,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -7132,6 +7144,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -7458,6 +7473,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -9117,6 +9135,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -9321,6 +9342,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -11596,6 +11620,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -11800,6 +11827,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -13484,6 +13514,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -13688,6 +13721,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -15239,6 +15275,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -15565,6 +15604,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -17224,6 +17266,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string
@ -17428,6 +17473,9 @@ spec:
elementScope: 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. 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 type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied. description: List specifies a JMESPath expression that results in one or more elements to which the validation logic is applied.
type: string type: string

View file

@ -82,7 +82,7 @@ func Test_Apply(t *testing.T) {
expectedPolicyReports: []preport.PolicyReport{ expectedPolicyReports: []preport.PolicyReport{
{ {
Summary: preport.PolicyReportSummary{ Summary: preport.PolicyReportSummary{
Pass: 9, Pass: 6,
Fail: 0, Fail: 0,
Skip: 0, Skip: 0,
Error: 0, Error: 0,

View file

@ -1729,6 +1729,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -2042,6 +2045,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -4815,6 +4821,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -5140,6 +5150,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -7631,6 +7645,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -8112,6 +8129,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -10845,6 +10865,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -11170,6 +11194,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which

View file

@ -1731,6 +1731,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -2044,6 +2047,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -4818,6 +4824,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -5143,6 +5153,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -7635,6 +7649,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -8116,6 +8133,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -10849,6 +10869,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -11174,6 +11198,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which

View file

@ -5155,6 +5155,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -5468,6 +5471,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -8241,6 +8247,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -8566,6 +8576,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -11057,6 +11071,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -11538,6 +11555,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -14271,6 +14291,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -14596,6 +14620,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -18072,6 +18100,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -18385,6 +18416,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -21159,6 +21193,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -21484,6 +21522,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -23976,6 +24018,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -24457,6 +24502,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -27190,6 +27238,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -27515,6 +27567,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which

View file

@ -5147,6 +5147,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -5460,6 +5463,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -8233,6 +8239,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -8558,6 +8568,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -11049,6 +11063,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -11530,6 +11547,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -14263,6 +14283,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -14588,6 +14612,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -18061,6 +18089,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -18374,6 +18405,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -21148,6 +21182,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -21473,6 +21511,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -23965,6 +24007,9 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -24446,6 +24491,9 @@ spec:
scope within the foreach block to allow referencing scope within the foreach block to allow referencing
other elements in the subtree. other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which the that results in one or more elements to which the
@ -27179,6 +27227,10 @@ spec:
type: object type: object
type: object type: object
type: array type: array
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which
@ -27504,6 +27556,10 @@ spec:
as the validation scope within the foreach block as the validation scope within the foreach block
to allow referencing other elements in the subtree. to allow referencing other elements in the subtree.
type: boolean type: boolean
foreach:
description: Foreach declares a nested foreach
iterator
x-kubernetes-preserve-unknown-fields: true
list: list:
description: List specifies a JMESPath expression description: List specifies a JMESPath expression
that results in one or more elements to which that results in one or more elements to which

View file

@ -1533,6 +1533,20 @@ string
See <a href="https://tools.ietf.org/html/rfc6902">https://tools.ietf.org/html/rfc6902</a> and <a href="https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/">https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/</a>.</p> See <a href="https://tools.ietf.org/html/rfc6902">https://tools.ietf.org/html/rfc6902</a> and <a href="https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/">https://kubectl.docs.kubernetes.io/references/kustomize/patchesjson6902/</a>.</p>
</td> </td>
</tr> </tr>
<tr>
<td>
<code>foreach</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#json-v1-apiextensions">
Kubernetes apiextensions/v1.JSON
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Foreach declares a nested foreach iterator</p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<hr /> <hr />
@ -1653,6 +1667,20 @@ Deny
<p>Deny defines conditions used to pass or fail a validation rule.</p> <p>Deny defines conditions used to pass or fail a validation rule.</p>
</td> </td>
</tr> </tr>
<tr>
<td>
<code>foreach</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#json-v1-apiextensions">
Kubernetes apiextensions/v1.JSON
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Foreach declares a nested foreach iterator</p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<hr /> <hr />

View file

@ -45,14 +45,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
// AddToScheme adds all types of this clientset into the given scheme. This allows composition // AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in: // of clientsets, like in:
// //
// import ( // import (
// "k8s.io/client-go/kubernetes" // "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme" // clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// ) // )
// //
// kclientset, _ := kubernetes.NewForConfig(c) // kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
// //
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly. // correctly.

View file

@ -45,14 +45,14 @@ var localSchemeBuilder = runtime.SchemeBuilder{
// AddToScheme adds all types of this clientset into the given scheme. This allows composition // AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in: // of clientsets, like in:
// //
// import ( // import (
// "k8s.io/client-go/kubernetes" // "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme" // clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// ) // )
// //
// kclientset, _ := kubernetes.NewForConfig(c) // kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
// //
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly. // correctly.

View file

@ -227,7 +227,6 @@ var scanPredicate = `
` `
func Test_Conditions(t *testing.T) { func Test_Conditions(t *testing.T) {
conditions := []v1.AnyAllConditions{ conditions := []v1.AnyAllConditions{
{ {
AnyConditions: []v1.Condition{ AnyConditions: []v1.Condition{

View file

@ -2,6 +2,7 @@ package context
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"sync" "sync"
@ -65,7 +66,7 @@ type Interface interface {
AddNamespace(namespace string) error AddNamespace(namespace string) error
// AddElement adds element info to the context // 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 adds image info to the context
AddImageInfo(info apiutils.ImageInfo) error AddImageInfo(info apiutils.ImageInfo) error
@ -239,10 +240,14 @@ func (ctx *context) AddNamespace(namespace string) error {
return addToContext(ctx, namespace, "request", "namespace") 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{}{ data = map[string]interface{}{
"element": data, "element": data,
"elementIndex": index, nestedElement: data,
"elementIndex": index,
nestedElementIndex: index,
} }
return addToContext(ctx, data) return addToContext(ctx, data)
} }

View file

@ -51,7 +51,8 @@ func Test_addResourceAndUserContext(t *testing.T) {
userRequestInfo := urkyverno.RequestInfo{ userRequestInfo := urkyverno.RequestInfo{
Roles: nil, Roles: nil,
ClusterRoles: nil, ClusterRoles: nil,
AdmissionUserInfo: userInfo} AdmissionUserInfo: userInfo,
}
var expectedResult string var expectedResult string
ctx := NewContext() ctx := NewContext()

View file

@ -3,12 +3,16 @@ package engine
import ( import (
"fmt" "fmt"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/mutate" "github.com/kyverno/kyverno/pkg/engine/mutate"
"github.com/kyverno/kyverno/pkg/engine/response" "github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/logging" "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" "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 { if r.Mutation.ForEachMutation != nil {
for i, foreach := range r.Mutation.ForEachMutation { patchedResource, err = applyForeachMutate(r.Name, r.Mutation.ForEachMutation, patchedResource, ctx, logger)
patcher := mutate.NewPatcher(r.Name, foreach.GetPatchStrategicMerge(), foreach.PatchesJSON6902, patchedResource, ctx, logger) if err != nil {
resp, mutatedResource := patcher.Patch() return patchedResource, err
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
} }
} else { } else {
m := r.Mutation m := r.Mutation
patcher := mutate.NewPatcher(r.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, patchedResource, ctx, logger) patchedResource, err = applyPatches(r.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, patchedResource, ctx, logger)
resp, mutatedResource := patcher.Patch() if err != nil {
if resp.Status != response.RuleStatusPass { return patchedResource, err
return patchedResource, fmt.Errorf("mutate result %q: %s", resp.Status.String(), resp.Message)
} }
patchedResource = mutatedResource
} }
} }
return patchedResource, nil 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 // removeConditions mutates the rule to remove AnyAllConditions
func removeConditions(rule *kyvernov1.Rule) { func removeConditions(rule *kyvernov1.Rule) {
if rule.GetAnyAllConditions() != nil { if rule.GetAnyAllConditions() != nil {

View file

@ -171,7 +171,6 @@ func Test_CosignMockAttest_fail(t *testing.T) {
} }
func buildContext(t *testing.T, policy, resource string, oldResource string) *PolicyContext { func buildContext(t *testing.T, policy, resource string, oldResource string) *PolicyContext {
var cpol kyverno.ClusterPolicy var cpol kyverno.ClusterPolicy
err := json.Unmarshal([]byte(policy), &cpol) err := json.Unmarshal([]byte(policy), &cpol)
assert.NilError(t, err) 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 (
var testOtherKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpNlOGZ323zMlhs4bcKSpAKQvbcWi5ZLRmijm6SqXDy0Fp0z0Eal+BekFnLzs8rUXUaXlhZ3hNudlgFJH+nFNMw==\n-----END PUBLIC KEY-----\n` 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) { func Test_ConfigMapMissingSuccess(t *testing.T) {
policyContext := buildContext(t, testConfigMapMissing, testConfigMapMissingResource, "") policyContext := buildContext(t, testConfigMapMissing, testConfigMapMissingResource, "")
@ -744,7 +745,7 @@ func Test_MarkImageVerified(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, len(patches), 2) assert.Equal(t, len(patches), 2)
resource := applyPatches(t, patches) resource := testApplyPatches(t, patches)
patchedAnnotations := resource.GetAnnotations() patchedAnnotations := resource.GetAnnotations()
assert.Equal(t, len(patchedAnnotations), 1) assert.Equal(t, len(patchedAnnotations), 1)
@ -756,7 +757,7 @@ func Test_MarkImageVerified(t *testing.T) {
assert.Equal(t, verified, true) 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) patchedResource, err := utils.ApplyPatches([]byte(testResource), patches)
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, patchedResource != nil) assert.Assert(t, patchedResource != nil)

View file

@ -1382,7 +1382,6 @@ func Test_Items(t *testing.T) {
} }
for i, tc := range testCases { for i, tc := range testCases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
query, err := New("items(`" + tc.object + "`,`" + tc.keyName + "`,`" + tc.valName + "`)") query, err := New("items(`" + tc.object + "`,`" + tc.keyName + "`,`" + tc.valName + "`)")
assert.NilError(t, err) assert.NilError(t, err)

View file

@ -22,11 +22,11 @@ type Response struct {
Message string Message string
} }
func newErrorResponse(msg string, err error) *Response { func NewErrorResponse(msg string, err error) *Response {
return newResponse(response.RuleStatusError, unstructured.Unstructured{}, nil, fmt.Sprintf("%s: %v", msg, err)) 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{ return &Response{
Status: status, Status: status,
PatchedResource: resource, 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 { func Mutate(rule *kyvernov1.Rule, ctx context.Interface, resource unstructured.Unstructured, logger logr.Logger) *Response {
updatedRule, err := variables.SubstituteAllInRule(logger, ctx, *rule) updatedRule, err := variables.SubstituteAllInRule(logger, ctx, *rule)
if err != nil { if err != nil {
return newErrorResponse("variable substitution failed", err) return NewErrorResponse("variable substitution failed", err)
} }
m := updatedRule.Mutation m := updatedRule.Mutation
patcher := NewPatcher(updatedRule.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, resource, ctx, logger) patcher := NewPatcher(updatedRule.Name, m.GetPatchStrategicMerge(), m.PatchesJSON6902, resource, ctx, logger)
if patcher == nil { 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() resp, patchedResource := patcher.Patch()
if resp.Status != response.RuleStatusPass { 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 { 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 { 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 { func ForEach(name string, foreach kyvernov1.ForEachMutation, ctx context.Interface, resource unstructured.Unstructured, logger logr.Logger) *Response {
fe, err := substituteAllInForEach(foreach, ctx, logger) fe, err := substituteAllInForEach(foreach, ctx, logger)
if err != nil { 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) patcher := NewPatcher(name, fe.GetPatchStrategicMerge(), fe.PatchesJSON6902, resource, ctx, logger)
if patcher == nil { 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() resp, patchedResource := patcher.Patch()
if resp.Status != response.RuleStatusPass { 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 { 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 { 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) { func substituteAllInForEach(fe kyvernov1.ForEachMutation, ctx context.Interface, logger logr.Logger) (*kyvernov1.ForEachMutation, error) {

View file

@ -68,7 +68,7 @@ func applyPatches(rule *types.Rule, resource unstructured.Unstructured) (*respon
} }
func TestProcessPatches_EmptyPatches(t *testing.T) { func TestProcessPatches_EmptyPatches(t *testing.T) {
var emptyRule = &types.Rule{Name: "emptyRule"} emptyRule := &types.Rule{Name: "emptyRule"}
resourceUnstructured, err := utils.ConvertToUnstructured([]byte(endpointsDocument)) resourceUnstructured, err := utils.ConvertToUnstructured([]byte(endpointsDocument))
if err != nil { if err != nil {
t.Error(err) t.Error(err)

View file

@ -182,7 +182,6 @@ spec:
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.testName, func(t *testing.T) { t.Run(testCase.testName, func(t *testing.T) {
expectedBytes, err := yaml.YAMLToJSON(testCase.expected) expectedBytes, err := yaml.YAMLToJSON(testCase.expected)
assert.Nil(t, err) assert.Nil(t, err)

View file

@ -11,7 +11,6 @@ import (
) )
func Test_GeneratePatches(t *testing.T) { func Test_GeneratePatches(t *testing.T) {
out, err := strategicMergePatch(logging.GlobalLogger(), string(baseBytes), string(overlayBytes)) out, err := strategicMergePatch(logging.GlobalLogger(), string(baseBytes), string(overlayBytes))
assert.NilError(t, err) assert.NilError(t, err)
@ -225,7 +224,6 @@ func Test_GeneratePatches_sortRemovalPatches(t *testing.T) {
fmt.Println(patches) fmt.Println(patches)
assertnew.Nil(t, err) assertnew.Nil(t, err)
assertnew.Equal(t, expectedPatches, patches) assertnew.Equal(t, expectedPatches, patches)
} }
func Test_sortRemovalPatches(t *testing.T) { func Test_sortRemovalPatches(t *testing.T) {

View file

@ -15,6 +15,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/response" "github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/registryclient" "github.com/kyverno/kyverno/pkg/registryclient"
"github.com/kyverno/kyverno/pkg/utils/api"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "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 parentResourceGVR = policyContext.requestResource
} }
patchedResources = append(patchedResources, resourceInfo{ 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()) 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 { 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 { } 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 { if ruleResponse != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp) resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResponse)
if ruleResp.Status == response.RuleStatusError { if ruleResponse.Status == response.RuleStatusError {
incrementErrorCount(resp) incrementErrorCount(resp)
} else { } else {
incrementAppliedCount(resp) incrementAppliedCount(resp)
@ -154,81 +168,81 @@ func Mutate(ctx context.Context, rclient registryclient.Client, policyContext *P
return resp 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()) preconditionsPassed, err := checkPreconditions(logger, ctx, rule.GetAnyAllConditions())
if err != nil { if err != nil {
return ruleError(rule, response.Mutation, "failed to evaluate preconditions", err), resource return mutate.NewErrorResponse("failed to evaluate preconditions", err)
} }
if !preconditionsPassed { 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) return mutate.Mutate(rule, ctx.JSONContext(), resource, logger)
ruleResp := buildRuleResponse(rule, mutateResp, &mutateResp.PatchedResource, subresourceName, parentResourceGVR)
return ruleResp, mutateResp.PatchedResource
} }
func mutateForEach(ctx context.Context, rclient registryclient.Client, rule *kyvernov1.Rule, enginectx *PolicyContext, resource unstructured.Unstructured, subresourceName string, parentResourceGVR metav1.GroupVersionResource, logger logr.Logger) (*response.RuleResponse, unstructured.Unstructured) { type forEachMutator struct {
foreachList := rule.Mutation.ForEachMutation rule *kyvernov1.Rule
if foreachList == nil { policyContext *PolicyContext
return nil, resource 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 var applyCount int
allPatches := make([][]byte, 0) allPatches := make([][]byte, 0)
for _, foreach := range foreachList { for _, foreach := range f.foreach {
if err := LoadContext(ctx, logger, rclient, rule.Context, enginectx, rule.Name); err != nil { if err := LoadContext(ctx, f.log, f.rclient, f.rule.Context, f.policyContext, f.rule.Name); err != nil {
logger.Error(err, "failed to load context") f.log.Error(err, "failed to load context")
return ruleError(rule, response.Mutation, "failed to load context", err), resource 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 { if err != nil {
return ruleError(rule, response.Mutation, "failed to evaluate preconditions", err), resource return mutate.NewErrorResponse("failed to evaluate preconditions", err)
} }
if !preconditionsPassed { 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 { if err != nil {
msg := fmt.Sprintf("failed to evaluate list %s", foreach.List) msg := fmt.Sprintf("failed to evaluate list %s: %v", foreach.List, err)
return ruleError(rule, response.Mutation, msg, err), resource 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 { if mutateResp.Status == response.RuleStatusError {
logger.Error(err, "failed to mutate elements") return mutate.NewErrorResponse("failed to mutate elements", err)
return buildRuleResponse(rule, mutateResp, nil, "", metav1.GroupVersionResource{}), resource
} }
if mutateResp.Status != response.RuleStatusSkip { if mutateResp.Status != response.RuleStatusSkip {
applyCount++ applyCount++
if len(mutateResp.Patches) > 0 { if len(mutateResp.Patches) > 0 {
patchedResource = mutateResp.PatchedResource f.resource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...) allPatches = append(allPatches, mutateResp.Patches...)
} }
} }
} }
msg := fmt.Sprintf("%d elements processed", applyCount)
if applyCount == 0 { 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) return mutate.NewResponse(response.RuleStatusPass, f.resource.unstructured, allPatches, msg)
r.Patches = allPatches
return r, patchedResource
} }
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 { func (f *forEachMutator) mutateElements(ctx context.Context, foreach kyvernov1.ForEachMutation, elements []interface{}) *mutate.Response {
enginectx.jsonContext.Checkpoint() f.policyContext.JSONContext().Checkpoint()
defer enginectx.jsonContext.Restore() defer f.policyContext.JSONContext().Restore()
patchedResource := resource patchedResource := f.resource
var allPatches [][]byte var allPatches [][]byte
if foreach.RawPatchStrategicMerge != nil { if foreach.RawPatchStrategicMerge != nil {
invertedElement(elements) invertedElement(elements)
@ -238,63 +252,79 @@ func mutateElements(ctx context.Context, rclient registryclient.Client, name str
if e == nil { if e == nil {
continue 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) store.SetForeachElement(i)
falseVar := false falseVar := false
if err := addElementToContext(enginectx, e, i, &falseVar); err != nil { if err := addElementToContext(policyContext, e, i, f.nesting, &falseVar); err != nil {
return mutateError(err, fmt.Sprintf("failed to add element to mutate.foreach[%d].context", i)) 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 { if err := LoadContext(ctx, f.log, f.rclient, foreach.Context, policyContext, f.rule.Name); err != nil {
return mutateError(err, fmt.Sprintf("failed to load to mutate.foreach[%d].context", i)) 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 { 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 { if !preconditionsPassed {
logger.Info("mutate.foreach.preconditions not met", "elementIndex", i) f.log.Info("mutate.foreach.preconditions not met", "elementIndex", i)
continue 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 { if mutateResp.Status == response.RuleStatusFail || mutateResp.Status == response.RuleStatusError {
return mutateResp return mutateResp
} }
if len(mutateResp.Patches) > 0 { if len(mutateResp.Patches) > 0 {
patchedResource = mutateResp.PatchedResource patchedResource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...) allPatches = append(allPatches, mutateResp.Patches...)
} }
} }
return &mutate.Response{ return mutate.NewResponse(response.RuleStatusPass, patchedResource.unstructured, allPatches, "")
Status: response.RuleStatusPass,
PatchedResource: patchedResource,
Patches: allPatches,
Message: "foreach mutation applied",
}
} }
func mutateError(err error, message string) *mutate.Response { func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, info resourceInfo) *response.RuleResponse {
return &mutate.Response{ resp := ruleResponse(*rule, response.Mutation, mutateResp.Message, mutateResp.Status)
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)
if resp.Status == response.RuleStatusPass { if resp.Status == response.RuleStatusPass {
resp.Patches = mutateResp.Patches resp.Patches = mutateResp.Patches
resp.Message = buildSuccessMessage(mutateResp.PatchedResource) 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 return resp
} }

View file

@ -93,7 +93,8 @@ func Test_VariableSubstitutionPatchStrategicMerge(t *testing.T) {
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext)
t.Log(string(expectedPatch)) t.Log(string(expectedPatch))
@ -166,7 +167,8 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) {
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1) assert.Equal(t, len(er.PolicyResponse.Rules), 1)
assert.Assert(t, strings.Contains(er.PolicyResponse.Rules[0].Message, "Unknown key \"name1\" in path")) 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 var policy kyverno.ClusterPolicy
err := json.Unmarshal(policyRaw, &policy) err := json.Unmarshal(policyRaw, &policy)
assert.NilError(t, err) assert.NilError(t, err)
@ -1013,22 +1038,127 @@ func Test_foreach_order_mutation_(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
er := Mutate(context.TODO(), registryclient.NewOrDie(), policyContext) 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, len(er.PolicyResponse.Rules), 1)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusPass) 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) assert.NilError(t, err)
for _, e := range tlsArr {
for i, c := range containers { tls := e.(map[string]interface{})
ctnr := c.(map[string]interface{}) hosts := tls["hosts"].([]interface{})
switch i { for _, h := range hosts {
case 0: s := h.(string)
assert.Equal(t, ctnr["name"], "mongod") assert.Assert(t, strings.HasSuffix(s, ".newfoo.com"))
case 1:
assert.Equal(t, ctnr["name"], "nginx")
case 3:
assert.Equal(t, ctnr["name"], "mongodb-agent")
} }
} }
} }

View file

@ -5,7 +5,6 @@ import (
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/clients/dclient" "github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/config"
"github.com/kyverno/kyverno/pkg/engine/context"
enginectx "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/engine/context/resolvers"
"github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/utils"
@ -54,7 +53,7 @@ type PolicyContext struct {
excludeResourceFunc ExcludeFunc excludeResourceFunc ExcludeFunc
// jsonContext is the variable context // jsonContext is the variable context
jsonContext context.Interface jsonContext enginectx.Interface
// namespaceLabels stores the label of namespace to be processed by namespace selector // namespaceLabels stores the label of namespace to be processed by namespace selector
namespaceLabels map[string]string namespaceLabels map[string]string
@ -95,7 +94,7 @@ func (c *PolicyContext) AdmissionInfo() kyvernov1beta1.RequestInfo {
return c.admissionInfo return c.admissionInfo
} }
func (c *PolicyContext) JSONContext() context.Interface { func (c *PolicyContext) JSONContext() enginectx.Interface {
return c.jsonContext return c.jsonContext
} }
@ -193,7 +192,7 @@ func (c *PolicyContext) WithSubresourcesInPolicy(subresourcesInPolicy []struct {
// Constructors // Constructors
func NewPolicyContextWithJsonContext(jsonContext context.Interface) *PolicyContext { func NewPolicyContextWithJsonContext(jsonContext enginectx.Interface) *PolicyContext {
return &PolicyContext{ return &PolicyContext{
jsonContext: jsonContext, jsonContext: jsonContext,
excludeGroupRole: []string{}, excludeGroupRole: []string{},
@ -204,7 +203,7 @@ func NewPolicyContextWithJsonContext(jsonContext context.Interface) *PolicyConte
} }
func NewPolicyContext() *PolicyContext { func NewPolicyContext() *PolicyContext {
return NewPolicyContextWithJsonContext(context.NewContext()) return NewPolicyContextWithJsonContext(enginectx.NewContext())
} }
func NewPolicyContextFromAdmissionRequest( func NewPolicyContextFromAdmissionRequest(

View file

@ -468,22 +468,6 @@ func ruleResponse(rule kyvernov1.Rule, ruleType response.RuleType, msg string, s
return resp 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) { func incrementAppliedCount(resp *response.EngineResponse) {
resp.PolicyResponse.RulesAppliedCount++ resp.PolicyResponse.RulesAppliedCount++
} }

View file

@ -1867,7 +1867,6 @@ func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment", "Pods"}, 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 { 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) t.Errorf("Testcase has failed due to the following:%v", err)
} }
} }
// Match resource name // Match resource name
@ -1927,7 +1925,6 @@ func TestResourceDescriptionMatch_Name(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -1986,7 +1983,6 @@ func TestResourceDescriptionMatch_GenerateName(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -2046,7 +2042,6 @@ func TestResourceDescriptionMatch_Name_Regex(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -2105,7 +2100,6 @@ func TestResourceDescriptionMatch_GenerateName_Regex(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -2165,7 +2159,6 @@ func TestResourceDescriptionMatch_Label_Expression_NotMatch(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -2233,7 +2226,6 @@ func TestResourceDescriptionMatch_Label_Expression_Match(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -2303,7 +2295,6 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
resource, err := utils.ConvertToUnstructured(rawResource) resource, err := utils.ConvertToUnstructured(rawResource)
if err != nil { if err != nil {
t.Errorf("unable to convert raw resource to unstructured: %v", err) t.Errorf("unable to convert raw resource to unstructured: %v", err)
} }
resourceDescription := v1.ResourceDescription{ resourceDescription := v1.ResourceDescription{
Kinds: []string{"Deployment"}, Kinds: []string{"Deployment"},
@ -2331,8 +2322,10 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
}, },
} }
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}, rule := v1.Rule{
ExcludeResources: v1.MatchResources{ResourceDescription: resourceDescriptionExclude}} 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 { 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") 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) { func TestWildCardLabels(t *testing.T) {
testSelector(t, &metav1.LabelSelector{}, map[string]string{}, true) testSelector(t, &metav1.LabelSelector{}, map[string]string{}, true)
testSelector(t, &metav1.LabelSelector{}, map[string]string{"foo": "bar"}, 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) { func TestWildCardAnnotation(t *testing.T) {
// test single annotation values // test single annotation values
testAnnotationMatch(t, map[string]string{}, map[string]string{}, true) testAnnotationMatch(t, map[string]string{}, map[string]string{}, true)
testAnnotationMatch(t, map[string]string{"test/*": "*"}, map[string]string{}, false) testAnnotationMatch(t, map[string]string{"test/*": "*"}, map[string]string{}, false)

View file

@ -1667,7 +1667,8 @@ func testMatchPattern(t *testing.T, testCase struct {
pattern []byte pattern []byte
resource []byte resource []byte
status response.RuleStatus status response.RuleStatus
}) { },
) {
var pattern, resource interface{} var pattern, resource interface{}
err := json.Unmarshal(testCase.pattern, &pattern) err := json.Unmarshal(testCase.pattern, &pattern)
assert.NilError(t, err) 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)) 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 { } 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)) assert.Assert(t, err == nil, fmt.Sprintf("\nexpected error - test: %s\npattern: %s\nresource: %s\n", testCase.name, pattern, resource))
} }
} }

View file

@ -21,6 +21,7 @@ import (
"github.com/kyverno/kyverno/pkg/pss" "github.com/kyverno/kyverno/pkg/pss"
"github.com/kyverno/kyverno/pkg/registryclient" "github.com/kyverno/kyverno/pkg/registryclient"
"github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/utils"
"github.com/kyverno/kyverno/pkg/utils/api"
"github.com/pkg/errors" "github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
@ -147,12 +148,8 @@ func validateResource(ctx context.Context, log logr.Logger, rclient registryclie
return resp return resp
} }
func processValidationRule(ctx context.Context, log logr.Logger, rclient registryclient.Client, enginectx *PolicyContext, rule *kyvernov1.Rule) *response.RuleResponse { func processValidationRule(ctx context.Context, log logr.Logger, rclient registryclient.Client, policyContext *PolicyContext, rule *kyvernov1.Rule) *response.RuleResponse {
v := newValidator(log, rclient, enginectx, rule) v := newValidator(log, rclient, policyContext, rule)
if rule.Validation.ForEachValidation != nil {
return v.validateForEach(ctx)
}
return v.validate(ctx) return v.validate(ctx)
} }
@ -172,7 +169,7 @@ func addRuleResponse(log logr.Logger, resp *response.EngineResponse, ruleResp *r
type validator struct { type validator struct {
log logr.Logger log logr.Logger
ctx *PolicyContext policyContext *PolicyContext
rule *kyvernov1.Rule rule *kyvernov1.Rule
contextEntries []kyvernov1.ContextEntry contextEntries []kyvernov1.ContextEntry
anyAllConditions apiextensions.JSON anyAllConditions apiextensions.JSON
@ -180,7 +177,9 @@ type validator struct {
anyPattern apiextensions.JSON anyPattern apiextensions.JSON
deny *kyvernov1.Deny deny *kyvernov1.Deny
podSecurity *kyvernov1.PodSecurity podSecurity *kyvernov1.PodSecurity
foreach []kyvernov1.ForEachValidation
rclient registryclient.Client rclient registryclient.Client
nesting int
} }
func newValidator(log logr.Logger, rclient registryclient.Client, ctx *PolicyContext, rule *kyvernov1.Rule) *validator { 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{ return &validator{
log: log, log: log,
rule: ruleCopy, rule: ruleCopy,
ctx: ctx, policyContext: ctx,
rclient: rclient,
contextEntries: ruleCopy.Context, contextEntries: ruleCopy.Context,
anyAllConditions: ruleCopy.GetAnyAllConditions(), anyAllConditions: ruleCopy.GetAnyAllConditions(),
pattern: ruleCopy.Validation.GetPattern(), pattern: ruleCopy.Validation.GetPattern(),
anyPattern: ruleCopy.Validation.GetAnyPattern(), anyPattern: ruleCopy.Validation.GetAnyPattern(),
deny: ruleCopy.Validation.Deny, deny: ruleCopy.Validation.Deny,
podSecurity: ruleCopy.Validation.PodSecurity, 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() ruleCopy := rule.DeepCopy()
anyAllConditions, err := utils.ToMap(foreach.AnyAllConditions) anyAllConditions, err := utils.ToMap(foreach.AnyAllConditions)
if err != nil { 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{ return &validator{
log: log, log: log,
ctx: ctx, policyContext: ctx,
rule: ruleCopy, rule: ruleCopy,
rclient: rclient,
contextEntries: foreach.Context, contextEntries: foreach.Context,
anyAllConditions: anyAllConditions, anyAllConditions: anyAllConditions,
pattern: foreach.GetPattern(), pattern: foreach.GetPattern(),
anyPattern: foreach.GetAnyPattern(), anyPattern: foreach.GetAnyPattern(),
deny: foreach.Deny, deny: foreach.Deny,
rclient: rclient, foreach: nestedForeach,
} nesting: nesting,
}, nil
} }
func (v *validator) validate(ctx context.Context) *response.RuleResponse { 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) 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 { if err != nil {
return ruleError(v.rule, response.Validation, "failed to evaluate preconditions", err) 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() ruleResponse := v.validateResourceWithRule()
return ruleResponse return ruleResponse
} }
if v.podSecurity != nil { if v.podSecurity != nil {
if !isDeleteRequest(v.ctx) { if !isDeleteRequest(v.policyContext) {
ruleResponse := v.validatePodSecurity() ruleResponse := v.validatePodSecurity()
return ruleResponse 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") v.log.V(2).Info("invalid validation rule: podSecurity, patterns, or deny expected")
return nil return nil
} }
func (v *validator) validateForEach(ctx context.Context) *response.RuleResponse { 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 applyCount := 0
if foreachList == nil { for _, foreach := range v.foreach {
return nil elements, err := evaluateList(foreach.List, (v.policyContext.JSONContext()))
}
for _, foreach := range foreachList {
elements, err := evaluateList(foreach.List, v.ctx.jsonContext)
if err != nil { if err != nil {
v.log.V(2).Info("failed to evaluate list", "list", foreach.List, "error", err.Error()) v.log.V(2).Info("failed to evaluate list", "list", foreach.List, "error", err.Error())
continue 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 { if resp.Status != response.RuleStatusPass {
return resp return resp
} }
@ -291,31 +287,42 @@ func (v *validator) validateForEach(ctx context.Context) *response.RuleResponse
} }
if applyCount == 0 { 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 skipped", response.RuleStatusSkip)
} }
return ruleResponse(*v.rule, response.Validation, "rule passed", response.RuleStatusPass) 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) { func (v *validator) validateElements(ctx context.Context, rclient registryclient.Client, foreach kyvernov1.ForEachValidation, elements []interface{}, elementScope *bool) (*response.RuleResponse, int) {
v.ctx.jsonContext.Checkpoint() v.policyContext.jsonContext.Checkpoint()
defer v.ctx.jsonContext.Restore() defer v.policyContext.jsonContext.Restore()
applyCount := 0 applyCount := 0
for i, e := range elements { for i, e := range elements {
if e == nil { if e == nil {
continue continue
} }
store.SetForeachElement(i)
v.ctx.jsonContext.Reset()
enginectx := v.ctx.Copy() // TODO - this needs to be refactored. The engine should not have a dependency to the CLI code
if err := addElementToContext(enginectx, e, i, elementScope); err != nil { 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") v.log.Error(err, "failed to add element to context")
return ruleError(v.rule, response.Validation, "failed to process foreach", err), applyCount 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) r := foreachValidator.validate(ctx)
if r == nil { if r == nil {
v.log.V(2).Info("skip rule due to empty result") 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 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) data, err := variables.DocumentToUntyped(e)
if err != nil { if err != nil {
return err 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) return errors.Wrapf(err, "failed to add element (%v) to JSON context", e)
} }
dataMap, ok := data.(map[string]interface{}) 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 { 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 { if _, ok := err.(gojmespath.NotFoundError); ok {
v.log.V(3).Info("failed to load context", "reason", err.Error()) v.log.V(3).Info("failed to load context", "reason", err.Error())
} else { } else {
@ -390,7 +397,7 @@ func (v *validator) loadContext(ctx context.Context) error {
func (v *validator) validateDeny() *response.RuleResponse { func (v *validator) validateDeny() *response.RuleResponse {
anyAllCond := v.deny.GetAnyAllConditions() 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 { if err != nil {
return ruleError(v.rule, response.Validation, "failed to substitute variables in deny conditions", err) 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) 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 { if deny {
return ruleResponse(*v.rule, response.Validation, v.getDenyMessage(deny), response.RuleStatusFail) 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) 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 { if err != nil {
return msg 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) { 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" { if kind == "DaemonSet" || kind == "Deployment" || kind == "Job" || kind == "StatefulSet" || kind == "ReplicaSet" || kind == "ReplicationController" {
var deployment appsv1.Deployment var deployment appsv1.Deployment
resourceBytes, err := v.ctx.newResource.MarshalJSON() resourceBytes, err := v.policyContext.newResource.MarshalJSON()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -450,7 +457,7 @@ func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta
} else if kind == "CronJob" { } else if kind == "CronJob" {
var cronJob batchv1.CronJob var cronJob batchv1.CronJob
resourceBytes, err := v.ctx.newResource.MarshalJSON() resourceBytes, err := v.policyContext.newResource.MarshalJSON()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -463,7 +470,7 @@ func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta
} else if kind == "Pod" { } else if kind == "Pod" {
var pod corev1.Pod var pod corev1.Pod
resourceBytes, err := v.ctx.newResource.MarshalJSON() resourceBytes, err := v.policyContext.newResource.MarshalJSON()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -508,16 +515,16 @@ func (v *validator) validatePodSecurity() *response.RuleResponse {
} }
func (v *validator) validateResourceWithRule() *response.RuleResponse { func (v *validator) validateResourceWithRule() *response.RuleResponse {
if !isEmptyUnstructured(&v.ctx.element) { if !isEmptyUnstructured(&v.policyContext.element) {
return v.validatePatterns(v.ctx.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") v.log.V(3).Info("skipping validation on deleted resource")
return nil return nil
} }
resp := v.validatePatterns(v.ctx.newResource) resp := v.validatePatterns(v.policyContext.newResource)
return resp 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()) 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 { if sErr != nil {
v.log.V(2).Info("failed to substitute variables in message", "error", sErr) 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()) 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 { func (v *validator) substitutePatterns() error {
if v.pattern != nil { 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 { if err != nil {
return err return err
} }
@ -714,7 +721,7 @@ func (v *validator) substitutePatterns() error {
} }
if v.anyPattern != nil { 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 { if err != nil {
return err return err
} }
@ -731,7 +738,7 @@ func (v *validator) substituteDeny() error {
return nil 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 { if err != nil {
return err return err
} }

View file

@ -1479,7 +1479,8 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1) assert.Equal(t, len(er.PolicyResponse.Rules), 1)
@ -1572,7 +1573,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSu
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1) assert.Equal(t, len(er.PolicyResponse.Rules), 1)
@ -1633,7 +1635,8 @@ func Test_VariableSubstitution_NotOperatorWithStringVariable(t *testing.T) {
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail) 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/") 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{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, len(er.PolicyResponse.Rules), 1) assert.Equal(t, len(er.PolicyResponse.Rules), 1)
@ -1817,7 +1821,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatter
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail) assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
@ -1922,7 +1927,8 @@ func Test_VariableSubstitutionValidate_VariablesInMessageAreResolved(t *testing.
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusFail) 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.") 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{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
for i, rule := range er.PolicyResponse.Rules { 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) { func Test_foreach_context_preconditions(t *testing.T) {
resourceRaw := []byte(`{ resourceRaw := []byte(`{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Deployment", "kind": "Deployment",
@ -2752,7 +2758,6 @@ func Test_foreach_context_preconditions(t *testing.T) {
} }
func Test_foreach_context_preconditions_fail(t *testing.T) { func Test_foreach_context_preconditions_fail(t *testing.T) {
resourceRaw := []byte(`{ resourceRaw := []byte(`{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Deployment", "kind": "Deployment",
@ -2847,7 +2852,6 @@ func Test_foreach_context_preconditions_fail(t *testing.T) {
} }
func Test_foreach_element_validation(t *testing.T) { func Test_foreach_element_validation(t *testing.T) {
resourceRaw := []byte(`{ resourceRaw := []byte(`{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
@ -2895,7 +2899,6 @@ func Test_foreach_element_validation(t *testing.T) {
} }
func Test_outof_foreach_element_validation(t *testing.T) { func Test_outof_foreach_element_validation(t *testing.T) {
resourceRaw := []byte(`{ resourceRaw := []byte(`{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
@ -2938,7 +2941,6 @@ func Test_outof_foreach_element_validation(t *testing.T) {
} }
func Test_foreach_skip_initContainer_pass(t *testing.T) { func Test_foreach_skip_initContainer_pass(t *testing.T) {
resourceRaw := []byte(`{"apiVersion": "v1", resourceRaw := []byte(`{"apiVersion": "v1",
"kind": "Deployment", "kind": "Deployment",
"metadata": {"name": "test"}, "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": { "pattern": {
"image": "trusted-registry.io/*" "image": "trusted-registry.io/*"
} }
@ -2992,6 +2994,102 @@ func Test_foreach_skip_initContainer_pass(t *testing.T) {
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass) 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) { func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string, status response.RuleStatus) {
var policy kyverno.ClusterPolicy var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy)) assert.NilError(t, json.Unmarshal(policyraw, &policy))
@ -3005,7 +3103,8 @@ func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string,
policyContext := &PolicyContext{ policyContext := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext) er := Validate(context.TODO(), registryclient.NewOrDie(), policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, status) 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) { func Test_delete_ignore_pattern(t *testing.T) {
resourceRaw := []byte(`{ resourceRaw := []byte(`{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
@ -3069,7 +3167,8 @@ func Test_delete_ignore_pattern(t *testing.T) {
policyContextCreate := &PolicyContext{ policyContextCreate := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
newResource: *resourceUnstructured} newResource: *resourceUnstructured,
}
engineResponseCreate := Validate(context.TODO(), registryclient.NewOrDie(), policyContextCreate) engineResponseCreate := Validate(context.TODO(), registryclient.NewOrDie(), policyContextCreate)
assert.Equal(t, len(engineResponseCreate.PolicyResponse.Rules), 1) assert.Equal(t, len(engineResponseCreate.PolicyResponse.Rules), 1)
assert.Equal(t, engineResponseCreate.PolicyResponse.Rules[0].Status, response.RuleStatusFail) assert.Equal(t, engineResponseCreate.PolicyResponse.Rules[0].Status, response.RuleStatusFail)
@ -3077,7 +3176,8 @@ func Test_delete_ignore_pattern(t *testing.T) {
policyContextDelete := &PolicyContext{ policyContextDelete := &PolicyContext{
policy: &policy, policy: &policy,
jsonContext: ctx, jsonContext: ctx,
oldResource: *resourceUnstructured} oldResource: *resourceUnstructured,
}
engineResponseDelete := Validate(context.TODO(), registryclient.NewOrDie(), policyContextDelete) engineResponseDelete := Validate(context.TODO(), registryclient.NewOrDie(), policyContextDelete)
assert.Equal(t, len(engineResponseDelete.PolicyResponse.Rules), 0) assert.Equal(t, len(engineResponseDelete.PolicyResponse.Rules), 0)
} }

View file

@ -189,6 +189,7 @@ func Test_variablesub_multiple(t *testing.T) {
t.Error("result does not match") t.Error("result does not match")
} }
} }
func Test_variablesubstitution(t *testing.T) { func Test_variablesubstitution(t *testing.T) {
patternMap := []byte(` patternMap := []byte(`
{ {
@ -277,7 +278,6 @@ func Test_variablesubstitution(t *testing.T) {
} }
func Test_variableSubstitutionValue(t *testing.T) { func Test_variableSubstitutionValue(t *testing.T) {
resourceRaw := []byte(` resourceRaw := []byte(`
{ {
"metadata": { "metadata": {
@ -336,7 +336,6 @@ func Test_variableSubstitutionValue(t *testing.T) {
} }
func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) { func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
resourceRaw := []byte(` resourceRaw := []byte(`
{ {
"metadata": { "metadata": {
@ -396,7 +395,6 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
} }
func Test_variableSubstitutionValueFail(t *testing.T) { func Test_variableSubstitutionValueFail(t *testing.T) {
resourceRaw := []byte(` resourceRaw := []byte(`
{ {
"metadata": { "metadata": {
@ -444,7 +442,6 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
t.Log("expected to fails") t.Log("expected to fails")
t.Fail() t.Fail()
} }
} }
func Test_variableSubstitutionObject(t *testing.T) { func Test_variableSubstitutionObject(t *testing.T) {

View file

@ -15,10 +15,11 @@ import (
"github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/context"
jsonUtils "github.com/kyverno/kyverno/pkg/engine/jsonutils" jsonUtils "github.com/kyverno/kyverno/pkg/engine/jsonutils"
"github.com/kyverno/kyverno/pkg/engine/operator" "github.com/kyverno/kyverno/pkg/engine/operator"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/utils/jsonpointer" "github.com/kyverno/kyverno/pkg/utils/jsonpointer"
) )
var RegexVariables = regexp.MustCompile(`(?:^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`) var RegexVariables = regexp.MustCompile(`(^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`)
var RegexEscpVariables = regexp.MustCompile(`\\\{\{(\{[^{}]*\}|[^{}])*\}\}`) var RegexEscpVariables = regexp.MustCompile(`\\\{\{(\{[^{}]*\}|[^{}])*\}\}`)
@ -30,7 +31,7 @@ var RegexEscpReferences = regexp.MustCompile(`\\\$\(.[^\ ]*\)`)
var regexVariableInit = 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 {{}} // IsVariable returns true if the element contains a 'valid' variable {{}}
func IsVariable(value string) bool { func IsVariable(value string) bool {
@ -569,12 +570,13 @@ func replaceSubstituteVariables(document interface{}) interface{} {
break break
} }
rawDocument = RegexVariables.ReplaceAll(rawDocument, []byte(`placeholderValue`)) rawDocument = RegexVariables.ReplaceAll(rawDocument, []byte(`${1}placeholderValue`))
} }
var output interface{} var output interface{}
err = json.Unmarshal(rawDocument, &output) err = json.Unmarshal(rawDocument, &output)
if err != nil { if err != nil {
logging.Error(err, "failed to unmarshall JSON: %s", string(rawDocument))
return document return document
} }

View file

@ -365,7 +365,7 @@ func Test_subVars_withRegexReplaceAll(t *testing.T) {
func Test_ReplacingPathWhenDeleting(t *testing.T) { func Test_ReplacingPathWhenDeleting(t *testing.T) {
patternRaw := []byte(`"{{request.object.metadata.annotations.target}}"`) patternRaw := []byte(`"{{request.object.metadata.annotations.target}}"`)
var resourceRaw = []byte(` resourceRaw := []byte(`
{ {
"request": { "request": {
"operation": "DELETE", "operation": "DELETE",
@ -408,7 +408,7 @@ func Test_ReplacingPathWhenDeleting(t *testing.T) {
func Test_ReplacingNestedVariableWhenDeleting(t *testing.T) { func Test_ReplacingNestedVariableWhenDeleting(t *testing.T) {
patternRaw := []byte(`"{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`) patternRaw := []byte(`"{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`)
var resourceRaw = []byte(` resourceRaw := []byte(`
{ {
"request":{ "request":{
"operation":"DELETE", "operation":"DELETE",
@ -468,8 +468,8 @@ func Test_SubstituteSuccess(t *testing.T) {
results, err := action(&ju.ActionData{ results, err := action(&ju.ActionData{
Document: nil, Document: nil,
Element: string(patternRaw), Element: string(patternRaw),
Path: "/"}) Path: "/",
})
if err != nil { if err != nil {
t.Errorf("substitution failed: %v", err.Error()) t.Errorf("substitution failed: %v", err.Error())
return return
@ -492,7 +492,8 @@ func Test_SubstituteRecursiveErrors(t *testing.T) {
results, err := action(&ju.ActionData{ results, err := action(&ju.ActionData{
Document: nil, Document: nil,
Element: string(patternRaw), Element: string(patternRaw),
Path: "/"}) Path: "/",
})
if err == nil { if err == nil {
t.Errorf("expected error but received: %v", results) t.Errorf("expected error but received: %v", results)
@ -505,7 +506,8 @@ func Test_SubstituteRecursiveErrors(t *testing.T) {
results, err = action(&ju.ActionData{ results, err = action(&ju.ActionData{
Document: nil, Document: nil,
Element: string(patternRaw), Element: string(patternRaw),
Path: "/"}) Path: "/",
})
if err == nil { if err == nil {
t.Errorf("expected error but received: %v", results) t.Errorf("expected error but received: %v", results)
@ -524,8 +526,8 @@ func Test_SubstituteRecursive(t *testing.T) {
results, err := action(&ju.ActionData{ results, err := action(&ju.ActionData{
Document: nil, Document: nil,
Element: string(patternRaw), Element: string(patternRaw),
Path: "/"}) Path: "/",
})
if err != nil { if err != nil {
t.Errorf("substitution failed: %v", err.Error()) t.Errorf("substitution failed: %v", err.Error())
return return
@ -1146,7 +1148,7 @@ func Test_EscpReferenceSubstitution(t *testing.T) {
func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) { func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
patternRaw := []byte(`"\\{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`) patternRaw := []byte(`"\\{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`)
var resourceRaw = []byte(` resourceRaw := []byte(`
{ {
"request":{ "request":{
"operation":"DELETE", "operation":"DELETE",
@ -1177,3 +1179,38 @@ func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%v", pattern), "{{request.object.metadata.annotations.target}}") 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 }}")
}

View file

@ -6,7 +6,7 @@ import (
) )
func TestExpandInMetadata(t *testing.T) { 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"}) // map[string]string{"test/0": "0"})
testExpand(t, map[string]string{"test/*": "*"}, map[string]string{"test/test": "test"}, testExpand(t, map[string]string{"test/*": "*"}, map[string]string{"test/test": "test"},

View file

@ -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"}]}}}}]}}`), 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, 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() o, _ := NewManager()

View file

@ -31,8 +31,8 @@ func containsUserVariables(policy kyvernov1.PolicyInterface, vars [][]string) er
} }
for _, s := range vars { for _, s := range vars {
for _, banned := range forbidden { for _, banned := range forbidden {
if banned.Match([]byte(s[1])) { if banned.Match([]byte(s[2])) {
return fmt.Errorf("variable %s is not allowed", s[1]) return fmt.Errorf("variable %s is not allowed", s[2])
} }
} }
} }

View file

@ -4,6 +4,9 @@ import (
"fmt" "fmt"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" 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 // Mutate provides implementation to validate 'mutate' rule
@ -21,7 +24,11 @@ func NewMutateFactory(m kyvernov1.Mutation) *Mutate {
// Validate validates the 'mutate' rule // Validate validates the 'mutate' rule
func (m *Mutate) Validate() (string, error) { func (m *Mutate) Validate() (string, error) {
if m.hasForEach() { 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() { if m.hasPatchesJSON6902() && m.hasPatchStrategicMerge() {
@ -31,21 +38,35 @@ func (m *Mutate) Validate() (string, error) {
return "", nil return "", nil
} }
func (m *Mutate) validateForEach() (string, error) { func (m *Mutate) validateForEach(tag string, foreach []kyvernov1.ForEachMutation) (string, error) {
if m.hasPatchStrategicMerge() || m.hasPatchesJSON6902() { for i, fe := range foreach {
return "foreach", fmt.Errorf("only one of `foreach`, `patchStrategicMerge`, or `patchesJson6902` is allowed") 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() psm := fe.GetPatchStrategicMerge()
if (fe.PatchesJSON6902 == "" && psm == nil) || (fe.PatchesJSON6902 != "" && psm != nil) { 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 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 { func (m *Mutate) hasForEach() bool {
return len(m.mutation.ForEachMutation) > 0 return len(m.mutation.ForEachMutation) > 0
} }

26
pkg/utils/api/json.go Normal file
View file

@ -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
}