@@ -2,6 +2,8 @@ package plugins
22
33import (
44 "fmt"
5+ "regexp"
6+ "strconv"
57 "strings"
68
79 "sigs.k8s.io/kustomize/api/resmap"
@@ -17,6 +19,7 @@ type FieldMapping struct {
1719 // This provides a fallback mechanism, making transformations more robust.
1820 DefaultValue any `json:"defaultValue,omitempty"`
1921 // TargetField is the dot-notation path to the field in the target object.
22+ // Supports array indices like "spec.ports[0].port"
2023 TargetField string `json:"targetField"`
2124 // TargetKind is the kind of resource to apply the transformation to.
2225 TargetKind string `json:"targetKind"`
@@ -40,6 +43,46 @@ type fieldMutator struct {
4043 config FieldMutatorConfig
4144}
4245
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+
4386// isEmpty checks if a value is nil or an empty string, slice, or map.
4487func isEmpty (v any ) bool {
4588 if v == nil {
@@ -90,7 +133,7 @@ func (t *fieldMutator) Config(h *resmap.PluginHelpers, _ []byte) error {
90133}
91134
92135// setTargetField modifies the resource by setting the specified value at the
93- // given dot-notation path.
136+ // given dot-notation path with support for array indices .
94137func setTargetField (res * resource.Resource , value any , mapping FieldMapping ) error {
95138 yamlBytes , err := res .AsYAML ()
96139 if err != nil {
@@ -102,35 +145,21 @@ func setTargetField(res *resource.Resource, value any, mapping FieldMapping) err
102145 return fmt .Errorf ("failed to unmarshal YAML: %w" , unmarshalErr )
103146 }
104147
105- // This loop navigates to the parent of the target field. We must stop one
106- // level short because Golang does not allow taking a pointer to a map value
107- // (e.g., `&myMap["key"]`). To mutate the map, we need a reference to the
108- // parent container to use the `parent[key] = value` syntax.
109- //
110- // This "stop at the parent" approach also gives us the `CreateIfNotExists`
111- // behavior for free, as we can create missing parent maps during traversal.
112- fields := strings .Split (mapping .TargetField , "." )
113- current := data
114- for _ , field := range fields [:len (fields )- 1 ] {
115- next , ok := current [field ]
116- if ! ok {
117- if ! mapping .CreateIfNotExists {
118- return fmt .Errorf ("failed to find field %s" , field )
119- }
120- next = make (map [string ]any )
121- current [field ] = next
122- }
123-
124- nextMap , ok := next .(map [string ]any )
125- if ! ok {
126- return fmt .Errorf ("failed to convert field %s to map" , field )
127- }
148+ segments , err := parseFieldPath (mapping .TargetField )
149+ if err != nil {
150+ return fmt .Errorf ("failed to parse field path %q: %w" , mapping .TargetField , err )
151+ }
128152
129- current = nextMap
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
130157 }
131158
132- lastField := fields [len (fields )- 1 ]
133- current [lastField ] = value
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 )
162+ }
134163
135164 // After modifying the map, we must marshal it back to YAML and create a new
136165 // resource object to ensure the internal state is consistent.
@@ -150,3 +179,130 @@ func setTargetField(res *resource.Resource, value any, mapping FieldMapping) err
150179 res .ResetRNode (newRes )
151180 return nil
152181}
182+
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
207+ }
208+ return current , nil
209+ }
210+
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 )
216+ }
217+
218+ next , exists := currentMap [segment .name ]
219+ if ! exists {
220+ if ! createIfNotExists {
221+ return nil , fmt .Errorf ("failed to find field %s" , segment .name )
222+ }
223+
224+ if segment .isArray {
225+ next = make ([]any , segment .index + 1 )
226+ } else {
227+ next = make (map [string ]any )
228+ }
229+ currentMap [segment .name ] = next
230+ }
231+
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 )
243+ if err != nil {
244+ return nil , err
245+ }
246+
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
255+ }
256+
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 )
272+ }
273+
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
285+ }
286+
287+ return arr , nil
288+ }
289+
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 )
294+ }
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
301+ }
302+ arr [segment .index ] = value
303+ return nil
304+ }
305+
306+ currentMap [segment .name ] = value
307+ return nil
308+ }
0 commit comments