Skip to content

Commit 9438961

Browse files
committed
Use goccy/go-yaml to update Helm values
1 parent 4de0920 commit 9438961

File tree

2 files changed

+350
-216
lines changed

2 files changed

+350
-216
lines changed

pkg/argocd/update.go

Lines changed: 114 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import (
2222

2323
"github.com/argoproj/argo-cd/v2/pkg/apiclient/application"
2424
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
25+
gyaml "github.com/goccy/go-yaml"
26+
"github.com/goccy/go-yaml/ast"
27+
"github.com/goccy/go-yaml/parser"
28+
"github.com/goccy/go-yaml/token"
2529
yaml "sigs.k8s.io/yaml/goyaml.v3"
2630
)
2731

@@ -465,55 +469,52 @@ func marshalHelmOverride(app *v1alpha1.Application, originalData []byte) (overri
465469
if appSource.Helm == nil {
466470
return []byte{}, nil
467471
}
468-
469-
if strings.HasPrefix(app.Annotations[common.WriteBackTargetAnnotation], common.HelmPrefix) {
472+
target := app.Annotations[common.WriteBackTargetAnnotation]
473+
if strings.HasPrefix(target, common.HelmPrefix) {
470474
images := GetImagesAndAliasesFromApplication(app)
471475

472-
helmNewValues := yaml.Node{}
473-
if unmarshalErr := yaml.Unmarshal(originalData, &helmNewValues); unmarshalErr != nil {
474-
return nil, unmarshalErr
476+
root, err := parser.ParseBytes([]byte(originalData), parser.ParseComments)
477+
if err != nil {
478+
return nil, fmt.Errorf("failed to parse original helm values: %w", err)
475479
}
476480

477481
for _, img := range images {
478482
if img.ImageAlias == "" {
479483
continue
480484
}
481-
482485
helmAnnotationParamName, helmAnnotationParamVersion := getHelmParamNamesFromAnnotation(app.Annotations, img)
483-
484-
if helmAnnotationParamName == "" {
485-
return nil, fmt.Errorf("could not find an image-name annotation for image %s", img.ImageName)
486-
}
487486
// for image-spec annotation, helmAnnotationParamName holds image-spec annotation value,
488-
// and helmAnnotationParamVersion is empty
487+
// and version is empty
489488
if helmAnnotationParamVersion == "" {
490489
if img.GetParameterHelmImageSpec(app.Annotations, common.ImageUpdaterAnnotationPrefix) == "" {
491490
// not a full image-spec, so image-tag is required
492491
return nil, fmt.Errorf("could not find an image-tag annotation for image %s", img.ImageName)
493492
}
494493
} else {
495-
// image-tag annotation is present, so continue to process image-tag
496494
helmParamVersion := getHelmParam(appSource.Helm.Parameters, helmAnnotationParamVersion)
497495
if helmParamVersion == nil {
498496
return nil, fmt.Errorf("%s parameter not found", helmAnnotationParamVersion)
499497
}
500-
err = setHelmValue(&helmNewValues, helmAnnotationParamVersion, helmParamVersion.Value)
498+
err = applyHelmParam(root, helmAnnotationParamVersion, helmParamVersion.Value)
501499
if err != nil {
502-
return nil, fmt.Errorf("failed to set image parameter version value: %v", err)
500+
return nil, err
503501
}
504502
}
505-
503+
if helmAnnotationParamName == "" {
504+
return nil, fmt.Errorf("could not find an image-name annotation for image %s", img.ImageName)
505+
}
506506
helmParamName := getHelmParam(appSource.Helm.Parameters, helmAnnotationParamName)
507507
if helmParamName == nil {
508508
return nil, fmt.Errorf("%s parameter not found", helmAnnotationParamName)
509509
}
510-
511-
err = setHelmValue(&helmNewValues, helmAnnotationParamName, helmParamName.Value)
510+
err = applyHelmParam(root, helmAnnotationParamName, helmParamName.Value)
512511
if err != nil {
513-
return nil, fmt.Errorf("failed to set image parameter name value: %v", err)
512+
return nil, err
514513
}
515514
}
516-
return marshalWithIndent(&helmNewValues, defaultIndent)
515+
516+
out := root.String()
517+
return []byte(out), nil
517518
}
518519

519520
var params helmOverride
@@ -586,102 +587,112 @@ func mergeKustomizeOverride(t *kustomizeOverride, o *kustomizeOverride) {
586587
}
587588
}
588589

589-
// Check if a key exists in a MappingNode and return the index of its value
590-
func findHelmValuesKey(m *yaml.Node, key string) (int, bool) {
591-
for i, item := range m.Content {
592-
if i%2 == 0 && item.Value == key {
593-
return i + 1, true
590+
func findAnchorByName(root ast.Node, name string) *ast.AnchorNode {
591+
for _, n := range ast.Filter(ast.AnchorType, root) {
592+
anchor := n.(*ast.AnchorNode)
593+
nameNode := anchor.Name.(*ast.StringNode)
594+
if nameNode.Value == name {
595+
return anchor
594596
}
595597
}
596-
return -1, false
597-
}
598-
599-
func nodeKindString(k yaml.Kind) string {
600-
return map[yaml.Kind]string{
601-
yaml.DocumentNode: "DocumentNode",
602-
yaml.SequenceNode: "SequenceNode",
603-
yaml.MappingNode: "MappingNode",
604-
yaml.ScalarNode: "ScalarNode",
605-
yaml.AliasNode: "AliasNode",
606-
}[k]
598+
return nil
607599
}
608600

609-
// set value of the parameter passed from the annotations.
610-
func setHelmValue(currentValues *yaml.Node, key string, value interface{}) error {
611-
current := currentValues
612-
613-
// an unmarshalled document has a DocumentNode at the root, but
614-
// we navigate from a MappingNode.
615-
if current.Kind == yaml.DocumentNode {
616-
current = current.Content[0]
617-
}
618-
619-
if current.Kind != yaml.MappingNode {
620-
return fmt.Errorf("unexpected type %s for root", nodeKindString(current.Kind))
621-
}
622-
623-
// Check if the full key exists
624-
if idx, found := findHelmValuesKey(current, key); found {
625-
(*current).Content[idx].Value = value.(string)
601+
func createOrUpdateNode(node ast.Node, path []string, value string, root ...ast.Node) error {
602+
// Keep track of the root in case we need to find an anchor for an alias
603+
rootNode := node
604+
if len(root) > 0 {
605+
rootNode = root[0]
606+
}
607+
// Base case. We've recursed all the way down the path and found a node
608+
if len(path) == 0 {
609+
switch currentNode := node.(type) {
610+
case *ast.StringNode:
611+
currentNode.Value = value
612+
case *ast.AnchorNode:
613+
currentNode.Value = ast.String(&token.Token{Value: value})
614+
case *ast.AliasNode:
615+
anchorName := currentNode.Value.(*ast.StringNode).Value
616+
anchor := findAnchorByName(rootNode.(*ast.MappingNode), anchorName)
617+
if anchor == nil {
618+
return fmt.Errorf("alias %q not found", anchorName)
619+
}
620+
anchor.Value = ast.String(&token.Token{Value: value})
621+
default:
622+
return fmt.Errorf("unexpected leaf node type %T", node)
623+
}
626624
return nil
627625
}
628-
629-
var err error
630-
keys := strings.Split(key, ".")
631-
632-
for i, k := range keys {
633-
if idx, found := findHelmValuesKey(current, k); found {
634-
// Navigate deeper into the map
635-
current = (*current).Content[idx]
636-
// unpack one level of alias; an alias of an alias is not supported
637-
if current.Kind == yaml.AliasNode {
638-
current = current.Alias
626+
key, rest := path[0], path[1:]
627+
switch currentNode := node.(type) {
628+
case *ast.DocumentNode:
629+
// Create a base mapping node if the incoming document is empty
630+
if currentNode.Body == nil {
631+
newNode, err := gyaml.ValueToNode(map[string]any{})
632+
if err != nil {
633+
return err
639634
}
640-
if i == len(keys)-1 {
641-
// If we're at the final key, set the value and return
642-
if current.Kind == yaml.ScalarNode {
643-
current.Value = value.(string)
644-
current.Tag = "!!str"
645-
} else {
646-
return fmt.Errorf("unexpected type %s for key %s", nodeKindString(current.Kind), k)
647-
}
648-
return nil
649-
} else if current.Kind != yaml.MappingNode {
650-
return fmt.Errorf("unexpected type %s for key %s", nodeKindString(current.Kind), k)
635+
mn, ok := newNode.(*ast.MappingNode)
636+
if !ok {
637+
return fmt.Errorf("expected a MappingNode but got %T", newNode)
651638
}
652-
} else {
653-
if i == len(keys)-1 {
654-
current.Content = append(current.Content,
655-
&yaml.Node{
656-
Kind: yaml.ScalarNode,
657-
Value: k,
658-
Tag: "!!str",
659-
},
660-
&yaml.Node{
661-
Kind: yaml.ScalarNode,
662-
Value: value.(string),
663-
Tag: "!!str",
664-
},
665-
)
666-
return nil
667-
} else {
668-
current.Content = append(current.Content,
669-
&yaml.Node{
670-
Kind: yaml.ScalarNode,
671-
Value: k,
672-
Tag: "!!str",
673-
},
674-
&yaml.Node{
675-
Kind: yaml.MappingNode,
676-
Content: []*yaml.Node{},
677-
},
678-
)
679-
current = current.Content[len(current.Content)-1]
639+
currentNode.Body = mn
640+
}
641+
return createOrUpdateNode(currentNode.Body, path, value, currentNode.Body)
642+
case *ast.AnchorNode:
643+
return createOrUpdateNode(currentNode.Value, path, value, rootNode)
644+
case *ast.AliasNode:
645+
aliasName := currentNode.Value.(*ast.StringNode).Value
646+
anchor := findAnchorByName(rootNode.(*ast.MappingNode), aliasName)
647+
if anchor == nil {
648+
return fmt.Errorf("alias %q not found", aliasName)
649+
}
650+
return createOrUpdateNode(anchor.Value, path, value, rootNode)
651+
case *ast.MappingNode:
652+
for _, mappingValueNode := range currentNode.Values {
653+
nodeKey := mappingValueNode.Key.String()
654+
if nodeKey == key {
655+
return createOrUpdateNode(mappingValueNode.Value, rest, value, rootNode)
680656
}
681657
}
658+
var newNodeData map[string]any
659+
if len(rest) == 0 {
660+
newNodeData = map[string]any{key: value}
661+
} else {
662+
newNodeData = map[string]any{key: map[string]any{}}
663+
}
664+
newNode, err := gyaml.ValueToNode(newNodeData)
665+
if err != nil {
666+
return err
667+
}
668+
if err := ast.Merge(currentNode, newNode); err != nil {
669+
return err
670+
}
671+
if mappingValue, ok := newNode.(*ast.MappingNode); ok {
672+
return createOrUpdateNode(mappingValue.Values[0].Value, rest, value, rootNode)
673+
}
682674
}
683675

684-
return err
676+
return fmt.Errorf("unexpected type %T for key attributes", node)
677+
}
678+
679+
func applyHelmParam(root *ast.File, attrPath string, value string) error {
680+
// check if literal path exists, and if it does, replace it
681+
path, _ := gyaml.PathString(fmt.Sprintf("$.'%s'", attrPath))
682+
if _, err := path.FilterFile(root); err == nil {
683+
stringNode, err := gyaml.ValueToNode(value)
684+
if err != nil {
685+
return err
686+
}
687+
if err := path.ReplaceWithNode(root, stringNode); err != nil {
688+
return err
689+
}
690+
return nil
691+
}
692+
if err := createOrUpdateNode(root.Docs[0], strings.Split(attrPath, "."), value); err != nil {
693+
return err
694+
}
695+
return nil
685696
}
686697

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

0 commit comments

Comments
 (0)