@@ -11,6 +11,7 @@ import (
1111 "io"
1212 "os"
1313 "path/filepath"
14+ "sort"
1415 "strings"
1516 "testing"
1617 "time"
@@ -19,7 +20,9 @@ import (
1920
2021 "helm.sh/helm/v3/pkg/action"
2122 "helm.sh/helm/v3/pkg/chart"
23+ "helm.sh/helm/v3/pkg/chartutil"
2224 "helm.sh/helm/v3/pkg/cli"
25+ "helm.sh/helm/v3/pkg/engine"
2326 kubefake "helm.sh/helm/v3/pkg/kube/fake"
2427 "helm.sh/helm/v3/pkg/release"
2528 helmstorage "helm.sh/helm/v3/pkg/storage"
@@ -654,6 +657,161 @@ func TestChartForUpgradeTargetReusesReleaseChartForSameVersion(t *testing.T) {
654657 }
655658}
656659
660+ // Regression for Values Preview/Apply on umbrella releases. Helm storage keeps
661+ // Chart.yaml's dependency declarations but JSON serialization drops the chart
662+ // dependency bodies. Both values paths select their chart through
663+ // chartForUpgradeTarget, so same-version selection must reconstruct the exact
664+ // installed version before rendering or applying.
665+ func TestValuesPreviewApplyReconstructSameVersionUmbrella (t * testing.T ) {
666+ complete := valuesIsolationUmbrellaChart ()
667+ stored := roundTripStoredChart (t , complete )
668+ if len (stored .Metadata .Dependencies ) != 1 || len (stored .Dependencies ()) != 0 {
669+ t .Fatalf ("test precondition failed: stored chart declarations=%d bodies=%d, want 1 and 0" , len (stored .Metadata .Dependencies ), len (stored .Dependencies ()))
670+ }
671+
672+ rel := helmTestRelease ("umbrella" , "demo" , 1 , release .StatusDeployed , "deployed" )
673+ rel .Chart = stored
674+ client := & Client {}
675+ loadCalls := 0
676+ selected , err := client .chartForUpgradeTargetWithLoader (nil , rel , "" , "" , func (string , string , string ) {}, func (_ * action.Configuration , gotRel * release.Release , version , repository string , _ func (string , string , string )) (* chart.Chart , error ) {
677+ loadCalls ++
678+ if gotRel != rel || version != "1.0.4" || repository != "" {
679+ t .Fatalf ("loader got release=%p version=%q repository=%q" , gotRel , version , repository )
680+ }
681+ return complete , nil
682+ })
683+ if err != nil {
684+ t .Fatal (err )
685+ }
686+ if loadCalls != 1 || selected != complete {
687+ t .Fatalf ("loader calls=%d selected=%p, want one call and complete chart %p" , loadCalls , selected , complete )
688+ }
689+
690+ before := renderValuesIsolationChart (t , selected , map [string ]any {"parentTag" : "1.0.0" , "child" : map [string ]any {"imageTag" : "2.0.0" }})
691+ after := renderValuesIsolationChart (t , selected , map [string ]any {"parentTag" : "1.0.1" , "child" : map [string ]any {"imageTag" : "2.0.0" }})
692+ if len (before ) != 2 || len (after ) != 2 {
693+ t .Fatalf ("rendered inventory before=%v after=%v, want parent and child" , mapKeys (before ), mapKeys (after ))
694+ }
695+ for name , body := range before {
696+ afterBody , ok := after [name ]
697+ if ! ok {
698+ t .Fatalf ("object inventory changed: %q missing after values edit" , name )
699+ }
700+ if strings .Contains (name , "/charts/child/" ) {
701+ if afterBody != body {
702+ t .Fatalf ("child workload changed for parent-only values override\n before:\n %s\n after:\n %s" , body , afterBody )
703+ }
704+ } else if afterBody == body || ! strings .Contains (afterBody , "parent:1.0.1" ) {
705+ t .Fatalf ("parent workload did not receive intended image override\n before:\n %s\n after:\n %s" , body , afterBody )
706+ }
707+ }
708+ }
709+
710+ func TestValuesPreviewApplyFailsClosedWhenDependencyBodiesUnavailable (t * testing.T ) {
711+ stored := roundTripStoredChart (t , valuesIsolationUmbrellaChart ())
712+ rel := helmTestRelease ("umbrella" , "demo" , 1 , release .StatusDeployed , "deployed" )
713+ rel .Chart = stored
714+
715+ t .Run ("source cannot resolve exact installed version" , func (t * testing.T ) {
716+ _ , err := (& Client {}).chartForUpgradeTargetWithLoader (nil , rel , "1.0.4" , "" , func (string , string , string ) {}, func (* action.Configuration , * release.Release , string , string , func (string , string , string )) (* chart.Chart , error ) {
717+ return nil , errors .New ("chart not found in configured sources" )
718+ })
719+ if err == nil || ! strings .Contains (err .Error (), "could not reconstruct complete chart umbrella version 1.0.4" ) {
720+ t .Fatalf ("error = %v, want fail-closed reconstruction error" , err )
721+ }
722+ })
723+
724+ t .Run ("resolved package also lacks dependency body" , func (t * testing.T ) {
725+ _ , err := (& Client {}).chartForUpgradeTargetWithLoader (nil , rel , "1.0.4" , "" , func (string , string , string ) {}, func (* action.Configuration , * release.Release , string , string , func (string , string , string )) (* chart.Chart , error ) {
726+ return stored , nil
727+ })
728+ if err == nil || ! strings .Contains (err .Error (), "refusing to render incomplete chart" ) || ! strings .Contains (err .Error (), "dependency body \" child\" " ) {
729+ t .Fatalf ("error = %v, want explicit missing-dependency refusal" , err )
730+ }
731+ })
732+ }
733+
734+ func valuesIsolationUmbrellaChart () * chart.Chart {
735+ child := & chart.Chart {
736+ Metadata : & chart.Metadata {Name : "child" , Version : "2.0.0" , Type : "application" },
737+ Values : map [string ]any {"imageTag" : "2.0.0" },
738+ Templates : []* chart.File {{Name : "templates/deployment.yaml" , Data : []byte (`apiVersion: apps/v1
739+ kind: Deployment
740+ metadata:
741+ name: child
742+ spec:
743+ selector:
744+ matchLabels: {app: child}
745+ template:
746+ metadata:
747+ labels: {app: child}
748+ spec:
749+ containers:
750+ - name: child
751+ image: child:{{ .Values.imageTag }}
752+ ` )}},
753+ }
754+ parent := & chart.Chart {
755+ Metadata : & chart.Metadata {
756+ Name : "umbrella" , Version : "1.0.4" , Type : "application" ,
757+ Dependencies : []* chart.Dependency {{Name : "child" , Version : "2.0.0" }},
758+ },
759+ Values : map [string ]any {"parentTag" : "1.0.0" , "child" : map [string ]any {"imageTag" : "2.0.0" }},
760+ Templates : []* chart.File {{Name : "templates/deployment.yaml" , Data : []byte (`apiVersion: apps/v1
761+ kind: Deployment
762+ metadata:
763+ name: parent
764+ spec:
765+ selector:
766+ matchLabels: {app: parent}
767+ template:
768+ metadata:
769+ labels: {app: parent}
770+ spec:
771+ containers:
772+ - name: parent
773+ image: parent:{{ .Values.parentTag }}
774+ ` )}},
775+ }
776+ parent .SetDependencies (child )
777+ return parent
778+ }
779+
780+ func roundTripStoredChart (t * testing.T , ch * chart.Chart ) * chart.Chart {
781+ t .Helper ()
782+ b , err := json .Marshal (ch )
783+ if err != nil {
784+ t .Fatal (err )
785+ }
786+ var stored chart.Chart
787+ if err := json .Unmarshal (b , & stored ); err != nil {
788+ t .Fatal (err )
789+ }
790+ return & stored
791+ }
792+
793+ func renderValuesIsolationChart (t * testing.T , ch * chart.Chart , values map [string ]any ) map [string ]string {
794+ t .Helper ()
795+ renderValues , err := chartutil .ToRenderValues (ch , values , chartutil.ReleaseOptions {Name : "umbrella" , Namespace : "demo" , IsUpgrade : true }, chartutil .DefaultCapabilities )
796+ if err != nil {
797+ t .Fatal (err )
798+ }
799+ rendered , err := engine .Render (ch , renderValues )
800+ if err != nil {
801+ t .Fatal (err )
802+ }
803+ return rendered
804+ }
805+
806+ func mapKeys [V any ](m map [string ]V ) []string {
807+ keys := make ([]string , 0 , len (m ))
808+ for key := range m {
809+ keys = append (keys , key )
810+ }
811+ sort .Strings (keys )
812+ return keys
813+ }
814+
657815func TestDiffResourceRefs (t * testing.T ) {
658816 left := []ResourceRef {
659817 {APIVersion : "apps/v1" , Kind : "Deployment" , Namespace : "demo" , Name : "cart" },
0 commit comments