Skip to content

Commit 5b2f081

Browse files
soulebMax Jonas Werner
authored and
Max Jonas Werner
committed
Add test case for RemoteBuilder
Refactor the downloadChartFrom methods to reduce code duplication. Signed-off-by: Soule BA <soule@weave.works>
1 parent 7773bc5 commit 5b2f081

File tree

4 files changed

+218
-76
lines changed

4 files changed

+218
-76
lines changed

internal/helm/chart/builder_remote.go

+35-47
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import (
3737
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
3838
)
3939

40-
// Remote is a repository.ChartRepository or a repository.OCIChartRegistry.
41-
// It is used to download a chart from a remote repository or registry.
40+
// Remote is a repository.ChartRepository or a repository.OCIChartRepository.
41+
// It is used to download a chart from a remote Helm repository or OCI registry.
4242
type Remote interface {
4343
// GetChart returns a chart.Chart from the remote repository.
4444
Get(name, version string) (*repo.ChartVersion, error)
@@ -160,39 +160,14 @@ func (b *remoteChartBuilder) downloadFromOCIRepository(remote *repository.OCICha
160160
return nil, &BuildError{Reason: ErrChartPull, Err: err}
161161
}
162162

163-
result := &Build{}
164-
result.Version = cv.Version
165-
result.Name = cv.Name
166-
167-
// Set build specific metadata if instructed
168-
if opts.VersionMetadata != "" {
169-
ver, err := setBuildMetaData(result.Version, opts.VersionMetadata)
170-
if err != nil {
171-
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
172-
}
173-
result.Version = ver.String()
163+
result, shouldReturn, err := generateBuildResult(cv, opts)
164+
if err != nil {
165+
return nil, err
174166
}
175167

176-
requiresPackaging := len(opts.GetValuesFiles()) != 0 || opts.VersionMetadata != ""
177-
178-
// If all the following is true, we do not need to download and/or build the chart:
179-
// - Chart name from cached chart matches resolved name
180-
// - Chart version from cached chart matches calculated version
181-
// - BuildOptions.Force is False
182-
if opts.CachedChart != "" && !opts.Force {
183-
if curMeta, err := LoadChartMetadataFromArchive(opts.CachedChart); err == nil {
184-
// If the cached metadata is corrupt, we ignore its existence
185-
// and continue the build
186-
if err = curMeta.Validate(); err == nil {
187-
if result.Name == curMeta.Name && result.Version == curMeta.Version {
188-
result.Path = opts.CachedChart
189-
result.ValuesFiles = opts.GetValuesFiles()
190-
result.Packaged = requiresPackaging
191-
*buildResult = *result
192-
return nil, nil
193-
}
194-
}
195-
}
168+
if shouldReturn {
169+
*buildResult = *result
170+
return nil, nil
196171
}
197172

198173
// Download the package for the resolved version
@@ -221,15 +196,38 @@ func (b *remoteChartBuilder) downloadFromRepository(remote *repository.ChartRepo
221196
return nil, &BuildError{Reason: ErrChartReference, Err: err}
222197
}
223198

199+
result, shouldReturn, err := generateBuildResult(cv, opts)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
if shouldReturn {
205+
*buildResult = *result
206+
return nil, nil
207+
}
208+
209+
// Download the package for the resolved version
210+
res, err := remote.DownloadChart(cv)
211+
if err != nil {
212+
err = fmt.Errorf("failed to download chart for remote reference: %w", err)
213+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
214+
}
215+
216+
*buildResult = *result
217+
218+
return res, nil
219+
}
220+
221+
func generateBuildResult(cv *repo.ChartVersion, opts BuildOptions) (*Build, bool, error) {
224222
result := &Build{}
225-
result.Name = cv.Name
226223
result.Version = cv.Version
224+
result.Name = cv.Name
227225

228226
// Set build specific metadata if instructed
229227
if opts.VersionMetadata != "" {
230228
ver, err := setBuildMetaData(result.Version, opts.VersionMetadata)
231229
if err != nil {
232-
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
230+
return nil, false, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
233231
}
234232
result.Version = ver.String()
235233
}
@@ -249,23 +247,13 @@ func (b *remoteChartBuilder) downloadFromRepository(remote *repository.ChartRepo
249247
result.Path = opts.CachedChart
250248
result.ValuesFiles = opts.GetValuesFiles()
251249
result.Packaged = requiresPackaging
252-
*buildResult = *result
253-
return nil, nil
250+
return result, true, nil
254251
}
255252
}
256253
}
257254
}
258255

259-
// Download the package for the resolved version
260-
res, err := remote.DownloadChart(cv)
261-
if err != nil {
262-
err = fmt.Errorf("failed to download chart for remote reference: %w", err)
263-
return nil, &BuildError{Reason: ErrChartPull, Err: err}
264-
}
265-
266-
*buildResult = *result
267-
268-
return res, nil
256+
return result, false, nil
269257
}
270258

271259
func setBuildMetaData(version, versionMetadata string) (*semver.Version, error) {

internal/helm/chart/builder_remote_test.go

+161-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package chart
1919
import (
2020
"bytes"
2121
"context"
22+
"fmt"
23+
"net/url"
2224
"os"
2325
"path/filepath"
2426
"strings"
@@ -29,11 +31,35 @@ import (
2931
helmchart "helm.sh/helm/v3/pkg/chart"
3032
"helm.sh/helm/v3/pkg/chartutil"
3133
helmgetter "helm.sh/helm/v3/pkg/getter"
34+
"helm.sh/helm/v3/pkg/registry"
3235

3336
"github.com/fluxcd/source-controller/internal/helm/chart/secureloader"
3437
"github.com/fluxcd/source-controller/internal/helm/repository"
3538
)
3639

40+
type mockRegistryClient struct {
41+
tags map[string][]string
42+
requestedURL string
43+
}
44+
45+
func (m *mockRegistryClient) Tags(url string) ([]string, error) {
46+
m.requestedURL = url
47+
if tags, ok := m.tags[url]; ok {
48+
return tags, nil
49+
}
50+
return nil, fmt.Errorf("no tags found for %s", url)
51+
}
52+
53+
func (m *mockRegistryClient) Login(url string, opts ...registry.LoginOption) error {
54+
m.requestedURL = url
55+
return nil
56+
}
57+
58+
func (m *mockRegistryClient) Logout(url string, opts ...registry.LogoutOption) error {
59+
m.requestedURL = url
60+
return nil
61+
}
62+
3763
// mockIndexChartGetter returns specific response for index and chart queries.
3864
type mockIndexChartGetter struct {
3965
IndexResponse []byte
@@ -54,7 +80,7 @@ func (g *mockIndexChartGetter) LastGet() string {
5480
return g.requestedURL
5581
}
5682

57-
func TestRemoteBuilder_Build(t *testing.T) {
83+
func TestRemoteBuilder__BuildFromChartRepository(t *testing.T) {
5884
g := NewWithT(t)
5985

6086
chartGrafana, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz")
@@ -195,6 +221,140 @@ entries:
195221
}
196222
}
197223

224+
func TestRemoteBuilder_BuildFromOCIChatRepository(t *testing.T) {
225+
g := NewWithT(t)
226+
227+
chartGrafana, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz")
228+
g.Expect(err).ToNot(HaveOccurred())
229+
g.Expect(chartGrafana).ToNot(BeEmpty())
230+
231+
registryClient := &mockRegistryClient{
232+
tags: map[string][]string{
233+
"localhost:5000/my_repo/grafana": {"6.17.4"},
234+
},
235+
}
236+
237+
mockGetter := &mockIndexChartGetter{
238+
ChartResponse: chartGrafana,
239+
}
240+
241+
u, err := url.Parse("oci://localhost:5000/my_repo")
242+
g.Expect(err).ToNot(HaveOccurred())
243+
244+
mockRepo := func() *repository.OCIChartRepository {
245+
return &repository.OCIChartRepository{
246+
URL: *u,
247+
Client: mockGetter,
248+
RegistryClient: registryClient,
249+
}
250+
}
251+
252+
tests := []struct {
253+
name string
254+
reference Reference
255+
buildOpts BuildOptions
256+
repository *repository.OCIChartRepository
257+
wantValues chartutil.Values
258+
wantVersion string
259+
wantPackaged bool
260+
wantErr string
261+
}{
262+
{
263+
name: "invalid reference",
264+
reference: LocalReference{},
265+
wantErr: "expected remote chart reference",
266+
},
267+
{
268+
name: "invalid reference - no name",
269+
reference: RemoteReference{},
270+
wantErr: "no name set for remote chart reference",
271+
},
272+
{
273+
name: "chart not in repository",
274+
reference: RemoteReference{Name: "foo"},
275+
repository: mockRepo(),
276+
wantErr: "failed to get chart version for remote reference",
277+
},
278+
{
279+
name: "chart version not in repository",
280+
reference: RemoteReference{Name: "grafana", Version: "1.1.1"},
281+
repository: mockRepo(),
282+
wantErr: "failed to get chart version for remote reference",
283+
},
284+
{
285+
name: "invalid version metadata",
286+
reference: RemoteReference{Name: "grafana"},
287+
repository: mockRepo(),
288+
buildOpts: BuildOptions{VersionMetadata: "^"},
289+
wantErr: "Invalid Metadata string",
290+
},
291+
{
292+
name: "with version metadata",
293+
reference: RemoteReference{Name: "grafana"},
294+
repository: mockRepo(),
295+
buildOpts: BuildOptions{VersionMetadata: "foo"},
296+
wantVersion: "6.17.4+foo",
297+
wantPackaged: true,
298+
},
299+
{
300+
name: "default values",
301+
reference: RemoteReference{Name: "grafana"},
302+
repository: mockRepo(),
303+
wantVersion: "0.1.0",
304+
wantValues: chartutil.Values{
305+
"replicaCount": float64(1),
306+
},
307+
},
308+
{
309+
name: "merge values",
310+
reference: RemoteReference{Name: "grafana"},
311+
buildOpts: BuildOptions{
312+
ValuesFiles: []string{"a.yaml", "b.yaml", "c.yaml"},
313+
},
314+
repository: mockRepo(),
315+
wantVersion: "6.17.4",
316+
wantValues: chartutil.Values{
317+
"a": "b",
318+
"b": "d",
319+
},
320+
wantPackaged: true,
321+
},
322+
}
323+
for _, tt := range tests {
324+
t.Run(tt.name, func(t *testing.T) {
325+
g := NewWithT(t)
326+
327+
tmpDir, err := os.MkdirTemp("", "remote-chart-builder-")
328+
g.Expect(err).ToNot(HaveOccurred())
329+
defer os.RemoveAll(tmpDir)
330+
targetPath := filepath.Join(tmpDir, "chart.tgz")
331+
332+
b := NewRemoteBuilder(tt.repository)
333+
334+
cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts)
335+
336+
if tt.wantErr != "" {
337+
g.Expect(err).To(HaveOccurred())
338+
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
339+
g.Expect(cb).To(BeZero())
340+
return
341+
}
342+
g.Expect(err).ToNot(HaveOccurred())
343+
g.Expect(cb.Packaged).To(Equal(tt.wantPackaged), "unexpected Build.Packaged value")
344+
g.Expect(cb.Path).ToNot(BeEmpty(), "empty Build.Path")
345+
346+
// Load the resulting chart and verify the values.
347+
resultChart, err := secureloader.LoadFile(cb.Path)
348+
g.Expect(err).ToNot(HaveOccurred())
349+
g.Expect(resultChart.Metadata.Version).To(Equal(tt.wantVersion))
350+
351+
for k, v := range tt.wantValues {
352+
g.Expect(v).To(Equal(resultChart.Values[k]))
353+
}
354+
})
355+
}
356+
}
357+
198358
func TestRemoteBuilder_Build_CachedChart(t *testing.T) {
199359
g := NewWithT(t)
200360

internal/helm/repository/oci_chart_repository.go

+7-11
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,14 @@ func WithTlsConfig(tlsConfig *tls.Config) OCIChartRepositoryOption {
100100

101101
// NewOCIChartRepository constructs and returns a new ChartRepository with
102102
// the ChartRepository.Client configured to the getter.Getter for the
103-
// repository URL scheme. It returns an error on URL parsing failures,
104-
// or if there is no getter available for the scheme.
103+
// repository URL scheme. It returns an error on URL parsing failures.
104+
// It assumes that the url scheme has been validated to be an OCI scheme.
105105
func NewOCIChartRepository(repositoryURL string, chartRepoOpts ...OCIChartRepositoryOption) (*OCIChartRepository, error) {
106106
u, err := url.Parse(repositoryURL)
107107
if err != nil {
108108
return nil, err
109109
}
110110

111-
if !registry.IsOCI(repositoryURL) {
112-
return nil, fmt.Errorf("the url scheme is not supported: %s", u.Scheme)
113-
}
114-
115111
r := newOCIChartRepository()
116112
r.URL = *u
117113
for _, opt := range chartRepoOpts {
@@ -157,7 +153,7 @@ func (r *OCIChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
157153
}, err
158154
}
159155

160-
// this function shall be called for OCI registries only
156+
// This function shall be called for OCI registries only
161157
// It assumes that the ref has been validated to be an OCI reference.
162158
func (r *OCIChartRepository) getTags(ref string) ([]string, error) {
163159
// Retrieve list of repository tags
@@ -182,10 +178,6 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf
182178
}
183179

184180
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-
189181
u, err := url.Parse(ref)
190182
if err != nil {
191183
err = fmt.Errorf("invalid chart URL format '%s': %w", ref, err)
@@ -201,6 +193,7 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf
201193
}
202194

203195
// Login attempts to login to the OCI registry.
196+
// It returns an error on failure.
204197
func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error {
205198
err := r.RegistryClient.Login(r.URL.Host, opts...)
206199
if err != nil {
@@ -210,6 +203,7 @@ func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error {
210203
}
211204

212205
// Logout attempts to logout from the OCI registry.
206+
// It returns an error on failure.
213207
func (r *OCIChartRepository) Logout() error {
214208
err := r.RegistryClient.Logout(r.URL.Host)
215209
if err != nil {
@@ -218,6 +212,8 @@ func (r *OCIChartRepository) Logout() error {
218212
return nil
219213
}
220214

215+
// getLastMatchingVersionOrConstraint returns the last version that matches the given version string.
216+
// If the version string is empty, the highest available version is returned.
221217
func getLastMatchingVersionOrConstraint(cvs []string, ver string) (string, error) {
222218
// Check for exact matches first
223219
if ver != "" {

0 commit comments

Comments
 (0)