Skip to content

Commit 7773bc5

Browse files
soulebMax Jonas Werner
authored and
Max Jonas Werner
committed
Add testCase for OCIChartRepository
This implementation also makes sure that HelmOCIRepository_reconciler uses an OCIChartRepository for validation Signed-off-by: Soule BA <[email protected]>
1 parent d0e1911 commit 7773bc5

File tree

4 files changed

+329
-27
lines changed

4 files changed

+329
-27
lines changed

controllers/helmrepository_controller_oci.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package controllers
33
import (
44
"context"
55
"fmt"
6-
"net/url"
6+
"strings"
77
"time"
88

99
"github.com/fluxcd/pkg/apis/meta"
@@ -13,6 +13,7 @@ import (
1313
"github.com/fluxcd/pkg/runtime/predicates"
1414
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
1515
serror "github.com/fluxcd/source-controller/internal/error"
16+
"github.com/fluxcd/source-controller/internal/helm/repository"
1617
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
1718
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
1819
helmgetter "helm.sh/helm/v3/pkg/getter"
@@ -267,30 +268,29 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
267268
// validateSource the HelmRepository object by checking the url and connecting to the underlying registry
268269
// with he provided credentials.
269270
func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *sourcev1.HelmRepository, loginOpts ...registry.LoginOption) (sreconcile.Result, error) {
270-
target, err := url.Parse(obj.Spec.URL)
271+
chartRepo, err := repository.NewOCIChartRepository(obj.Spec.URL, repository.WithOCIRegistryClient(r.RegistryClient))
271272
if err != nil {
272-
e := &serror.Event{
273-
Err: fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err),
274-
Reason: "ValidationError",
275-
}
276-
conditions.MarkFalse(obj, sourcev1.SourceValidCondition, e.Reason, e.Err.Error())
277-
return sreconcile.ResultEmpty, e
278-
}
279-
280-
// Check if the registry is supported
281-
if !registry.IsOCI(obj.Spec.URL) {
282-
e := &serror.Event{
283-
Err: fmt.Errorf("unsupported registry scheme '%s'", target.Scheme),
284-
Reason: "ValidationError",
273+
if strings.Contains(err.Error(), "parse") {
274+
e := &serror.Event{
275+
Err: fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err),
276+
Reason: "ValidationError",
277+
}
278+
conditions.MarkFalse(obj, sourcev1.SourceValidCondition, e.Reason, e.Err.Error())
279+
return sreconcile.ResultEmpty, e
280+
} else if strings.Contains(err.Error(), "the url scheme is not supported") {
281+
e := &serror.Event{
282+
Err: err,
283+
Reason: "ValidationError",
284+
}
285+
conditions.MarkFalse(obj, sourcev1.SourceValidCondition, e.Reason, e.Err.Error())
286+
return sreconcile.ResultEmpty, e
285287
}
286-
conditions.MarkFalse(obj, sourcev1.SourceValidCondition, e.Reason, e.Err.Error())
287-
return sreconcile.ResultEmpty, e
288288
}
289289

290-
err = r.RegistryClient.Login(target.Host+target.Path, loginOpts...)
290+
err = chartRepo.Login(loginOpts...)
291291
if err != nil {
292292
e := &serror.Event{
293-
Err: fmt.Errorf("failed to login to registry '%s': %w", target.String(), err),
293+
Err: fmt.Errorf("failed to login to registry: %w", err),
294294
Reason: "ValidationError",
295295
}
296296
conditions.MarkFalse(obj, sourcev1.SourceValidCondition, e.Reason, e.Err.Error())

internal/helm/repository/oci_chart_repository.go

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,49 @@ import (
2121
"crypto/tls"
2222
"fmt"
2323
"net/url"
24+
"sort"
2425
"strings"
2526

2627
"helm.sh/helm/v3/pkg/chart"
2728
"helm.sh/helm/v3/pkg/getter"
2829
"helm.sh/helm/v3/pkg/registry"
2930
"helm.sh/helm/v3/pkg/repo"
3031

32+
"github.com/Masterminds/semver/v3"
33+
"github.com/fluxcd/pkg/version"
3134
"github.com/fluxcd/source-controller/internal/transport"
3235
)
3336

37+
type RegistryClient interface {
38+
Login(host string, opts ...registry.LoginOption) error
39+
Logout(host string, opts ...registry.LogoutOption) error
40+
Tags(url string) ([]string, error)
41+
}
42+
3443
// OCIChartRepository represents a Helm chart repository, and the configuration
3544
// required to download the repository tags and charts from the repository.
3645
// All methods are thread safe unless defined otherwise.
3746
type OCIChartRepository struct {
38-
// URL the ChartRepository's index.yaml can be found at,
39-
// without the index.yaml suffix.
47+
// URL is the location of the repository.
4048
URL url.URL
41-
// Client to use while downloading the Index or a chart from the URL.
49+
// Client to use while accessing the repository's contents.
4250
Client getter.Getter
43-
// Options to configure the Client with while downloading the Index
51+
// Options to configure the Client with while downloading tags
4452
// or a chart from the URL.
4553
Options []getter.Option
4654

4755
tlsConfig *tls.Config
4856

4957
// RegistryClient is a client to use while downloading tags or charts from a registry.
50-
RegistryClient *registry.Client
58+
RegistryClient RegistryClient
5159
}
5260

5361
// OCIChartRepositoryOption is a function that can be passed to NewOCIChartRepository
5462
// to configure an OCIChartRepository.
5563
type OCIChartRepositoryOption func(*OCIChartRepository) error
5664

5765
// WithOCIRegistryClient returns a ChartRepositoryOption that will set the registry client
58-
func WithOCIRegistryClient(client *registry.Client) OCIChartRepositoryOption {
66+
func WithOCIRegistryClient(client RegistryClient) OCIChartRepositoryOption {
5967
return func(r *OCIChartRepository) error {
6068
r.RegistryClient = client
6169
return nil
@@ -139,7 +147,7 @@ func (r *OCIChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
139147
// If empty, try to get the highest available tag
140148
// If exact version, try to find it
141149
// If semver constraint string, try to find a match
142-
tag, err := registry.GetTagMatchingVersionOrConstraint(cvs, ver)
150+
tag, err := getLastMatchingVersionOrConstraint(cvs, ver)
143151
return &repo.ChartVersion{
144152
URLs: []string{fmt.Sprintf("%s/%s:%s", r.URL.String(), name, tag)},
145153
Metadata: &chart.Metadata{
@@ -174,6 +182,10 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf
174182
}
175183

176184
ref := chart.URLs[0]
185+
if !registry.IsOCI(ref) {
186+
return nil, fmt.Errorf("chart '%s' is not an OCI chart", chart.Name)
187+
}
188+
177189
u, err := url.Parse(ref)
178190
if err != nil {
179191
err = fmt.Errorf("invalid chart URL format '%s': %w", ref, err)
@@ -187,3 +199,67 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf
187199
// trim the oci scheme prefix if needed
188200
return r.Client.Get(strings.TrimPrefix(u.String(), fmt.Sprintf("%s://", registry.OCIScheme)), clientOpts...)
189201
}
202+
203+
// Login attempts to login to the OCI registry.
204+
func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error {
205+
err := r.RegistryClient.Login(r.URL.Host, opts...)
206+
if err != nil {
207+
return err
208+
}
209+
return nil
210+
}
211+
212+
// Logout attempts to logout from the OCI registry.
213+
func (r *OCIChartRepository) Logout() error {
214+
err := r.RegistryClient.Logout(r.URL.Host)
215+
if err != nil {
216+
return err
217+
}
218+
return nil
219+
}
220+
221+
func getLastMatchingVersionOrConstraint(cvs []string, ver string) (string, error) {
222+
// Check for exact matches first
223+
if ver != "" {
224+
for _, cv := range cvs {
225+
if ver == cv {
226+
return cv, nil
227+
}
228+
}
229+
}
230+
231+
// Continue to look for a (semantic) version match
232+
verConstraint, err := semver.NewConstraint("*")
233+
if err != nil {
234+
return "", err
235+
}
236+
latestStable := ver == "" || ver == "*"
237+
if !latestStable {
238+
verConstraint, err = semver.NewConstraint(ver)
239+
if err != nil {
240+
return "", err
241+
}
242+
}
243+
244+
matchingVersions := make([]string, 0, len(cvs))
245+
for _, cv := range cvs {
246+
v, err := version.ParseVersion(cv)
247+
if err != nil {
248+
continue
249+
}
250+
251+
if !verConstraint.Check(v) {
252+
continue
253+
}
254+
255+
matchingVersions = append(matchingVersions, cv)
256+
}
257+
if len(matchingVersions) == 0 {
258+
return "", fmt.Errorf("could not locate a version matching provided version string %s", ver)
259+
}
260+
261+
// Sort versions
262+
sort.Sort(sort.Reverse(sort.StringSlice(matchingVersions)))
263+
264+
return matchingVersions[0], nil
265+
}

0 commit comments

Comments
 (0)