diff --git a/pkg/action/pull.go b/pkg/action/pull.go index be71d0ed0..76d347ee0 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -17,7 +17,9 @@ limitations under the License. package action import ( + "crypto/sha256" "fmt" + "io" "os" "path/filepath" "strings" @@ -138,6 +140,36 @@ func (p *Pull) Run(chartRef string) (string, error) { return out.String(), err } + // Print a pull summary for repo mode and non-OCI downloads. + // Some repos (e.g., Bitnami) resolve --repo pulls to oci:// refs, but that + // path does not go through the registry client's summary printer, so we emit + // a consistent summary here. We mirror the OCI output format: + // + // Pulled: : + // Checksum: sha256: + // + // For HTTP/repo pulls, the digest is the SHA-256 of the downloaded .tgz archive. + // For direct OCI pulls, the registry client already prints "Pulled:" and + // "Digest:" (manifest digest), so we do not print here to avoid duplicates. + if p.RepoURL != "" || !registry.IsOCI(downloadSourceRef) { + base := strings.TrimSuffix(downloadSourceRef, ".tgz") + chart, ver := splitChartNameVersion(base) + + tag := chart + if ver != "" { + tag += ":" + ver + } else if p.Version != "" { + tag += ":" + p.Version + } + fmt.Fprintf(&out, "Pulled: %s\n", tag) + + if sum, err := sha256File(saved); err == nil { + fmt.Fprintf(&out, "Checksum: sha256:%x\n", sum) + } else { + fmt.Fprintf(&out, "Checksum failed: %v\n", err) + } + } + if p.Verify { for name := range v.SignedBy.Identities { fmt.Fprintf(&out, "Signed by: %v\n", name) @@ -173,3 +205,24 @@ func (p *Pull) Run(chartRef string) (string, error) { } return out.String(), nil } + +func splitChartNameVersion(s string) (name, version string) { + if i := strings.LastIndex(s, "-"); i >= 0 { + return s[:i], s[i+1:] + } + return s, "" +} + +func sha256File(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + return h.Sum(nil), nil +} diff --git a/pkg/action/pull_test.go b/pkg/action/pull_test.go new file mode 100644 index 000000000..49068466c --- /dev/null +++ b/pkg/action/pull_test.go @@ -0,0 +1,130 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package action + +import ( + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "helm.sh/helm/v4/pkg/cli" +) + +// helm pull testchart --repo=http://127.0.0.1: --version 1.2.3 +func TestPull_PrintsSummary_ForHTTPRepo(t *testing.T) { + t.Parallel() + + // Minimal chart payload; verification is off so a plain byte buffer is fine. + chartBytes := []byte("dummy-chart-content") + sum := sha256.Sum256(chartBytes) + wantDigest := fmt.Sprintf("sha256:%x", sum) + + // Serve a valid index.yaml and the chart archive. + mux := http.NewServeMux() + mux.HandleFunc("/index.yaml", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/yaml") + // Valid index entry requires both name and version. + fmt.Fprintf(w, `apiVersion: v1 +entries: + testchart: + - name: testchart + version: 1.2.3 + urls: + - testchart-1.2.3.tgz + created: "2020-01-01T00:00:00Z" +`) + }) + mux.HandleFunc("/testchart-1.2.3.tgz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/gzip") + _, _ = w.Write(chartBytes) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + settings := cli.New() + settings.RepositoryCache = t.TempDir() + settings.RepositoryConfig = filepath.Join(t.TempDir(), "repositories.yaml") + settings.ContentCache = t.TempDir() + + cfg := &Configuration{} + + p := NewPull(WithConfig(cfg)) + p.Settings = settings + p.DestDir = t.TempDir() + p.RepoURL = srv.URL + p.Version = "1.2.3" + + out, err := p.Run("testchart") + require.NoError(t, err, "Pull.Run() should succeed. Output:\n%s", out) + + expectedURL := srv.URL + "/testchart:1.2.3" + assert.Contains(t, out, "Pulled: "+expectedURL, "expected Pulled summary in output") + assert.Contains(t, out, "Checksum: "+wantDigest, "expected archive checksum in output") + + // Ensure the chart file was saved. + _, statErr := os.Stat(filepath.Join(p.DestDir, "testchart-1.2.3.tgz")) + require.NoError(t, statErr, "expected chart archive to be saved") +} + +// helm pull http://127.0.0.1:/directchart-9.9.9.tgz +func TestPull_PrintsSummary_ForDirectHTTPURL(t *testing.T) { + t.Parallel() + + chartBytes := []byte("another-dummy-chart") + sum := sha256.Sum256(chartBytes) + wantDigest := fmt.Sprintf("sha256:%x", sum) + + mux := http.NewServeMux() + mux.HandleFunc("/directchart-9.9.9.tar.gz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/gzip") + _, _ = w.Write(chartBytes) + }) + srv := httptest.NewServer(mux) + defer srv.Close() + + settings := cli.New() + settings.RepositoryCache = t.TempDir() + settings.RepositoryConfig = filepath.Join(t.TempDir(), "repositories.yaml") + settings.ContentCache = t.TempDir() + + cfg := &Configuration{} + + p := NewPull(WithConfig(cfg)) + p.Settings = settings + p.DestDir = t.TempDir() + + // Direct HTTP URL (absolute URL). Version is ignored for absolute URLs. + chartURL := srv.URL + "/directchart-9.9.9.tar.gz" + + out, err := p.Run(chartURL) + require.NoError(t, err, "Pull.Run() should succeed. Output:\n%s", out) + + // Output should reflect name-version.tgz from the URL. + expectedURL := srv.URL + "/directchart:9.9.9" + assert.Contains(t, out, "Pulled: "+expectedURL, "expected Pulled summary in output") + assert.Contains(t, out, "Checksum: "+wantDigest, "expected archive checksum in output") + + // Ensure the chart file was saved. + _, statErr := os.Stat(filepath.Join(p.DestDir, "directchart-9.9.9.tar.gz")) + require.NoError(t, statErr, "expected chart archive to be saved") +} diff --git a/pkg/cmd/pull_test.go b/pkg/cmd/pull_test.go index c24bf33b7..cd437af91 100644 --- a/pkg/cmd/pull_test.go +++ b/pkg/cmd/pull_test.go @@ -44,7 +44,9 @@ func TestPullCmd(t *testing.T) { t.Fatal(err) } - helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + + helmTestKeyOut := "Pulled: test/signtest\n" + + "Checksum: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\n" + + "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + "Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" + "Chart Hash Verified: "