Skip to content

Commit e6f980d

Browse files
fix(update): preserve Kptfile formatting during upgrades
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 #4306 Signed-off-by: Jaisheesh-2006 <jaicodes2006@gmail.com>
1 parent 712ada1 commit e6f980d

7 files changed

Lines changed: 591 additions & 39 deletions

File tree

internal/builtins/pkg_context_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"bytes"
1919
"os"
2020
"path/filepath"
21+
"strings"
2122
"testing"
2223

2324
"github.com/google/go-cmp/cmp"
@@ -62,7 +63,9 @@ func TestPkgContextGenerator(t *testing.T) {
6263
if err != test.expErr {
6364
t.Errorf("exp: %v got: %v", test.expErr, err)
6465
}
65-
if diff := cmp.Diff(string(exp), out.String()); diff != "" {
66+
expected := strings.ReplaceAll(string(exp), "\r\n", "\n")
67+
actual := strings.ReplaceAll(out.String(), "\r\n", "\n")
68+
if diff := cmp.Diff(expected, actual); diff != "" {
6669
t.Errorf("pkg context mistmach (-want +got):\n%s", diff)
6770
}
6871
})

internal/util/get/get_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,29 @@ func TestCommand_Run_subdir_symlinks(t *testing.T) {
213213
}.Run(fake.CtxWithPrinter(cliOutput, cliOutput))
214214
assert.NoError(t, err)
215215

216-
// ensure warning for symlink is printed on the CLI
217-
assert.Contains(t, cliOutput.String(), `[Warn] Ignoring symlink "config-symlink"`)
216+
sourceSymlinkPath := filepath.Join(g.DatasetDirectory, testutil.Dataset6, subdir, "config-symlink")
217+
info, statErr := os.Lstat(sourceSymlinkPath)
218+
assert.NoError(t, statErr)
219+
isSymlinkInSource := info.Mode()&os.ModeSymlink != 0
220+
221+
if isSymlinkInSource {
222+
// ensure warning for symlink is printed on the CLI
223+
assert.Contains(t, cliOutput.String(), `[Warn] Ignoring symlink "config-symlink"`)
224+
} else {
225+
// on environments without symlink materialization, there is no ignore warning
226+
assert.NotContains(t, cliOutput.String(), `[Warn] Ignoring symlink "config-symlink"`)
227+
}
218228

219229
// verify the cloned contents do not contains symlinks
220230
diff, err := testutil.Diff(filepath.Join(g.DatasetDirectory, testutil.Dataset6, subdir), absPath, true)
221231
assert.NoError(t, err)
222232
diff = diff.Difference(testutil.KptfileSet)
223-
// original repo contains symlink and cloned doesn't, so the difference
224-
assert.Contains(t, diff.List(), "config-symlink")
233+
if isSymlinkInSource {
234+
// original repo contains symlink and cloned doesn't, so the difference
235+
assert.Contains(t, diff.List(), "config-symlink")
236+
} else {
237+
assert.NotContains(t, diff.List(), "config-symlink")
238+
}
225239

226240
// verify the KptFile contains the expected values
227241
commit, err := g.GetCommit()

pkg/kptfile/kptfileutil/util.go

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ import (
2121
"io"
2222
"os"
2323
"path/filepath"
24+
"reflect"
2425
"slices"
2526
"strings"
2627

2728
"github.com/kptdev/kpt/internal/types"
2829
"github.com/kptdev/kpt/internal/util/git"
2930
kptfilev1 "github.com/kptdev/kpt/pkg/api/kptfile/v1"
3031
"github.com/kptdev/kpt/pkg/lib/errors"
32+
"github.com/kptdev/krm-functions-sdk/go/fn"
3133
"k8s.io/apimachinery/pkg/runtime/schema"
3234
"sigs.k8s.io/kustomize/kyaml/filesys"
3335
"sigs.k8s.io/kustomize/kyaml/sets"
@@ -44,6 +46,13 @@ var SupportedKptfileVersions = []schema.GroupVersionKind{
4446
kptfilev1.KptFileGVK(),
4547
}
4648

49+
var sdkInternalKptfileAnnotations = []string{
50+
"config.kubernetes.io/index",
51+
"internal.config.kubernetes.io/index",
52+
"internal.config.kubernetes.io/path",
53+
"internal.config.kubernetes.io/seqindent",
54+
}
55+
4756
// KptfileError records errors regarding reading or parsing of a Kptfile.
4857
type KptfileError struct {
4958
Path types.UniquePath
@@ -78,6 +87,20 @@ func (e *UnknownKptfileResourceError) Error() string {
7887

7988
func WriteFile(dir string, k any) error {
8089
const op errors.Op = "kptfileutil.WriteFile"
90+
if kf, ok := k.(*kptfilev1.KptFile); ok {
91+
if err := writeKptfilePreservingFormat(dir, kf); err != nil {
92+
return errors.E(op, types.UniquePath(dir), err)
93+
}
94+
return nil
95+
}
96+
97+
if kf, ok := k.(kptfilev1.KptFile); ok {
98+
if err := writeKptfilePreservingFormat(dir, &kf); err != nil {
99+
return errors.E(op, types.UniquePath(dir), err)
100+
}
101+
return nil
102+
}
103+
81104
b, err := yaml.MarshalWithOptions(k, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle})
82105
if err != nil {
83106
return err
@@ -94,6 +117,156 @@ func WriteFile(dir string, k any) error {
94117
return nil
95118
}
96119

120+
func writeKptfilePreservingFormat(dir string, kf *kptfilev1.KptFile) error {
121+
kptfilePath := filepath.Join(dir, kptfilev1.KptFileName)
122+
if _, err := os.Stat(dir); err != nil {
123+
return err
124+
}
125+
126+
content, err := os.ReadFile(kptfilePath)
127+
if err != nil {
128+
if goerrors.Is(err, os.ErrNotExist) {
129+
b, marshalErr := yaml.MarshalWithOptions(kf, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle})
130+
if marshalErr != nil {
131+
return marshalErr
132+
}
133+
return os.WriteFile(kptfilePath, b, 0600)
134+
}
135+
return err
136+
}
137+
138+
existingResources := map[string]string{kptfilev1.KptFileName: string(content)}
139+
existingKptfile, err := fn.NewKptfileFromPackage(existingResources)
140+
if err != nil {
141+
return err
142+
}
143+
if err := applyTypedKptfileToSDK(existingKptfile, kf); err != nil {
144+
return err
145+
}
146+
if err := existingKptfile.WriteToPackage(existingResources); err != nil {
147+
return err
148+
}
149+
return os.WriteFile(kptfilePath, []byte(existingResources[kptfilev1.KptFileName]), 0600)
150+
}
151+
152+
func applyTypedKptfileToSDK(sdkKptfile *fn.Kptfile, desired *kptfilev1.KptFile) error {
153+
if sdkKptfile == nil || sdkKptfile.Obj == nil {
154+
return fmt.Errorf("cannot update empty sdk Kptfile")
155+
}
156+
157+
if err := sdkKptfile.Obj.SetNestedString(desired.APIVersion, "apiVersion"); err != nil {
158+
return err
159+
}
160+
if err := sdkKptfile.Obj.SetNestedString(desired.Kind, "kind"); err != nil {
161+
return err
162+
}
163+
if err := sdkKptfile.Obj.SetNestedString(desired.Name, "metadata", "name"); err != nil {
164+
return err
165+
}
166+
167+
if err := setOrRemoveNestedField(sdkKptfile.Obj, desired.Annotations, "metadata", "annotations"); err != nil {
168+
return err
169+
}
170+
if err := setOrRemoveNestedField(sdkKptfile.Obj, desired.Labels, "metadata", "labels"); err != nil {
171+
return err
172+
}
173+
if err := setOrRemoveNestedField(sdkKptfile.Obj, desired.Pipeline, "pipeline"); err != nil {
174+
return err
175+
}
176+
if err := setOrRemoveNestedField(sdkKptfile.Obj, desired.Info, "info"); err != nil {
177+
return err
178+
}
179+
if err := setOrRemoveNestedField(sdkKptfile.Obj, desired.Inventory, "inventory"); err != nil {
180+
return err
181+
}
182+
if err := setOrRemoveNestedField(sdkKptfile.Obj, desired.Status, "status"); err != nil {
183+
return err
184+
}
185+
186+
if err := setOrRemoveUpstream(sdkKptfile.Obj, desired.Upstream); err != nil {
187+
return err
188+
}
189+
190+
if err := setOrRemoveUpstreamLock(sdkKptfile.Obj, desired.UpstreamLock); err != nil {
191+
return err
192+
}
193+
194+
return nil
195+
}
196+
197+
func setOrRemoveNestedField(obj *fn.KubeObject, val any, fields ...string) error {
198+
if val == nil || reflect.ValueOf(val).IsZero() {
199+
_, err := obj.RemoveNestedField(fields...)
200+
return err
201+
}
202+
return obj.SetNestedField(val, fields...)
203+
}
204+
205+
func setOrRemoveNestedString(obj *fn.KubeObject, value string, fields ...string) error {
206+
if strings.TrimSpace(value) == "" {
207+
_, err := obj.RemoveNestedField(fields...)
208+
return err
209+
}
210+
return obj.SetNestedString(value, fields...)
211+
}
212+
213+
func setOrRemoveUpstream(obj *fn.KubeObject, upstream *kptfilev1.Upstream) error {
214+
if upstream == nil {
215+
_, err := obj.RemoveNestedField("upstream")
216+
return err
217+
}
218+
219+
obj.UpsertMap("upstream")
220+
if err := setOrRemoveNestedString(obj, string(upstream.Type), "upstream", "type"); err != nil {
221+
return err
222+
}
223+
if err := setOrRemoveNestedString(obj, string(upstream.UpdateStrategy), "upstream", "updateStrategy"); err != nil {
224+
return err
225+
}
226+
227+
if upstream.Git == nil {
228+
_, err := obj.RemoveNestedField("upstream", "git")
229+
return err
230+
}
231+
232+
obj.UpsertMap("upstream").UpsertMap("git")
233+
if err := setOrRemoveNestedString(obj, upstream.Git.Repo, "upstream", "git", "repo"); err != nil {
234+
return err
235+
}
236+
if err := setOrRemoveNestedString(obj, upstream.Git.Directory, "upstream", "git", "directory"); err != nil {
237+
return err
238+
}
239+
return setOrRemoveNestedString(obj, upstream.Git.Ref, "upstream", "git", "ref")
240+
}
241+
242+
func setOrRemoveUpstreamLock(obj *fn.KubeObject, upstreamLock *kptfilev1.UpstreamLock) error {
243+
if upstreamLock == nil {
244+
_, err := obj.RemoveNestedField("upstreamLock")
245+
return err
246+
}
247+
248+
obj.UpsertMap("upstreamLock")
249+
if err := setOrRemoveNestedString(obj, string(upstreamLock.Type), "upstreamLock", "type"); err != nil {
250+
return err
251+
}
252+
253+
if upstreamLock.Git == nil {
254+
_, err := obj.RemoveNestedField("upstreamLock", "git")
255+
return err
256+
}
257+
258+
obj.UpsertMap("upstreamLock").UpsertMap("git")
259+
if err := setOrRemoveNestedString(obj, upstreamLock.Git.Repo, "upstreamLock", "git", "repo"); err != nil {
260+
return err
261+
}
262+
if err := setOrRemoveNestedString(obj, upstreamLock.Git.Directory, "upstreamLock", "git", "directory"); err != nil {
263+
return err
264+
}
265+
if err := setOrRemoveNestedString(obj, upstreamLock.Git.Ref, "upstreamLock", "git", "ref"); err != nil {
266+
return err
267+
}
268+
return setOrRemoveNestedString(obj, upstreamLock.Git.Commit, "upstreamLock", "git", "commit")
269+
}
97270
// ValidateInventory returns true and a nil error if the passed inventory
98271
// is valid; otherwiste, false and the reason the inventory is not valid
99272
// is returned. A valid inventory must have a non-empty namespace, name,
@@ -305,12 +478,61 @@ func DecodeKptfile(in io.Reader) (*kptfilev1.KptFile, error) {
305478

306479
d := yaml.NewDecoder(bytes.NewBuffer(c))
307480
d.KnownFields(true)
308-
if err := d.Decode(kf); err != nil {
481+
if err := d.Decode(&kptfilev1.KptFile{}); err != nil {
482+
return kf, err
483+
}
484+
485+
kubeObjects, err := fn.ReadKubeObjectsFromFile(kptfilev1.KptFileName, string(c))
486+
if err != nil {
309487
return kf, err
310488
}
489+
490+
sdkKptfile, err := fn.NewKptfileFromKubeObjectList(kubeObjects)
491+
if err != nil {
492+
return kf, err
493+
}
494+
495+
if err := sdkKptfile.Obj.As(kf); err != nil {
496+
return kf, err
497+
}
498+
499+
for _, key := range sdkInternalKptfileAnnotations {
500+
delete(kf.Annotations, key)
501+
}
502+
if len(kf.Annotations) == 0 {
503+
kf.Annotations = nil
504+
}
505+
311506
return kf, nil
312507
}
313508

509+
// UpdateKptfileContent updates Kptfile YAML content in-memory using SDK Kptfile
510+
// read/write APIs while preserving existing YAML document structure and comments.
511+
func UpdateKptfileContent(content string, mutator func(*kptfilev1.KptFile)) (string, error) {
512+
resources := map[string]string{kptfilev1.KptFileName: content}
513+
sdkKptfile, err := fn.NewKptfileFromPackage(resources)
514+
if err != nil {
515+
return "", err
516+
}
517+
518+
typedKptfile := &kptfilev1.KptFile{}
519+
if err := sdkKptfile.Obj.As(typedKptfile); err != nil {
520+
return "", err
521+
}
522+
523+
mutator(typedKptfile)
524+
525+
if err := applyTypedKptfileToSDK(sdkKptfile, typedKptfile); err != nil {
526+
return "", err
527+
}
528+
529+
if err := sdkKptfile.WriteToPackage(resources); err != nil {
530+
return "", err
531+
}
532+
533+
return resources[kptfilev1.KptFileName], nil
534+
}
535+
314536
// checkKptfileVersion verifies the apiVersion and kind of the resource
315537
// within the Kptfile. If the legacy version is found, the DeprecatedKptfileError
316538
// is returned. If the currently supported apiVersion and kind is found, no

0 commit comments

Comments
 (0)