pull/32243/merge
Simone Tollardo 3 months ago committed by GitHub
commit c30bb8a16b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -47,6 +47,11 @@ type Dependency struct {
ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"` ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"`
// Alias usable alias to be used for the chart // Alias usable alias to be used for the chart
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
// Digest is the SHA256 hash of the dependency chart archive (sha256:...).
//
// This field is populated in Chart.lock to record the exact tarball bytes
// that were downloaded. It is omitted from Chart.yaml dependency entries.
Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
} }
// Validate checks for common problems with the dependency datastructure in // Validate checks for common problems with the dependency datastructure in
@ -75,7 +80,10 @@ func (d *Dependency) Validate() error {
type Lock struct { type Lock struct {
// Generated is the date the lock file was last generated. // Generated is the date the lock file was last generated.
Generated time.Time `json:"generated"` Generated time.Time `json:"generated"`
// Digest is a hash of the dependencies in Chart.yaml. // Digest is a hash of Chart.yaml dependency constraints and the resolved
// lock metadata (name, version, repository). It detects drift between
// Chart.yaml and Chart.lock. Per-dependency tarball content is tracked
// separately via Dependency.Digest.
Digest string `json:"digest"` Digest string `json:"digest"`
// Dependencies is the list of dependencies that this lock file has locked. // Dependencies is the list of dependencies that this lock file has locked.
Dependencies []*Dependency `json:"dependencies"` Dependencies []*Dependency `json:"dependencies"`

@ -209,9 +209,13 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
// HashReq generates a hash of the dependencies. // HashReq generates a hash of the dependencies.
// //
// This should be used only to compare against another hash generated by this // This should be used only to compare against another hash generated by this
// function. // function. Per-dependency content digests are excluded; the top-level lock
// digest tracks Chart.yaml constraints and resolved metadata only.
func HashReq(req, lock []*chart.Dependency) (string, error) { func HashReq(req, lock []*chart.Dependency) (string, error) {
data, err := json.Marshal([2][]*chart.Dependency{req, lock}) data, err := json.Marshal([2][]*chart.Dependency{
dependenciesForMetadataHash(req),
dependenciesForMetadataHash(lock),
})
if err != nil { if err != nil {
return "", err return "", err
} }
@ -219,6 +223,23 @@ func HashReq(req, lock []*chart.Dependency) (string, error) {
return "sha256:" + s, err return "sha256:" + s, err
} }
// dependenciesForMetadataHash returns a copy of deps with content digests cleared.
func dependenciesForMetadataHash(deps []*chart.Dependency) []*chart.Dependency {
if len(deps) == 0 {
return nil
}
out := make([]*chart.Dependency, len(deps))
for i, d := range deps {
if d == nil {
continue
}
cp := *d
cp.Digest = ""
out[i] = &cp
}
return out
}
// HashV2Req generates a hash of requirements generated in Helm v2. // HashV2Req generates a hash of requirements generated in Helm v2.
// //
// This should be used only to compare against another hash generated by the // This should be used only to compare against another hash generated by the

@ -297,3 +297,21 @@ func TestGetLocalPath(t *testing.T) {
}) })
} }
} }
func TestHashReqIgnoresContentDigest(t *testing.T) {
req := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},
}
lockWithout := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},
}
lockWith := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts", Digest: "sha256:abc123"},
}
h1, err := HashReq(req, lockWithout)
require.NoError(t, err)
h2, err := HashReq(req, lockWith)
require.NoError(t, err)
assert.Equal(t, h1, h2)
}

@ -47,6 +47,11 @@ type Dependency struct {
ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"` ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"`
// Alias usable alias to be used for the chart // Alias usable alias to be used for the chart
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
// Digest is the SHA256 hash of the dependency chart archive (sha256:...).
//
// This field is populated in Chart.lock to record the exact tarball bytes
// that were downloaded. It is omitted from Chart.yaml dependency entries.
Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
} }
// Validate checks for common problems with the dependency datastructure in // Validate checks for common problems with the dependency datastructure in
@ -75,7 +80,10 @@ func (d *Dependency) Validate() error {
type Lock struct { type Lock struct {
// Generated is the date the lock file was last generated. // Generated is the date the lock file was last generated.
Generated time.Time `json:"generated"` Generated time.Time `json:"generated"`
// Digest is a hash of the dependencies in Chart.yaml. // Digest is a hash of Chart.yaml dependency constraints and the resolved
// lock metadata (name, version, repository). It detects drift between
// Chart.yaml and Chart.lock. Per-dependency tarball content is tracked
// separately via Dependency.Digest.
Digest string `json:"digest"` Digest string `json:"digest"`
// Dependencies is the list of dependencies that this lock file has locked. // Dependencies is the list of dependencies that this lock file has locked.
Dependencies []*Dependency `json:"dependencies"` Dependencies []*Dependency `json:"dependencies"`

@ -85,6 +85,10 @@ type ChartDownloader struct {
// Cache specifies the cache implementation to use. // Cache specifies the cache implementation to use.
Cache Cache Cache Cache
// ExpectedDigest is the digest from the repository index or OCI reference.
// When set, downloaded content is compared and a warning is emitted on mismatch.
ExpectedDigest string
} }
// DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
@ -152,6 +156,10 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if err := verifyDownloadedDigest(c.Out, ref, c.ExpectedDigest, hash, data); err != nil {
return "", nil, err
}
} }
name := filepath.Base(u.Path) name := filepath.Base(u.Path)
@ -592,6 +600,27 @@ func loadRepoConfig(file string) (*repo.File, error) {
return r, nil return r, nil
} }
// verifyDownloadedDigest compares downloaded chart bytes against the digest from
// the repository index or OCI reference. A mismatch emits a warning to out.
func verifyDownloadedDigest(out io.Writer, ref, expectedDigest, resolvedDigest string, data *bytes.Buffer) error {
digest := expectedDigest
if digest == "" {
digest = resolvedDigest
}
if digest == "" || data == nil {
return nil
}
computed, err := provenance.Digest(bytes.NewReader(data.Bytes()))
if err != nil {
return err
}
if !digestEqual(digest, computed) {
fmt.Fprintf(out, "WARNING: digest mismatch for %s: repository index has sha256:%s, downloaded content has sha256:%s\n",
ref, stripDigestAlgorithm(digest), computed)
}
return nil
}
// stripDigestAlgorithm removes the algorithm prefix (e.g., "sha256:") from a digest string. // stripDigestAlgorithm removes the algorithm prefix (e.g., "sha256:") from a digest string.
// If no prefix is present, the original string is returned unchanged. // If no prefix is present, the original string is returned unchanged.
func stripDigestAlgorithm(digest string) string { func stripDigestAlgorithm(digest string) string {

@ -0,0 +1,95 @@
/*
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 downloader
import (
"fmt"
"strings"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/provenance"
)
// formatDigest returns a digest string in the canonical sha256: form.
func formatDigest(hex string) string {
if hex == "" {
return ""
}
if strings.HasPrefix(hex, "sha256:") {
return hex
}
return "sha256:" + hex
}
// digestEqual compares two digests regardless of sha256: prefix.
func digestEqual(a, b string) bool {
if a == "" || b == "" {
return a == b
}
return stripDigestAlgorithm(a) == stripDigestAlgorithm(b)
}
// digestFile computes the SHA256 digest of a chart archive file.
func digestFile(path string) (string, error) {
hex, err := provenance.DigestFile(path)
if err != nil {
return "", err
}
return formatDigest(hex), nil
}
// recordDependencyDigest computes and stores the tarball digest on a lock dependency entry.
func recordDependencyDigest(dep *chart.Dependency, tarballPath string) error {
digest, err := digestFile(tarballPath)
if err != nil {
return err
}
if dep.Digest != "" && !digestEqual(dep.Digest, digest) {
return fmt.Errorf("chart.lock digest mismatch for %s-%s: expected %s, got %s", dep.Name, dep.Version, formatDigest(dep.Digest), digest)
}
dep.Digest = digest
return nil
}
// lockNeedsWrite reports whether the new lock differs from the old lock in metadata
// or per-dependency content digests.
func lockNeedsWrite(old, updated *chart.Lock) bool {
if old == nil {
return true
}
if old.Digest != updated.Digest {
return true
}
return !dependencyDigestsEqual(old.Dependencies, updated.Dependencies)
}
func dependencyDigestsEqual(a, b []*chart.Dependency) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] == nil || b[i] == nil {
if a[i] != b[i] {
return false
}
continue
}
if a[i].Name != b[i].Name || !digestEqual(a[i].Digest, b[i].Digest) {
return false
}
}
return true
}

@ -0,0 +1,322 @@
/*
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 downloader
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/internal/resolver"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/provenance"
"helm.sh/helm/v4/pkg/repo/v1/repotest"
)
func setupDigestTestManager(t *testing.T, chartPath string, srv *repotest.Server) (*Manager, *bytes.Buffer) {
t.Helper()
out := bytes.NewBuffer(nil)
contentCache := t.TempDir()
m := &Manager{
ChartPath: chartPath,
Out: out,
Getters: getter.Providers{{Schemes: []string{"http", "https"}, New: getter.NewHTTPGetter}},
RepositoryConfig: filepath.Join(srv.Root(), "repositories.yaml"),
RepositoryCache: srv.Root(),
ContentCache: contentCache,
}
return m, out
}
func setupParentChart(t *testing.T, srv *repotest.Server, dep *chart.Dependency) (string, *chart.Chart) {
t.Helper()
dir := func(p ...string) string {
return filepath.Join(append([]string{srv.Root()}, p...)...)
}
parent := &chart.Chart{
Metadata: &chart.Metadata{
Name: "parent",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{dep},
},
}
require.NoError(t, chartutil.SaveDir(parent, dir()))
return dir("parent"), parent
}
func TestContentDigestUpdateOnRepublish(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
require.NoError(t, srv.LinkIndices())
parentPath, _ := setupParentChart(t, srv, &chart.Dependency{
Name: "local-subchart",
Version: "0.1.0",
Repository: srv.URL(),
})
m, _ := setupDigestTestManager(t, parentPath, srv)
require.NoError(t, m.Update())
lockData, err := os.ReadFile(filepath.Join(parentPath, "Chart.lock"))
require.NoError(t, err)
var lock1 chart.Lock
require.NoError(t, yaml.Unmarshal(lockData, &lock1))
firstDigest := lock1.Dependencies[0].Digest
require.NotEmpty(t, firstDigest)
assert.True(t, strings.HasPrefix(firstDigest, "sha256:"))
// Republish same version with different chart bytes.
sub, err := loader.LoadDir(filepath.Join("testdata", "local-subchart"))
require.NoError(t, err)
sub.Metadata.Description = "modified content"
_, err = chartutil.Save(sub, srv.Root())
require.NoError(t, err)
require.NoError(t, srv.CreateIndex())
require.NoError(t, m.Update())
lockData, err = os.ReadFile(filepath.Join(parentPath, "Chart.lock"))
require.NoError(t, err)
var lock2 chart.Lock
require.NoError(t, yaml.Unmarshal(lockData, &lock2))
secondDigest := lock2.Dependencies[0].Digest
require.NotEmpty(t, secondDigest)
assert.NotEqual(t, firstDigest, secondDigest)
assert.Equal(t, lock1.Digest, lock2.Digest, "metadata digest should be unchanged when version pin is exact")
}
func TestContentDigestStableOnSameBytes(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
require.NoError(t, srv.LinkIndices())
parentPath, _ := setupParentChart(t, srv, &chart.Dependency{
Name: "local-subchart",
Version: "0.1.0",
Repository: srv.URL(),
})
m, _ := setupDigestTestManager(t, parentPath, srv)
require.NoError(t, m.Update())
lockBefore, err := os.ReadFile(filepath.Join(parentPath, "Chart.lock"))
require.NoError(t, err)
require.NoError(t, m.Update())
lockAfter, err := os.ReadFile(filepath.Join(parentPath, "Chart.lock"))
require.NoError(t, err)
assert.Equal(t, string(lockBefore), string(lockAfter))
}
func TestBuildDigestMismatch(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
require.NoError(t, srv.LinkIndices())
parentPath, _ := setupParentChart(t, srv, &chart.Dependency{
Name: "local-subchart",
Version: "0.1.0",
Repository: srv.URL(),
})
m, _ := setupDigestTestManager(t, parentPath, srv)
require.NoError(t, m.Update())
sub, err := loader.LoadDir(filepath.Join("testdata", "local-subchart"))
require.NoError(t, err)
sub.Metadata.Description = "tampered upstream content"
_, err = chartutil.Save(sub, srv.Root())
require.NoError(t, err)
require.NoError(t, srv.CreateIndex())
err = m.Build()
require.Error(t, err)
assert.Contains(t, err.Error(), "chart.lock digest mismatch for local-subchart-0.1.0")
}
func TestLegacyLockWithoutDigest(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
require.NoError(t, srv.LinkIndices())
parentPath, parent := setupParentChart(t, srv, &chart.Dependency{
Name: "local-subchart",
Version: "0.1.0",
Repository: srv.URL(),
})
legacyLock := &chart.Lock{
Generated: time.Now(),
Dependencies: []*chart.Dependency{{
Name: "local-subchart",
Version: "0.1.0",
Repository: srv.URL(),
}},
}
digest, err := resolver.HashReq(parent.Metadata.Dependencies, legacyLock.Dependencies)
require.NoError(t, err)
legacyLock.Digest = digest
lockData, err := yaml.Marshal(legacyLock)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(parentPath, "Chart.lock"), lockData, 0644))
m, _ := setupDigestTestManager(t, parentPath, srv)
require.NoError(t, m.Build())
require.NoError(t, m.Update())
lockData, err = os.ReadFile(filepath.Join(parentPath, "Chart.lock"))
require.NoError(t, err)
var updated chart.Lock
require.NoError(t, yaml.Unmarshal(lockData, &updated))
require.NotEmpty(t, updated.Dependencies[0].Digest)
}
func TestIndexDigestMismatchWarning(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
require.NoError(t, srv.LinkIndices())
depArchive := filepath.Join(srv.Root(), "local-subchart-0.1.0.tgz")
realDigest, err := provenance.DigestFile(depArchive)
require.NoError(t, err)
dest := t.TempDir()
contentCache := t.TempDir()
out := bytes.NewBuffer(nil)
dl := ChartDownloader{
Out: out,
RepositoryConfig: filepath.Join(srv.Root(), "repositories.yaml"),
RepositoryCache: srv.Root(),
ContentCache: contentCache,
Getters: getter.Providers{{Schemes: []string{"http", "https"}, New: getter.NewHTTPGetter}},
ExpectedDigest: strings.Repeat("a", len(realDigest)),
}
_, _, err = dl.DownloadTo(srv.URL()+"/local-subchart-0.1.0.tgz", "0.1.0", dest)
require.NoError(t, err)
assert.Contains(t, out.String(), "WARNING: digest mismatch")
}
func TestSemverRangeRecordsContentDigest(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
sub, err := loader.LoadDir(filepath.Join("testdata", "local-subchart"))
require.NoError(t, err)
sub.Metadata.Version = "0.2.0"
_, err = chartutil.Save(sub, srv.Root())
require.NoError(t, err)
require.NoError(t, srv.CreateIndex())
require.NoError(t, srv.LinkIndices())
parentPath, _ := setupParentChart(t, srv, &chart.Dependency{
Name: "local-subchart",
Version: ">=0.1.0",
Repository: srv.URL(),
})
m, _ := setupDigestTestManager(t, parentPath, srv)
require.NoError(t, m.Update())
lockData, err := os.ReadFile(filepath.Join(parentPath, "Chart.lock"))
require.NoError(t, err)
var lock chart.Lock
require.NoError(t, yaml.Unmarshal(lockData, &lock))
require.Len(t, lock.Dependencies, 1)
assert.Equal(t, "0.2.0", lock.Dependencies[0].Version)
require.NotEmpty(t, lock.Dependencies[0].Digest)
tgzPath := filepath.Join(parentPath, "charts", "local-subchart-0.2.0.tgz")
require.FileExists(t, tgzPath)
got, err := provenance.DigestFile(tgzPath)
require.NoError(t, err)
assert.Equal(t, "sha256:"+got, lock.Dependencies[0].Digest)
}
func TestLockNeedsWrite(t *testing.T) {
metaDigest := "sha256:abc"
old := &chart.Lock{
Digest: metaDigest,
Dependencies: []*chart.Dependency{{
Name: "foo",
Version: "1.0.0",
Digest: "sha256:111",
}},
}
same := &chart.Lock{
Digest: metaDigest,
Dependencies: []*chart.Dependency{{
Name: "foo",
Version: "1.0.0",
Digest: "sha256:111",
}},
}
changedContent := &chart.Lock{
Digest: metaDigest,
Dependencies: []*chart.Dependency{{
Name: "foo",
Version: "1.0.0",
Digest: "sha256:222",
}},
}
assert.False(t, lockNeedsWrite(old, same))
assert.True(t, lockNeedsWrite(old, changedContent))
assert.True(t, lockNeedsWrite(nil, same))
withNil := &chart.Lock{
Digest: metaDigest,
Dependencies: []*chart.Dependency{
nil,
{Name: "foo", Version: "1.0.0", Digest: "sha256:111"},
},
}
assert.True(t, lockNeedsWrite(old, withNil))
assert.True(t, dependencyDigestsEqual(withNil.Dependencies, withNil.Dependencies))
}
func TestLoadLockWithDigestField(t *testing.T) {
srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/local-subchart-0.1.0.tgz"))
defer srv.Stop()
require.NoError(t, srv.LinkIndices())
parentPath, _ := setupParentChart(t, srv, &chart.Dependency{
Name: "local-subchart",
Version: "0.1.0",
Repository: srv.URL(),
})
m, _ := setupDigestTestManager(t, parentPath, srv)
require.NoError(t, m.Update())
c, err := loader.LoadDir(parentPath)
require.NoError(t, err)
require.NotNil(t, c.Lock)
require.NotEmpty(t, c.Lock.Dependencies[0].Digest)
}

@ -211,9 +211,8 @@ func (m *Manager) Update() error {
} }
lock.Digest = newDigest lock.Digest = newDigest
// If the lock file hasn't changed, don't write a new one. // Rewrite the lock when metadata or per-dependency content digests changed.
oldLock := c.Lock if !lockNeedsWrite(c.Lock, lock) {
if oldLock != nil && oldLock.Digest == lock.Digest {
return nil return nil
} }
@ -273,6 +272,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
var saveError error var saveError error
churls := make(map[string]struct{}) churls := make(map[string]struct{})
churlFiles := make(map[string]string)
for _, dep := range deps { for _, dep := range deps {
// No repository means the chart is in charts directory // No repository means the chart is in charts directory
if dep.Repository == "" { if dep.Repository == "" {
@ -310,12 +310,17 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
break break
} }
dep.Version = ver dep.Version = ver
tgzPath := filepath.Join(tmpPath, fmt.Sprintf("%s-%s.tgz", dep.Name, ver))
if err := recordDependencyDigest(dep, tgzPath); err != nil {
saveError = err
break
}
continue continue
} }
// Any failure to resolve/download a chart should fail: // Any failure to resolve/download a chart should fail:
// https://github.com/helm/helm/issues/1439 // https://github.com/helm/helm/issues/1439
churl, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) churl, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, indexDigest, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos)
if err != nil { if err != nil {
saveError = fmt.Errorf("could not find %s: %w", churl, err) saveError = fmt.Errorf("could not find %s: %w", churl, err)
break break
@ -323,6 +328,12 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
if _, ok := churls[churl]; ok { if _, ok := churls[churl]; ok {
fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository) fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository)
if tgzPath, ok := churlFiles[churl]; ok {
if err := recordDependencyDigest(dep, tgzPath); err != nil {
saveError = err
break
}
}
continue continue
} }
@ -343,6 +354,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithInsecureSkipVerifyTLS(insecureSkipTLSVerify), getter.WithInsecureSkipVerifyTLS(insecureSkipTLSVerify),
getter.WithTLSClientConfig(certFile, keyFile, caFile), getter.WithTLSClientConfig(certFile, keyFile, caFile),
}, },
ExpectedDigest: indexDigest,
} }
version := "" version := ""
@ -356,12 +368,19 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version)) getter.WithTagName(version))
} }
if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil { destfile, _, err := dl.DownloadTo(churl, version, tmpPath)
if err != nil {
saveError = fmt.Errorf("could not download %s: %w", churl, err) saveError = fmt.Errorf("could not download %s: %w", churl, err)
break break
} }
if err := recordDependencyDigest(dep, destfile); err != nil {
saveError = err
break
}
churls[churl] = struct{}{} churls[churl] = struct{}{}
churlFiles[churl] = destfile
} }
// TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins". // TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins".
@ -722,9 +741,9 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
// repoURL is the repository to search // repoURL is the repository to search
// //
// If it finds a URL that is "relative", it will prepend the repoURL. // If it finds a URL that is "relative", it will prepend the repoURL.
func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureSkipTLSVerify, passCredentialsAll bool, caFile, certFile, keyFile string, err error) { func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureSkipTLSVerify, passCredentialsAll bool, caFile, certFile, keyFile, indexDigest string, err error) {
if registry.IsOCI(repoURL) { if registry.IsOCI(repoURL) {
return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", "", nil
} }
for _, cr := range repos { for _, cr := range repos {
@ -744,6 +763,7 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
//nolint:nakedret //nolint:nakedret
return return
} }
indexDigest = ve.Digest
url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0]) url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0])
if err != nil { if err != nil {
//nolint:nakedret //nolint:nakedret
@ -762,10 +782,10 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
} }
url, err = repo.FindChartInRepoURL(repoURL, name, m.Getters, repo.WithChartVersion(version), repo.WithClientTLS(certFile, keyFile, caFile)) url, err = repo.FindChartInRepoURL(repoURL, name, m.Getters, repo.WithChartVersion(version), repo.WithClientTLS(certFile, keyFile, caFile))
if err == nil { if err == nil {
return url, username, password, false, false, "", "", "", err return url, username, password, false, false, "", "", "", "", err
} }
err = fmt.Errorf("chart %s not found in %s: %w", name, repoURL, err) err = fmt.Errorf("chart %s not found in %s: %w", name, repoURL, err)
return url, username, password, false, false, "", "", "", err return url, username, password, false, false, "", "", "", "", err
} }
// findEntryByName finds an entry in the chart repository whose name matches the given name. // findEntryByName finds an entry in the chart repository whose name matches the given name.

@ -71,7 +71,7 @@ func TestFindChartURL(t *testing.T) {
version := "0.1.0" version := "0.1.0"
repoURL := "http://example.com/charts" repoURL := "http://example.com/charts"
churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err := m.findChartURL(name, version, repoURL, repos) churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, _, err := m.findChartURL(name, version, repoURL, repos)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -96,7 +96,7 @@ func TestFindChartURL(t *testing.T) {
version = "1.2.3" version = "1.2.3"
repoURL = "https://example-https-insecureskiptlsverify.com" repoURL = "https://example-https-insecureskiptlsverify.com"
churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err = m.findChartURL(name, version, repoURL, repos) churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, _, err = m.findChartURL(name, version, repoURL, repos)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -121,7 +121,7 @@ func TestFindChartURL(t *testing.T) {
version = "1.2.3" version = "1.2.3"
repoURL = "http://example.com/helm" repoURL = "http://example.com/helm"
churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err = m.findChartURL(name, version, repoURL, repos) churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, _, err = m.findChartURL(name, version, repoURL, repos)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

Loading…
Cancel
Save