fix conflic in resolver package

Signed-off-by: yxxhero <aiopsclub@163.com>
pull/11258/head
yxxhero 4 years ago
parent 72e2bff0af
commit 9bdc07b0ba

@ -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 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 repository added to helm by "helm add repo". Version matching is also supported
for this case. 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.
# requirements.yaml
dependencies:
- name: nginx
version: "master"
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 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 = ` const dependencyListDesc = `

@ -29,12 +29,18 @@ import (
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/gates"
"helm.sh/helm/v3/pkg/gitutil"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/provenance" "helm.sh/helm/v3/pkg/provenance"
"helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/repo"
) )
const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI")
var gitGetRefs = gitutil.GetRefs
// Resolver resolves dependencies from semantic version ranges to a particular version. // Resolver resolves dependencies from semantic version ranges to a particular version.
type Resolver struct { type Resolver struct {
chartpath string chartpath string
@ -107,6 +113,32 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
continue continue
} }
if strings.HasPrefix(d.Repository, "git://") {
refs, err := gitGetRefs(strings.TrimPrefix(d.Repository, "git://"))
if err != nil {
return nil, err
}
_, found := refs[d.Version]
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
}
if err != nil {
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
}
repoName := repoNames[d.Name] repoName := repoNames[d.Name]
// if the repository was not defined, but the dependency defines a repository url, bypass the cache // if the repository was not defined, but the dependency defines a repository url, bypass the cache
if repoName == "" && d.Repository != "" { if repoName == "" && d.Repository != "" {

@ -23,7 +23,15 @@ import (
"helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/registry"
) )
func fakeGetRefs(gitRepoURL string) (map[string]string, error) {
refs := map[string]string{
"master": "9668ad4d90c5e95bd520e58e7387607be6b63bb6",
}
return refs, nil
}
func TestResolve(t *testing.T) { func TestResolve(t *testing.T) {
gitGetRefs = fakeGetRefs
tests := []struct { tests := []struct {
name string name string
req []*chart.Dependency req []*chart.Dependency
@ -137,6 +145,17 @@ func TestResolve(t *testing.T) {
}, },
err: true, err: true,
}, },
{
name: "repo from git url",
req: []*chart.Dependency{
{Name: "gitdependency", Repository: "git:git@github.com:helm/gitdependency.git", Version: "master"},
},
expect: &chart.Lock{
Dependencies: []*chart.Dependency{
{Name: "gitdependency", Repository: "git:git@github.com:helm/gitdependency.git", Version: "master"},
},
},
},
} }
repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"} repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"}

@ -0,0 +1,79 @@
/*
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"
"io/ioutil"
"os"
"path/filepath"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/gitutil"
)
// Assigned here so it can be overridden for testing.
var gitCloneTo = gitutil.CloneTo
// GitDownloader handles downloading a chart from a git url.
type GitDownloader struct{}
// ensureGitDirIgnored will append ".git/" to the .helmignore file in a directory.
// Create the .helmignore file if it does not exist.
func (g *GitDownloader) 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
}
// DownloadTo will create a temp directory, then fetch a git repo into it.
// The git repo will be archived into a chart and copied to the destPath.
func (g *GitDownloader) DownloadTo(gitURL string, ref string, destPath string) error {
// the git archive command returns a tgz archive. we need to extract it to get the actual chart files.
tmpDir, err := ioutil.TempDir("", "helm")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
if err = gitCloneTo(gitURL, ref, tmpDir); err != nil {
return 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,
// 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(tmpDir)
// Turn the extracted git archive into a chart and move it into the charts directory.
// This is using chartutil.Save() so that .helmignore logic is applied.
loadedChart, loadErr := loader.LoadDir(tmpDir)
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 fmt.Errorf("Unable to save the git repo %s as a chart. %s", gitURL, err)
}
return nil
}

@ -310,6 +310,20 @@ 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
@ -594,6 +608,16 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
reposMap[dd.Name] = dd.Repository reposMap[dd.Name] = dd.Repository
continue 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 found := false

@ -193,6 +193,13 @@ func TestGetRepoNames(t *testing.T) {
}, },
expect: map[string]string{}, 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 { for _, tt := range tests {

@ -0,0 +1,76 @@
/*
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 gitutil
import (
"bufio"
"fmt"
"os/exec"
"regexp"
"strings"
)
var execCommand = exec.Command
// This regex is designed to match output from git of the style:
// ebeb6eafceb61dd08441ffe086c77eb472842494 refs/tags/v0.21.0
// and extract the hash and ref name as capture groups
var gitRefLineRegexp = regexp.MustCompile(`^([a-fA-F0-9]+)\s+(refs\/(?:tags|heads|pull|remotes)\/)(.*)$`)
// Run a git command as a child process.
// If git is not on the path, an error will be returned.
// Returns the command output or an error.
func gitExec(args ...string) ([]byte, error) {
cmd := execCommand("git", args...)
output, err := cmd.CombinedOutput()
if err != nil {
err = fmt.Errorf("Error executing git command:\ngit %s\n\n%s\n%s", strings.Join(args, " "), string(output), err)
}
return output, err
}
// GetRefs loads the tags, refs, branches (commit-ish) from a git repo.
// Returns a map of tags and branch names to commit shas
func GetRefs(gitRepoURL string) (map[string]string, error) {
output, err := gitExec("ls-remote", "--tags", "--heads", gitRepoURL)
if err != nil {
return nil, err
}
tagsToCommitShas := map[string]string{}
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := scanner.Bytes()
match := gitRefLineRegexp.FindSubmatch(line)
if len(match) == 4 {
// As documented in gitrevisions: https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html#_specifying_revisions
// "A suffix ^ followed by an empty brace pair means the object could be a tag, and dereference the tag recursively until a non-tag object is found."
// In other words, the hash without ^{} is the hash of the tag, and the hash with ^{} is the hash of the commit at which the tag was made.
// For our purposes, either will work.
var name = strings.TrimSuffix(string(match[3]), "^{}")
tagsToCommitShas[name] = string(match[1])
}
}
return tagsToCommitShas, nil
}
// CloneTo fetches a git repo at a specific ref from a git url
func CloneTo(gitRepoURL string, ref string, destinationPath string) error {
_, err := gitExec("clone", "--depth", "1", gitRepoURL, "--branch", ref, "--single-branch", destinationPath)
return err
}

@ -0,0 +1,93 @@
/*
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 gitutil
import (
"fmt"
"os"
"os/exec"
"reflect"
"testing"
)
func fakeExecCommand(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
// TestHelperProcess is not a test. It is used to create a mock process to spawn as a child process for testing exec.Command().
// Borrowed from the way Go tests exec.Command() internally: https://github.com/golang/go/blob/master/src/os/exec/exec_test.go#L727
func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
args := os.Args
cmd := args[len(args)-1] // the last arg is the github url. using this to determine what to return.
result := "Unknown Command"
exitCode := 1
switch cmd {
case "success":
exitCode = 0
result = `From git@github.com:helm/helm.git
9b42702a4bced339ff424a78ad68dd6be6e1a80a refs/heads/dev
9668ad4d90c5e95bd520e58e7387607be6b63bb6 refs/heads/master
44fb06eb69fecd4b6a5b2443a4768ba12bd70c09 refs/tags/v2.10.0
9ad53aac42165a5fadc6c87be0dea6b115f93090 refs/tags/v2.10.0^{}
4fdd07f21418abb43925998cf690857adc16451b refs/tags/v2.10.0-rc.1
aa98e7e3dd2356bce72e8e367e8c87e8085c692b refs/tags/v2.10.0-rc.1^{}`
case "error":
exitCode = 1
result = `ssh: Could not resolve hostname git: nodename nor servname provided, or not known
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.`
}
fmt.Fprintf(os.Stdout, result)
os.Exit(exitCode)
}
func TestGetRefsWhenGitReturnsRefs(t *testing.T) {
expected := map[string]string{
"dev": "9b42702a4bced339ff424a78ad68dd6be6e1a80a",
"master": "9668ad4d90c5e95bd520e58e7387607be6b63bb6",
"v2.10.0": "9ad53aac42165a5fadc6c87be0dea6b115f93090",
"v2.10.0-rc.1": "aa98e7e3dd2356bce72e8e367e8c87e8085c692b",
}
execCommand = fakeExecCommand
defer func() { execCommand = exec.Command }()
result, err := GetRefs("success")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Expected %q, got %q", expected, result)
}
}
func TestGetRefsWhenGitCommandReturnsError(t *testing.T) {
execCommand = fakeExecCommand
defer func() { execCommand = exec.Command }()
_, err := GetRefs("error")
if err == nil {
t.Errorf("Error should have been returned, but was not")
}
}
Loading…
Cancel
Save