Skip to content

Commit 825c1cf

Browse files
committed
feat: preserve formatting and comments when updating Helm values file
1 parent add409e commit 825c1cf

File tree

4 files changed

+218
-141
lines changed

4 files changed

+218
-141
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/bradleyfalzon/ghinstallation/v2 v2.12.0
1414
github.com/distribution/distribution/v3 v3.0.0-20230722181636-7b502560cad4
1515
github.com/go-git/go-git/v5 v5.13.2
16+
github.com/goccy/go-yaml v1.15.22
1617
github.com/google/uuid v1.6.0
1718
github.com/patrickmn/go-cache v2.1.0+incompatible
1819
github.com/prometheus/client_golang v1.20.5
@@ -26,7 +27,6 @@ require (
2627
golang.org/x/oauth2 v0.25.0
2728
golang.org/x/sync v0.10.0
2829
google.golang.org/grpc v1.70.0
29-
gopkg.in/yaml.v2 v2.4.0
3030
k8s.io/api v0.31.0
3131
k8s.io/apimachinery v0.31.0
3232
k8s.io/client-go v0.31.0
@@ -154,6 +154,7 @@ require (
154154
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
155155
gopkg.in/inf.v0 v0.9.1 // indirect
156156
gopkg.in/warnings.v0 v0.1.2 // indirect
157+
gopkg.in/yaml.v2 v2.4.0 // indirect
157158
gopkg.in/yaml.v3 v3.0.1 // indirect
158159
k8s.io/apiextensions-apiserver v0.31.2 // indirect
159160
k8s.io/apiserver v0.31.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
192192
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
193193
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
194194
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
195+
github.com/goccy/go-yaml v1.15.22 h1:iQI1hvCoiYYiVFq76P4AI8ImgDOfgiyKnl/AWjK8/gA=
196+
github.com/goccy/go-yaml v1.15.22/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
195197
github.com/gogits/go-gogs-client v0.0.0-20200905025246-8bb8a50cb355 h1:HTVNOdTWO/gHYeFnr/HwpYwY6tgMcYd+Rgf1XrHnORY=
196198
github.com/gogits/go-gogs-client v0.0.0-20200905025246-8bb8a50cb355/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0=
197199
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=

pkg/argocd/update.go

Lines changed: 101 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"path/filepath"
7+
"regexp"
78
"strings"
89
"sync"
910
"text/template"
@@ -21,7 +22,11 @@ import (
2122

2223
"github.com/argoproj/argo-cd/v2/pkg/apiclient/application"
2324
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
24-
"gopkg.in/yaml.v2"
25+
26+
"github.com/goccy/go-yaml"
27+
"github.com/goccy/go-yaml/ast"
28+
"github.com/goccy/go-yaml/parser"
29+
"github.com/goccy/go-yaml/token"
2530
)
2631

2732
// Stores some statistics about the results of a run
@@ -459,8 +464,7 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by
459464
if strings.HasPrefix(app.Annotations[common.WriteBackTargetAnnotation], common.HelmPrefix) {
460465
images := GetImagesAndAliasesFromApplication(app)
461466

462-
helmNewValues := yaml.MapSlice{}
463-
err = yaml.Unmarshal(originalData, &helmNewValues)
467+
helmNewValues, err := parser.ParseBytes(originalData, parser.ParseComments)
464468
if err != nil {
465469
return nil, err
466470
}
@@ -488,7 +492,7 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by
488492
if helmParamVersion == nil {
489493
return nil, fmt.Errorf("%s parameter not found", helmAnnotationParamVersion)
490494
}
491-
err = setHelmValue(&helmNewValues, helmAnnotationParamVersion, helmParamVersion.Value)
495+
err = setHelmValue(helmNewValues, helmAnnotationParamVersion, helmParamVersion.Value)
492496
if err != nil {
493497
return nil, fmt.Errorf("failed to set image parameter version value: %v", err)
494498
}
@@ -499,13 +503,13 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by
499503
return nil, fmt.Errorf("%s parameter not found", helmAnnotationParamName)
500504
}
501505

502-
err = setHelmValue(&helmNewValues, helmAnnotationParamName, helmParamName.Value)
506+
err = setHelmValue(helmNewValues, helmAnnotationParamName, helmParamName.Value)
503507
if err != nil {
504508
return nil, fmt.Errorf("failed to set image parameter name value: %v", err)
505509
}
506510
}
507511

508-
override, err = yaml.Marshal(helmNewValues)
512+
override = []byte(helmNewValues.String())
509513
} else {
510514
var params helmOverride
511515
newParams := helmOverride{
@@ -561,76 +565,113 @@ func mergeKustomizeOverride(t *kustomizeOverride, o *kustomizeOverride) {
561565
}
562566
}
563567

564-
// Check if a key exists in a MapSlice and return its index and value
565-
func findHelmValuesKey(m yaml.MapSlice, key string) (int, bool) {
566-
for i, item := range m {
567-
if item.Key == key {
568-
return i, true
568+
func splitKeys(input string) []string {
569+
// Regular expression to match quoted and unquoted segments
570+
re := regexp.MustCompile(`'([^']*)'|"([^"]*)"|([^.".]+)`)
571+
matches := re.FindAllStringSubmatch(input, -1)
572+
573+
var result []string
574+
for _, match := range matches {
575+
if match[1] != "" { // Single-quoted string
576+
result = append(result, match[1])
577+
} else if match[2] != "" { // Double-quoted string
578+
result = append(result, match[2])
579+
} else if match[3] != "" { // Unquoted segment
580+
result = append(result, match[3])
569581
}
570582
}
571-
return -1, false
583+
584+
return result
572585
}
573586

574587
// set value of the parameter passed from the annotations.
575-
func setHelmValue(currentValues *yaml.MapSlice, key string, value interface{}) error {
576-
// Check if the full key exists
577-
if idx, found := findHelmValuesKey(*currentValues, key); found {
578-
(*currentValues)[idx].Value = value
579-
return nil
588+
func setHelmValue(file *ast.File, keyPath, value string) error {
589+
path := splitKeys(keyPath)
590+
if len(path) == 0 {
591+
return fmt.Errorf("empty key provided")
580592
}
581593

582-
var err error
583-
keys := strings.Split(key, ".")
584-
current := currentValues
585-
var parent *yaml.MapSlice
586-
parentIdx := -1
587-
588-
for i, k := range keys {
589-
if idx, found := findHelmValuesKey(*current, k); found {
590-
if i == len(keys)-1 {
591-
// If we're at the final key, set the value and return
592-
(*current)[idx].Value = value
593-
return nil
594-
} else {
595-
// Navigate deeper into the map
596-
if nestedMap, ok := (*current)[idx].Value.(yaml.MapSlice); ok {
597-
parent = current
598-
parentIdx = idx
599-
current = &nestedMap
600-
} else {
601-
return fmt.Errorf("unexpected type %T for key %s", (*current)[idx].Value, k)
594+
mapping, ok := file.Docs[0].Body.(*ast.MappingNode)
595+
if !ok {
596+
tk := token.New("$", "$", &token.Position{})
597+
mapping = ast.Mapping(tk, false)
598+
file.Docs[0].Body = mapping
599+
}
600+
601+
// Traverse the path
602+
var lastNode *ast.MappingValueNode
603+
for index, key := range path {
604+
found := false
605+
var currentNode *ast.MappingValueNode
606+
607+
for _, v := range mapping.Values {
608+
if v.Key.GetToken().Value == key {
609+
currentNode = v
610+
if index == len(path)-1 {
611+
lastNode = currentNode
612+
found = true
613+
break
614+
}
615+
// Move deeper into the structure
616+
if nextMapping, ok := v.Value.(*ast.MappingNode); ok {
617+
mapping = nextMapping
618+
found = true
619+
break
602620
}
603621
}
604-
} else {
605-
newCurrent := yaml.MapSlice{}
606-
var newParent yaml.MapSlice
622+
}
607623

608-
if i == len(keys)-1 {
609-
newParent = append(*current, yaml.MapItem{Key: k, Value: value})
610-
} else {
611-
newParent = append(*current, yaml.MapItem{Key: k, Value: newCurrent})
612-
}
624+
// If key does not exist, create it
625+
if !found {
626+
// Create a token with proper position (assuming default line/column)
627+
keyToken := token.New(key, key, &token.Position{Column: index*2 + 1})
628+
newKey := ast.String(keyToken) // Create key node
629+
mappingToken := token.New(key, key, &token.Position{})
630+
newMapping := ast.Mapping(mappingToken, false) // Create empty mapping
613631

614-
if parent == nil {
615-
*currentValues = newParent
616-
} else {
617-
// if parentIdx has not been set (parent element is also new), set it to the last element
618-
if parentIdx == -1 {
619-
parentIdx = len(*parent) - 1
620-
if parentIdx < 0 {
621-
parentIdx = 0
622-
}
632+
if currentNode != nil {
633+
comment := currentNode.Value.GetComment()
634+
currentNode.Value = newMapping
635+
err := currentNode.Value.SetComment(comment)
636+
if err != nil {
637+
return err
623638
}
624-
(*parent)[parentIdx].Value = newParent
639+
lastNode = currentNode
640+
} else {
641+
// Add the new mapping to the parent mapping
642+
lastNode = ast.MappingValue(mappingToken, newKey, newMapping)
643+
mapping.Values = append(mapping.Values, lastNode)
625644
}
626-
627-
parent = &newParent
628-
current = &newCurrent
629-
parentIdx = -1
645+
mapping = newMapping
630646
}
631647
}
632648

633-
return err
649+
if lastNode == nil {
650+
return fmt.Errorf("key not found")
651+
}
652+
653+
var valueToken *token.Token
654+
if token.IsNeedQuoted(value) {
655+
valueToken = token.SingleQuote(value, value, &token.Position{})
656+
} else {
657+
valueToken = token.New(value, value, &token.Position{})
658+
}
659+
newValue := ast.String(valueToken)
660+
comment := lastNode.Value.GetComment()
661+
if comment == nil {
662+
comment = lastNode.Key.GetComment()
663+
}
664+
lastNode.Value = newValue
665+
err := lastNode.Key.SetComment(nil)
666+
if err != nil {
667+
return err
668+
}
669+
err = lastNode.Value.SetComment(comment)
670+
if err != nil {
671+
return err
672+
}
673+
674+
return nil
634675
}
635676

636677
func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.ImageUpdaterKubernetesClient, argoClient ArgoCD) (*WriteBackConfig, error) {

0 commit comments

Comments
 (0)