Skip to content

Commit 0c43076

Browse files
authored
Merge pull request #175 from relu/gitrepository-helm-deps
2 parents c66425f + bfd8d4b commit 0c43076

30 files changed

+1158
-38
lines changed

.github/workflows/e2e.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
- name: Run HelmChart from Bucket tests
9696
run: |
9797
./mc mb minio/charts
98-
./mc mirror ./controllers/testdata/helmchart/ minio/charts/helmchart
98+
./mc mirror ./controllers/testdata/charts/helmchart/ minio/charts/helmchart
9999
100100
kubectl -n source-system apply -f ./config/testdata/helmchart-from-bucket/source.yaml
101101
kubectl -n source-system wait bucket/charts --for=condition=ready --timeout=1m

api/v1beta1/helmrepository_types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ import (
2525
)
2626

2727
const (
28-
HelmRepositoryKind = "HelmRepository"
28+
// HelmRepositoryKind is the string representation of a HelmRepository.
29+
HelmRepositoryKind = "HelmRepository"
30+
// HelmRepositoryTimeout is the default timeout used for Helm repository
31+
// operations like fetching indexes, or downloading charts from a repository.
2932
HelmRepositoryTimeout = time.Second * 60
33+
// HelmRepositoryURLIndexKey is the key to use for indexing HelmRepository
34+
// resources by their HelmRepositorySpec.URL.
35+
HelmRepositoryURLIndexKey = ".metadata.helmRepositoryURL"
3036
)
3137

3238
// HelmRepositorySpec defines the reference to a Helm repository.

controllers/dependency_manager.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
Copyright 2020 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"path"
24+
"path/filepath"
25+
"strings"
26+
27+
"github.com/Masterminds/semver/v3"
28+
"github.com/fluxcd/source-controller/internal/helm"
29+
"golang.org/x/sync/errgroup"
30+
helmchart "helm.sh/helm/v3/pkg/chart"
31+
"helm.sh/helm/v3/pkg/chart/loader"
32+
)
33+
34+
// DependencyWithRepository is a container for a dependency and its respective
35+
// repository
36+
type DependencyWithRepository struct {
37+
Dependency *helmchart.Dependency
38+
Repo *helm.ChartRepository
39+
}
40+
41+
// DependencyManager manages dependencies for helm charts
42+
type DependencyManager struct {
43+
Chart *helmchart.Chart
44+
ChartPath string
45+
Dependencies []*DependencyWithRepository
46+
}
47+
48+
// Build compiles and builds the chart dependencies
49+
func (dm *DependencyManager) Build() error {
50+
if dm.Dependencies == nil {
51+
return nil
52+
}
53+
54+
ctx := context.Background()
55+
errs, ctx := errgroup.WithContext(ctx)
56+
57+
for _, item := range dm.Dependencies {
58+
dep := item.Dependency
59+
chartRepo := item.Repo
60+
errs.Go(func() error {
61+
var (
62+
ch *helmchart.Chart
63+
err error
64+
)
65+
if strings.HasPrefix(dep.Repository, "file://") {
66+
ch, err = chartForLocalDependency(dep, dm.ChartPath)
67+
} else {
68+
ch, err = chartForRemoteDependency(dep, chartRepo)
69+
}
70+
if err != nil {
71+
return err
72+
}
73+
dm.Chart.AddDependency(ch)
74+
return nil
75+
})
76+
}
77+
78+
return errs.Wait()
79+
}
80+
81+
func chartForLocalDependency(dep *helmchart.Dependency, cp string) (*helmchart.Chart, error) {
82+
origPath, err := filepath.Abs(path.Join(cp, strings.TrimPrefix(dep.Repository, "file://")))
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
if _, err := os.Stat(origPath); os.IsNotExist(err) {
88+
err := fmt.Errorf("chart path %s not found: %w", origPath, err)
89+
return nil, err
90+
} else if err != nil {
91+
return nil, err
92+
}
93+
94+
ch, err := loader.Load(origPath)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
constraint, err := semver.NewConstraint(dep.Version)
100+
if err != nil {
101+
err := fmt.Errorf("dependency %s has an invalid version/constraint format: %w", dep.Name, err)
102+
return nil, err
103+
}
104+
105+
v, err := semver.NewVersion(ch.Metadata.Version)
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
if !constraint.Check(v) {
111+
err = fmt.Errorf("can't get a valid version for dependency %s", dep.Name)
112+
return nil, err
113+
}
114+
115+
return ch, nil
116+
}
117+
118+
func chartForRemoteDependency(dep *helmchart.Dependency, chartrepo *helm.ChartRepository) (*helmchart.Chart, error) {
119+
if chartrepo == nil {
120+
err := fmt.Errorf("chartrepo should not be nil")
121+
return nil, err
122+
}
123+
124+
// Lookup the chart version in the chart repository index
125+
chartVer, err := chartrepo.Get(dep.Name, dep.Version)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
// Download chart
131+
res, err := chartrepo.DownloadChart(chartVer)
132+
if err != nil {
133+
return nil, err
134+
}
135+
136+
ch, err := loader.LoadArchive(res)
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
return ch, nil
142+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
Copyright 2020 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"bytes"
21+
"io/ioutil"
22+
"strings"
23+
"testing"
24+
25+
"github.com/fluxcd/source-controller/internal/helm"
26+
helmchart "helm.sh/helm/v3/pkg/chart"
27+
"helm.sh/helm/v3/pkg/getter"
28+
"helm.sh/helm/v3/pkg/repo"
29+
)
30+
31+
var (
32+
helmPackageFile = "testdata/charts/helmchart-0.1.0.tgz"
33+
34+
localDepFixture helmchart.Dependency = helmchart.Dependency{
35+
Name: "helmchart",
36+
Version: "0.1.0",
37+
Repository: "file://../helmchart",
38+
}
39+
remoteDepFixture helmchart.Dependency = helmchart.Dependency{
40+
Name: "helmchart",
41+
Version: "0.1.0",
42+
Repository: "https://example.com/charts",
43+
}
44+
chartFixture helmchart.Chart = helmchart.Chart{
45+
Metadata: &helmchart.Metadata{
46+
Name: "test",
47+
},
48+
}
49+
)
50+
51+
func TestBuild_WithEmptyDependencies(t *testing.T) {
52+
dm := DependencyManager{
53+
Dependencies: nil,
54+
}
55+
if err := dm.Build(); err != nil {
56+
t.Errorf("Build() should return nil")
57+
}
58+
}
59+
60+
func TestBuild_WithLocalChart(t *testing.T) {
61+
loc := localDepFixture
62+
chart := chartFixture
63+
dm := DependencyManager{
64+
Chart: &chart,
65+
ChartPath: "testdata/charts/helmchart",
66+
Dependencies: []*DependencyWithRepository{
67+
{
68+
Dependency: &loc,
69+
Repo: nil,
70+
},
71+
},
72+
}
73+
74+
if err := dm.Build(); err != nil {
75+
t.Errorf("Build() expected to not return error: %s", err)
76+
}
77+
78+
deps := dm.Chart.Dependencies()
79+
if len(deps) != 1 {
80+
t.Fatalf("chart expected to have one dependency registered")
81+
}
82+
if deps[0].Metadata.Name != localDepFixture.Name {
83+
t.Errorf("chart dependency has incorrect name, expected: %s, got: %s", localDepFixture.Name, deps[0].Metadata.Name)
84+
}
85+
if deps[0].Metadata.Version != localDepFixture.Version {
86+
t.Errorf("chart dependency has incorrect version, expected: %s, got: %s", localDepFixture.Version, deps[0].Metadata.Version)
87+
}
88+
89+
tests := []struct {
90+
name string
91+
dep helmchart.Dependency
92+
expectError string
93+
}{
94+
{
95+
name: "invalid path",
96+
dep: helmchart.Dependency{
97+
Name: "helmchart",
98+
Version: "0.1.0",
99+
Repository: "file://../invalid",
100+
},
101+
expectError: "no such file or directory",
102+
},
103+
{
104+
name: "invalid version constraint format",
105+
dep: helmchart.Dependency{
106+
Name: "helmchart",
107+
Version: "!2.0",
108+
Repository: "file://../helmchart",
109+
},
110+
expectError: "has an invalid version/constraint format",
111+
},
112+
{
113+
name: "invalid version",
114+
dep: helmchart.Dependency{
115+
Name: "helmchart",
116+
Version: "1.0.0",
117+
Repository: "file://../helmchart",
118+
},
119+
expectError: "can't get a valid version for dependency",
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
c := chartFixture
126+
dm = DependencyManager{
127+
Chart: &c,
128+
ChartPath: "testdata/charts/helmchart",
129+
Dependencies: []*DependencyWithRepository{
130+
{
131+
Dependency: &tt.dep,
132+
Repo: nil,
133+
},
134+
},
135+
}
136+
137+
if err := dm.Build(); err == nil {
138+
t.Errorf("Build() expected to return error")
139+
} else if !strings.Contains(err.Error(), tt.expectError) {
140+
t.Errorf("Build() expected to return error: %s, got: %s", tt.expectError, err)
141+
}
142+
if len(dm.Chart.Dependencies()) > 0 {
143+
t.Fatalf("chart expected to have no dependencies registered")
144+
}
145+
})
146+
}
147+
}
148+
149+
func TestBuild_WithRemoteChart(t *testing.T) {
150+
chart := chartFixture
151+
b, err := ioutil.ReadFile(helmPackageFile)
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
i := repo.NewIndexFile()
156+
i.Add(&helmchart.Metadata{Name: "helmchart", Version: "0.1.0"}, "helmchart-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
157+
mg := mockGetter{response: b}
158+
cr := &helm.ChartRepository{
159+
URL: remoteDepFixture.Repository,
160+
Index: i,
161+
Client: &mg,
162+
}
163+
dm := DependencyManager{
164+
Chart: &chart,
165+
Dependencies: []*DependencyWithRepository{
166+
{
167+
Dependency: &remoteDepFixture,
168+
Repo: cr,
169+
},
170+
},
171+
}
172+
173+
if err := dm.Build(); err != nil {
174+
t.Errorf("Build() expected to not return error: %s", err)
175+
}
176+
177+
deps := dm.Chart.Dependencies()
178+
if len(deps) != 1 {
179+
t.Fatalf("chart expected to have one dependency registered")
180+
}
181+
if deps[0].Metadata.Name != remoteDepFixture.Name {
182+
t.Errorf("chart dependency has incorrect name, expected: %s, got: %s", remoteDepFixture.Name, deps[0].Metadata.Name)
183+
}
184+
if deps[0].Metadata.Version != remoteDepFixture.Version {
185+
t.Errorf("chart dependency has incorrect version, expected: %s, got: %s", remoteDepFixture.Version, deps[0].Metadata.Version)
186+
}
187+
188+
// When repo is not set
189+
dm.Dependencies[0].Repo = nil
190+
if err := dm.Build(); err == nil {
191+
t.Errorf("Build() expected to return error")
192+
} else if !strings.Contains(err.Error(), "chartrepo should not be nil") {
193+
t.Errorf("Build() expected to return different error, got: %s", err)
194+
}
195+
}
196+
197+
type mockGetter struct {
198+
response []byte
199+
}
200+
201+
func (g *mockGetter) Get(url string, options ...getter.Option) (*bytes.Buffer, error) {
202+
return bytes.NewBuffer(g.response), nil
203+
}

0 commit comments

Comments
 (0)