@@ -20,6 +20,11 @@ import (
2020 "encoding/json"
2121 "fmt"
2222 "os"
23+ "archive/tar"
24+ "compress/gzip"
25+ "os/exec"
26+ "path/filepath"
27+ "io"
2328
2429 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
2530 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
@@ -119,3 +124,89 @@ func ReleaseNamespace(namespace string, release latest.HelmRelease) (string, err
119124 }
120125 return "" , nil
121126}
127+
128+ // PullAndExtractChartFile pulls a remote Helm chart and extracts a file from it (e.g., values-prod.yaml).
129+ // chartRef: the remote chart reference (e.g., oci://...)
130+ // version: the chart version
131+ // fileInChart: the file to extract (e.g., values-prod.yaml)
132+ // Returns the path to the extracted file, and a cleanup function.
133+ func PullAndExtractChartFile (chartRef , version , fileInChart string ) (string , func (), error ) {
134+ tmpDir , err := os .MkdirTemp ("" , "skaffold-helm-pull-*" )
135+ if err != nil {
136+ return "" , nil , err
137+ }
138+ cleanup := func () { os .RemoveAll (tmpDir ) }
139+
140+ // Pull the chart
141+ pullArgs := []string {"pull" , chartRef , "--version" , version , "--destination" , tmpDir }
142+ cmd := exec .Command ("helm" , pullArgs ... )
143+ if out , err := cmd .CombinedOutput (); err != nil {
144+ cleanup ()
145+ return "" , nil , fmt .Errorf ("failed to pull chart: %v\n %s" , err , string (out ))
146+ }
147+
148+ // Find the .tgz file
149+ var tgzPath string
150+ dirEntries , err := os .ReadDir (tmpDir )
151+ if err != nil {
152+ cleanup ()
153+ return "" , nil , err
154+ }
155+ for _ , entry := range dirEntries {
156+ if filepath .Ext (entry .Name ()) == ".tgz" {
157+ tgzPath = filepath .Join (tmpDir , entry .Name ())
158+ break
159+ }
160+ }
161+ if tgzPath == "" {
162+ cleanup ()
163+ return "" , nil , fmt .Errorf ("no chart archive found after helm pull" )
164+ }
165+
166+ // Extract the requested file
167+ tgzFile , err := os .Open (tgzPath )
168+ if err != nil {
169+ cleanup ()
170+ return "" , nil , err
171+ }
172+ defer tgzFile .Close ()
173+ gzReader , err := gzip .NewReader (tgzFile )
174+ if err != nil {
175+ cleanup ()
176+ return "" , nil , err
177+ }
178+ tarReader := tar .NewReader (gzReader )
179+
180+ var extractedPath string
181+ for {
182+ hdr , err := tarReader .Next ()
183+ if err == io .EOF {
184+ break
185+ }
186+ if err != nil {
187+ cleanup ()
188+ return "" , nil , err
189+ }
190+ // Chart files are inside a top-level dir, e.g. united/values-prod.yaml
191+ if filepath .Base (hdr .Name ) == fileInChart {
192+ extractedPath = filepath .Join (tmpDir , fileInChart )
193+ outFile , err := os .Create (extractedPath )
194+ if err != nil {
195+ cleanup ()
196+ return "" , nil , err
197+ }
198+ if _ , err := io .Copy (outFile , tarReader ); err != nil {
199+ outFile .Close ()
200+ cleanup ()
201+ return "" , nil , err
202+ }
203+ outFile .Close ()
204+ break
205+ }
206+ }
207+ if extractedPath == "" {
208+ cleanup ()
209+ return "" , nil , fmt .Errorf ("file %s not found in chart" , fileInChart )
210+ }
211+ return extractedPath , cleanup , nil
212+ }
0 commit comments