From d38b8fe193612e184a1572a8622a855125ba9088 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 28 Aug 2020 13:34:50 +0200 Subject: [PATCH] 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. --- config/samples/source_v1alpha1_helmchart.yaml | 2 +- controllers/helmchart_controller.go | 28 +------ controllers/helmchart_controller_test.go | 5 +- internal/helm/repository.go | 80 +++++++++++++++++++ 4 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 internal/helm/repository.go diff --git a/config/samples/source_v1alpha1_helmchart.yaml b/config/samples/source_v1alpha1_helmchart.yaml index 14ff6880..aa6be555 100644 --- a/config/samples/source_v1alpha1_helmchart.yaml +++ b/config/samples/source_v1alpha1_helmchart.yaml @@ -4,7 +4,7 @@ metadata: name: helmchart-sample spec: name: podinfo - version: '^2.0.0' + version: '>=2.0.0 <3.0.0' helmRepositoryRef: name: helmrepository-sample interval: 1m diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 20c387b9..b7408ac7 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -26,7 +26,6 @@ import ( "github.com/go-logr/logr" "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/repo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,9 +35,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/recorder" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" "github.com/fluxcd/source-controller/internal/helm" ) @@ -174,30 +173,8 @@ func (r *HelmChartReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts } func (r *HelmChartReconciler) reconcile(ctx context.Context, repository sourcev1.HelmRepository, chart sourcev1.HelmChart) (sourcev1.HelmChart, error) { - indexBytes, err := ioutil.ReadFile(repository.Status.Artifact.Path) + cv, err := helm.GetDownloadableChartVersionFromIndex(repository.Status.Artifact.Path, chart.Spec.Name, chart.Spec.Version) if err != nil { - err = fmt.Errorf("failed to read Helm repository index file: %w", err) - return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err - } - index := &repo.IndexFile{} - if err := yaml.Unmarshal(indexBytes, index); err != nil { - return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err - } - - // find referenced chart in index - cv, err := index.Get(chart.Spec.Name, chart.Spec.Version) - if err != nil { - switch err { - case repo.ErrNoChartName: - err = fmt.Errorf("chart '%s' could not be found in Helm repository '%s'", chart.Spec.Name, repository.Name) - case repo.ErrNoChartVersion: - err = fmt.Errorf("no chart with version '%s' found for '%s'", chart.Spec.Version, chart.Spec.Name) - } - return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err - } - - if len(cv.URLs) == 0 { - err = fmt.Errorf("chart '%s' has no downloadable URLs", cv.Name) return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err } @@ -207,7 +184,6 @@ func (r *HelmChartReconciler) reconcile(ctx context.Context, repository sourcev1 u, err := url.Parse(ref) if err != nil { err = fmt.Errorf("invalid chart URL format '%s': %w", ref, err) - return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err } c, err := r.Getters.ByScheme(u.Scheme) diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 6f1e8ca4..67c8c0ad 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -101,7 +101,7 @@ var _ = Describe("HelmChartReconciler", func() { }, Spec: sourcev1.HelmChartSpec{ Name: "helmchart", - Version: "*", + Version: "", HelmRepositoryRef: corev1.LocalObjectReference{Name: repositoryKey.Name}, Interval: metav1.Duration{Duration: pullInterval}, }, @@ -203,7 +203,6 @@ var _ = Describe("HelmChartReconciler", func() { }, Spec: sourcev1.HelmChartSpec{ Name: "helmchart", - Version: "*", HelmRepositoryRef: corev1.LocalObjectReference{Name: repositoryKey.Name}, Interval: metav1.Duration{Duration: 1 * time.Hour}, }, @@ -218,7 +217,7 @@ var _ = Describe("HelmChartReconciler", func() { return "" }, timeout, interval).Should(Equal("1.0.0")) - chart.Spec.Version = "~0.1.0" + chart.Spec.Version = "<0.2.0" Expect(k8sClient.Update(context.Background(), chart)).Should(Succeed()) Eventually(func() string { _ = k8sClient.Get(context.Background(), key, chart) diff --git a/internal/helm/repository.go b/internal/helm/repository.go new file mode 100644 index 00000000..50a76fd6 --- /dev/null +++ b/internal/helm/repository.go @@ -0,0 +1,80 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 helm + +import ( + "fmt" + "io/ioutil" + + "github.com/blang/semver" + "helm.sh/helm/v3/pkg/repo" + "sigs.k8s.io/yaml" +) + +func GetDownloadableChartVersionFromIndex(path, chart, version string) (*repo.ChartVersion, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read Helm repository index file: %w", err) + } + index := &repo.IndexFile{} + if err := yaml.Unmarshal(b, index); err != nil { + return nil, fmt.Errorf("failed to unmarshal Helm repository index file: %w", err) + } + + var cv *repo.ChartVersion + if version == "" || version == "*" { + cv, err = index.Get(chart, version) + if err != nil { + if err == repo.ErrNoChartName { + err = fmt.Errorf("chart '%s' could not be found in Helm repository index", chart) + } + return nil, err + } + } else { + entries, ok := index.Entries[chart] + if !ok { + return nil, fmt.Errorf("chart '%s' could not be found in Helm repository index", chart) + } + + rng, err := semver.ParseRange(version) + if err != nil { + return nil, fmt.Errorf("semver range parse error: %w", err) + } + versionEntryLookup := make(map[string]*repo.ChartVersion) + var versionsInRange []semver.Version + for _, e := range entries { + v, _ := semver.ParseTolerant(e.Version) + if rng(v) { + versionsInRange = append(versionsInRange, v) + versionEntryLookup[v.String()] = e + } + } + if len(versionsInRange) == 0 { + return nil, fmt.Errorf("no match found for semver: %s", version) + } + semver.Sort(versionsInRange) + + latest := versionsInRange[len(versionsInRange)-1] + cv = versionEntryLookup[latest.String()] + } + + if len(cv.URLs) == 0 { + return nil, fmt.Errorf("no downloadable URLs for chart '%s' with version '%s'", cv.Name, cv.Version) + } + + return cv, nil +}