diff --git a/docs-v2/content/en/docs/deployers/helm.md b/docs-v2/content/en/docs/deployers/helm.md index 543eec298c6..e2da1b75f2d 100644 --- a/docs-v2/content/en/docs/deployers/helm.md +++ b/docs-v2/content/en/docs/deployers/helm.md @@ -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. + +--- diff --git a/pkg/skaffold/deploy/helm/helm.go b/pkg/skaffold/deploy/helm/helm.go index 4199bd38bf0..d31f81a1aff 100644 --- a/pkg/skaffold/deploy/helm/helm.go +++ b/pkg/skaffold/deploy/helm/helm.go @@ -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 diff --git a/pkg/skaffold/helm/util.go b/pkg/skaffold/helm/util.go index d209052929a..beba9c5b312 100644 --- a/pkg/skaffold/helm/util.go +++ b/pkg/skaffold/helm/util.go @@ -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" @@ -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 +}