Implement the Getter interface for the git:// protocol.

Signed-off-by: yxxhero <aiopsclub@163.com>
pull/9482/head
yxxhero 5 years ago
parent 4c86566a27
commit 0c5855799d

@ -75,7 +75,7 @@ for this case.
A repository can be defined as a git URL. The path must start with a prefix of 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. "git:" followed by a valid git repository URL.
# requirements.yaml # Chart.yaml
dependencies: dependencies:
- name: nginx - name: nginx
version: "master" version: "master"

@ -21,6 +21,11 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"archive/tar"
"bytes"
"compress/gzip"
"helm.sh/helm/v3/internal/third_party/dep/fs" "helm.sh/helm/v3/internal/third_party/dep/fs"
) )
@ -49,3 +54,57 @@ func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error
return fs.RenameWithFallback(tempName, filename) return fs.RenameWithFallback(tempName, filename)
} }
func CompressDirToTgz(src, tmpdir string) (*bytes.Buffer, error) {
// tar => gzip => buf
buf := bytes.NewBuffer(nil)
zr := gzip.NewWriter(buf)
tw := tar.NewWriter(zr)
// walk through every file in the folder
walkErr := filepath.Walk(src, 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+"/")
// 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
}

@ -97,13 +97,20 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
return "", nil, err return "", nil, err
} }
data, err := g.Get(u.String(), c.Options...) downloadURL := ""
if u.Scheme == "git" {
downloadURL = u.Host + u.Path
} else {
downloadURL = u.String()
}
data, err := g.Get(downloadURL, c.Options...)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
name := filepath.Base(u.Path) name := filepath.Base(u.Path)
if u.Scheme == registry.OCIScheme { if u.Scheme == registry.OCIScheme || u.Scheme == "git" {
idx := strings.LastIndexByte(name, ':') idx := strings.LastIndexByte(name, ':')
name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:]) name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:])
} }
@ -190,6 +197,19 @@ 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 version is empty, this will return the URL for the latest version
// * If no version can be found, an error is returned // * If no version can be found, an error is returned
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) { func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) {
if strings.HasPrefix(ref, "git:") {
gitRefSplitResult := strings.Split(ref, "git:")
gitURL := gitRefSplitResult[1]
u, err := url.Parse(gitURL)
if err != nil {
return nil, errors.Errorf("invalid git URL format: %s", gitURL)
}
return &url.URL{
Scheme: "git",
Host: u.Scheme + "://" + u.Host,
Path: u.Path,
}, nil
}
u, err := url.Parse(ref) u, err := url.Parse(ref)
if err != nil { if err != nil {
return nil, errors.Errorf("invalid chart URL format: %s", ref) return nil, errors.Errorf("invalid chart URL format: %s", ref)

@ -310,20 +310,6 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
dep.Version = ver dep.Version = ver
continue continue
} }
if strings.HasPrefix(dep.Repository, "git:") {
destPath := filepath.Join(m.ChartPath, "charts")
gitURL := strings.TrimPrefix(dep.Repository, "git:")
if m.Debug {
fmt.Fprintf(m.Out, "Downloading %s from git repo %s\n", dep.Name, gitURL)
}
dl := GitDownloader{}
if err := dl.DownloadTo(gitURL, dep.Version, destPath); err != nil {
saveError = fmt.Errorf("could not download %s: %s", gitURL, err)
break
}
continue
}
fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
// 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
@ -357,7 +343,13 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
} }
version := "" version := ""
if registry.IsOCI(churl) { if registry.IsOCI(churl) {
if !resolver.FeatureGateOCI.IsEnabled() {
return errors.Wrapf(resolver.FeatureGateOCI.Error(),
"the repository %s is an OCI registry", churl)
}
churl, version, err = parseOCIRef(churl) churl, version, err = parseOCIRef(churl)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not parse OCI reference") return errors.Wrapf(err, "could not parse OCI reference")
@ -367,6 +359,17 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version)) 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 { if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl) saveError = errors.Wrapf(err, "could not download %s", churl)
break break
@ -733,6 +736,10 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
// //
// 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 string, err error) {
if strings.HasPrefix(repoURL, "git://") {
return repoURL, "", "", false, false, "", "", "", nil
}
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
} }

@ -42,6 +42,7 @@ type options struct {
passCredentialsAll bool passCredentialsAll bool
userAgent string userAgent string
version string version string
chartName string
registryClient *registry.Client registryClient *registry.Client
timeout time.Duration timeout time.Duration
transport *http.Transport transport *http.Transport
@ -66,6 +67,11 @@ func WithBasicAuth(username, password string) Option {
opts.password = password opts.password = password
} }
} }
func WithChartName(chartName string) Option {
return func(opts *options) {
opts.chartName = chartName
}
}
func WithPassCredentialsAll(pass bool) Option { func WithPassCredentialsAll(pass bool) Option {
return func(opts *options) { return func(opts *options) {
@ -182,11 +188,16 @@ var ociProvider = Provider{
New: NewOCIGetter, New: NewOCIGetter,
} }
var gitProvider = Provider{
Schemes: []string{"git"},
New: NewGITGetter,
}
// All finds all of the registered getters as a list of Provider instances. // All finds all of the registered getters as a list of Provider instances.
// Currently, the built-in getters and the discovered plugins with downloader // Currently, the built-in getters and the discovered plugins with downloader
// notations are collected. // notations are collected.
func All(settings *cli.EnvSettings) Providers { func All(settings *cli.EnvSettings) Providers {
result := Providers{httpProvider, ociProvider} result := Providers{httpProvider, ociProvider, gitProvider}
pluginDownloaders, _ := collectPlugins(settings) pluginDownloaders, _ := collectPlugins(settings)
result = append(result, pluginDownloaders...) result = append(result, pluginDownloaders...)
return result return result

@ -57,8 +57,8 @@ func TestAll(t *testing.T) {
env.PluginsDirectory = pluginDir env.PluginsDirectory = pluginDir
all := All(env) all := All(env)
if len(all) != 4 { if len(all) != 5 {
t.Errorf("expected 4 providers (default plus three plugins), got %d", len(all)) t.Errorf("expected 5 providers (default plus three plugins), got %d", len(all))
} }
if _, err := all.ByScheme("test2"); err != nil { if _, err := all.ByScheme("test2"); err != nil {

@ -13,28 +13,32 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package downloader package getter
import ( import (
"bytes"
"strings"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/internal/fileutil"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/gitutil" "helm.sh/helm/v3/pkg/gitutil"
) )
// Assigned here so it can be overridden for testing. // Assigned here so it can be overridden for testing.
var gitCloneTo = gitutil.CloneTo var gitCloneTo = gitutil.CloneTo
// GitDownloader handles downloading a chart from a git url. // GITGetter is the default HTTP(/S) backend handler
type GitDownloader struct{} type GITGetter struct {
opts options
}
// ensureGitDirIgnored will append ".git/" to the .helmignore file in a directory. // ensureGitDirIgnored will append ".git/" to the .helmignore file in a directory.
// Create the .helmignore file if it does not exist. // Create the .helmignore file if it does not exist.
func (g *GitDownloader) ensureGitDirIgnored(repoPath string) error { func (g *GITGetter) ensureGitDirIgnored(repoPath string) error {
helmignorePath := filepath.Join(repoPath, ".helmignore") helmignorePath := filepath.Join(repoPath, ".helmignore")
f, err := os.OpenFile(helmignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) f, err := os.OpenFile(helmignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
@ -47,17 +51,34 @@ func (g *GitDownloader) ensureGitDirIgnored(repoPath string) error {
return nil return nil
} }
// DownloadTo will create a temp directory, then fetch a git repo into it. //Get performs a Get from repo.Getter and returns the body.
// The git repo will be archived into a chart and copied to the destPath. func (g *GITGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
func (g *GitDownloader) DownloadTo(gitURL string, ref string, destPath string) 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 := ioutil.TempDir("", "helm") tmpDir, err := ioutil.TempDir("", "helm")
if err != nil { if err != nil {
return err return nil, err
}
chartTmpDir := filepath.Join(tmpDir, chartName)
if err := os.MkdirAll(chartTmpDir, 0755); err != nil {
return nil, err
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
if err = gitCloneTo(gitURL, ref, tmpDir); err != nil { if err = gitCloneTo(gitURL, version, chartTmpDir); err != nil {
return fmt.Errorf("Unable to retrieve git repo. %s", err) return nil, fmt.Errorf("Unable to retrieve git repo. %s", err)
} }
// A .helmignore that includes an ignore for .git/ should be included in the git repo itself, // A .helmignore that includes an ignore for .git/ should be included in the git repo itself,
@ -65,14 +86,21 @@ func (g *GitDownloader) DownloadTo(gitURL string, ref string, destPath string) e
// To prevent the git history from bleeding into the charts archive, append/create .helmignore. // To prevent the git history from bleeding into the charts archive, append/create .helmignore.
g.ensureGitDirIgnored(tmpDir) g.ensureGitDirIgnored(tmpDir)
// Turn the extracted git archive into a chart and move it into the charts directory. buf, err := fileutil.CompressDirToTgz(chartTmpDir, tmpDir)
// This is using chartutil.Save() so that .helmignore logic is applied. if err != nil {
loadedChart, loadErr := loader.LoadDir(tmpDir) return nil, fmt.Errorf("Unable to tar and compress dir %s to tgz file. %s", tmpDir, err)
if loadErr != nil {
return fmt.Errorf("Unable to process the git repo %s as a chart. %s", gitURL, err)
} }
if _, saveErr := chartutil.Save(loadedChart, destPath); saveErr != nil { return buf, nil
return fmt.Errorf("Unable to save the git repo %s as a chart. %s", gitURL, err)
} }
return nil
// NewGITGetter constructs a valid http/https client as a Getter
func NewGITGetter(ops ...Option) (Getter, error) {
client := GITGetter{}
for _, opt := range ops {
opt(&client.opts)
}
return &client, nil
} }

@ -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")
}
}
Loading…
Cancel
Save