Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs-v2/content/en/docs/deployers/helm.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,48 @@ The `helm` type offers the following options:
Each `release` includes the following fields:

{{< schema root="HelmRelease" >}}

## Using values files from remote Helm charts (NEW)

Skaffold now supports using values files that are co-located inside remote Helm charts (e.g., charts from OCI registries or remote repositories).

### How to use

In your `skaffold.yaml`, specify a values file from inside the remote chart using the special `:chart:` prefix:

```yaml
deploy:
helm:
releases:
- name: my-remote-release
remoteChart: oci://harbor.example.com/myrepo/mychart
version: 1.2.3
valuesFiles:
- ":chart:values-prod.yaml" # This will extract values-prod.yaml from the remote chart
```

- The `:chart:` prefix tells Skaffold to pull the remote chart, extract the specified file, and use it as a values file override.
- 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).
- You can mix local and remote values files in the list.

### Example

Suppose your remote chart contains both `values.yaml` and `values-prod.yaml`. To use the production values:

```yaml
deploy:
helm:
releases:
- name: my-remote-release
remoteChart: oci://harbor.example.com/myrepo/mychart
version: 1.2.3
valuesFiles:
- ":chart:values-prod.yaml"
```

### Limitations
- The `:chart:` syntax only works for remote charts (using `remoteChart`).
- The file must exist in the root of the chart archive.
- Skaffold will pull the chart and extract the file to a temporary location for each deploy.

---
20 changes: 20 additions & 0 deletions pkg/skaffold/deploy/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,26 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
version: chartVersion,
}

// --- Begin: Handle :chart:values.yaml for remote charts ---
var tempFiles []func()
for i, vf := range r.ValuesFiles {
if strings.HasPrefix(vf, ":chart:") && r.RemoteChart != "" {
fileInChart := strings.TrimPrefix(vf, ":chart:")
localPath, cleanup, err := helm.PullAndExtractChartFile(r.RemoteChart, chartVersion, fileInChart)
if err != nil {
return nil, nil, fmt.Errorf("failed to extract %s from remote chart: %w", fileInChart, err)
}
r.ValuesFiles[i] = localPath
tempFiles = append(tempFiles, cleanup)
}
}
defer func() {
for _, cleanup := range tempFiles {
cleanup()
}
}()
// --- End: Handle :chart:values.yaml for remote charts ---

opts.namespace, err = helm.ReleaseNamespace(h.namespace, r)
if err != nil {
return nil, nil, err
Expand Down
89 changes: 89 additions & 0 deletions pkg/skaffold/helm/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ limitations under the License.
package helm

import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"

"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
Expand Down Expand Up @@ -119,3 +124,87 @@ func ReleaseNamespace(namespace string, release latest.HelmRelease) (string, err
}
return "", nil
}

// PullAndExtractChartFile pulls a remote Helm chart and extracts a file from it (e.g., values-prod.yaml).
// chartRef: the remote chart reference (e.g., oci://...)
// version: the chart version
// fileInChart: the file to extract (e.g., values-prod.yaml)
// Returns the path to the extracted file, and a cleanup function.
func PullAndExtractChartFile(chartRef, version, fileInChart string) (string, func(), error) {
tmpDir, err := os.MkdirTemp("", "skaffold-helm-pull-*")
if err != nil {
return "", nil, err
}
success := false
cleanup := func() { os.RemoveAll(tmpDir) }
defer func() {
if !success {
cleanup()
}
}()

// Pull the chart
pullArgs := []string{"pull", chartRef, "--version", version, "--destination", tmpDir}
cmd := exec.Command("helm", pullArgs...)
if out, err := cmd.CombinedOutput(); err != nil {
return "", nil, fmt.Errorf("failed to pull chart: %v\n%s", err, string(out))
}

// Find the .tgz file
var tgzPath string
dirEntries, err := os.ReadDir(tmpDir)
if err != nil {
return "", nil, err
}
for _, entry := range dirEntries {
if filepath.Ext(entry.Name()) == ".tgz" {
tgzPath = filepath.Join(tmpDir, entry.Name())
break
}
}
if tgzPath == "" {
return "", nil, fmt.Errorf("no chart archive found after helm pull")
}

// Extract the requested file
tgzFile, err := os.Open(tgzPath)
if err != nil {
return "", nil, err
}
defer tgzFile.Close()
gzReader, err := gzip.NewReader(tgzFile)
if err != nil {
return "", nil, err
}
tarReader := tar.NewReader(gzReader)

var extractedPath string
for {
hdr, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", nil, err
}
// Chart files are inside a top-level dir, e.g. united/values-prod.yaml
pathParts := strings.Split(filepath.ToSlash(hdr.Name), "/")
if hdr.Typeflag == tar.TypeReg && len(pathParts) == 2 && pathParts[1] == fileInChart {
extractedPath = filepath.Join(tmpDir, fileInChart)
outFile, err := os.Create(extractedPath)
if err != nil {
return "", nil, err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tarReader); err != nil {
return "", nil, err
}
break
}
}
if extractedPath == "" {
return "", nil, fmt.Errorf("file %s not found in chart", fileInChart)
}
success = true
return extractedPath, cleanup, nil
}