Downloading chart dependencies and saving them using their alias when one is configured.

When multiple dependencies having the same name but different aliases are specified,
dependencies are not saved to disk using their configured alias and an error is reported
with regards to an invalid version.

Signed-off-by: Etienne Hardy <etienne.hardy@gmail.com>
pull/12930/head
Etienne Hardy 3 months ago committed by Etienne Hardy
parent e5e3fac3ca
commit 3735729044

@ -73,6 +73,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
Name: d.Name,
Repository: "",
Version: d.Version,
Alias: d.Alias,
}
continue
}
@ -103,17 +104,19 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
Name: d.Name,
Repository: d.Repository,
Version: ch.Metadata.Version,
Alias: d.Alias,
}
continue
}
repoName := repoNames[d.Name]
repoName := repoNames[d.ActualName()]
// if the repository was not defined, but the dependency defines a repository url, bypass the cache
if repoName == "" && d.Repository != "" {
locked[i] = &chart.Dependency{
Name: d.Name,
Repository: d.Repository,
Version: d.Version,
Alias: d.Alias,
}
continue
}
@ -172,6 +175,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
Name: d.Name,
Repository: d.Repository,
Version: version,
Alias: d.Alias,
}
// The version are already sorted and hence the first one to satisfy the constraint is used
for _, ver := range vs {

@ -16,6 +16,7 @@ limitations under the License.
package chart
import (
"fmt"
"path/filepath"
"regexp"
"strings"
@ -56,6 +57,15 @@ type Chart struct {
dependencies []*Chart
}
// Archive is a helm package on disk with a possible alias
type Archive struct {
Name string
Version string
URL string
Alias string
Dir string
}
type CRD struct {
// Name is the File.Name for the crd file
Name string
@ -167,6 +177,22 @@ func (ch *Chart) CRDObjects() []CRD {
return crds
}
func (a Archive) FileName() string {
if a.Alias != "" {
return filepath.Join(a.Dir, fmt.Sprintf("%s-%s.tgz", a.Alias, a.Version))
} else {
return filepath.Join(a.Dir, fmt.Sprintf("%s-%s.tgz", a.Name, a.Version))
}
}
func (a Archive) ProvFileName() string {
return fmt.Sprintf("%s.prov", a.FileName())
}
func (a Archive) SignatureFileName() string {
return fmt.Sprintf("%s-%s.tgz", a.Name, a.Version)
}
func hasManifestExtension(fname string) bool {
ext := filepath.Ext(fname)
return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")

@ -69,6 +69,14 @@ func (d *Dependency) Validate() error {
return nil
}
func (d *Dependency) ActualName() string {
if d.Alias != "" {
return d.Alias
} else {
return d.Name
}
}
// Lock is a lock file for dependencies.
//
// It represents the state that the dependencies should be in.

@ -17,6 +17,7 @@ package downloader
import (
"fmt"
"helm.sh/helm/v3/pkg/chart"
"io"
"net/url"
"os"
@ -141,6 +142,62 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
return destfile, ver, nil
}
func (c *ChartDownloader) DownloadArchiveTo(archive *chart.Archive) (string, *provenance.Verification, error) {
u, err := c.ResolveChartVersion(archive.URL, archive.Version)
if err != nil {
return "", nil, err
}
g, err := c.Getters.ByScheme(u.Scheme)
if err != nil {
return "", nil, err
}
data, err := g.Get(u.String(), c.Options...)
if err != nil {
return "", nil, err
}
name := filepath.Base(u.Path)
if u.Scheme == registry.OCIScheme {
idx := strings.LastIndexByte(name, ':')
name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:])
}
//destfile := archive.FileName()
if err := fileutil.AtomicWriteFile(archive.FileName(), data, 0644); err != nil {
return archive.FileName(), nil, err
}
// If provenance is requested, verify it.
ver := &provenance.Verification{}
if c.Verify > VerifyNever {
body, err := g.Get(u.String() + ".prov")
if err != nil {
if c.Verify == VerifyAlways {
return archive.FileName(), ver, errors.Errorf("failed to fetch provenance %q", u.String()+".prov")
}
fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", archive.URL, err)
return archive.FileName(), ver, nil
}
//provfile := destfile + ".prov"
if err := fileutil.AtomicWriteFile(archive.ProvFileName(), body, 0644); err != nil {
return archive.FileName(), nil, err
}
if c.Verify != VerifyLater {
ver, err = VerifyChartArchive(archive, c.Keyring)
if err != nil {
// Fail always in this case, since it means the verification step
// failed.
return archive.FileName(), ver, err
}
}
}
return archive.FileName(), ver, nil
}
func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, error) {
var tag string
var err error
@ -330,6 +387,30 @@ func VerifyChart(path, keyring string) (*provenance.Verification, error) {
return sig.Verify(path, provfile)
}
func VerifyChartArchive(archive *chart.Archive, keyring string) (*provenance.Verification, error) {
// For now, error out if it's not a tar file.
path := archive.FileName()
switch fi, err := os.Stat(path); {
case err != nil:
return nil, err
case fi.IsDir():
return nil, errors.New("unpacked charts cannot be verified")
case !isTar(path):
return nil, errors.New("chart must be a tgz file")
}
provfile := archive.ProvFileName()
if _, err := os.Stat(provfile); err != nil {
return nil, errors.Wrapf(err, "could not load provenance file %s", provfile)
}
sig, err := provenance.NewFromKeyring(keyring, "")
if err != nil {
return nil, errors.Wrap(err, "failed to load keyring")
}
return sig.VerifyArchive(archive)
}
// isTar tests whether the given file is a tar file.
//
// Currently, this simply checks extension, since a subsequent function will

@ -16,6 +16,7 @@ limitations under the License.
package downloader
import (
"helm.sh/helm/v3/pkg/chart"
"os"
"path/filepath"
"testing"
@ -153,6 +154,25 @@ func TestVerifyChart(t *testing.T) {
}
}
func TestVerifyChartArchive(t *testing.T) {
archive := chart.Archive{
Name: "signtest",
Version: "0.1.0",
Alias: "signtest-alias",
Dir: "testdata",
}
v, err := VerifyChartArchive(&archive, "testdata/helm-test-key.pub")
if err != nil {
t.Fatal(err)
}
// The verification is tested at length in the provenance package. Here,
// we just want a quick sanity check that the v is not empty.
if len(v.FileHash) == 0 {
t.Error("Digest missing")
}
}
func TestIsTar(t *testing.T) {
tests := map[string]bool{
"foo.tgz": true,
@ -216,6 +236,60 @@ func TestDownloadTo(t *testing.T) {
}
}
func TestDownloadArchiveTo(t *testing.T) {
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*")
defer srv.Stop()
if err := srv.CreateIndex(); err != nil {
t.Fatal(err)
}
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
c := ChartDownloader{
Out: os.Stderr,
Verify: VerifyAlways,
Keyring: "testdata/helm-test-key.pub",
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
Getters: getter.All(&cli.EnvSettings{
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
}),
Options: []getter.Option{
getter.WithBasicAuth("username", "password"),
getter.WithPassCredentialsAll(false),
},
}
cname := "/signtest-0.1.0.tgz"
saveAs := "signtest-alias-0.1.0.tgz"
dest := srv.Root()
archive := chart.Archive{
Name: "signtest",
Version: "0.1.0",
URL: srv.URL() + cname,
Alias: "signtest-alias",
Dir: dest,
}
where, v, err := c.DownloadArchiveTo(&archive)
if err != nil {
t.Fatal(err)
}
if expect := filepath.Join(dest, saveAs); where != expect {
t.Errorf("Expected download to %s, got %s", expect, where)
}
if v.FileHash == "" {
t.Error("File hash was empty, but verification is required.")
}
if _, err := os.Stat(filepath.Join(dest, saveAs)); err != nil {
t.Error(err)
}
}
func TestDownloadTo_TLS(t *testing.T) {
// Set up mock server w/ tls enabled
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")

@ -352,7 +352,15 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version))
}
if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
archive := chart.Archive{
Name: dep.Name,
Version: dep.Version,
Alias: dep.Alias,
URL: churl,
Dir: tmpPath,
}
if _, _, err = dl.DownloadArchiveTo(&archive); err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl)
break
}
@ -585,12 +593,12 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
if m.Debug {
fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository)
}
reposMap[dd.Name] = dd.Repository
reposMap[dd.ActualName()] = dd.Repository
continue
}
if registry.IsOCI(dd.Repository) {
reposMap[dd.Name] = dd.Repository
reposMap[dd.ActualName()] = dd.Repository
continue
}
@ -605,7 +613,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
break
} else if urlutil.Equal(repo.URL, dd.Repository) {
found = true
reposMap[dd.Name] = repo.Name
reposMap[dd.ActualName()] = repo.Name
break
}
}

@ -0,0 +1,21 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
apiVersion: v1
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0
...
files:
signtest-0.1.0.tgz: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55
-----BEGIN PGP SIGNATURE-----
wsBcBAEBCgAQBQJcoosfCRCEO7+YH8GHYgAA220IALAs8T8NPgkcLvHu+5109cAN
BOCNPSZDNsqLZW/2Dc9cKoBG7Jen4Qad+i5l9351kqn3D9Gm6eRfAWcjfggRobV/
9daZ19h0nl4O1muQNAkjvdgZt8MOP3+PB3I3/Tu2QCYjI579SLUmuXlcZR5BCFPR
PJy+e3QpV2PcdeU2KZLG4tjtlrq+3QC9ZHHEJLs+BVN9d46Dwo6CxJdHJrrrAkTw
M8MhA92vbiTTPRSCZI9x5qDAwJYhoq0oxLflpuL2tIlo3qVoCsaTSURwMESEHO32
XwYG7BaVDMELWhAorBAGBGBwWFbJ1677qQ2gd9CN0COiVhekWlFRcnn60800r84=
=k9Y9
-----END PGP SIGNATURE-----

@ -295,6 +295,52 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
return ver, nil
}
func (s *Signatory) VerifyArchive(archive *hapi.Archive) (*Verification, error) {
ver := &Verification{}
for _, fname := range []string{archive.FileName(), archive.ProvFileName()} {
if fi, err := os.Stat(fname); err != nil {
return ver, err
} else if fi.IsDir() {
return ver, errors.Errorf("%s cannot be a directory", fname)
}
}
// First verify the signature
sig, err := s.decodeSignature(archive.ProvFileName())
if err != nil {
return ver, errors.Wrap(err, "failed to decode signature")
}
by, err := s.verifySignature(sig)
if err != nil {
return ver, err
}
ver.SignedBy = by
// Second, verify the hash of the tarball.
sum, err := DigestFile(archive.FileName())
if err != nil {
return ver, err
}
_, sums, err := parseMessageBlock(sig.Plaintext)
if err != nil {
return ver, err
}
sum = "sha256:" + sum
if sha, ok := sums.Files[archive.SignatureFileName()]; !ok {
return ver, errors.Errorf("provenance does not contain a SHA for a file named %q", archive.SignatureFileName())
} else if sha != sum {
return ver, errors.Errorf("sha256 sum does not match for %s: %q != %q", archive.SignatureFileName(), sha, sum)
}
ver.FileHash = sum
ver.FileName = archive.SignatureFileName()
// TODO: when image signing is added, verify that here.
return ver, nil
}
func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {
data, err := os.ReadFile(filename)
if err != nil {

@ -18,6 +18,7 @@ package provenance
import (
"crypto"
"fmt"
"helm.sh/helm/v3/pkg/chart"
"io"
"os"
"path/filepath"
@ -331,6 +332,40 @@ func TestVerify(t *testing.T) {
}
}
func TestVerifyArchive(t *testing.T) {
signer, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
archive := chart.Archive{
Name: "hashtest",
Version: "1.2.3",
Dir: "testdata",
}
if ver, err := signer.VerifyArchive(&archive); err != nil {
t.Errorf("Failed to pass verify. Err: %s", err)
} else if len(ver.FileHash) == 0 {
t.Error("Verification is missing hash.")
} else if ver.SignedBy == nil {
t.Error("No SignedBy field")
} else if ver.FileName != filepath.Base(testChartfile) {
t.Errorf("FileName is unexpectedly %q", ver.FileName)
}
if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil {
t.Errorf("Expected %s to fail.", testTamperedSigBlock)
}
switch err.(type) {
case pgperrors.SignatureError:
t.Logf("Tampered sig block error: %s (%T)", err, err)
default:
t.Errorf("Expected invalid signature error, got %q (%T)", err, err)
}
}
// readSumFile reads a file containing a sum generated by the UNIX shasum tool.
func readSumFile(sumfile string) (string, error) {
data, err := os.ReadFile(sumfile)

Loading…
Cancel
Save