@@ -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.
4857type KptfileError struct {
4958 Path types.UniquePath
@@ -78,6 +87,20 @@ func (e *UnknownKptfileResourceError) Error() string {
7887
7988func 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