Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Git based dependencies #1

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
48d9476
fix conflic in resolver package
yxxhero Jul 8, 2021
a5f02a8
fix 'helm dep build' command when chart contains 'git:' dependencies
rally25rs Mar 5, 2020
9d0581a
Implement the Getter interface for the git:// protocol.
yxxhero Mar 16, 2021
27de594
add unittest for git:// protocol
yxxhero Apr 25, 2021
7c37f14
move GITGetter to GitGetter
yxxhero Jul 20, 2021
b1f4791
fix TestCompressDirToTgz unittest
yxxhero Jul 21, 2021
4780dc0
move gitutil to vcs
yxxhero Jul 24, 2021
517ff93
update resolver unittest for git
yxxhero Jul 27, 2021
3ba8354
fix oci issue
yxxhero Jan 22, 2022
39f9df2
docs: update docs to follow the latest version of the HIP
dominykas Dec 4, 2023
347ca1c
chore: gofmt
dominykas Dec 4, 2023
c53f14a
chore: typo
dominykas Dec 4, 2023
f28781f
refactor: extract gitutil.IsGitURL helper
dominykas Dec 15, 2023
7960f12
refactor: consolidate git:// url normalization
dominykas Dec 15, 2023
d3943d8
feat: use git[+subprotocol] pattern
dominykas Dec 15, 2023
8debb15
docs: correct description
dominykas Dec 15, 2023
51bec62
refactor: collapse conditions into a single line and update docs
dominykas Dec 20, 2023
9c7cb57
test: enforce error message expectations
dominykas Dec 20, 2023
9ca20e2
feat: forbid credentials in git url
dominykas Dec 20, 2023
bba5e99
test: enforce error message expectations
dominykas Dec 20, 2023
bbb4950
feat: allow git repositories to be cloned from non-semver versions
dominykas Dec 20, 2023
8cc0fa0
chore: standardize temporary dir name prefix
dominykas Dec 20, 2023
16d3f9c
refactor: use existing `chartutil.Save` to package up the git based c…
dominykas Dec 20, 2023
310f1eb
refactor: register all git schemes explicitly
dominykas Dec 20, 2023
72af2e1
refactor: introduce a struct for git repository URL encapsulation
dominykas Dec 20, 2023
46dfe59
feat: support charts from sub-folders inside git repository
dominykas Dec 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cmd/helm/dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ 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+ssh://github.com:helm/helm-chart.git' or
'git+https://github.com/helm/helm-chart.git'

When using a 'git[+subprotocol]>://' repository, the 'version' must be a valid
tag or branch name for the git repo, for example 'main'.

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/password 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. Authentication can be
configured by using a git credentials helper which can read the credentials
from environment variables, from operating system keychain, etc.
`

const dependencyListDesc = `
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/whilp/git-urls v1.0.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.17.0
golang.org/x/term v0.15.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
Expand Down
1 change: 1 addition & 0 deletions internal/fileutil/testdata/testdir/testfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
helm
93 changes: 93 additions & 0 deletions internal/gitutil/gitutil.go
Original file line number Diff line number Diff line change
@@ -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 (
"net/url"
"os"
"regexp"
"strings"

"github.com/pkg/errors"
giturls "github.com/whilp/git-urls"

"github.com/Masterminds/vcs"
)

var gitRepositoryURLRe = regexp.MustCompile(`^git(\+\w+)?://`)

type GitRepositoryURL struct {
RepositoryURL string
GitRemoteURL *url.URL
PathUnderGitRepository string
}

// HasGitReference returns true if a git repository contains a specified ref (branch/tag)
func HasGitReference(gitRepo, ref string) (bool, error) {
local, err := os.MkdirTemp("", "helm-git-")
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
}

// IsGitRepository determines whether a URL is to be treated as a git repository URL
func IsGitRepository(url string) bool {
return gitRepositoryURLRe.MatchString(url)
}

// ParseGitRepositoryURL creates a new GitRepositoryURL from a string
func ParseGitRepositoryURL(repositoryURL string) (*GitRepositoryURL, error) {
gitRemoteURL, err := giturls.Parse(strings.TrimPrefix(repositoryURL, "git+"))

if err != nil {
return nil, err
}

if gitRemoteURL.User != nil {
return nil, errors.Errorf("git repository URL should not contain credentials - please use git credential helpers")
}

path := ""

if gitRemoteURL.Fragment != "" {
query, err := url.ParseQuery(gitRemoteURL.Fragment)
if err != nil {
return nil, err
}

path = query.Get("subdirectory")
}

gitRemoteURL.Fragment = ""

return &GitRepositoryURL{
RepositoryURL: repositoryURL,
GitRemoteURL: gitRemoteURL,
PathUnderGitRepository: path,
}, err
}
79 changes: 79 additions & 0 deletions internal/gitutil/gitutil_test.go
Original file line number Diff line number Diff line change
@@ -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 gitutil

import (
"testing"
)

func TestIsGitUrl(t *testing.T) {
// Test table: Given url, IsGitRepository should return expect.
tests := []struct {
url string
expect bool
}{
{"oci://example.com/example/chart", false},
{"git://example.com/example/chart", true},
{"git+https://example.com/example/chart", true},
}

for _, test := range tests {
if IsGitRepository(test.url) != test.expect {
t.Errorf("Expected %t for %s", test.expect, test.url)
}
}
}

func TestParseGitRepositoryURL(t *testing.T) {
// Test table: Given url, ParseGitRepositoryURL should return expect.
tests := []struct {
url string
expectRepositoryURL string
expectGitRemoteURL string
expectedPathUnderGitRepository string
}{
{
url: "git://example.com/example/chart",
expectRepositoryURL: "git://example.com/example/chart",
expectGitRemoteURL: "git://example.com/example/chart",
},
{
url: "git+https://example.com/example/chart",
expectRepositoryURL: "git+https://example.com/example/chart",
expectGitRemoteURL: "https://example.com/example/chart",
},
{
url: "git+https://example.com/example/chart#subdirectory=charts/some-chart",
expectRepositoryURL: "git+https://example.com/example/chart#subdirectory=charts/some-chart",
expectGitRemoteURL: "https://example.com/example/chart",
expectedPathUnderGitRepository: "charts/some-chart",
},
}

for _, test := range tests {
parsed, _ := ParseGitRepositoryURL(test.url)
if parsed.RepositoryURL != test.expectRepositoryURL {
t.Errorf("Expected RepositoryURL %s for %s, but got %s", test.expectRepositoryURL, test.url, parsed.RepositoryURL)
}
if parsed.GitRemoteURL.String() != test.expectGitRemoteURL {
t.Errorf("Expected GitRemoteURL %s for %s, but got %s", test.expectGitRemoteURL, test.url, parsed.GitRemoteURL)
}
if parsed.PathUnderGitRepository != test.expectedPathUnderGitRepository {
t.Errorf("Expected PathUnderGitRepository %s for %s, but got %s", test.expectGitRemoteURL, test.url, parsed.PathUnderGitRepository)
}
}
}
39 changes: 36 additions & 3 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"

"helm.sh/helm/v3/internal/gitutil"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/helmpath"
Expand All @@ -35,6 +36,8 @@ import (
"helm.sh/helm/v3/pkg/repo"
)

var hasGitReference = gitutil.HasGitReference

// Resolver resolves dependencies from semantic version ranges to a particular version.
type Resolver struct {
chartpath string
Expand All @@ -58,9 +61,13 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
locked := make([]*chart.Dependency, len(reqs))
missing := []string{}
for i, d := range reqs {
constraint, err := semver.NewConstraint(d.Version)
if err != nil {
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
var constraint *semver.Constraints
var err error
if !gitutil.IsGitRepository(d.Repository) {
constraint, err = semver.NewConstraint(d.Version)
if err != nil {
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
}
}

if d.Repository == "" {
Expand All @@ -76,6 +83,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
}
continue
}

if strings.HasPrefix(d.Repository, "file://") {

chartpath, err := GetLocalPath(d.Repository, r.chartpath)
Expand Down Expand Up @@ -107,6 +115,31 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
continue
}

if gitutil.IsGitRepository(d.Repository) {

gitURL, err := gitutil.ParseGitRepositoryURL(d.Repository)
if err != nil {
return nil, err
}

found, err := hasGitReference(gitURL.GitRemoteURL.String(), d.Version)
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[+subprotocol]://" 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 != "" {
Expand Down
Loading
Loading