Skip to content

Commit d38b8fe

Browse files
committed
Support proper semver ranges for Helm charts
This commit changes the semver range parser to `blang/semver`, which is also used to parse semver tags for GitRepository sources.
1 parent 25f0552 commit d38b8fe

File tree

4 files changed

+85
-30
lines changed

4 files changed

+85
-30
lines changed

config/samples/source_v1alpha1_helmchart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ metadata:
44
name: helmchart-sample
55
spec:
66
name: podinfo
7-
version: '^2.0.0'
7+
version: '>=2.0.0 <3.0.0'
88
helmRepositoryRef:
99
name: helmrepository-sample
1010
interval: 1m

controllers/helmchart_controller.go

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626

2727
"github.com/go-logr/logr"
2828
"helm.sh/helm/v3/pkg/getter"
29-
"helm.sh/helm/v3/pkg/repo"
3029
corev1 "k8s.io/api/core/v1"
3130
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3231
"k8s.io/apimachinery/pkg/runtime"
@@ -36,9 +35,9 @@ import (
3635
ctrl "sigs.k8s.io/controller-runtime"
3736
"sigs.k8s.io/controller-runtime/pkg/client"
3837
"sigs.k8s.io/controller-runtime/pkg/controller"
39-
"sigs.k8s.io/yaml"
4038

4139
"github.com/fluxcd/pkg/recorder"
40+
4241
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
4342
"github.com/fluxcd/source-controller/internal/helm"
4443
)
@@ -174,30 +173,8 @@ func (r *HelmChartReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts
174173
}
175174

176175
func (r *HelmChartReconciler) reconcile(ctx context.Context, repository sourcev1.HelmRepository, chart sourcev1.HelmChart) (sourcev1.HelmChart, error) {
177-
indexBytes, err := ioutil.ReadFile(repository.Status.Artifact.Path)
178-
if err != nil {
179-
err = fmt.Errorf("failed to read Helm repository index file: %w", err)
180-
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
181-
}
182-
index := &repo.IndexFile{}
183-
if err := yaml.Unmarshal(indexBytes, index); err != nil {
184-
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
185-
}
186-
187-
// find referenced chart in index
188-
cv, err := index.Get(chart.Spec.Name, chart.Spec.Version)
176+
cv, err := helm.GetDownloadableChartVersionFromIndex(repository.Status.Artifact.Path, chart.Spec.Name, chart.Spec.Version)
189177
if err != nil {
190-
switch err {
191-
case repo.ErrNoChartName:
192-
err = fmt.Errorf("chart '%s' could not be found in Helm repository '%s'", chart.Spec.Name, repository.Name)
193-
case repo.ErrNoChartVersion:
194-
err = fmt.Errorf("no chart with version '%s' found for '%s'", chart.Spec.Version, chart.Spec.Name)
195-
}
196-
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
197-
}
198-
199-
if len(cv.URLs) == 0 {
200-
err = fmt.Errorf("chart '%s' has no downloadable URLs", cv.Name)
201178
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
202179
}
203180

@@ -207,7 +184,6 @@ func (r *HelmChartReconciler) reconcile(ctx context.Context, repository sourcev1
207184
u, err := url.Parse(ref)
208185
if err != nil {
209186
err = fmt.Errorf("invalid chart URL format '%s': %w", ref, err)
210-
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
211187
}
212188

213189
c, err := r.Getters.ByScheme(u.Scheme)

controllers/helmchart_controller_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ var _ = Describe("HelmChartReconciler", func() {
101101
},
102102
Spec: sourcev1.HelmChartSpec{
103103
Name: "helmchart",
104-
Version: "*",
104+
Version: "",
105105
HelmRepositoryRef: corev1.LocalObjectReference{Name: repositoryKey.Name},
106106
Interval: metav1.Duration{Duration: pullInterval},
107107
},
@@ -203,7 +203,6 @@ var _ = Describe("HelmChartReconciler", func() {
203203
},
204204
Spec: sourcev1.HelmChartSpec{
205205
Name: "helmchart",
206-
Version: "*",
207206
HelmRepositoryRef: corev1.LocalObjectReference{Name: repositoryKey.Name},
208207
Interval: metav1.Duration{Duration: 1 * time.Hour},
209208
},
@@ -218,7 +217,7 @@ var _ = Describe("HelmChartReconciler", func() {
218217
return ""
219218
}, timeout, interval).Should(Equal("1.0.0"))
220219

221-
chart.Spec.Version = "~0.1.0"
220+
chart.Spec.Version = "<0.2.0"
222221
Expect(k8sClient.Update(context.Background(), chart)).Should(Succeed())
223222
Eventually(func() string {
224223
_ = k8sClient.Get(context.Background(), key, chart)

internal/helm/repository.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright 2020 The Flux CD contributors.
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 helm
18+
19+
import (
20+
"fmt"
21+
"io/ioutil"
22+
23+
"github.com/blang/semver"
24+
"helm.sh/helm/v3/pkg/repo"
25+
"sigs.k8s.io/yaml"
26+
)
27+
28+
func GetDownloadableChartVersionFromIndex(path, chart, version string) (*repo.ChartVersion, error) {
29+
b, err := ioutil.ReadFile(path)
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to read Helm repository index file: %w", err)
32+
}
33+
index := &repo.IndexFile{}
34+
if err := yaml.Unmarshal(b, index); err != nil {
35+
return nil, fmt.Errorf("failed to unmarshal Helm repository index file: %w", err)
36+
}
37+
38+
var cv *repo.ChartVersion
39+
if version == "" || version == "*" {
40+
cv, err = index.Get(chart, version)
41+
if err != nil {
42+
if err == repo.ErrNoChartName {
43+
err = fmt.Errorf("chart '%s' could not be found in Helm repository index", chart)
44+
}
45+
return nil, err
46+
}
47+
} else {
48+
entries, ok := index.Entries[chart]
49+
if !ok {
50+
return nil, fmt.Errorf("chart '%s' could not be found in Helm repository index", chart)
51+
}
52+
53+
rng, err := semver.ParseRange(version)
54+
if err != nil {
55+
return nil, fmt.Errorf("semver range parse error: %w", err)
56+
}
57+
versionEntryLookup := make(map[string]*repo.ChartVersion)
58+
var versionsInRange []semver.Version
59+
for _, e := range entries {
60+
v, _ := semver.ParseTolerant(e.Version)
61+
if rng(v) {
62+
versionsInRange = append(versionsInRange, v)
63+
versionEntryLookup[v.String()] = e
64+
}
65+
}
66+
if len(versionsInRange) == 0 {
67+
return nil, fmt.Errorf("no match found for semver: %s", version)
68+
}
69+
semver.Sort(versionsInRange)
70+
71+
latest := versionsInRange[len(versionsInRange)-1]
72+
cv = versionEntryLookup[latest.String()]
73+
}
74+
75+
if len(cv.URLs) == 0 {
76+
return nil, fmt.Errorf("no downloadable URLs for chart '%s' with version '%s'", cv.Name, cv.Version)
77+
}
78+
79+
return cv, nil
80+
}

0 commit comments

Comments
 (0)