@@ -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
687698func getWriteBackConfig (app * v1alpha1.Application , kubeClient * kube.ImageUpdaterKubernetesClient , argoClient ArgoCD ) (* WriteBackConfig , error ) {
0 commit comments