@@ -2,10 +2,9 @@ package plugins
22
33import (
44 "fmt"
5- "regexp"
6- "strconv"
75 "strings"
86
7+ "github.com/go-openapi/jsonpointer"
98 "sigs.k8s.io/kustomize/api/resmap"
109 "sigs.k8s.io/kustomize/api/resource"
1110 "sigs.k8s.io/yaml"
@@ -18,8 +17,10 @@ type FieldMapping struct {
1817 // DefaultValue is the value to use if SourceValue is empty.
1918 // This provides a fallback mechanism, making transformations more robust.
2019 DefaultValue any `json:"defaultValue,omitempty"`
21- // TargetField is the dot-notation path to the field in the target object.
22- // Supports array indices like "spec.ports[0].port"
20+ // TargetField is the JSON Pointer path to the field in the target object.
21+ // Uses RFC 6901 JSON Pointer syntax like "/spec/ports/0/port"
22+ // Special characters in field names are escaped: ~0 for ~ and ~1 for /
23+ // Example: "/spec/selector/app.kubernetes.io~1instance"
2324 TargetField string `json:"targetField"`
2425 // TargetKind is the kind of resource to apply the transformation to.
2526 TargetKind string `json:"targetKind"`
@@ -43,46 +44,6 @@ type fieldMutator struct {
4344 config FieldMutatorConfig
4445}
4546
46- type fieldSegment struct {
47- name string // field name (e.g., "ports")
48- isArray bool // true if this segment has an array index
49- index int // array index if isArray is true
50- original string // original segment string for error messages
51- }
52-
53- func parseFieldPath (path string ) ([]fieldSegment , error ) {
54- // Regular expression to match field names with optional array indices
55- // Matches: "fieldname" or "fieldname[123]"
56- re := regexp .MustCompile (`^([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]$` )
57-
58- parts := strings .Split (path , "." )
59- segments := make ([]fieldSegment , len (parts ))
60-
61- for i , part := range parts {
62- if matches := re .FindStringSubmatch (part ); matches != nil {
63- index , err := strconv .Atoi (matches [2 ])
64- if err != nil {
65- return nil , fmt .Errorf ("failed to parse array index in field path segment %q: %w" , part , err )
66- }
67- segments [i ] = fieldSegment {
68- name : matches [1 ],
69- isArray : true ,
70- index : index ,
71- original : part ,
72- }
73- } else {
74- segments [i ] = fieldSegment {
75- name : part ,
76- isArray : false ,
77- index : - 1 ,
78- original : part ,
79- }
80- }
81- }
82-
83- return segments , nil
84- }
85-
8647// isEmpty checks if a value is nil or an empty string, slice, or map.
8748func isEmpty (v any ) bool {
8849 if v == nil {
@@ -133,176 +94,116 @@ func (t *fieldMutator) Config(h *resmap.PluginHelpers, _ []byte) error {
13394}
13495
13596// setTargetField modifies the resource by setting the specified value at the
136- // given dot-notation path with support for array indices .
97+ // given JSON Pointer path .
13798func setTargetField (res * resource.Resource , value any , mapping FieldMapping ) error {
13899 yamlBytes , err := res .AsYAML ()
139100 if err != nil {
140101 return fmt .Errorf ("failed to get YAML: %w" , err )
141102 }
142103
143- var data map [ string ] any
104+ var data any
144105 if unmarshalErr := yaml .Unmarshal (yamlBytes , & data ); unmarshalErr != nil {
145106 return fmt .Errorf ("failed to unmarshal YAML: %w" , unmarshalErr )
146107 }
147108
148- segments , err := parseFieldPath (mapping .TargetField )
109+ ptr , err := jsonpointer . New (mapping .TargetField )
149110 if err != nil {
150- return fmt .Errorf ("failed to parse field path %q: %w" , mapping .TargetField , err )
111+ return fmt .Errorf ("failed to parse JSON Pointer path %q: %w" , mapping .TargetField , err )
151112 }
152113
153- // Navigate to parent first, then set value - arrays vs maps need different handling.
154- current , err := navigateToParent (data , segments , mapping .CreateIfNotExists )
155- if err != nil {
156- return err
157- }
158-
159- lastSegment := segments [len (segments )- 1 ]
160- if setErr := setFieldValue (current , lastSegment , value , mapping .CreateIfNotExists ); setErr != nil {
161- return fmt .Errorf ("failed to set field %q: %w" , lastSegment .original , setErr )
114+ var updatedData any
115+ if mapping .CreateIfNotExists {
116+ updatedData , err = setWithPathCreation (data , ptr , value )
117+ } else {
118+ updatedData , err = ptr .Set (data , value )
162119 }
163120
164- // After modifying the map, we must marshal it back to YAML and create a new
165- // resource object to ensure the internal state is consistent.
166- updatedYAML , err := yaml .Marshal (data )
167- if err != nil {
168- return fmt .Errorf ("failed to marshal updated YAML: %w" , err )
169- }
170-
171- rf := resource .NewFactory (nil )
172- newRes , err := rf .FromBytes (updatedYAML )
173121 if err != nil {
174- return fmt .Errorf ("failed to create resource from updated YAML : %w" , err )
122+ return fmt .Errorf ("failed to set field at path %q : %w" , mapping . TargetField , err )
175123 }
176124
177- // Atomically replace the old resource content with the new, fully updated content
178- // to prevent partial updates or data loss.
179- res .ResetRNode (newRes )
180- return nil
125+ return updateResource (res , updatedData )
181126}
182127
183- // navigateToParent walks through all field segments except the last one,
184- // returning the parent container where the final field should be set.
185- func navigateToParent (data map [string ]any , segments []fieldSegment , createIfNotExists bool ) (map [string ]any , error ) {
186- current := data
187- // This loop navigates to the parent of the target field. We must stop one
188- // level short because Golang does not allow taking a pointer to a map value
189- // (e.g., `&myMap["key"]`). To mutate the map, we need a reference to the
190- // parent container to use the `parent[key] = value` syntax.
191- //
192- // This "stop at the parent" approach also gives us the `CreateIfNotExists`
193- // behavior for free, as we can create missing parent maps during traversal.
194- for _ , segment := range segments [:len (segments )- 1 ] {
195- next , navErr := navigateToField (current , segment , createIfNotExists )
196- if navErr != nil {
197- if strings .Contains (navErr .Error (), "failed to find field" ) {
198- return nil , navErr
199- }
200- return nil , fmt .Errorf ("failed to navigate to field %q: %w" , segment .original , navErr )
201- }
202- currentMap , ok := next .(map [string ]any )
203- if ! ok {
204- return nil , fmt .Errorf ("failed to convert field %s to map" , segment .name )
205- }
206- current = currentMap
128+ func setWithPathCreation (data any , ptr jsonpointer.Pointer , value any ) (any , error ) {
129+ // try setting value if the path already exists
130+ if updatedData , err := ptr .Set (data , value ); err == nil {
131+ return updatedData , nil
207132 }
208- return current , nil
209- }
210133
211- // navigateToField returns the value at the specified field, creating it if needed.
212- func navigateToField (current any , segment fieldSegment , createIfNotExists bool ) (any , error ) {
213- currentMap , ok := current .(map [string ]any )
214- if ! ok {
215- return nil , fmt .Errorf ("failed to convert current value to map[string]any, got %T" , current )
134+ // otherwise, we need to create the path first
135+ tokens := ptr .DecodedTokens ()
136+ if len (tokens ) == 0 {
137+ return value , nil
216138 }
139+ result := data
217140
218- next , exists := currentMap [segment .name ]
219- if ! exists {
220- if ! createIfNotExists {
221- return nil , fmt .Errorf ("failed to find field %s" , segment .name )
141+ // JSON Pointer Set() fails if intermediate paths don't exist, so we must
142+ // build the path incrementally from root to target, creating missing containers
143+ for i := 1 ; i <= len (tokens )- 1 ; i ++ {
144+ partialPath := "/" + strings .Join (tokens [:i ], "/" )
145+ partialPtr , err := jsonpointer .New (partialPath )
146+ if err != nil {
147+ return nil , fmt .Errorf ("failed to create partial pointer: %w" , err )
222148 }
149+ // Get() is used as existence test - error means path doesn't exist and needs creation
150+ _ , _ , err = partialPtr .Get (result )
151+ if err != nil {
152+ nextToken := tokens [i ]
153+ var newContainer any
154+ // create array if next token is numeric (e.g., "/ports/0")
155+ if isNumericString (nextToken ) {
156+ newContainer = make ([]any , 0 )
157+ } else {
158+ // create map otherwise (e.g., "/spec/strategy")
159+ newContainer = make (map [string ]any )
160+ }
223161
224- if segment .isArray {
225- next = make ([]any , segment .index + 1 )
226- } else {
227- next = make (map [string ]any )
162+ // create the missing path segment
163+ result , err = partialPtr .Set (result , newContainer )
164+ if err != nil {
165+ return nil , fmt .Errorf ("failed to create intermediate path %q: %w" , partialPath , err )
166+ }
228167 }
229- currentMap [segment .name ] = next
230168 }
231169
232- if segment .isArray {
233- return handleArrayAccess (currentMap , segment , next , createIfNotExists )
234- }
235-
236- return next , nil
237- }
238-
239- // handleArrayAccess navigates to a specific array index, expanding the array and
240- // creating missing elements if needed. Returns the element at the specified index.
241- func handleArrayAccess (currentMap map [string ]any , segment fieldSegment , next any , createIfNotExists bool ) (any , error ) {
242- arr , err := ensureArrayWithCapacity (currentMap , segment , next , createIfNotExists )
170+ result , err := ptr .Set (result , value )
243171 if err != nil {
244- return nil , err
172+ return nil , fmt . Errorf ( "failed to set final value: %w" , err )
245173 }
246174
247- if arr [segment .index ] == nil {
248- if ! createIfNotExists {
249- return nil , fmt .Errorf ("failed to access array element at index %d for field %q" , segment .index , segment .name )
250- }
251- arr [segment .index ] = make (map [string ]any )
252- }
253-
254- return arr [segment .index ], nil
175+ return result , nil
255176}
256177
257- // ensureArrayWithCapacity ensures an array exists at the specified field with sufficient capacity.
258- // If the array doesn't exist or is too small, it creates or expands it as needed.
259- func ensureArrayWithCapacity (currentMap map [string ]any , segment fieldSegment , field any , createIfNotExists bool ) ([]any , error ) {
260- if field == nil {
261- if ! createIfNotExists {
262- return nil , fmt .Errorf ("failed to find array field %q" , segment .name )
263- }
264- arr := make ([]any , segment .index + 1 )
265- currentMap [segment .name ] = arr
266- return arr , nil
267- }
268-
269- arr , ok := field .([]any )
270- if ! ok {
271- return nil , fmt .Errorf ("failed to convert field %q to array, got %T" , segment .name , field )
178+ func updateResource (res * resource.Resource , updatedData any ) error {
179+ // After modifying the map, we must marshal it back to YAML and create a new
180+ // resource object to ensure the internal state is consistent.
181+ updatedYAML , err := yaml .Marshal (updatedData )
182+ if err != nil {
183+ return fmt .Errorf ("failed to marshal updated YAML: %w" , err )
272184 }
273185
274- if segment .index >= len (arr ) {
275- if ! createIfNotExists {
276- return nil , fmt .Errorf ("failed to access array index %d for field %q (length: %d)" , segment .index , segment .name , len (arr ))
277- }
278- newArr := make ([]any , segment .index + 1 )
279- copy (newArr , arr )
280- for i := len (arr ); i <= segment .index ; i ++ {
281- newArr [i ] = make (map [string ]any )
282- }
283- currentMap [segment .name ] = newArr
284- return newArr , nil
186+ rf := resource .NewFactory (nil )
187+ newRes , err := rf .FromBytes (updatedYAML )
188+ if err != nil {
189+ return fmt .Errorf ("failed to create resource from updated YAML: %w" , err )
285190 }
286191
287- return arr , nil
192+ // Atomically replace the old resource content with the new, fully updated content
193+ // to prevent partial updates or data loss.
194+ res .ResetRNode (newRes )
195+ return nil
288196}
289197
290- func setFieldValue ( current any , segment fieldSegment , value any , createIfNotExists bool ) error {
291- currentMap , ok := current .( map [ string ] any )
292- if ! ok {
293- return fmt . Errorf ( "failed to convert current value to map[string]any, got %T" , current )
198+ // isNumericString checks if a string represents a valid non-negative integer.
199+ func isNumericString ( s string ) bool {
200+ if s == "" {
201+ return false
294202 }
295-
296- if segment .isArray {
297- field := currentMap [segment .name ]
298- arr , err := ensureArrayWithCapacity (currentMap , segment , field , createIfNotExists )
299- if err != nil {
300- return err
203+ for _ , r := range s {
204+ if r < '0' || r > '9' {
205+ return false
301206 }
302- arr [segment .index ] = value
303- return nil
304207 }
305-
306- currentMap [segment .name ] = value
307- return nil
208+ return true
308209}
0 commit comments