Merge pull request #140 from fluxcd/chart-values-overwrite

This commit is contained in:
Hidde Beydals 2020-09-11 13:46:02 +02:00 committed by GitHub
commit a41e58e697
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 256 additions and 55 deletions

View File

@ -57,7 +57,13 @@ jobs:
kubectl -n source-system wait gitrepository/gitrepository-sample --for=condition=ready --timeout=1m
kubectl -n source-system wait helmrepository/helmrepository-sample --for=condition=ready --timeout=1m
kubectl -n source-system wait helmchart/helmchart-sample --for=condition=ready --timeout=1m
kubectl -n source-system logs deploy/source-controller
kubectl -n source-system delete -f ./config/samples
- name: Run HelmChart values file tests
run: |
kubectl -n source-system apply -f ./config/testdata/helmchart-valuesfile
kubectl -n source-system wait helmchart/mariadb --for=condition=ready --timeout=5m
kubectl -n source-system wait helmchart/mariadb-git --for=condition=ready --timeout=5m
kubectl -n source-system delete -f ./config/testdata/helmchart-valuesfile
- name: Debug failure
if: failure()
run: |

View File

@ -41,6 +41,11 @@ type HelmChartSpec struct {
// The interval at which to check the Source for updates.
// +required
Interval metav1.Duration `json:"interval"`
// Alternative values file to use as the default chart values, expected to be
// a relative path in the SourceRef. Ignored when omitted.
// +optional
ValuesFile string `json:"valuesFile,omitempty"`
}
// LocalHelmChartSourceReference contains enough information to let you locate the

View File

@ -85,6 +85,10 @@ spec:
- kind
- name
type: object
valuesFile:
description: Alternative values file to use as the default chart values,
expected to be a relative path in the SourceRef. Ignored when omitted.
type: string
version:
description: The chart version semver expression, ignored for charts
from GitRepository sources. Defaults to latest when omitted.

View File

@ -4,7 +4,6 @@ metadata:
name: helmchart-git-sample
spec:
chart: charts/podinfo
version: '^2.0.0'
sourceRef:
kind: GitRepository
name: gitrepository-sample

View File

@ -0,0 +1,12 @@
apiVersion: source.toolkit.fluxcd.io/v1alpha1
kind: GitRepository
metadata:
name: bitnami-charts
spec:
interval: 1m
url: https://github.com/bitnami/charts
ref:
branch: master
ignore:
/*
!/bitnami/

View File

@ -0,0 +1,11 @@
apiVersion: source.toolkit.fluxcd.io/v1alpha1
kind: HelmChart
metadata:
name: mariadb-git
spec:
chart: bitnami/mariadb
valuesFile: values-production.yaml
sourceRef:
kind: GitRepository
name: bitnami-charts
interval: 1m

View File

@ -0,0 +1,11 @@
apiVersion: source.toolkit.fluxcd.io/v1alpha1
kind: HelmChart
metadata:
name: mariadb
spec:
chart: mariadb
valuesFile: values-production.yaml
sourceRef:
kind: HelmRepository
name: bitnami-charts
interval: 1m

View File

@ -0,0 +1,7 @@
apiVersion: source.toolkit.fluxcd.io/v1alpha1
kind: HelmRepository
metadata:
name: bitnami-charts
spec:
interval: 1m
url: https://charts.bitnami.com/bitnami

View File

@ -104,8 +104,9 @@ func (r *HelmChartReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
}
// set initial status
if resetChart, ok := r.resetStatus(chart); ok {
// Conditionally set progressing condition in status
resetChart, changed := r.resetStatus(chart)
if changed {
chart = resetChart
if err := r.Status().Update(ctx, &chart); err != nil {
log.Error(err, "unable to update status")
@ -113,11 +114,12 @@ func (r *HelmChartReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
}
// purge old artifacts from storage
// Purge all but current artifact from storage
if err := r.gc(chart, false); err != nil {
log.Error(err, "unable to purge old artifacts")
}
// Perform the reconciliation for the chart source type
var reconciledChart sourcev1.HelmChart
var reconcileErr error
switch chart.Spec.SourceRef.Kind {
@ -130,7 +132,7 @@ func (r *HelmChartReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
return ctrl.Result{Requeue: true}, err
}
reconciledChart, reconcileErr = r.reconcileFromHelmRepository(ctx, repository, *chart.DeepCopy())
reconciledChart, reconcileErr = r.reconcileFromHelmRepository(ctx, repository, *chart.DeepCopy(), changed)
case sourcev1.GitRepositoryKind:
repository, err := r.getGitRepositoryWithArtifact(ctx, chart)
if err != nil {
@ -140,25 +142,25 @@ func (r *HelmChartReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
return ctrl.Result{Requeue: true}, err
}
reconciledChart, reconcileErr = r.reconcileFromGitRepository(ctx, repository, *chart.DeepCopy())
reconciledChart, reconcileErr = r.reconcileFromGitRepository(ctx, repository, *chart.DeepCopy(), changed)
default:
err := fmt.Errorf("unable to reconcile unsupported source reference kind '%s'", chart.Spec.SourceRef.Kind)
return ctrl.Result{}, err
}
// update status with the reconciliation result
// Update status with the reconciliation result
if err := r.Status().Update(ctx, &reconciledChart); err != nil {
log.Error(err, "unable to update status")
return ctrl.Result{Requeue: true}, err
}
// if reconciliation failed, record the failure and requeue immediately
// If reconciliation failed, record the failure and requeue immediately
if reconcileErr != nil {
r.event(reconciledChart, recorder.EventSeverityError, reconcileErr.Error())
return ctrl.Result{Requeue: true}, reconcileErr
}
// emit revision change event
// Emit an event if we did not have an artifact before, or the revision has changed
if chart.Status.Artifact == nil || reconciledChart.Status.Artifact.Revision != chart.Status.Artifact.Revision {
r.event(reconciledChart, recorder.EventSeverityInfo, sourcev1.HelmChartReadyMessage(reconciledChart))
}
@ -167,7 +169,6 @@ func (r *HelmChartReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
time.Now().Sub(start).String(),
chart.GetInterval().Duration.String(),
))
return ctrl.Result{RequeueAfter: chart.GetInterval().Duration}, nil
}
@ -188,16 +189,17 @@ func (r *HelmChartReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts
}
func (r *HelmChartReconciler) reconcileFromHelmRepository(ctx context.Context,
repository sourcev1.HelmRepository, chart sourcev1.HelmChart) (sourcev1.HelmChart, error) {
repository sourcev1.HelmRepository, chart sourcev1.HelmChart, force bool) (sourcev1.HelmChart, error) {
cv, err := helm.GetDownloadableChartVersionFromIndex(r.Storage.LocalPath(*repository.GetArtifact()),
chart.Spec.Chart, chart.Spec.Version)
if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
}
// return early on unchanged chart version
artifact := r.Storage.NewArtifactFor(chart.Kind, chart.GetObjectMeta(), cv.Version, fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version))
if repository.GetArtifact() != nil && repository.GetArtifact().Revision == cv.Version {
// Return early if the revision is still the same as the current artifact
artifact := r.Storage.NewArtifactFor(chart.Kind, chart.GetObjectMeta(), cv.Version,
fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version))
if !force && repository.GetArtifact() != nil && repository.GetArtifact().Revision == cv.Version {
if artifact.URL != repository.GetArtifact().URL {
r.Storage.SetArtifactURL(repository.GetArtifact())
repository.Status.URL = r.Storage.SetHostname(repository.Status.URL)
@ -228,6 +230,7 @@ func (r *HelmChartReconciler) reconcileFromHelmRepository(ctx context.Context,
u.RawQuery = q.Encode()
}
// Get the getter for the protocol
c, err := r.Getters.ByScheme(u.Scheme)
if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
@ -258,21 +261,14 @@ func (r *HelmChartReconciler) reconcileFromHelmRepository(ctx context.Context,
clientOpts = opts
}
// TODO(hidde): implement timeout from the HelmRepository
// https://github.com/helm/helm/pull/7950
res, err := c.Get(u.String(), clientOpts...)
if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
}
// create artifact dir
// Ensure artifact directory exists
err = r.Storage.MkdirAll(artifact)
if err != nil {
err = fmt.Errorf("unable to create chart directory: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
// acquire lock
// Acquire a lock for the artifact
unlock, err := r.Storage.Lock(artifact)
if err != nil {
err = fmt.Errorf("unable to acquire lock: %w", err)
@ -280,24 +276,84 @@ func (r *HelmChartReconciler) reconcileFromHelmRepository(ctx context.Context,
}
defer unlock()
// save artifact to storage
if err := r.Storage.AtomicWriteFile(&artifact, res, 0644); err != nil {
err = fmt.Errorf("unable to write chart file: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
// TODO(hidde): implement timeout from the HelmRepository
// https://github.com/helm/helm/pull/7950
res, err := c.Get(u.String(), clientOpts...)
if err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
}
// update symlink
// Either repackage the chart with the declared default values file,
// or write the chart directly to storage.
var (
readyReason = sourcev1.ChartPullSucceededReason
readyMessage = fmt.Sprintf("Fetched revision: %s", artifact.Revision)
)
switch {
case chart.Spec.ValuesFile != "" && chart.Spec.ValuesFile != chartutil.ValuesfileName:
// Create temporary working directory
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s-%s-", chart.Namespace, chart.Name))
if err != nil {
err = fmt.Errorf("tmp dir error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
defer os.RemoveAll(tmpDir)
// Untar chart into working directory
if _, err = untar.Untar(res, tmpDir); err != nil {
err = fmt.Errorf("chart untar error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
// Overwrite values file
chartPath := path.Join(tmpDir, cv.Name)
if err := helm.OverwriteChartDefaultValues(chartPath, chart.Spec.ValuesFile); err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err
}
// Package the chart with the new default values
pkg := action.NewPackage()
pkg.Destination = tmpDir
pkgPath, err := pkg.Run(chartPath, nil)
if err != nil {
err = fmt.Errorf("chart package error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err
}
// Copy the packaged chart to the artifact path
cf, err := os.Open(pkgPath)
if err != nil {
err = fmt.Errorf("failed to open chart package: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
if err := r.Storage.Copy(&artifact, cf); err != nil {
cf.Close()
err = fmt.Errorf("failed to copy chart package to storage: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
cf.Close()
readyMessage = fmt.Sprintf("Fetched and packaged revision: %s", artifact.Revision)
readyReason = sourcev1.ChartPackageSucceededReason
default:
// Write artifact to storage
if err := r.Storage.AtomicWriteFile(&artifact, res, 0644); err != nil {
err = fmt.Errorf("unable to write chart file: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
}
// Update symlink
chartUrl, err := r.Storage.Symlink(artifact, fmt.Sprintf("%s-latest.tgz", cv.Name))
if err != nil {
err = fmt.Errorf("storage error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
message := fmt.Sprintf("Fetched revision: %s", artifact.Revision)
return sourcev1.HelmChartReady(chart, artifact, chartUrl, sourcev1.ChartPullSucceededReason, message), nil
return sourcev1.HelmChartReady(chart, artifact, chartUrl, readyReason, readyMessage), nil
}
// getChartRepositoryWithArtifact attempts to get the ChartRepository
// getChartRepositoryWithArtifact attempts to get the v1alpha1.HelmRepository
// for the given chart. It returns an error if the HelmRepository could
// not be retrieved or if does not have an artifact.
func (r *HelmChartReconciler) getChartRepositoryWithArtifact(ctx context.Context, chart sourcev1.HelmChart) (sourcev1.HelmRepository, error) {
@ -318,52 +374,53 @@ func (r *HelmChartReconciler) getChartRepositoryWithArtifact(ctx context.Context
}
if repository.GetArtifact() == nil {
err = fmt.Errorf("no repository index artifact found in HelmRepository '%s'", repository.Name)
err = fmt.Errorf("no repository index artifact found for HelmRepository '%s'", name)
}
return repository, err
}
func (r *HelmChartReconciler) reconcileFromGitRepository(ctx context.Context,
repository sourcev1.GitRepository, chart sourcev1.HelmChart) (sourcev1.HelmChart, error) {
// create tmp dir
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s-%s", chart.Namespace, chart.Name))
repository sourcev1.GitRepository, chart sourcev1.HelmChart, force bool) (sourcev1.HelmChart, error) {
// Create temporary working directory
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("%s-%s-", chart.Namespace, chart.Name))
if err != nil {
err = fmt.Errorf("tmp dir error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
defer os.RemoveAll(tmpDir)
// open file
// Open GitRepository artifact file and untar files into working directory
f, err := os.Open(r.Storage.LocalPath(*repository.GetArtifact()))
if err != nil {
err = fmt.Errorf("artifact open error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
// extract artifact files
if _, err = untar.Untar(f, tmpDir); err != nil {
f.Close()
err = fmt.Errorf("artifact untar error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
f.Close()
// ensure configured path is a chart directory
// Ensure configured path is a chart directory
chartPath := path.Join(tmpDir, chart.Spec.Chart)
if _, err := chartutil.IsChartDir(chartPath); err != nil {
err = fmt.Errorf("chart path error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
// read chart metadata
// Read the chart metadata
chartMetadata, err := chartutil.LoadChartfile(path.Join(chartPath, chartutil.ChartfileName))
if err != nil {
err = fmt.Errorf("load chart metadata error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
// return early on unchanged chart version
artifact := r.Storage.NewArtifactFor(chart.Kind, chart.ObjectMeta.GetObjectMeta(), chartMetadata.Version, fmt.Sprintf("%s-%s.tgz", chartMetadata.Name, chartMetadata.Version))
if chart.GetArtifact() != nil && chart.GetArtifact().Revision == chartMetadata.Version {
// Return early if the revision is still the same as the current chart artifact
artifact := r.Storage.NewArtifactFor(chart.Kind, chart.ObjectMeta.GetObjectMeta(), chartMetadata.Version,
fmt.Sprintf("%s-%s.tgz", chartMetadata.Name, chartMetadata.Version))
if !force && chart.GetArtifact() != nil && chart.GetArtifact().Revision == chartMetadata.Version {
if artifact.URL != repository.GetArtifact().URL {
r.Storage.SetArtifactURL(repository.GetArtifact())
repository.Status.URL = r.Storage.SetHostname(repository.Status.URL)
@ -371,14 +428,21 @@ func (r *HelmChartReconciler) reconcileFromGitRepository(ctx context.Context,
return chart, nil
}
// create artifact dir
// Overwrite default values if instructed to
if chart.Spec.ValuesFile != "" {
if err := helm.OverwriteChartDefaultValues(chartPath, chart.Spec.ValuesFile); err != nil {
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err
}
}
// Ensure artifact directory exists
err = r.Storage.MkdirAll(artifact)
if err != nil {
err = fmt.Errorf("unable to create artifact directory: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
// acquire lock
// Acquire a lock for the artifact
unlock, err := r.Storage.Lock(artifact)
if err != nil {
err = fmt.Errorf("unable to acquire lock: %w", err)
@ -386,17 +450,18 @@ func (r *HelmChartReconciler) reconcileFromGitRepository(ctx context.Context,
}
defer unlock()
// package chart
// Package the chart, we use the action here instead of relying on the
// chartutil.Save method as the action performs a dependency check for us
pkg := action.NewPackage()
pkg.Destination = tmpDir
src, err := pkg.Run(chartPath, nil)
pkgPath, err := pkg.Run(chartPath, nil)
if err != nil {
err = fmt.Errorf("chart package error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err
}
// copy chart package
cf, err := os.Open(src)
// Copy the packaged chart to the artifact path
cf, err := os.Open(pkgPath)
if err != nil {
err = fmt.Errorf("failed to open chart package: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
@ -408,7 +473,7 @@ func (r *HelmChartReconciler) reconcileFromGitRepository(ctx context.Context,
}
cf.Close()
// update symlink
// Update symlink
cUrl, err := r.Storage.Symlink(artifact, fmt.Sprintf("%s-latest.tgz", chartMetadata.Name))
if err != nil {
err = fmt.Errorf("storage error: %w", err)
@ -420,8 +485,8 @@ func (r *HelmChartReconciler) reconcileFromGitRepository(ctx context.Context,
}
// getGitRepositoryWithArtifact attempts to get the GitRepository for the given
// chart. It returns an error if the GitRepository could not be retrieved or
// does not have an artifact.
// chart. It returns an error if the v1alpha1.GitRepository could not be retrieved
// or does not have an artifact.
func (r *HelmChartReconciler) getGitRepositoryWithArtifact(ctx context.Context, chart sourcev1.HelmChart) (sourcev1.GitRepository, error) {
if chart.Spec.SourceRef.Name == "" {
return sourcev1.GitRepository{}, fmt.Errorf("no GitRepository reference given")
@ -449,18 +514,20 @@ func (r *HelmChartReconciler) getGitRepositoryWithArtifact(ctx context.Context,
// resetStatus returns a modified v1alpha1.HelmChart and a boolean indicating
// if the status field has been reset.
func (r *HelmChartReconciler) resetStatus(chart sourcev1.HelmChart) (sourcev1.HelmChart, bool) {
// The artifact does no longer exist
if chart.GetArtifact() != nil && !r.Storage.ArtifactExist(*chart.GetArtifact()) {
chart = sourcev1.HelmChartProgressing(chart)
chart.Status.Artifact = nil
return chart, true
}
// The chart specification has changed
if chart.Generation != chart.Status.ObservedGeneration {
return sourcev1.HelmChartProgressing(chart), true
}
return chart, false
}
// gc performs a garbage collection on all but current artifacts of
// gc performs a garbage collection on all but the current artifact of
// the given chart.
func (r *HelmChartReconciler) gc(chart sourcev1.HelmChart, all bool) error {
if all {
@ -472,7 +539,8 @@ func (r *HelmChartReconciler) gc(chart sourcev1.HelmChart, all bool) error {
return nil
}
// event emits a Kubernetes event and forwards the event to notification controller if configured
// event emits a Kubernetes event and forwards the event to notification
// controller if configured.
func (r *HelmChartReconciler) event(chart sourcev1.HelmChart, severity, msg string) {
if r.EventRecorder != nil {
r.EventRecorder.Eventf(&chart, "Normal", severity, msg)

View File

@ -298,6 +298,19 @@ Kubernetes meta/v1.Duration
<p>The interval at which to check the Source for updates.</p>
</td>
</tr>
<tr>
<td>
<code>valuesFile</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Alternative values file to use as the default chart values, expected to be
a relative path in the SourceRef. Ignored when omitted.</p>
</td>
</tr>
</table>
</td>
</tr>
@ -911,6 +924,19 @@ Kubernetes meta/v1.Duration
<p>The interval at which to check the Source for updates.</p>
</td>
</tr>
<tr>
<td>
<code>valuesFile</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Alternative values file to use as the default chart values, expected to be
a relative path in the SourceRef. Ignored when omitted.</p>
</td>
</tr>
</tbody>
</table>
</div>

52
internal/helm/chart.go Normal file
View File

@ -0,0 +1,52 @@
/*
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"
"os"
"path"
"helm.sh/helm/v3/pkg/chartutil"
)
// OverwriteChartDefaultValues overwrites the chart default values file in the
// given chartPath with the contents of the given valuesFile.
func OverwriteChartDefaultValues(chartPath, valuesFile string) error {
if valuesFile == chartutil.ValuesfileName {
return nil
}
srcPath := path.Join(chartPath, valuesFile)
if f, err := os.Stat(srcPath); os.IsNotExist(err) || !f.Mode().IsRegular() {
return fmt.Errorf("invalid values file path: %s", valuesFile)
}
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("failed to open values file '%s': %w", valuesFile, err)
}
defer src.Close()
t, err := os.OpenFile(path.Join(chartPath, chartutil.ValuesfileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open values file '%s': %w", chartutil.ValuesfileName, err)
}
defer t.Close()
if _, err := io.Copy(t, src); err != nil {
return fmt.Errorf("failed to overwrite default values with '%s': %w", valuesFile, err)
}
return nil
}