1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-13 19:28:55 +00:00

Support @ for mutate targets (#3998)

Signed-off-by: ShutingZhao <shuting@nirmata.com>
This commit is contained in:
shuting 2022-05-24 20:19:36 +08:00 committed by GitHub
parent c9f8a68d8a
commit 85b486eb27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 384 additions and 179 deletions

View file

@ -49,6 +49,9 @@ type Interface interface {
// AddOldResource merges resource json under request.oldObject
AddOldResource(data map[string]interface{}) error
// AddTargetResource merges resource json under target
AddTargetResource(data map[string]interface{}) error
// AddUserInfo merges userInfo json under kyverno.userInfo
AddUserInfo(userInfo kyvernov1beta1.RequestInfo) error
@ -165,6 +168,11 @@ func (ctx *context) AddOldResource(data map[string]interface{}) error {
return addToContext(ctx, data, "request", "oldObject")
}
// AddTargetResource adds data at path: target
func (ctx *context) AddTargetResource(data map[string]interface{}) error {
return addToContext(ctx, data, "target")
}
// AddUserInfo adds userInfo at path request.userInfo
func (ctx *context) AddUserInfo(userRequestInfo kyvernov1beta1.RequestInfo) error {
return addToContext(ctx, userRequestInfo, "request")

View file

@ -97,6 +97,14 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
continue
}
if !policyContext.AdmissionOperation && rule.IsMutateExisting() {
policyContext := policyContext.Copy()
if err := policyContext.JSONContext.AddTargetResource(patchedResource.Object); err != nil {
log.Log.Error(err, "failed to add target resource to the context")
continue
}
}
logger.V(4).Info("apply rule to resource", "rule", rule.Name, "resource namespace", patchedResource.GetNamespace(), "resource name", patchedResource.GetName())
var ruleResp *response.RuleResponse
if rule.Mutation.ForEachMutation != nil {

View file

@ -1035,134 +1035,220 @@ func Test_mutate_existing_resources(t *testing.T) {
name string
policy []byte
trigger []byte
target []byte
targets [][]byte
targetList string
patches []string
}{
{
name: "test-different-trigger-target",
policy: []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-post-mutation"
},
"spec": {
"rules": [
{
"name": "mutate-deploy-on-configmap-update",
"match": {
"any": [
{
"resources": {
"kinds": [
"ConfigMap"
],
"names": [
"dictionary"
],
"namespaces": [
"staging"
]
}
}
]
},
"preconditions": {
"any": [
{
"key": "{{ request.object.data.foo }}",
"operator": "Equals",
"value": "bar"
}
]
},
"mutate": {
"targets": [
{
"apiVersion": "v1",
"kind": "Deployment",
"name": "example-A",
"namespace": "staging"
}
],
"patchStrategicMerge": {
"metadata": {
"labels": {
"foo": "bar"
}
}
}
}
}
]
}
}`),
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-post-mutation"
},
"spec": {
"rules": [
{
"name": "mutate-deploy-on-configmap-update",
"match": {
"any": [
{
"resources": {
"kinds": [
"ConfigMap"
],
"names": [
"dictionary"
],
"namespaces": [
"staging"
]
}
}
]
},
"preconditions": {
"any": [
{
"key": "{{ request.object.data.foo }}",
"operator": "Equals",
"value": "bar"
}
]
},
"mutate": {
"targets": [
{
"apiVersion": "v1",
"kind": "Deployment",
"name": "example-A",
"namespace": "staging"
}
],
"patchStrategicMerge": {
"metadata": {
"labels": {
"foo": "bar"
}
}
}
}
}
]
}
}`),
trigger: []byte(`{
"apiVersion": "v1",
"data": {
"foo": "bar"
},
"kind": "ConfigMap",
"metadata": {
"name": "dictionary",
"namespace": "staging"
}
}`),
target: []byte(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "example-A",
"namespace": "staging",
"labels": {
"app": "nginx"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:1.14.2",
"ports": [
{
"containerPort": 80
}
]
}
]
}
}
}
}`),
"apiVersion": "v1",
"data": {
"foo": "bar"
},
"kind": "ConfigMap",
"metadata": {
"name": "dictionary",
"namespace": "staging"
}
}`),
targets: [][]byte{[]byte(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "example-A",
"namespace": "staging",
"labels": {
"app": "nginx"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:1.14.2",
"ports": [
{
"containerPort": 80
}
]
}
]
}
}
}
}`)},
targetList: "DeploymentList",
patches: []string{`{"op":"add","path":"/metadata/labels/foo","value":"bar"}`},
},
{
name: "test-same-trigger-target",
policy: []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-post-mutation"
},
"spec": {
"rules": [
{
"name": "mutate-deploy-on-configmap-update",
"match": {
"any": [
{
"resources": {
"kinds": [
"ConfigMap"
],
"names": [
"dictionary"
],
"namespaces": [
"staging"
]
}
}
]
},
"preconditions": {
"any": [
{
"key": "{{ request.object.data.foo }}",
"operator": "Equals",
"value": "bar"
}
]
},
"mutate": {
"targets": [
{
"apiVersion": "v1",
"kind": "ConfigMap",
"name": "dictionary",
"namespace": "staging"
}
],
"patchStrategicMerge": {
"metadata": {
"labels": {
"foo": "bar"
}
}
}
}
}
]
}
}`),
trigger: []byte(`{
"apiVersion": "v1",
"data": {
"foo": "bar"
},
"kind": "ConfigMap",
"metadata": {
"name": "dictionary",
"namespace": "staging"
}
}`),
targets: [][]byte{[]byte(`{
"apiVersion": "v1",
"data": {
"foo": "bar"
},
"kind": "ConfigMap",
"metadata": {
"name": "dictionary",
"namespace": "staging"
}
}`)},
targetList: "ComfigMapList",
patches: []string{`{"op":"add","path":"/metadata/labels","value":{"foo":"bar"}}`},
},
{
name: "test-in-place-variable",
policy: []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-post-mutation"
"name": "sync-cms"
},
"spec": {
"mutateExistingOnPolicyUpdate": false,
"rules": [
{
"name": "mutate-deploy-on-configmap-update",
"name": "concat-cm",
"match": {
"any": [
{
@ -1171,72 +1257,168 @@ func Test_mutate_existing_resources(t *testing.T) {
"ConfigMap"
],
"names": [
"dictionary"
"cmone"
],
"namespaces": [
"staging"
"foo"
]
}
}
]
},
"preconditions": {
"any": [
{
"key": "{{ request.object.data.foo }}",
"operator": "Equals",
"value": "bar"
}
]
},
"mutate": {
"targets": [
{
"apiVersion": "v1",
"kind": "ConfigMap",
"name": "dictionary",
"namespace": "staging"
"name": "cmtwo",
"namespace": "bar"
}
],
"patchStrategicMerge": {
"metadata": {
"labels": {
"foo": "bar"
}
"data": {
"keytwo": "{{@}}-{{request.object.data.keyone}}"
}
}
}
}
]
}
}`),
trigger: []byte(`{
"apiVersion": "v1",
"data": {
"foo": "bar"
},
"kind": "ConfigMap",
"metadata": {
"name": "dictionary",
"namespace": "staging"
}
}`),
target: []byte(`{
"apiVersion": "v1",
"data": {
"foo": "bar"
},
"kind": "ConfigMap",
"metadata": {
"name": "dictionary",
"namespace": "staging"
`),
trigger: []byte(`
{
"apiVersion": "v1",
"data": {
"keyone": "valueone"
},
"kind": "ConfigMap",
"metadata": {
"name": "cmone",
"namespace": "foo"
}
}
}`),
`),
targets: [][]byte{[]byte(`
{
"apiVersion": "v1",
"data": {
"keytwo": "valuetwo"
},
"kind": "ConfigMap",
"metadata": {
"name": "cmtwo",
"namespace": "bar"
}
}
`)},
targetList: "ComfigMapList",
patches: []string{`{"op":"add","path":"/metadata/labels","value":{"foo":"bar"}}`},
patches: []string{`{"op":"replace","path":"/data/keytwo","value":"valuetwo-valueone"}`},
},
{
name: "test-in-place-variable",
policy: []byte(`
{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "sync-cms"
},
"spec": {
"mutateExistingOnPolicyUpdate": false,
"rules": [
{
"name": "concat-cm",
"match": {
"any": [
{
"resources": {
"kinds": [
"ConfigMap"
],
"names": [
"cmone"
],
"namespaces": [
"foo"
]
}
}
]
},
"mutate": {
"targets": [
{
"apiVersion": "v1",
"kind": "ConfigMap",
"name": "cmtwo",
"namespace": "bar"
},
{
"apiVersion": "v1",
"kind": "ConfigMap",
"name": "cmthree",
"namespace": "bar"
}
],
"patchStrategicMerge": {
"data": {
"key": "{{@}}-{{request.object.data.keyone}}"
}
}
}
}
]
}
}
`),
trigger: []byte(`
{
"apiVersion": "v1",
"data": {
"keyone": "valueone"
},
"kind": "ConfigMap",
"metadata": {
"name": "cmone",
"namespace": "foo"
}
}
`),
targets: [][]byte{
[]byte(`
{
"apiVersion": "v1",
"data": {
"key": "valuetwo"
},
"kind": "ConfigMap",
"metadata": {
"name": "cmtwo",
"namespace": "bar"
}
}
`),
[]byte(`
{
"apiVersion": "v1",
"data": {
"key": "valuethree"
},
"kind": "ConfigMap",
"metadata": {
"name": "cmthree",
"namespace": "bar"
}
}
`),
},
targetList: "ComfigMapList",
patches: []string{`{"op":"replace","path":"/data/key","value":"valuetwo-valueone"}`, `{"op":"replace","path":"/data/key","value":"valuethree-valueone"}`},
},
}
var policyContext *PolicyContext
for _, test := range tests {
var policy kyverno.ClusterPolicy
err := json.Unmarshal(test.policy, &policy)
@ -1245,38 +1427,41 @@ func Test_mutate_existing_resources(t *testing.T) {
trigger, err := utils.ConvertToUnstructured(test.trigger)
assert.NilError(t, err)
target, err := utils.ConvertToUnstructured(test.target)
assert.NilError(t, err)
for _, target := range test.targets {
target, err := utils.ConvertToUnstructured(target)
assert.NilError(t, err)
ctx := context.NewContext()
err = ctx.AddResource(trigger.Object)
assert.NilError(t, err)
ctx := context.NewContext()
err = ctx.AddResource(trigger.Object)
assert.NilError(t, err)
gvrToListKind := map[schema.GroupVersionResource]string{
{Group: target.GroupVersionKind().Group, Version: target.GroupVersionKind().Version, Resource: target.GroupVersionKind().Kind}: test.targetList,
gvrToListKind := map[schema.GroupVersionResource]string{
{Group: target.GroupVersionKind().Group, Version: target.GroupVersionKind().Version, Resource: target.GroupVersionKind().Kind}: test.targetList,
}
objects := []runtime.Object{target}
scheme := runtime.NewScheme()
dclient, err := client.NewMockClient(scheme, gvrToListKind, objects...)
assert.NilError(t, err)
dclient.SetDiscovery(client.NewFakeDiscoveryClient(nil))
_, err = dclient.GetResource(target.GetAPIVersion(), target.GetKind(), target.GetNamespace(), target.GetName())
assert.NilError(t, err)
policyContext = &PolicyContext{
Client: dclient,
Policy: &policy,
JSONContext: ctx,
NewResource: *trigger,
}
}
objects := []runtime.Object{target}
scheme := runtime.NewScheme()
dclient, err := client.NewMockClient(scheme, gvrToListKind, objects...)
assert.NilError(t, err)
dclient.SetDiscovery(client.NewFakeDiscoveryClient(nil))
_, err = dclient.GetResource(target.GetAPIVersion(), target.GetKind(), target.GetNamespace(), target.GetName())
assert.NilError(t, err)
policyContext := &PolicyContext{
Client: dclient,
Policy: &policy,
JSONContext: ctx,
NewResource: *trigger,
}
er := Mutate(policyContext)
for _, rr := range er.PolicyResponse.Rules {
assert.Equal(t, test.patches[0], string(rr.Patches[0]))
assert.Equal(t, rr.Status, response.RuleStatusPass, rr.Status)
for i, p := range rr.Patches {
assert.Equal(t, test.patches[i], string(p), "test %s failed:\nGot %s\nExpected: %s", test.name, rr.Patches[i], test.patches[i])
assert.Equal(t, rr.Status, response.RuleStatusPass, rr.Status)
}
}
}
}

View file

@ -285,7 +285,7 @@ func substituteReferencesIfAny(log logr.Logger) jsonUtils.Action {
}
if resolvedReference == nil {
return data.Element, fmt.Errorf("failed to resolve %v at path %s: %v", v, data.Path, err)
return data.Element, fmt.Errorf("got nil resolved variable %v at path %s: %v", v, data.Path, err)
}
log.V(3).Info("reference resolved", "reference", v, "value", resolvedReference, "path", data.Path)
@ -348,12 +348,16 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
variable := replaceBracesAndTrimSpaces(v)
if variable == "@" {
pathPrefix := "target"
if _, err := ctx.Query("target"); err != nil {
pathPrefix = "request.object"
}
path := getJMESPath(data.Path)
var val string
if strings.HasPrefix(path, "[") {
val = fmt.Sprintf("request.object%s", path)
val = fmt.Sprintf("%s%s", pathPrefix, path)
} else {
val = fmt.Sprintf("request.object.%s", path)
val = fmt.Sprintf("%s.%s", pathPrefix, path)
}
variable = strings.Replace(variable, "@", val, -1)

View file

@ -31,9 +31,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
var allowedVariables = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images\.|([a-z_0-9]+\()[^{}]`)
var allowedVariables = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images\.|target\.|([a-z_0-9]+\()[^{}]`)
var allowedVariablesBackground = regexp.MustCompile(`request\.|element|elementIndex|@|images\.|([a-z_0-9]+\()[^{}]`)
var allowedVariablesBackground = regexp.MustCompile(`request\.|element|elementIndex|@|images\.|target\.|([a-z_0-9]+\()[^{}]`)
// wildCardAllowedVariables represents regex for the allowed fields in wildcards
var wildCardAllowedVariables = regexp.MustCompile(`\{\{\s*(request\.|serviceAccountName|serviceAccountNamespace)[^{}]*\}\}`)
@ -405,7 +405,7 @@ func ValidateOnPolicyUpdate(p kyvernov1.PolicyInterface, onPolicyUpdate bool) er
}
if err := hasInvalidVariables(p, onPolicyUpdate); err != nil {
return fmt.Errorf("policy contains invalid variables: %s", err.Error())
return fmt.Errorf("update event, policy contains invalid variables: %s", err.Error())
}
if err := containsUserVariables(p, vars); err != nil {