diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index 03874742c..76a8ed846 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -71,6 +71,36 @@ the dependency charts stored locally. The path should start with a prefix of If the dependency chart is retrieved locally, it is not required to have the repository added to helm by "helm add repo". Version matching is also supported for this case. + +A repository can be defined as a git URL. The path must start with a prefix of +"git://" followed by a valid git repository URL. + + # Chart.yaml + dependencies: + - name: helm-chart + version: "main" + repository: "git://https://github.com/helm/helm-chart.git" + +The 'repository' can be the https or ssh URL that you would use to clone a git +repo or add as a git remote, prefixed with 'git:'. +For example 'git://git@github.com:helm/helm-chart.git' or +'git://https://github.com/helm/helm-chart.git' + +When using a 'git://' repository, the 'version' must be a valid semantic tag or branch +name for the git repo. For example 'master'. + +Limitations when working with git repositories: +* Helm will use the 'git' executable on your system to retrieve information +about the repo. The 'git' command must be properly configured and available +on the PATH. +* When specifying a private repo, if git tries to query the user for +username/passowrd for an HTTPS url, or for a certificate password for an SSH +url, it may cause Helm to hang. Input is not forwarded to the child git +process, so it will not be able to receive user input. For private repos +it is recommended to use an SSH git url, and have your git client configured +with an SSH cert that does not require a password. +* The helm chart and 'Chart.yaml' must be in the root of the git repo. +The chart cannot be loaded from a subdirectory. ` const dependencyListDesc = ` diff --git a/go.mod b/go.mod index b4ccd0bbc..c915f768c 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.1 + github.com/whilp/git-urls v1.0.0 github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 @@ -124,7 +125,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo v1.16.4 // indirect - github.com/onsi/gomega v1.15.0 // indirect + github.com/onsi/gomega v1.17.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index b9110d5c3..3f4e89e46 100644 --- a/go.sum +++ b/go.sum @@ -870,7 +870,6 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -1071,6 +1070,8 @@ github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmF github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= +github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= diff --git a/internal/fileutil/fileutil.go b/internal/fileutil/fileutil.go index 739093f3b..9292ead8a 100644 --- a/internal/fileutil/fileutil.go +++ b/internal/fileutil/fileutil.go @@ -21,6 +21,12 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" + "time" + + "archive/tar" + "bytes" + "compress/gzip" "helm.sh/helm/v3/internal/third_party/dep/fs" ) @@ -49,3 +55,81 @@ func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error return fs.RenameWithFallback(tempName, filename) } + +func CompressDirToTgz(chartTmpDir, tmpdir string) (*bytes.Buffer, error) { + + _, err := os.Stat(chartTmpDir) + if err != nil { + return nil, err + } + + _, err = os.Stat(tmpdir) + if err != nil { + return nil, err + } + + // tar => gzip => buf + buf := bytes.NewBuffer(nil) + zr := gzip.NewWriter(buf) + + zr.ModTime = time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC) + zr.Header.ModTime = time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC) + zr.Header.OS = 3 // Unix + zr.OS = 3 //Unix + zr.Extra = nil + + tw := tar.NewWriter(zr) + + // walk through every file in the folder + walkErr := filepath.Walk(chartTmpDir, func(file string, fi os.FileInfo, err error) error { + + // generate tar header + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(fi, strings.TrimPrefix(file, tmpdir+"/")) + if err != nil { + return err + } + + // must provide real name + // (see https://golang.org/src/archive/tar/common.go?#L626) + header.Name = strings.TrimPrefix(filepath.ToSlash(file), tmpdir+"/") + header.ModTime = time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC) + header.AccessTime = time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC) + header.ChangeTime = time.Date(1977, time.May, 25, 0, 0, 0, 0, time.UTC) + header.Format = tar.FormatPAX + header.PAXRecords = nil + + // write header + if err := tw.WriteHeader(header); err != nil { + return err + } + + // if not a dir, write file content + if !fi.IsDir() { + data, err := os.Open(file) + if err != nil { + return err + } + if _, err := io.Copy(tw, data); err != nil { + return err + } + } + return nil + }) + if walkErr != nil { + return nil, walkErr + } + + // produce tar + if err := tw.Close(); err != nil { + return nil, err + } + // produce gzip + if err := zr.Close(); err != nil { + return nil, err + } + return buf, nil +} diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go index 76cd8f074..01d50c188 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -17,7 +17,10 @@ limitations under the License. package fileutil import ( + "archive/tar" "bytes" + "compress/gzip" + "io" "io/ioutil" "os" "path/filepath" @@ -56,3 +59,47 @@ func TestAtomicWriteFile(t *testing.T) { mode, gotinfo.Mode()) } } + +func TestCompressDirToTgz(t *testing.T) { + + testDataDir := "testdata" + chartTestDir := "testdata/testdir" + + chartBytes, err := CompressDirToTgz(chartTestDir, testDataDir) + if err != nil { + t.Fatal(err) + } + + // gzip read + gr, err := gzip.NewReader(chartBytes) + if err != nil { + t.Fatal(err) + } + defer gr.Close() + + // tar read + tr := tar.NewReader(gr) + defer gr.Close() + + found := false + fileBytes := bytes.NewBuffer(nil) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if hdr.Name == "testdir/testfile" { + found = true + _, err := io.Copy(fileBytes, tr) + if err != nil { + t.Fatal(err) + } + } + } + if !found { + t.Fatal("testdir/testfile not found") + } + if !bytes.Equal(fileBytes.Bytes(), []byte("helm")) { + t.Fatalf("testdir/testfile's content not match, excpcted %s, got %s", "helm", fileBytes.String()) + } +} diff --git a/internal/fileutil/testdata/testdir/testfile b/internal/fileutil/testdata/testdir/testfile new file mode 100644 index 000000000..5a8e18c53 --- /dev/null +++ b/internal/fileutil/testdata/testdir/testfile @@ -0,0 +1 @@ +helm \ No newline at end of file diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 5e8921f96..788982af2 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -29,12 +29,15 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/gitutils" "helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/provenance" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/repo" ) +var hasGitReference = gitutils.HasGitReference + // Resolver resolves dependencies from semantic version ranges to a particular version. type Resolver struct { chartpath string @@ -107,6 +110,27 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string continue } + if strings.HasPrefix(d.Repository, "git://") { + + found, err := hasGitReference(strings.TrimPrefix(d.Repository, "git://"), d.Version, d.Name) + + if err != nil { + return nil, err + } + + if !found { + return nil, fmt.Errorf(`dependency %q is missing git branch or tag: %s. + When using a "git://" type repository, the "version" should be a valid branch or tag name`, d.Name, d.Version) + } + + locked[i] = &chart.Dependency{ + Name: d.Name, + Repository: d.Repository, + Version: d.Version, + } + continue + } + repoName := repoNames[d.Name] // if the repository was not defined, but the dependency defines a repository url, bypass the cache if repoName == "" && d.Repository != "" { diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go index a79852175..54fbf939f 100644 --- a/internal/resolver/resolver_test.go +++ b/internal/resolver/resolver_test.go @@ -23,7 +23,16 @@ import ( "helm.sh/helm/v3/pkg/registry" ) +func fakeGitReference(gitRepo, ref, repoName string) (bool, error) { + gitRefs := map[string]string{ + "1.0.0": "", + } + + _, found := gitRefs[ref] + return found, nil +} func TestResolve(t *testing.T) { + hasGitReference = fakeGitReference tests := []struct { name string req []*chart.Dependency @@ -137,6 +146,54 @@ func TestResolve(t *testing.T) { }, err: true, }, + { + name: "repo from git https url", + req: []*chart.Dependency{ + {Name: "gitdependencyok", Repository: "git://https://github.com/helm/helmchart.git", Version: "1.0.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "gitdependencyok", Repository: "git://https://github.com/helm/helmchart.git", Version: "1.0.0"}, + }, + }, + err: false, + }, + { + name: "repo from git https url", + req: []*chart.Dependency{ + {Name: "gitdependencyerror", Repository: "git://https://github.com/helm/helmchart.git", Version: "2.0.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "gitdependencyerror", Repository: "git://https://github.com/helm/helmchart.git", Version: "2.0.0"}, + }, + }, + err: true, + }, + { + name: "repo from git ssh url", + req: []*chart.Dependency{ + {Name: "gitdependency", Repository: "git://git@github.com:helm/helmchart.git", Version: "1.0.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "gitdependency", Repository: "git://git@github.com:helm/helmchart.git", Version: "1.0.0"}, + }, + }, + err: false, + }, + { + name: "repo from git ssh url", + req: []*chart.Dependency{ + {Name: "gitdependencyerror", Repository: "git://git@github.com:helm/helmchart.git", Version: "2.0.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "gitdependencyerror", Repository: "git://git@github.com:helm/helmchart.git", Version: "2.0.0"}, + }, + }, + err: true, + }, } repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"} diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 3feb5b702..720b3e69b 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -26,6 +26,8 @@ import ( "github.com/Masterminds/semver/v3" "github.com/pkg/errors" + giturls "github.com/whilp/git-urls" + "helm.sh/helm/v3/internal/fileutil" "helm.sh/helm/v3/internal/urlutil" "helm.sh/helm/v3/pkg/getter" @@ -92,7 +94,15 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return "", nil, err } - g, err := c.Getters.ByScheme(u.Scheme) + scheme := "" + + if strings.HasPrefix(ref, "git://") { + scheme = "git" + } else { + scheme = u.Scheme + } + + g, err := c.Getters.ByScheme(scheme) if err != nil { return "", nil, err } @@ -103,10 +113,17 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven } name := filepath.Base(u.Path) - if u.Scheme == registry.OCIScheme { + if u.Scheme == registry.OCIScheme || u.Scheme == "git" { idx := strings.LastIndexByte(name, ':') name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:]) } + if scheme == "git" { + gitGetter, ok := g.(*getter.GitGetter) + if !ok { + return "", nil, fmt.Errorf("can't convert to GITGetter") + } + name = fmt.Sprintf("%s-%s.tgz", gitGetter.ChartName(), version) + } destfile := filepath.Join(dest, name) if err := fileutil.AtomicWriteFile(destfile, data, 0644); err != nil { @@ -180,7 +197,7 @@ func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, // the URL using the appropriate Getter. // // A reference may be an HTTP URL, an oci reference URL, a 'reponame/chartname' -// reference, or a local path. +// reference, git URL, or a local path. // // A version is a SemVer string (1.2.3-beta.1+f334a6789). // @@ -190,6 +207,14 @@ func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, // * If version is empty, this will return the URL for the latest version // * If no version can be found, an error is returned func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) { + if strings.HasPrefix(ref, "git://") { + gitURL := strings.TrimPrefix(ref, "git://") + u, err := giturls.Parse(gitURL) + if err != nil { + return nil, errors.Errorf("invalid git URL format: %s", gitURL) + } + return u, nil + } u, err := url.Parse(ref) if err != nil { return nil, errors.Errorf("invalid chart URL format: %s", ref) diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index f70a56422..3c27f1cfa 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -40,6 +40,7 @@ func TestResolveChartRef(t *testing.T) { {name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, {name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"}, {name: "full URL, with authentication", ref: "http://username:password@example.com/foo-1.2.3.tgz", expect: "http://username:password@example.com/foo-1.2.3.tgz"}, + {name: "helmchart", ref: "git://https://github.com/helmchart/helmchart.git", expect: "https://github.com/helmchart/helmchart.git"}, {name: "reference, testing repo", ref: "testing/alpine", expect: "http://example.com/alpine-1.2.3.tgz"}, {name: "reference, version, testing repo", ref: "testing/alpine", version: "0.2.0", expect: "http://example.com/alpine-0.2.0.tgz"}, {name: "reference, version, malformed repo", ref: "malformed/alpine", version: "1.2.3", expect: "http://dl.example.com/alpine-1.2.3.tgz"}, diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 18b28dde1..4ba0bcb70 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -343,6 +343,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { } version := "" + if registry.IsOCI(churl) { churl, version, err = parseOCIRef(churl) if err != nil { @@ -353,6 +354,17 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { getter.WithTagName(version)) } + if strings.HasPrefix(churl, "git://") { + version = dep.Version + + dl.Options = append(dl.Options, getter.WithTagName(version)) + dl.Options = append(dl.Options, getter.WithChartName(dep.Name)) + + if m.Debug { + fmt.Fprintf(m.Out, "Downloading %s from git repo %s\n", dep.Name, churl) + } + } + if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil { saveError = errors.Wrapf(err, "could not download %s", churl) break @@ -477,6 +489,11 @@ Loop: continue } + // If repo is from git url, continue + if strings.HasPrefix(dd.Repository, "git://") { + continue + } + if dd.Repository == "" { continue } @@ -594,6 +611,16 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, reposMap[dd.Name] = dd.Repository continue } + // if dep chart is from a git url, assume it is valid for now. + // if the repo does not exist then it will later error when we try to fetch branches and tags. + // we could check for the repo existence here, but trying to avoid anotehr git request. + if strings.HasPrefix(dd.Repository, "git://") { + if m.Debug { + fmt.Fprintf(m.Out, "Repository from git url: %s\n", strings.TrimPrefix(dd.Repository, "git:")) + } + reposMap[dd.Name] = dd.Repository + continue + } found := false @@ -704,6 +731,10 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { // // 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) { + if strings.HasPrefix(repoURL, "git://") { + return repoURL, "", "", false, false, "", "", "", nil + } + if registry.IsOCI(repoURL) { return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil } diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index f7ab1a568..0652f6440 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -80,55 +80,36 @@ func TestFindChartURL(t *testing.T) { t.Fatal(err) } - name := "alpine" - version := "0.1.0" - repoURL := "http://example.com/charts" - - churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err := m.findChartURL(name, version, repoURL, repos) - if err != nil { - t.Fatal(err) - } - - if churl != "https://charts.helm.sh/stable/alpine-0.1.0.tgz" { - t.Errorf("Unexpected URL %q", churl) - } - if username != "" { - t.Errorf("Unexpected username %q", username) - } - if password != "" { - t.Errorf("Unexpected password %q", password) - } - if passcredentialsall != false { - t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) - } - if insecureSkipTLSVerify { - t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify) + tests := []struct { + name, version, repoURL, expectChurl, expectUserName, expectPassword string + expectInsecureSkipTLSVerify, expectPasscredentialsall bool + }{ + {name: "alpine", version: "0.1.0", repoURL: "http://example.com/charts", expectChurl: "https://charts.helm.sh/stable/alpine-0.1.0.tgz", expectUserName: "", expectPassword: "", expectInsecureSkipTLSVerify: false, expectPasscredentialsall: false}, + {name: "tlsfoo", version: "1.2.3", repoURL: "https://example-https-insecureskiptlsverify.com", expectChurl: "https://example.com/tlsfoo-1.2.3.tgz", expectUserName: "", expectPassword: "", expectInsecureSkipTLSVerify: true, expectPasscredentialsall: false}, + {name: "helm-test", version: "master", repoURL: "git://https://github.com/rally25rs/helm-test-chart.git", expectChurl: "git://https://github.com/rally25rs/helm-test-chart.git", expectUserName: "", expectPassword: "", expectInsecureSkipTLSVerify: false, expectPasscredentialsall: false}, } - - name = "tlsfoo" - version = "1.2.3" - repoURL = "https://example-https-insecureskiptlsverify.com" - - churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err = m.findChartURL(name, version, repoURL, repos) - if err != nil { - t.Fatal(err) + for _, tt := range tests { + churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err := m.findChartURL(tt.name, tt.version, tt.repoURL, repos) + if err != nil { + t.Fatal(err) + } + if churl != tt.expectChurl { + t.Errorf("Unexpected URL %q", churl) + } + if username != tt.expectUserName { + t.Errorf("Unexpected username %q", username) + } + if password != tt.expectPassword { + t.Errorf("Unexpected password %q", password) + } + if insecureSkipTLSVerify != tt.expectInsecureSkipTLSVerify { + t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify) + } + if passcredentialsall != tt.expectPasscredentialsall { + t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) + } } - if !insecureSkipTLSVerify { - t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify) - } - if churl != "https://example.com/tlsfoo-1.2.3.tgz" { - t.Errorf("Unexpected URL %q", churl) - } - if username != "" { - t.Errorf("Unexpected username %q", username) - } - if password != "" { - t.Errorf("Unexpected password %q", password) - } - if passcredentialsall != false { - t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) - } } func TestGetRepoNames(t *testing.T) { @@ -193,6 +174,13 @@ func TestGetRepoNames(t *testing.T) { }, expect: map[string]string{}, }, + { + name: "repo from git url", + req: []*chart.Dependency{ + {Name: "local-dep", Repository: "git://https://github.com/git/git"}, + }, + expect: map[string]string{"local-dep": "git://https://github.com/git/git"}, + }, } for _, tt := range tests { diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 653b032fe..a9e33524c 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -42,6 +42,7 @@ type options struct { passCredentialsAll bool userAgent string version string + chartName string registryClient *registry.Client timeout time.Duration transport *http.Transport @@ -66,6 +67,11 @@ func WithBasicAuth(username, password string) Option { opts.password = password } } +func WithChartName(chartName string) Option { + return func(opts *options) { + opts.chartName = chartName + } +} func WithPassCredentialsAll(pass bool) Option { return func(opts *options) { @@ -182,11 +188,16 @@ var ociProvider = Provider{ New: NewOCIGetter, } +var gitProvider = Provider{ + Schemes: []string{"git"}, + New: NewGitGetter, +} + // All finds all of the registered getters as a list of Provider instances. // Currently, the built-in getters and the discovered plugins with downloader // notations are collected. func All(settings *cli.EnvSettings) Providers { - result := Providers{httpProvider, ociProvider} + result := Providers{httpProvider, ociProvider, gitProvider} pluginDownloaders, _ := collectPlugins(settings) result = append(result, pluginDownloaders...) return result diff --git a/pkg/getter/getter_test.go b/pkg/getter/getter_test.go index ab14784ab..c331f2c0f 100644 --- a/pkg/getter/getter_test.go +++ b/pkg/getter/getter_test.go @@ -57,8 +57,8 @@ func TestAll(t *testing.T) { env.PluginsDirectory = pluginDir all := All(env) - if len(all) != 4 { - t.Errorf("expected 4 providers (default plus three plugins), got %d", len(all)) + if len(all) != 5 { + t.Errorf("expected 5 providers (default plus three plugins), got %d", len(all)) } if _, err := all.ByScheme("test2"); err != nil { diff --git a/pkg/getter/gitgetter.go b/pkg/getter/gitgetter.go new file mode 100644 index 000000000..e234e5a47 --- /dev/null +++ b/pkg/getter/gitgetter.go @@ -0,0 +1,114 @@ +/* +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 getter + +import ( + "bytes" + "strings" + + "fmt" + "os" + "path/filepath" + + "github.com/Masterminds/vcs" + + "helm.sh/helm/v3/internal/fileutil" +) + +// GitGetter is the default HTTP(/S) backend handler +type GitGetter struct { + opts options +} + +func (g *GitGetter) ChartName() string { + return g.opts.chartName +} + +// ensureGitDirIgnored will append ".git/" to the .helmignore file in a directory. +// Create the .helmignore file if it does not exist. +func (g *GitGetter) ensureGitDirIgnored(repoPath string) error { + helmignorePath := filepath.Join(repoPath, ".helmignore") + f, err := os.OpenFile(helmignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + if _, err := f.WriteString("\n.git/\n"); err != nil { + return err + } + return nil +} + +//Get performs a Get from repo.Getter and returns the body. +func (g *GitGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { + for _, opt := range options { + opt(&g.opts) + } + return g.get(href) +} + +func (g *GitGetter) get(href string) (*bytes.Buffer, error) { + gitURL := strings.TrimPrefix(href, "git://") + version := g.opts.version + chartName := g.opts.chartName + if version == "" { + return nil, fmt.Errorf("the version must be a valid tag or branch name for the git repo, not nil") + } + tmpDir, err := os.MkdirTemp("", "helm") + if err != nil { + return nil, err + } + chartTmpDir := filepath.Join(tmpDir, chartName) + + if err := os.MkdirAll(chartTmpDir, 0755); err != nil { + return nil, err + } + defer os.RemoveAll(tmpDir) + + repo, err := vcs.NewRepo(gitURL, chartTmpDir) + if err != nil { + return nil, err + } + if err := repo.Get(); err != nil { + return nil, err + } + if err := repo.UpdateVersion(version); err != nil { + return nil, err + } + + // A .helmignore that includes an ignore for .git/ should be included in the git repo itself, + // but a lot of people will probably not think about that. + // To prevent the git history from bleeding into the charts archive, append/create .helmignore. + g.ensureGitDirIgnored(chartTmpDir) + + buf, err := fileutil.CompressDirToTgz(chartTmpDir, tmpDir) + if err != nil { + return nil, fmt.Errorf("unable to tar and compress dir %s to tgz file. %s", tmpDir, err) + } + return buf, nil +} + +// NewGitGetter constructs a valid git client as a Getter +func NewGitGetter(ops ...Option) (Getter, error) { + + client := GitGetter{} + + for _, opt := range ops { + opt(&client.opts) + } + + return &client, nil +} diff --git a/pkg/getter/gitgetter_test.go b/pkg/getter/gitgetter_test.go new file mode 100644 index 000000000..94bbb0d62 --- /dev/null +++ b/pkg/getter/gitgetter_test.go @@ -0,0 +1,31 @@ +/* +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 getter + +import ( + "testing" +) + +func TestNewGITGetter(t *testing.T) { + g, err := NewGitGetter() + if err != nil { + t.Fatal(err) + } + + if _, ok := g.(*GitGetter); !ok { + t.Fatal("Expected NewGITGetter to produce an *GITGetter") + } +} diff --git a/pkg/gitutils/gitutils.go b/pkg/gitutils/gitutils.go new file mode 100644 index 000000000..1795caf68 --- /dev/null +++ b/pkg/gitutils/gitutils.go @@ -0,0 +1,47 @@ +/* +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 engine implements the Go text template engine as needed for Helm. + +When Helm renders templates it does so with additional functions and different +modes (e.g., strict, lint mode). This package handles the helm specific +implementation. +*/ +package gitutils + +import ( + "os" + + "github.com/Masterminds/vcs" +) + +func HasGitReference(gitRepo, ref, repoName string) (bool, error) { + local, err := os.MkdirTemp("", repoName) + if err != nil { + return false, err + } + repo, err := vcs.NewRepo(gitRepo, local) + + if err != nil { + return false, err + } + + if err := repo.Get(); err != nil { + return false, err + } + defer os.RemoveAll(local) + return repo.IsReference(ref), nil +}