fix(update): preserve Kptfile formatting during upgrades#4427
fix(update): preserve Kptfile formatting during upgrades#4427Jaisheesh-2006 wants to merge 1 commit intokptdev:mainfrom
Conversation
✅ Deploy Preview for kptdocs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
This PR addresses Kptfile formatting/comment drift during kpt pkg update by switching upgrade-time Kptfile mutations to an SDK-backed read/modify/write flow intended to preserve YAML structure and comments.
Changes:
- Refactors
kptopsKptfile update helpers to usekptfileutil.UpdateKptfileContentrather than re-marshalling via kyaml. - Adds SDK-based Kptfile write/update logic in
kptfileutilto better preserve formatting/comments. - Hardens tests for cross-platform behavior (CRLF normalization, environment-aware symlink assertions) and adds Kptfile preservation coverage.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/lib/kptops/render_test.go | Normalizes CRLF/LF in golden comparisons to reduce cross-platform diffs. |
| pkg/lib/kptops/clone_test.go | Adds tests asserting UpdateUpstream/UpdateName preserve comments/formatting. |
| pkg/lib/kptops/clone.go | Routes Kptfile mutations through kptfileutil.UpdateKptfileContent to preserve formatting. |
| pkg/kptfile/kptfileutil/util_test.go | Adds tests for comment/format preservation during UpdateKptfile. |
| pkg/kptfile/kptfileutil/util.go | Introduces SDK-based Kptfile write/update utilities and updates decode flow. |
| internal/util/get/get_test.go | Makes symlink assertions conditional on whether symlinks materialize in the test environment. |
| internal/builtins/pkg_context_test.go | Normalizes CRLF/LF for deterministic output comparison. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| expected := strings.ReplaceAll(string(exp), "\r\n", "\n") | ||
| actual := strings.ReplaceAll(out.String(), "\r\n", "\n") | ||
| if diff := cmp.Diff(expected, actual); diff != "" { | ||
| t.Errorf("pkg context mistmach (-want +got):\n%s", diff) |
There was a problem hiding this comment.
Typo in test failure message: "pkg context mistmach" should be "pkg context mismatch".
| t.Errorf("pkg context mistmach (-want +got):\n%s", diff) | |
| t.Errorf("pkg context mismatch (-want +got):\n%s", diff) |
| assert.NotContains(t, cliOutput.String(), `[Warn] Ignoring symlink "config-symlink"`) | ||
| } | ||
|
|
||
| // verify the cloned contents do not contains symlinks |
There was a problem hiding this comment.
Grammar in comment: "verify the cloned contents do not contains symlinks" -> "do not contain symlinks".
| // verify the cloned contents do not contains symlinks | |
| // verify the cloned contents do not contain symlinks |
| directory: package | ||
| ref: v1.0.0 # keep ref inline comment | ||
|
|
||
| upstreamLock: | ||
| type: git |
There was a problem hiding this comment.
This test asserts "PreservesExactFormatting", but the input includes a blank line before upstreamLock: (line 754) while the expected output removes that blank line. Either keep the blank line in the expected output to truly test exact formatting, or rename/relax the test to only validate comment/field preservation.
| typedKptfile := &kptfilev1.KptFile{} | ||
| if err := sdkKptfile.Obj.As(typedKptfile); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| mutator(typedKptfile) | ||
|
|
||
| if err := applyTypedKptfileToSDK(sdkKptfile, typedKptfile); err != nil { | ||
| return "", err |
There was a problem hiding this comment.
UpdateKptfileContent calls sdkKptfile.Obj.As(typedKptfile) but (unlike DecodeKptfile) does not strip SDK-internal annotations (config.kubernetes.io/index, internal.config.kubernetes.io/*). Those keys can then be written back into the user's Kptfile via applyTypedKptfileToSDK, leaking internal metadata and/or creating an empty annotations map. Consider filtering these keys (and nil-ing annotations when empty) before applying/writing.
| if val == nil || reflect.ValueOf(val).IsZero() { | ||
| _, err := obj.RemoveNestedField(fields...) | ||
| return err | ||
| } | ||
| return obj.SetNestedField(val, fields...) | ||
| } | ||
|
|
There was a problem hiding this comment.
setOrRemoveNestedField removes the field only when val is nil or reflect.ValueOf(val).IsZero(). For non-nil but empty maps/slices, IsZero is false, so this can write empty objects (e.g. annotations: {}) and introduce structural/formatting drift. Consider treating empty maps/slices as absent and removing the nested field instead.
| if val == nil || reflect.ValueOf(val).IsZero() { | |
| _, err := obj.RemoveNestedField(fields...) | |
| return err | |
| } | |
| return obj.SetNestedField(val, fields...) | |
| } | |
| if isEmptyValue(val) { | |
| _, err := obj.RemoveNestedField(fields...) | |
| return err | |
| } | |
| return obj.SetNestedField(val, fields...) | |
| } | |
| func isEmptyValue(val any) bool { | |
| if val == nil { | |
| return true | |
| } | |
| v := reflect.ValueOf(val) | |
| if !v.IsValid() { | |
| return true | |
| } | |
| // Treat nil pointers as empty. | |
| if v.Kind() == reflect.Ptr { | |
| if v.IsNil() { | |
| return true | |
| } | |
| v = v.Elem() | |
| } | |
| // Treat empty maps, slices, and arrays as empty. | |
| switch v.Kind() { | |
| case reflect.Map, reflect.Slice, reflect.Array: | |
| if v.Len() == 0 { | |
| return true | |
| } | |
| } | |
| return v.IsZero() | |
| } |
e6f980d to
ffd9c47
Compare
Use SDK-backed Kptfile read/update flow in upgrade paths, remove legacy rewrite behavior from kptops helpers, and harden tests for cross-platform stability. Fixes kptdev#4306 Signed-off-by: Jaisheesh-2006 <jaicodes2006@gmail.com>
ffd9c47 to
a4335c5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| typedKptfile := &kptfilev1.KptFile{} | ||
| if err := sdkKptfile.Obj.As(typedKptfile); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| mutator(typedKptfile) | ||
|
|
||
| if err := applyTypedKptfileToSDK(sdkKptfile, typedKptfile); err != nil { | ||
| return "", err | ||
| } |
There was a problem hiding this comment.
UpdateKptfileContent reads a Kptfile via the SDK (Obj.As) but does not strip the SDK-internal metadata annotations (config.kubernetes.io/index, internal.config.kubernetes.io/*) the way DecodeKptfile does. If those keys are present after As, applyTypedKptfileToSDK will write them back out, causing internal annotations to leak into user Kptfiles. Consider deleting sdkInternalKptfileAnnotations (and nil-ing the map when empty) on typedKptfile before applying the mutator / writing.
| func UpdateKptfileContent(content string, mutator func(*kptfilev1.KptFile)) (string, error) { | ||
| resources := map[string]string{kptfilev1.KptFileName: content} | ||
| sdkKptfile, err := fn.NewKptfileFromPackage(resources) | ||
| if err != nil { | ||
| return "", err | ||
| } |
There was a problem hiding this comment.
UpdateKptfileContent bypasses the version/Kind validation done by DecodeKptfile (checkKptfileVersion + KnownFields decode). Since clone/update paths previously went through DecodeKptfile, this can change behavior by allowing updates to deprecated/unknown Kptfile versions (or structurally invalid content) instead of failing early. Consider reusing checkKptfileVersion (and, if needed, the KnownFields validation) before calling fn.NewKptfileFromPackage to keep error behavior consistent.
Description
This PR fixes Kptfile formatting drift during package upgrades by switching upgrade-time Kptfile handling to an SDK-backed read/update flow and removing legacy rewrite logic from
kptopshelpers.Motivation
During
kpt pkg update, Kptfile rewrites could alter user formatting/comments in ways that were not intended.Issue #4306 tracks this regression and expects upgrade behavior to preserve user-authored Kptfile structure as much as possible.
What Changed
kptfileutil.pkg/lib/kptopshelper flows, reusing the centralized updater.Scope
This change targets upgrade and related Kptfile mutation paths only, with no intended functional change to unrelated package/resource update behavior.
Testing
Validated with targeted and package-level tests, including:
go test ./pkg/kptfile/... -count=1go test ./pkg/lib/kptops -count=1go test ./commands/pkg/update -count=1go test ./internal/util/update/... -count=1Issue
Fixes #4306