Skip to content

Commit 2b8134c

Browse files
committed
internal/helm: introduce typed BuildError
This commit introduces a typed `BuildError` to be returned by `Builder.Build` in case of a failure. The `Reason` field in combination with `BuildErrorReason` can be used to signal (or determine) the reason of a returned error within the context of the build process. At present this is used to determine the correct Condition Reason, but in a future iteration this can be used to determine the negative polarity condition that should be set to indicate a precise failure to the user. Signed-off-by: Hidde Beydals <hello@hidde.co>
1 parent 4fd6e6e commit 2b8134c

File tree

7 files changed

+217
-38
lines changed

7 files changed

+217
-38
lines changed

controllers/helmchart_controller.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"net/url"
2324
"os"
@@ -29,7 +30,7 @@ import (
2930
"github.com/go-logr/logr"
3031
helmgetter "helm.sh/helm/v3/pkg/getter"
3132
corev1 "k8s.io/api/core/v1"
32-
"k8s.io/apimachinery/pkg/api/errors"
33+
apierrs "k8s.io/apimachinery/pkg/api/errors"
3334
apimeta "k8s.io/apimachinery/pkg/api/meta"
3435
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3536
"k8s.io/apimachinery/pkg/runtime"
@@ -445,19 +446,19 @@ func (r *HelmChartReconciler) fromTarballArtifact(ctx context.Context, source so
445446
}
446447

447448
// Build chart
448-
chartB := chart.NewLocalBuilder(dm)
449-
build, err := chartB.Build(ctx, chart.LocalReference{WorkDir: sourceDir, Path: chartPath}, filepath.Join(workDir, "chart.tgz"), buildsOpts)
449+
chartBuilder := chart.NewLocalBuilder(dm)
450+
result, err := chartBuilder.Build(ctx, chart.LocalReference{WorkDir: sourceDir, Path: chartPath}, filepath.Join(workDir, "chart.tgz"), buildsOpts)
450451
if err != nil {
451-
return sourcev1.HelmChartNotReady(c, sourcev1.ChartPackageFailedReason, err.Error()), err
452+
return sourcev1.HelmChartNotReady(c, reasonForBuildError(err), err.Error()), err
452453
}
453454

454-
newArtifact := r.Storage.NewArtifactFor(c.Kind, c.GetObjectMeta(), build.Version,
455-
fmt.Sprintf("%s-%s.tgz", build.Name, build.Version))
455+
newArtifact := r.Storage.NewArtifactFor(c.Kind, c.GetObjectMeta(), result.Version,
456+
fmt.Sprintf("%s-%s.tgz", result.Name, result.Version))
456457

457458
// If the path of the returned build equals the cache path,
458459
// there are no changes to the chart
459460
if apimeta.IsStatusConditionTrue(c.Status.Conditions, meta.ReadyCondition) &&
460-
build.Path == buildsOpts.CachedChart {
461+
result.Path == buildsOpts.CachedChart {
461462
// Ensure hostname is updated
462463
if c.GetArtifact().URL != newArtifact.URL {
463464
r.Storage.SetArtifactURL(c.GetArtifact())
@@ -482,19 +483,19 @@ func (r *HelmChartReconciler) fromTarballArtifact(ctx context.Context, source so
482483
defer unlock()
483484

484485
// Copy the packaged chart to the artifact path
485-
if err = r.Storage.CopyFromPath(&newArtifact, build.Path); err != nil {
486+
if err = r.Storage.CopyFromPath(&newArtifact, result.Path); err != nil {
486487
err = fmt.Errorf("failed to write chart package to storage: %w", err)
487488
return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
488489
}
489490

490491
// Update symlink
491-
cUrl, err := r.Storage.Symlink(newArtifact, fmt.Sprintf("%s-latest.tgz", build.Name))
492+
cUrl, err := r.Storage.Symlink(newArtifact, fmt.Sprintf("%s-latest.tgz", result.Name))
492493
if err != nil {
493494
err = fmt.Errorf("storage error: %w", err)
494495
return sourcev1.HelmChartNotReady(c, sourcev1.StorageOperationFailedReason, err.Error()), err
495496
}
496497

497-
return sourcev1.HelmChartReady(c, newArtifact, cUrl, sourcev1.ChartPackageSucceededReason, build.Summary()), nil
498+
return sourcev1.HelmChartReady(c, newArtifact, cUrl, reasonForBuildSuccess(result), result.Summary()), nil
498499
}
499500

500501
// namespacedChartRepositoryCallback returns a chart.GetChartRepositoryCallback
@@ -508,7 +509,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
508509
repo, err := r.resolveDependencyRepository(ctx, url, namespace)
509510
if err != nil {
510511
// Return Kubernetes client errors, but ignore others
511-
if errors.ReasonForError(err) != metav1.StatusReasonUnknown {
512+
if apierrs.ReasonForError(err) != metav1.StatusReasonUnknown {
512513
return nil, err
513514
}
514515
repo = &sourcev1.HelmRepository{
@@ -807,3 +808,23 @@ func (r *HelmChartReconciler) recordSuspension(ctx context.Context, chart source
807808
r.MetricsRecorder.RecordSuspend(*objRef, chart.Spec.Suspend)
808809
}
809810
}
811+
812+
func reasonForBuildError(err error) string {
813+
var buildErr *chart.BuildError
814+
if ok := errors.As(err, &buildErr); !ok {
815+
return sourcev1.ChartPullFailedReason
816+
}
817+
switch buildErr.Reason {
818+
case chart.ErrChartMetadataPatch, chart.ErrValueFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
819+
return sourcev1.ChartPackageFailedReason
820+
default:
821+
return sourcev1.ChartPullFailedReason
822+
}
823+
}
824+
825+
func reasonForBuildSuccess(result *chart.Build) string {
826+
if result.Packaged {
827+
return sourcev1.ChartPackageSucceededReason
828+
}
829+
return sourcev1.ChartPullSucceededReason
830+
}

internal/helm/chart/builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func (b *Build) Summary() string {
144144

145145
var s strings.Builder
146146

147-
action := "Fetched"
147+
var action = "Pulled"
148148
if b.Packaged {
149149
action = "Packaged"
150150
}

internal/helm/chart/builder_local.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
5151
}
5252

5353
if err := ref.Validate(); err != nil {
54-
return nil, err
54+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
5555
}
5656

5757
// Load the chart metadata from the LocalReference to ensure it points
5858
// to a chart
5959
curMeta, err := LoadChartMetadata(localRef.Path)
6060
if err != nil {
61-
return nil, err
61+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
6262
}
6363

6464
result := &Build{}
@@ -69,10 +69,12 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
6969
if opts.VersionMetadata != "" {
7070
ver, err := semver.NewVersion(curMeta.Version)
7171
if err != nil {
72-
return nil, fmt.Errorf("failed to parse chart version from metadata as SemVer: %w", err)
72+
err = fmt.Errorf("failed to parse version from chart metadata as SemVer: %w", err)
73+
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
7374
}
7475
if *ver, err = ver.SetMetadata(opts.VersionMetadata); err != nil {
75-
return nil, fmt.Errorf("failed to set metadata on chart version: %w", err)
76+
err = fmt.Errorf("failed to set SemVer metadata on chart version: %w", err)
77+
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
7678
}
7779
result.Version = ver.String()
7880
}
@@ -92,8 +94,8 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
9294
// options are set, we can copy the chart without making modifications
9395
isChartDir := pathIsDir(localRef.Path)
9496
if !isChartDir && len(opts.GetValueFiles()) == 0 {
95-
if err := copyFileToPath(localRef.Path, p); err != nil {
96-
return nil, err
97+
if err = copyFileToPath(localRef.Path, p); err != nil {
98+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
9799
}
98100
result.Path = p
99101
return result, nil
@@ -103,7 +105,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
103105
var mergedValues map[string]interface{}
104106
if len(opts.GetValueFiles()) > 0 {
105107
if mergedValues, err = mergeFileValues(localRef.WorkDir, opts.ValueFiles); err != nil {
106-
return nil, fmt.Errorf("failed to merge value files: %w", err)
108+
return nil, &BuildError{Reason: ErrValueFilesMerge, Err: err}
107109
}
108110
}
109111

@@ -112,32 +114,33 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
112114
// or because we have merged values and need to repackage
113115
chart, err := loader.Load(localRef.Path)
114116
if err != nil {
115-
return nil, err
117+
return nil, &BuildError{Reason: ErrChartPackage, Err: err}
116118
}
117119
// Set earlier resolved version (with metadata)
118120
chart.Metadata.Version = result.Version
119121

120122
// Overwrite default values with merged values, if any
121123
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
122124
if err != nil {
123-
return nil, err
125+
return nil, &BuildError{Reason: ErrValueFilesMerge, Err: err}
124126
}
125127
result.ValueFiles = opts.GetValueFiles()
126128
}
127129

128130
// Ensure dependencies are fetched if building from a directory
129131
if isChartDir {
130132
if b.dm == nil {
131-
return nil, fmt.Errorf("local chart builder requires dependency manager for unpackaged charts")
133+
err = fmt.Errorf("local chart builder requires dependency manager for unpackaged charts")
134+
return nil, &BuildError{Reason: ErrDependencyBuild, Err: err}
132135
}
133136
if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, chart); err != nil {
134-
return nil, err
137+
return nil, &BuildError{Reason: ErrDependencyBuild, Err: err}
135138
}
136139
}
137140

138141
// Package the chart
139142
if err = packageToPath(chart, p); err != nil {
140-
return nil, err
143+
return nil, &BuildError{Reason: ErrChartPackage, Err: err}
141144
}
142145
result.Path = p
143146
result.Packaged = true

internal/helm/chart/builder_remote.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,20 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
5454
}
5555

5656
if err := ref.Validate(); err != nil {
57-
return nil, err
57+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
5858
}
5959

6060
if err := b.remote.LoadFromCache(); err != nil {
61-
return nil, fmt.Errorf("could not load repository index for remote chart reference: %w", err)
61+
err = fmt.Errorf("could not load repository index for remote chart reference: %w", err)
62+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
6263
}
6364
defer b.remote.Unload()
6465

6566
// Get the current version for the RemoteReference
6667
cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version)
6768
if err != nil {
68-
return nil, fmt.Errorf("failed to get chart version for remote reference: %w", err)
69+
err = fmt.Errorf("failed to get chart version for remote reference: %w", err)
70+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
6971
}
7072

7173
result := &Build{}
@@ -75,10 +77,12 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
7577
if opts.VersionMetadata != "" {
7678
ver, err := semver.NewVersion(result.Version)
7779
if err != nil {
78-
return nil, err
80+
err = fmt.Errorf("failed to parse version from chart metadata as SemVer: %w", err)
81+
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
7982
}
8083
if *ver, err = ver.SetMetadata(opts.VersionMetadata); err != nil {
81-
return nil, err
84+
err = fmt.Errorf("failed to set SemVer metadata on chart version: %w", err)
85+
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
8286
}
8387
result.Version = ver.String()
8488
}
@@ -97,14 +101,15 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
97101
// Download the package for the resolved version
98102
res, err := b.remote.DownloadChart(cv)
99103
if err != nil {
100-
return nil, fmt.Errorf("failed to download chart for remote reference: %w", err)
104+
err = fmt.Errorf("failed to download chart for remote reference: %w", err)
105+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
101106
}
102107

103108
// Use literal chart copy from remote if no custom value files options are
104109
// set or build option version metadata isn't set.
105110
if len(opts.GetValueFiles()) == 0 && opts.VersionMetadata == "" {
106111
if err = validatePackageAndWriteToPath(res, p); err != nil {
107-
return nil, err
112+
return nil, &BuildError{Reason: ErrChartPull, Err: err}
108113
}
109114
result.Path = p
110115
return result, nil
@@ -113,26 +118,27 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
113118
// Load the chart and merge chart values
114119
var chart *helmchart.Chart
115120
if chart, err = loader.LoadArchive(res); err != nil {
116-
return nil, fmt.Errorf("failed to load downloaded chart: %w", err)
121+
err = fmt.Errorf("failed to load downloaded chart: %w", err)
122+
return nil, &BuildError{Reason: ErrChartPackage, Err: err}
117123
}
124+
chart.Metadata.Version = result.Version
118125

119126
mergedValues, err := mergeChartValues(chart, opts.ValueFiles)
120127
if err != nil {
121-
return nil, fmt.Errorf("failed to merge chart values: %w", err)
128+
err = fmt.Errorf("failed to merge chart values: %w", err)
129+
return nil, &BuildError{Reason: ErrValueFilesMerge, Err: err}
122130
}
123131
// Overwrite default values with merged values, if any
124132
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
125133
if err != nil {
126-
return nil, err
134+
return nil, &BuildError{Reason: ErrValueFilesMerge, Err: err}
127135
}
128136
result.ValueFiles = opts.GetValueFiles()
129137
}
130138

131-
chart.Metadata.Version = result.Version
132-
133139
// Package the chart with the custom values
134140
if err = packageToPath(chart, p); err != nil {
135-
return nil, err
141+
return nil, &BuildError{Reason: ErrChartPackage, Err: err}
136142
}
137143
result.Path = p
138144
result.Packaged = true

internal/helm/chart/builder_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func TestChartBuildResult_Summary(t *testing.T) {
143143
Name: "chart",
144144
Version: "1.2.3-rc.1+bd6bf40",
145145
},
146-
want: "Fetched 'chart' chart with version '1.2.3-rc.1+bd6bf40'.",
146+
want: "Pulled 'chart' chart with version '1.2.3-rc.1+bd6bf40'.",
147147
},
148148
{
149149
name: "With value files",

internal/helm/chart/errors.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
Copyright 2021 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 chart
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
)
23+
24+
// BuildErrorReason is the descriptive reason for a BuildError.
25+
type BuildErrorReason string
26+
27+
// Error returns the string representation of BuildErrorReason.
28+
func (e BuildErrorReason) Error() string {
29+
return string(e)
30+
}
31+
32+
// BuildError contains a wrapped Err and a Reason indicating why it occurred.
33+
type BuildError struct {
34+
Reason error
35+
Err error
36+
}
37+
38+
// Error returns Err as a string, prefixed with the Reason, if any.
39+
func (e *BuildError) Error() string {
40+
if e.Reason == nil {
41+
return e.Err.Error()
42+
}
43+
return fmt.Sprintf("%s: %s", e.Reason.Error(), e.Err.Error())
44+
}
45+
46+
// Is returns true of the Reason or Err equals target.
47+
func (e *BuildError) Is(target error) bool {
48+
if e.Reason != nil && e.Reason == target {
49+
return true
50+
}
51+
return errors.Is(e.Err, target)
52+
}
53+
54+
// Unwrap returns the underlying Err.
55+
func (e *BuildError) Unwrap() error {
56+
return e.Err
57+
}
58+
59+
var (
60+
ErrChartPull = BuildErrorReason("chart pull error")
61+
ErrChartMetadataPatch = BuildErrorReason("chart metadata patch error")
62+
ErrValueFilesMerge = BuildErrorReason("value files merge error")
63+
ErrDependencyBuild = BuildErrorReason("dependency build error")
64+
ErrChartPackage = BuildErrorReason("chart package error")
65+
)

0 commit comments

Comments
 (0)