Skip to content

Commit a548683

Browse files
authored
log the JSON patch document when updating resource (#95)
This patch updates the `resourceReconciler.patchResourceMetadataAndSpec` and `resourceReconciler.patchResourceStatus` methods in a couple ways. First, I ensure that the desired and latest resource parameters are *never* modified by either method by ensuring that only copies of desired and latest are used when calling Patch against the API server. Second, I added some debug-level logging of the exact JSON patch document that is sent over the wire to the Kubernetes API server when patching either the metadata/spec *or* the status. Here is what this looks like in the debug logging output: metadata+spec: ``` 1.65540769296847e+09 DEBUG ackrt >>>> kc.Patch (metadata + spec) {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-u8loelgfy", "is_adopted": false, "generation": 1} 1.6554076929743059e+09 DEBUG ackrt patched resource metadata + spec {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-u8loelgfy", "is_adopted": false, "generation": 1, "json": "{\"metadata\":{\"generation\":2,\"resourceVersion\":\"1049\"},\"spec\":{\"encryptionConfiguration\":{\"encryptionType\":\"AES256\"},\"imageScanningConfiguration\":{\"scanOnPush\":false},\"imageTagMutability\":\"MUTABLE\",\"registryID\":\"750630568209\",\"tags\":null},\"status\":{\"conditions\":null}}"} 1.6554076929743316e+09 DEBUG ackrt <<<< kc.Patch (metadata + spec) {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-u8loelgfy", "is_adopted": false, "generation": 1} ``` status: ``` 1.6554077304127564e+09 DEBUG ackrt >> kc.Patch (status) {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-yq327yxdo", "is_adopted": false, "generation": 5} 1.6554077304208796e+09 DEBUG ackrt patched resource status {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-yq327yxdo", "is_adopted": false, "generation": 5, "json": "{\"metadata\":{\"resourceVersion\":\"1129\"},\"spec\":{\"tags\":[{\"key\":\"k1\",\"value\":\"v1\"}]},\"status\":{\"conditions\":[{\"lastTransitionTime\":\"2022-06-16T19:28:50Z\",\"message\":\"Late initialization successful\",\"reason\":\"Late initialization successful\",\"status\":\"True\",\"type\":\"ACK.LateInitialized\"},{\"lastTransitionTime\":\"2022-06-16T19:28:50Z\",\"message\":\"Resource synced successfully\",\"reason\":\"\",\"status\":\"True\",\"type\":\"ACK.ResourceSynced\"}]}}"} 1.6554077304209113e+09 DEBUG ackrt << kc.Patch (status) {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-yq327yxdo", "is_adopted": false, "generation": 5} 1.655407730420916e+09 DEBUG ackrt < r.patchResourceStatus {"account": "750630568209", "role": "", "region": "us-west-2", "kind": "Repository", "namespace": "default", "name": "ecr-repository-yq327yxdo", "is_adopted": false, "generation": 5} ``` I specifically strip out all of the Kubernetes server-side apply annotations out of the patch document because they are super verbose and are not helpful since we don't use server-side apply. I'm hoping this extra bit of logging information will be useful when attempting to diagnose exactly when and what changes are being applied to resources in the Kubernetes API. Signed-off-by: Jay Pipes <[email protected]> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent a1d54b7 commit a548683

File tree

5 files changed

+93
-26
lines changed

5 files changed

+93
-26
lines changed

mocks/pkg/types/logger.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/runtime/log/noop.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var (
2626
// testing and mocking...
2727
type voidLogger struct{}
2828

29+
func (l *voidLogger) IsDebugEnabled() bool { return false }
2930
func (l *voidLogger) WithValues(...interface{}) {}
3031
func (l *voidLogger) Info(string, ...interface{}) {}
3132
func (l *voidLogger) Debug(string, ...interface{}) {}

pkg/runtime/log/resource.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ type ResourceLogger struct {
3131
blockDepth int
3232
}
3333

34+
// IsDebugEnabled returns true when the underlying logger is configured to
35+
// write debug messages, false otherwise.
36+
func (rl *ResourceLogger) IsDebugEnabled() bool {
37+
return rl.log.V(1).Enabled()
38+
}
39+
3440
// WithValues adapts the internal logger with a set of additional values
3541
func (rl *ResourceLogger) WithValues(
3642
values ...interface{},

pkg/runtime/reconciler.go

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package runtime
1515

1616
import (
1717
"context"
18+
"encoding/json"
1819
"fmt"
1920
"time"
2021

@@ -584,8 +585,46 @@ func (r *resourceReconciler) lateInitializeResource(
584585
return lateInitializedLatest, err
585586
}
586587

587-
// patchResourceMetadataAndSpec patches the custom resource in the Kubernetes API to match the
588-
// supplied latest resource's metadata and spec.
588+
// getPatchDocument returns a JSON string containing the object that will be
589+
// patched in the Kubernetes API server.
590+
//
591+
// NOTE(jaypipes): Because the Kubernetes API server's server-side apply
592+
// functionality introduces an enormous amount of verbose annotations in the
593+
// resource metadata, and because those annotations are pretty unhelpful to
594+
// ACK, we strip all that stuff out of the returned patch document.
595+
func getPatchDocument(
596+
patch client.Patch,
597+
obj client.Object, // the diff of this will be represented in the patch
598+
) string {
599+
js, _ := patch.Data(obj)
600+
var m map[string]interface{}
601+
_ = json.Unmarshal(js, &m)
602+
if md, ok := m["metadata"]; ok {
603+
// Strip out managedFields stuff, since it's super verbose and
604+
// doesn't offer any value to us (since we don't use server-side
605+
// apply
606+
if mv, ok := md.(map[string]interface{}); ok {
607+
if _, ok := mv["managedFields"]; ok {
608+
delete(mv, "managedFields")
609+
}
610+
}
611+
}
612+
js, _ = json.Marshal(m)
613+
return string(js)
614+
}
615+
616+
// patchResourceMetadataAndSpec patches the custom resource in the Kubernetes
617+
// API to match the supplied latest resource's metadata and spec.
618+
//
619+
// NOTE(jaypipes): The latest parameter is *mutated* by this method: the
620+
// resource's metadata.resourceVersion is incremented in the process of calling
621+
// Patch. This is intentional, because without updating the resource's
622+
// metadata.resourceVersion, the resource cannot be passed to Patch again later
623+
// in the reconciliation loop if Patch is called with the Optimistic Locking
624+
// option.
625+
//
626+
// See https://github.com/kubernetes-sigs/controller-runtime/blob/165a8c869c4388b861c7c91cb1e5330f6e07ee16/pkg/client/patch.go#L81-L84
627+
// for more information.
589628
func (r *resourceReconciler) patchResourceMetadataAndSpec(
590629
ctx context.Context,
591630
desired acktypes.AWSResource,
@@ -608,27 +647,29 @@ func (r *resourceReconciler) patchResourceMetadataAndSpec(
608647
}
609648

610649
rlog.Enter("kc.Patch (metadata + spec)")
611-
// Save a copy of the latest object, to reset 'Status' after performing
612-
// the kc.Patch() operation
613-
latestCopy := latest.DeepCopy()
614-
err = r.kc.Patch(
615-
ctx,
616-
latest.RuntimeObject(),
617-
client.MergeFrom(desired.DeepCopy().RuntimeObject()),
618-
)
619-
// Reset the status of latest object after patching.
620-
latest.SetStatus(latestCopy)
621-
rlog.Exit("kc.Patch (metadata + spec)", err)
622-
623-
if err != nil {
624-
return err
650+
dobj := desired.DeepCopy().RuntimeObject()
651+
lorig := latest.DeepCopy()
652+
patch := client.MergeFrom(dobj)
653+
err = r.kc.Patch(ctx, latest.RuntimeObject(), patch)
654+
if err == nil {
655+
if rlog.IsDebugEnabled() {
656+
js := getPatchDocument(patch, lorig.RuntimeObject())
657+
rlog.Debug("patched resource metadata + spec", "json", js)
658+
}
625659
}
626-
rlog.Debug("patched resource metadata and spec", "latest", latest)
627-
return nil
660+
// The call to Patch() above ends up setting the latest variable's Status
661+
// to the value of the desired variable's Status. We do not want this
662+
// behaviour; instead, we want to keep latest's original Status value.
663+
latest.SetStatus(lorig)
664+
rlog.Exit("kc.Patch (metadata + spec)", err)
665+
return err
628666
}
629667

630-
// patchResourceStatus patches the custom resource in the Kubernetes API to match the
631-
// supplied latest resource.
668+
// patchResourceStatus patches the custom resource in the Kubernetes API to
669+
// match the supplied latest resource.
670+
//
671+
// NOTE(jaypipes): We make a copy of both desired and latest parameters to
672+
// avoid mutating either
632673
func (r *resourceReconciler) patchResourceStatus(
633674
ctx context.Context,
634675
desired acktypes.AWSResource,
@@ -642,13 +683,15 @@ func (r *resourceReconciler) patchResourceStatus(
642683
}()
643684

644685
rlog.Enter("kc.Patch (status)")
645-
err = r.kc.Status().Patch(
646-
ctx,
647-
latest.RuntimeObject(),
648-
client.MergeFrom(desired.DeepCopy().RuntimeObject()),
649-
)
686+
dobj := desired.DeepCopy().RuntimeObject()
687+
lobj := latest.DeepCopy().RuntimeObject()
688+
patch := client.MergeFrom(dobj)
689+
err = r.kc.Status().Patch(ctx, lobj, patch)
650690
if err == nil {
651-
rlog.Debug("patched resource status")
691+
if rlog.IsDebugEnabled() {
692+
js := getPatchDocument(patch, lobj)
693+
rlog.Debug("patched resource status", "json", js)
694+
}
652695
} else if apierrors.IsNotFound(err) {
653696
// reset the NotFound error so it is not printed in controller logs
654697
// providing false positive error

pkg/types/logger.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ package types
1616
// Logger is responsible for writing log messages
1717
type Logger interface {
1818
Tracer
19+
// IsDebugEnabled returns true when the underlying logger is configured to
20+
// write debug messages, false otherwise.
21+
IsDebugEnabled() bool
1922
// WithValues adapts the internal logger with a set of additional key/value
2023
// data
2124
WithValues(...interface{})

0 commit comments

Comments
 (0)