HelmChartReconciler refactor

Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
Hidde Beydals 2021-12-08 22:15:27 +01:00
parent e0e048ad6d
commit 8e107ea60e
13 changed files with 783 additions and 592 deletions

View File

@ -32,6 +32,10 @@ const (
// Source may be outdated. // Source may be outdated.
// This is a "negative polarity" or "abnormal-true" type, and is only present on the resource if it is True. // This is a "negative polarity" or "abnormal-true" type, and is only present on the resource if it is True.
FetchFailedCondition string = "FetchFailed" FetchFailedCondition string = "FetchFailed"
// BuildFailedCondition indicates a transient or persistent build failure of a Source's Artifact.
// If True, the Source can be in an ArtifactOutdatedCondition
BuildFailedCondition string = "BuildFailed"
) )
const ( const (

View File

@ -19,12 +19,10 @@ package v1beta2
import ( import (
"time" "time"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
) )
// HelmChartKind is the string representation of a HelmChart. // HelmChartKind is the string representation of a HelmChart.
@ -115,6 +113,16 @@ type HelmChartStatus struct {
// +optional // +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"` ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// ObservedSourceArtifactRevision is the last observed Artifact.Revision
// of the Source reference.
// +optional
ObservedSourceArtifactRevision string `json:"observedSourceArtifactRevision,omitempty"`
// ObservedChartName is the last observed chart name as defined by the
// resolved chart reference.
// +optional
ObservedChartName string `json:"observedChartName,omitempty"`
// Conditions holds the conditions for the HelmChart. // Conditions holds the conditions for the HelmChart.
// +optional // +optional
Conditions []metav1.Condition `json:"conditions,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"`
@ -148,46 +156,6 @@ const (
ChartPackageSucceededReason string = "ChartPackageSucceeded" ChartPackageSucceededReason string = "ChartPackageSucceeded"
) )
// HelmChartProgressing resets the conditions of the HelmChart to meta.Condition
// of type meta.ReadyCondition with status 'Unknown' and meta.ProgressingReason
// reason and message. It returns the modified HelmChart.
func HelmChartProgressing(chart HelmChart) HelmChart {
chart.Status.ObservedGeneration = chart.Generation
chart.Status.URL = ""
chart.Status.Conditions = []metav1.Condition{}
conditions.MarkUnknown(&chart, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
return chart
}
// HelmChartReady sets the given Artifact and URL on the HelmChart and sets the
// meta.ReadyCondition to 'True', with the given reason and message. It returns
// the modified HelmChart.
func HelmChartReady(chart HelmChart, artifact Artifact, url, reason, message string) HelmChart {
chart.Status.Artifact = &artifact
chart.Status.URL = url
conditions.MarkTrue(&chart, meta.ReadyCondition, reason, message)
return chart
}
// HelmChartNotReady sets the meta.ReadyCondition on the given HelmChart to
// 'False', with the given reason and message. It returns the modified
// HelmChart.
func HelmChartNotReady(chart HelmChart, reason, message string) HelmChart {
conditions.MarkFalse(&chart, meta.ReadyCondition, reason, message)
return chart
}
// HelmChartReadyMessage returns the message of the meta.ReadyCondition with
// status 'True', or an empty string.
func HelmChartReadyMessage(chart HelmChart) string {
if c := apimeta.FindStatusCondition(chart.Status.Conditions, meta.ReadyCondition); c != nil {
if c.Status == metav1.ConditionTrue {
return c.Message
}
}
return ""
}
// GetConditions returns the status conditions of the object. // GetConditions returns the status conditions of the object.
func (in HelmChart) GetConditions() []metav1.Condition { func (in HelmChart) GetConditions() []metav1.Condition {
return in.Status.Conditions return in.Status.Conditions

View File

@ -517,10 +517,18 @@ spec:
reconcile request value, so a change of the annotation value can reconcile request value, so a change of the annotation value can
be detected. be detected.
type: string type: string
observedChartName:
description: ObservedChartName is the last observed chart name as
defined by the resolved chart reference.
type: string
observedGeneration: observedGeneration:
description: ObservedGeneration is the last observed generation. description: ObservedGeneration is the last observed generation.
format: int64 format: int64
type: integer type: integer
observedSourceArtifactRevision:
description: ObservedSourceArtifactRevision is the last observed Artifact.Revision
of the Source reference.
type: string
url: url:
description: URL is the download link for the last chart pulled. description: URL is the download link for the last chart pulled.
type: string type: string

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,7 @@ import (
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/gittestserver" "github.com/fluxcd/pkg/gittestserver"
"github.com/fluxcd/pkg/helmtestserver" "github.com/fluxcd/pkg/helmtestserver"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
@ -49,7 +50,7 @@ import (
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
) )
var _ = Describe("HelmChartReconciler", func() { var _ = FDescribe("HelmChartReconciler", func() {
const ( const (
timeout = time.Second * 30 timeout = time.Second * 30
@ -270,7 +271,7 @@ var _ = Describe("HelmChartReconciler", func() {
got := &sourcev1.HelmChart{} got := &sourcev1.HelmChart{}
Eventually(func() bool { Eventually(func() bool {
_ = k8sClient.Get(context.Background(), key, got) _ = k8sClient.Get(context.Background(), key, got)
return got.Status.ObservedGeneration > updated.Status.ObservedGeneration && return got.Status.ObservedGeneration > updated.Status.ObservedGeneration && got.GetArtifact() != nil &&
ginkgoTestStorage.ArtifactExist(*got.Status.Artifact) ginkgoTestStorage.ArtifactExist(*got.Status.Artifact)
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
f, err := os.Stat(ginkgoTestStorage.LocalPath(*got.Status.Artifact)) f, err := os.Stat(ginkgoTestStorage.LocalPath(*got.Status.Artifact))
@ -292,6 +293,9 @@ var _ = Describe("HelmChartReconciler", func() {
Eventually(func() bool { Eventually(func() bool {
_ = k8sClient.Get(context.Background(), key, updated) _ = k8sClient.Get(context.Background(), key, updated)
for _, c := range updated.Status.Conditions { for _, c := range updated.Status.Conditions {
fmt.Fprintf(GinkgoWriter, "condition type: %s\n", c.Type)
fmt.Fprintf(GinkgoWriter, "condition reason: %s\n", c.Reason)
fmt.Fprintf(GinkgoWriter, "condition message: %s\n", c.Message)
if c.Reason == sourcev1.ChartPullFailedReason && if c.Reason == sourcev1.ChartPullFailedReason &&
strings.Contains(c.Message, "failed to retrieve source") { strings.Contains(c.Message, "failed to retrieve source") {
return true return true
@ -394,12 +398,8 @@ var _ = Describe("HelmChartReconciler", func() {
Expect(k8sClient.Update(context.Background(), chart)).Should(Succeed()) Expect(k8sClient.Update(context.Background(), chart)).Should(Succeed())
Eventually(func() bool { Eventually(func() bool {
_ = k8sClient.Get(context.Background(), key, chart) _ = k8sClient.Get(context.Background(), key, chart)
for _, c := range chart.Status.Conditions { return conditions.GetReason(chart, sourcev1.FetchFailedCondition) == "InvalidChartReference" &&
if c.Reason == sourcev1.ChartPullFailedReason { conditions.IsStalled(chart)
return true
}
}
return false
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
Expect(chart.GetArtifact()).NotTo(BeNil()) Expect(chart.GetArtifact()).NotTo(BeNil())
Expect(chart.Status.Artifact.Revision).Should(Equal("0.1.1")) Expect(chart.Status.Artifact.Revision).Should(Equal("0.1.1"))
@ -495,13 +495,7 @@ var _ = Describe("HelmChartReconciler", func() {
got := &sourcev1.HelmChart{} got := &sourcev1.HelmChart{}
Eventually(func() bool { Eventually(func() bool {
_ = k8sClient.Get(context.Background(), key, got) _ = k8sClient.Get(context.Background(), key, got)
for _, c := range got.Status.Conditions { return conditions.GetReason(got, sourcev1.FetchFailedCondition) == sourcev1.AuthenticationFailedReason
if c.Reason == sourcev1.AuthenticationFailedReason &&
strings.Contains(c.Message, "auth secret error") {
return true
}
}
return false
}, timeout, interval).Should(BeTrue()) }, timeout, interval).Should(BeTrue())
By("Applying secret with missing keys") By("Applying secret with missing keys")
@ -515,7 +509,7 @@ var _ = Describe("HelmChartReconciler", func() {
got := &sourcev1.HelmChart{} got := &sourcev1.HelmChart{}
_ = k8sClient.Get(context.Background(), key, got) _ = k8sClient.Get(context.Background(), key, got)
for _, c := range got.Status.Conditions { for _, c := range got.Status.Conditions {
if c.Reason == sourcev1.ChartPullFailedReason && if c.Reason == "ChartPullError" &&
strings.Contains(c.Message, "401 Unauthorized") { strings.Contains(c.Message, "401 Unauthorized") {
return true return true
} }
@ -833,7 +827,7 @@ var _ = Describe("HelmChartReconciler", func() {
// if the artifact was changed due to the current update. // if the artifact was changed due to the current update.
// Use status condition to be sure. // Use status condition to be sure.
for _, condn := range got.Status.Conditions { for _, condn := range got.Status.Conditions {
if strings.Contains(condn.Message, "with merged values files [./testdata/charts/helmchart/override.yaml]") && if strings.Contains(condn.Message, "merged values files [./testdata/charts/helmchart/override.yaml]") &&
ginkgoTestStorage.ArtifactExist(*got.Status.Artifact) { ginkgoTestStorage.ArtifactExist(*got.Status.Artifact) {
return true return true
} }

View File

@ -137,9 +137,9 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred(), "failed to setup HelmRepositoryReconciler") Expect(err).ToNot(HaveOccurred(), "failed to setup HelmRepositoryReconciler")
err = (&HelmChartReconciler{ err = (&HelmChartReconciler{
Client: k8sManager.GetClient(), Client: k8sManager.GetClient(),
Scheme: scheme.Scheme, Storage: ginkgoTestStorage,
Storage: ginkgoTestStorage, EventRecorder: record.NewFakeRecorder(32),
Getters: getter.Providers{getter.Provider{ Getters: getter.Providers{getter.Provider{
Schemes: []string{"http", "https"}, Schemes: []string{"http", "https"},
New: getter.NewHTTPGetter, New: getter.NewHTTPGetter,

View File

@ -115,15 +115,16 @@ func (o BuildOptions) GetValuesFiles() []string {
return o.ValuesFiles return o.ValuesFiles
} }
// Build contains the Builder.Build result, including specific // Build contains the (partial) Builder.Build result, including specific
// information about the built chart like ResolvedDependencies. // information about the built chart like ResolvedDependencies.
type Build struct { type Build struct {
// Path is the absolute path to the packaged chart. // Name of the chart.
Path string
// Name of the packaged chart.
Name string Name string
// Version of the packaged chart. // Version of the chart.
Version string Version string
// Path is the absolute path to the packaged chart.
// Can be empty, in which case a failure should be assumed.
Path string
// ValuesFiles is the list of files used to compose the chart's // ValuesFiles is the list of files used to compose the chart's
// default "values.yaml". // default "values.yaml".
ValuesFiles []string ValuesFiles []string
@ -138,30 +139,45 @@ type Build struct {
// Summary returns a human-readable summary of the Build. // Summary returns a human-readable summary of the Build.
func (b *Build) Summary() string { func (b *Build) Summary() string {
if b == nil || b.Name == "" || b.Version == "" { if !b.HasMetadata() {
return "No chart build." return "No chart build"
} }
var s strings.Builder var s strings.Builder
var action = "Pulled" var action = "New"
if b.Packaged { if b.Path != "" {
action = "Packaged" action = "Pulled"
if b.Packaged {
action = "Packaged"
}
} }
s.WriteString(fmt.Sprintf("%s '%s' chart with version '%s'", action, b.Name, b.Version)) s.WriteString(fmt.Sprintf("%s '%s' chart with version '%s'", action, b.Name, b.Version))
if b.Packaged && len(b.ValuesFiles) > 0 { if len(b.ValuesFiles) > 0 {
s.WriteString(fmt.Sprintf(", with merged values files %v", b.ValuesFiles)) s.WriteString(fmt.Sprintf(" and merged values files %v", b.ValuesFiles))
} }
if b.Packaged && b.ResolvedDependencies > 0 {
s.WriteString(fmt.Sprintf(", resolving %d dependencies before packaging", b.ResolvedDependencies))
}
s.WriteString(".")
return s.String() return s.String()
} }
// HasMetadata returns if the Build contains chart metadata.
//
// NOTE: This may return True while the build did not Complete successfully.
// Which means it was able to successfully collect the metadata from the chart,
// but failed further into the process.
func (b *Build) HasMetadata() bool {
if b == nil {
return false
}
return b.Name != "" && b.Version != ""
}
// Complete returns if the Build completed successfully.
func (b *Build) Complete() bool {
return b.HasMetadata() && b.Path != ""
}
// String returns the Path of the Build. // String returns the Path of the Build.
func (b *Build) String() string { func (b *Build) String() string {
if b == nil { if b == nil {

View File

@ -101,6 +101,9 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
result.Version = ver.String() result.Version = ver.String()
} }
isChartDir := pathIsDir(localRef.Path)
requiresPackaging := isChartDir || opts.VersionMetadata != "" || len(opts.GetValuesFiles()) != 0
// If all the following is true, we do not need to package the chart: // If all the following is true, we do not need to package the chart:
// - Chart name from cached chart matches resolved name // - Chart name from cached chart matches resolved name
// - Chart version from cached chart matches calculated version // - Chart version from cached chart matches calculated version
@ -112,7 +115,9 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
if err = curMeta.Validate(); err == nil { if err = curMeta.Validate(); err == nil {
if result.Name == curMeta.Name && result.Version == curMeta.Version { if result.Name == curMeta.Name && result.Version == curMeta.Version {
result.Path = opts.CachedChart result.Path = opts.CachedChart
result.ValuesFiles = opts.ValuesFiles result.ValuesFiles = opts.GetValuesFiles()
result.Packaged = requiresPackaging
return result, nil return result, nil
} }
} }
@ -121,10 +126,9 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
// If the chart at the path is already packaged and no custom values files // If the chart at the path is already packaged and no custom values files
// options are set, we can copy the chart without making modifications // options are set, we can copy the chart without making modifications
isChartDir := pathIsDir(localRef.Path) if !requiresPackaging {
if !isChartDir && len(opts.GetValuesFiles()) == 0 {
if err = copyFileToPath(localRef.Path, p); err != nil { if err = copyFileToPath(localRef.Path, p); err != nil {
return nil, &BuildError{Reason: ErrChartPull, Err: err} return result, &BuildError{Reason: ErrChartPull, Err: err}
} }
result.Path = p result.Path = p
return result, nil return result, nil
@ -134,7 +138,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
var mergedValues map[string]interface{} var mergedValues map[string]interface{}
if len(opts.GetValuesFiles()) > 0 { if len(opts.GetValuesFiles()) > 0 {
if mergedValues, err = mergeFileValues(localRef.WorkDir, opts.ValuesFiles); err != nil { if mergedValues, err = mergeFileValues(localRef.WorkDir, opts.ValuesFiles); err != nil {
return nil, &BuildError{Reason: ErrValuesFilesMerge, Err: err} return result, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
} }
} }
@ -143,7 +147,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
// or because we have merged values and need to repackage // or because we have merged values and need to repackage
chart, err := loader.Load(localRef.Path) chart, err := loader.Load(localRef.Path)
if err != nil { if err != nil {
return nil, &BuildError{Reason: ErrChartPackage, Err: err} return result, &BuildError{Reason: ErrChartPackage, Err: err}
} }
// Set earlier resolved version (with metadata) // Set earlier resolved version (with metadata)
chart.Metadata.Version = result.Version chart.Metadata.Version = result.Version
@ -151,7 +155,7 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
// Overwrite default values with merged values, if any // Overwrite default values with merged values, if any
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil { if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
if err != nil { if err != nil {
return nil, &BuildError{Reason: ErrValuesFilesMerge, Err: err} return result, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
} }
result.ValuesFiles = opts.GetValuesFiles() result.ValuesFiles = opts.GetValuesFiles()
} }
@ -160,19 +164,19 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string,
if isChartDir { if isChartDir {
if b.dm == nil { if b.dm == nil {
err = fmt.Errorf("local chart builder requires dependency manager for unpackaged charts") err = fmt.Errorf("local chart builder requires dependency manager for unpackaged charts")
return nil, &BuildError{Reason: ErrDependencyBuild, Err: err} return result, &BuildError{Reason: ErrDependencyBuild, Err: err}
} }
if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, chart); err != nil { if result.ResolvedDependencies, err = b.dm.Build(ctx, ref, chart); err != nil {
return nil, &BuildError{Reason: ErrDependencyBuild, Err: err} return result, &BuildError{Reason: ErrDependencyBuild, Err: err}
} }
} }
// Package the chart // Package the chart
if err = packageToPath(chart, p); err != nil { if err = packageToPath(chart, p); err != nil {
return nil, &BuildError{Reason: ErrChartPackage, Err: err} return result, &BuildError{Reason: ErrChartPackage, Err: err}
} }
result.Path = p result.Path = p
result.Packaged = true result.Packaged = requiresPackaging
return result, nil return result, nil
} }

View File

@ -82,12 +82,13 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version) cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version)
if err != nil { if err != nil {
err = fmt.Errorf("failed to get chart version for remote reference: %w", err) err = fmt.Errorf("failed to get chart version for remote reference: %w", err)
return nil, &BuildError{Reason: ErrChartPull, Err: err} return nil, &BuildError{Reason: ErrChartReference, Err: err}
} }
result := &Build{} result := &Build{}
result.Name = cv.Name result.Name = cv.Name
result.Version = cv.Version result.Version = cv.Version
// Set build specific metadata if instructed // Set build specific metadata if instructed
if opts.VersionMetadata != "" { if opts.VersionMetadata != "" {
ver, err := semver.NewVersion(result.Version) ver, err := semver.NewVersion(result.Version)
@ -102,6 +103,8 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
result.Version = ver.String() result.Version = ver.String()
} }
requiresPackaging := len(opts.GetValuesFiles()) != 0 || opts.VersionMetadata != ""
// If all the following is true, we do not need to download and/or build the chart: // If all the following is true, we do not need to download and/or build the chart:
// - Chart name from cached chart matches resolved name // - Chart name from cached chart matches resolved name
// - Chart version from cached chart matches calculated version // - Chart version from cached chart matches calculated version
@ -114,6 +117,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
if result.Name == curMeta.Name && result.Version == curMeta.Version { if result.Name == curMeta.Name && result.Version == curMeta.Version {
result.Path = opts.CachedChart result.Path = opts.CachedChart
result.ValuesFiles = opts.GetValuesFiles() result.ValuesFiles = opts.GetValuesFiles()
result.Packaged = requiresPackaging
return result, nil return result, nil
} }
} }
@ -124,12 +128,12 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
res, err := b.remote.DownloadChart(cv) res, err := b.remote.DownloadChart(cv)
if err != nil { if err != nil {
err = fmt.Errorf("failed to download chart for remote reference: %w", err) err = fmt.Errorf("failed to download chart for remote reference: %w", err)
return nil, &BuildError{Reason: ErrChartPull, Err: err} return result, &BuildError{Reason: ErrChartPull, Err: err}
} }
// Use literal chart copy from remote if no custom values files options are // Use literal chart copy from remote if no custom values files options are
// set or build option version metadata isn't set. // set or version metadata isn't set.
if len(opts.GetValuesFiles()) == 0 && opts.VersionMetadata == "" { if !requiresPackaging {
if err = validatePackageAndWriteToPath(res, p); err != nil { if err = validatePackageAndWriteToPath(res, p); err != nil {
return nil, &BuildError{Reason: ErrChartPull, Err: err} return nil, &BuildError{Reason: ErrChartPull, Err: err}
} }
@ -141,14 +145,14 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
var chart *helmchart.Chart var chart *helmchart.Chart
if chart, err = loader.LoadArchive(res); err != nil { if chart, err = loader.LoadArchive(res); err != nil {
err = fmt.Errorf("failed to load downloaded chart: %w", err) err = fmt.Errorf("failed to load downloaded chart: %w", err)
return nil, &BuildError{Reason: ErrChartPackage, Err: err} return result, &BuildError{Reason: ErrChartPackage, Err: err}
} }
chart.Metadata.Version = result.Version chart.Metadata.Version = result.Version
mergedValues, err := mergeChartValues(chart, opts.ValuesFiles) mergedValues, err := mergeChartValues(chart, opts.ValuesFiles)
if err != nil { if err != nil {
err = fmt.Errorf("failed to merge chart values: %w", err) err = fmt.Errorf("failed to merge chart values: %w", err)
return nil, &BuildError{Reason: ErrValuesFilesMerge, Err: err} return result, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
} }
// Overwrite default values with merged values, if any // Overwrite default values with merged values, if any
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil { if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {

View File

@ -138,12 +138,32 @@ func TestChartBuildResult_Summary(t *testing.T) {
want string want string
}{ }{
{ {
name: "Simple", name: "Build with metadata",
build: &Build{ build: &Build{
Name: "chart", Name: "chart",
Version: "1.2.3-rc.1+bd6bf40", Version: "1.2.3-rc.1+bd6bf40",
}, },
want: "Pulled 'chart' chart with version '1.2.3-rc.1+bd6bf40'.", want: "New 'chart' chart with version '1.2.3-rc.1+bd6bf40'",
},
{
name: "Pulled chart",
build: &Build{
Name: "chart",
Version: "1.2.3-rc.1+bd6bf40",
Path: "chart.tgz",
},
want: "Pulled 'chart' chart with version '1.2.3-rc.1+bd6bf40'",
},
{
name: "Packaged chart",
build: &Build{
Name: "chart",
Version: "arbitrary-version",
Packaged: true,
ValuesFiles: []string{"a.yaml", "b.yaml"},
Path: "chart.tgz",
},
want: "Packaged 'chart' chart with version 'arbitrary-version' and merged values files [a.yaml b.yaml]",
}, },
{ {
name: "With values files", name: "With values files",
@ -152,28 +172,19 @@ func TestChartBuildResult_Summary(t *testing.T) {
Version: "arbitrary-version", Version: "arbitrary-version",
Packaged: true, Packaged: true,
ValuesFiles: []string{"a.yaml", "b.yaml"}, ValuesFiles: []string{"a.yaml", "b.yaml"},
Path: "chart.tgz",
}, },
want: "Packaged 'chart' chart with version 'arbitrary-version', with merged values files [a.yaml b.yaml].", want: "Packaged 'chart' chart with version 'arbitrary-version' and merged values files [a.yaml b.yaml]",
},
{
name: "With dependencies",
build: &Build{
Name: "chart",
Version: "arbitrary-version",
Packaged: true,
ResolvedDependencies: 5,
},
want: "Packaged 'chart' chart with version 'arbitrary-version', resolving 5 dependencies before packaging.",
}, },
{ {
name: "Empty build", name: "Empty build",
build: &Build{}, build: &Build{},
want: "No chart build.", want: "No chart build",
}, },
{ {
name: "Nil build", name: "Nil build",
build: nil, build: nil,
want: "No chart build.", want: "No chart build",
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -22,22 +22,29 @@ import (
) )
// BuildErrorReason is the descriptive reason for a BuildError. // BuildErrorReason is the descriptive reason for a BuildError.
type BuildErrorReason string type BuildErrorReason struct {
// Reason is the programmatic build error reason in CamelCase.
Reason string
// Summary is the human build error reason, used to provide
// the Error string, and further context to the BuildError.
Summary string
}
// Error returns the string representation of BuildErrorReason. // Error returns the string representation of BuildErrorReason.
func (e BuildErrorReason) Error() string { func (e BuildErrorReason) Error() string {
return string(e) return e.Summary
} }
// BuildError contains a wrapped Err and a Reason indicating why it occurred. // BuildError contains a wrapped Err and a Reason indicating why it occurred.
type BuildError struct { type BuildError struct {
Reason error Reason BuildErrorReason
Err error Err error
} }
// Error returns Err as a string, prefixed with the Reason to provide context. // Error returns Err as a string, prefixed with the Reason to provide context.
func (e *BuildError) Error() string { func (e *BuildError) Error() string {
if e.Reason == nil { if e.Reason.Error() == "" {
return e.Err.Error() return e.Err.Error()
} }
return fmt.Sprintf("%s: %s", e.Reason.Error(), e.Err.Error()) return fmt.Sprintf("%s: %s", e.Reason.Error(), e.Err.Error())
@ -49,7 +56,7 @@ func (e *BuildError) Error() string {
// err := &BuildError{Reason: ErrChartPull, Err: errors.New("arbitrary transport error")} // err := &BuildError{Reason: ErrChartPull, Err: errors.New("arbitrary transport error")}
// errors.Is(err, ErrChartPull) // errors.Is(err, ErrChartPull)
func (e *BuildError) Is(target error) bool { func (e *BuildError) Is(target error) bool {
if e.Reason != nil && e.Reason == target { if e.Reason == target {
return true return true
} }
return errors.Is(e.Err, target) return errors.Is(e.Err, target)
@ -60,11 +67,21 @@ func (e *BuildError) Unwrap() error {
return e.Err return e.Err
} }
func IsPersistentBuildErrorReason(err error) bool {
switch err {
case ErrChartReference, ErrChartMetadataPatch, ErrValuesFilesMerge:
return true
default:
return false
}
}
var ( var (
ErrChartReference = BuildErrorReason("chart reference error") ErrChartReference = BuildErrorReason{Reason: "InvalidChartReference", Summary: "invalid chart reference"}
ErrChartPull = BuildErrorReason("chart pull error") ErrChartPull = BuildErrorReason{Reason: "ChartPullError", Summary: "chart pull error"}
ErrChartMetadataPatch = BuildErrorReason("chart metadata patch error") ErrChartMetadataPatch = BuildErrorReason{Reason: "MetadataPatchError", Summary: "chart metadata patch error"}
ErrValuesFilesMerge = BuildErrorReason("values files merge error") ErrValuesFilesMerge = BuildErrorReason{Reason: "ValuesFilesError", Summary: "values files merge error"}
ErrDependencyBuild = BuildErrorReason("dependency build error") ErrDependencyBuild = BuildErrorReason{Reason: "DependencyBuildError", Summary: "dependency build error"}
ErrChartPackage = BuildErrorReason("chart package error") ErrChartPackage = BuildErrorReason{Reason: "ChartPackageError", Summary: "chart package error"}
ErrUnknown = BuildErrorReason{Reason: "Unknown", Summary: "unknown build error"}
) )

View File

@ -26,7 +26,7 @@ import (
func TestBuildErrorReason_Error(t *testing.T) { func TestBuildErrorReason_Error(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
err := BuildErrorReason("reason") err := BuildErrorReason{"Reason", "reason"}
g.Expect(err.Error()).To(Equal("reason")) g.Expect(err.Error()).To(Equal("reason"))
} }
@ -39,7 +39,7 @@ func TestBuildError_Error(t *testing.T) {
{ {
name: "with reason", name: "with reason",
err: &BuildError{ err: &BuildError{
Reason: BuildErrorReason("reason"), Reason: BuildErrorReason{"Reason", "reason"},
Err: errors.New("error"), Err: errors.New("error"),
}, },
want: "reason: error", want: "reason: error",

11
main.go
View File

@ -189,12 +189,11 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if err = (&controllers.HelmChartReconciler{ if err = (&controllers.HelmChartReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Storage: storage,
Storage: storage, Getters: getters,
Getters: getters, EventRecorder: eventRecorder,
EventRecorder: eventRecorder, Metrics: metricsH,
MetricsRecorder: metricsH.MetricsRecorder,
}).SetupWithManagerAndOptions(mgr, controllers.HelmChartReconcilerOptions{ }).SetupWithManagerAndOptions(mgr, controllers.HelmChartReconcilerOptions{
MaxConcurrentReconciles: concurrent, MaxConcurrentReconciles: concurrent,
}); err != nil { }); err != nil {