Skip to content

Commit 25f2d67

Browse files
committed
feat(helm): implement remote chart values file extraction
1 parent 5c866a1 commit 25f2d67

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

docs-v2/content/en/docs/deployers/helm.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,48 @@ The `helm` type offers the following options:
298298
Each `release` includes the following fields:
299299

300300
{{< schema root="HelmRelease" >}}
301+
302+
## Using values files from remote Helm charts (NEW)
303+
304+
Skaffold now supports using values files that are co-located inside remote Helm charts (e.g., charts from OCI registries or remote repositories).
305+
306+
### How to use
307+
308+
In your `skaffold.yaml`, specify a values file from inside the remote chart using the special `:chart:` prefix:
309+
310+
```yaml
311+
deploy:
312+
helm:
313+
releases:
314+
- name: my-remote-release
315+
remoteChart: oci://harbor.example.com/myrepo/mychart
316+
version: 1.2.3
317+
valuesFiles:
318+
- ":chart:values-prod.yaml" # This will extract values-prod.yaml from the remote chart
319+
```
320+
321+
- The `:chart:` prefix tells Skaffold to pull the remote chart, extract the specified file, and use it as a values file override.
322+
- You can use this for any file that exists in the root of the chart archive (e.g., `values-prod.yaml`, `values-staging.yaml`, etc).
323+
- You can mix local and remote values files in the list.
324+
325+
### Example
326+
327+
Suppose your remote chart contains both `values.yaml` and `values-prod.yaml`. To use the production values:
328+
329+
```yaml
330+
deploy:
331+
helm:
332+
releases:
333+
- name: my-remote-release
334+
remoteChart: oci://harbor.example.com/myrepo/mychart
335+
version: 1.2.3
336+
valuesFiles:
337+
- ":chart:values-prod.yaml"
338+
```
339+
340+
### Limitations
341+
- The `:chart:` syntax only works for remote charts (using `remoteChart`).
342+
- The file must exist in the root of the chart archive.
343+
- Skaffold will pull the chart and extract the file to a temporary location for each deploy.
344+
345+
---

pkg/skaffold/deploy/helm/helm.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,26 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
521521
version: chartVersion,
522522
}
523523

524+
// --- Begin: Handle :chart:values.yaml for remote charts ---
525+
var tempFiles []func()
526+
for i, vf := range r.ValuesFiles {
527+
if strings.HasPrefix(vf, ":chart:") && r.RemoteChart != "" {
528+
fileInChart := strings.TrimPrefix(vf, ":chart:")
529+
localPath, cleanup, err := helm.PullAndExtractChartFile(r.RemoteChart, chartVersion, fileInChart)
530+
if err != nil {
531+
return nil, nil, fmt.Errorf("failed to extract %s from remote chart: %w", fileInChart, err)
532+
}
533+
r.ValuesFiles[i] = localPath
534+
tempFiles = append(tempFiles, cleanup)
535+
}
536+
}
537+
defer func() {
538+
for _, cleanup := range tempFiles {
539+
cleanup()
540+
}
541+
}()
542+
// --- End: Handle :chart:values.yaml for remote charts ---
543+
524544
opts.namespace, err = helm.ReleaseNamespace(h.namespace, r)
525545
if err != nil {
526546
return nil, nil, err

pkg/skaffold/helm/util.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ limitations under the License.
1717
package helm
1818

1919
import (
20+
"archive/tar"
21+
"compress/gzip"
2022
"encoding/json"
2123
"fmt"
24+
"io"
2225
"os"
26+
"os/exec"
27+
"path/filepath"
2328

2429
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
2530
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
@@ -119,3 +124,86 @@ 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+
success := false
139+
cleanup := func() { os.RemoveAll(tmpDir) }
140+
defer func() {
141+
if !success {
142+
cleanup()
143+
}
144+
}()
145+
146+
// Pull the chart
147+
pullArgs := []string{"pull", chartRef, "--version", version, "--destination", tmpDir}
148+
cmd := exec.Command("helm", pullArgs...)
149+
if out, err := cmd.CombinedOutput(); err != nil {
150+
return "", nil, fmt.Errorf("failed to pull chart: %v\n%s", err, string(out))
151+
}
152+
153+
// Find the .tgz file
154+
var tgzPath string
155+
dirEntries, err := os.ReadDir(tmpDir)
156+
if err != nil {
157+
return "", nil, err
158+
}
159+
for _, entry := range dirEntries {
160+
if filepath.Ext(entry.Name()) == ".tgz" {
161+
tgzPath = filepath.Join(tmpDir, entry.Name())
162+
break
163+
}
164+
}
165+
if tgzPath == "" {
166+
return "", nil, fmt.Errorf("no chart archive found after helm pull")
167+
}
168+
169+
// Extract the requested file
170+
tgzFile, err := os.Open(tgzPath)
171+
if err != nil {
172+
return "", nil, err
173+
}
174+
defer tgzFile.Close()
175+
gzReader, err := gzip.NewReader(tgzFile)
176+
if err != nil {
177+
return "", nil, err
178+
}
179+
tarReader := tar.NewReader(gzReader)
180+
181+
var extractedPath string
182+
for {
183+
hdr, err := tarReader.Next()
184+
if err == io.EOF {
185+
break
186+
}
187+
if err != nil {
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+
return "", nil, err
196+
}
197+
defer outFile.Close()
198+
if _, err := io.Copy(outFile, tarReader); err != nil {
199+
return "", nil, err
200+
}
201+
break
202+
}
203+
}
204+
if extractedPath == "" {
205+
return "", nil, fmt.Errorf("file %s not found in chart", fileInChart)
206+
}
207+
success = true
208+
return extractedPath, cleanup, nil
209+
}

0 commit comments

Comments
 (0)