Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api/v1/helmchart_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ type HelmChartSpec struct {
// Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.
// +optional
Verify *OCIRepositoryVerification `json:"verify,omitempty"`

// MinAge is the minimum age a chart version must have before it is
// published as an artifact. This can be used to reduce exposure to
// supply-chain attacks by ensuring a newly published chart version has
// had time for community scrutiny before being deployed.
// The age is computed from the chart's creation timestamp in the Helm
// repository index. When the creation timestamp is unavailable (e.g.
// OCI registries without annotations), the field is ignored.
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
// +optional
MinAge *metav1.Duration `json:"minAge,omitempty"`
}

const (
Expand Down Expand Up @@ -163,6 +175,10 @@ const (
// ChartPackageSucceededReason signals that the package of the Helm
// chart succeeded.
ChartPackageSucceededReason string = "ChartPackageSucceeded"

// ChartVersionTooNewReason signals that the resolved chart version does
// not yet satisfy the MinAge requirement.
ChartVersionTooNewReason string = "ChartVersionTooNew"
)

// GetConditions returns the status conditions of the object.
Expand Down
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ spec:
efficient use of resources.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
minAge:
description: |-
MinAge is the minimum age a chart version must have before it is
published as an artifact. This can be used to reduce exposure to
supply-chain attacks by ensuring a newly published chart version has
had time for community scrutiny before being deployed.
The age is computed from the chart's creation timestamp in the Helm
repository index. When the creation timestamp is unavailable (e.g.
OCI registries without annotations), the field is ignored.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
reconcileStrategy:
default: ChartVersion
description: |-
Expand Down
72 changes: 72 additions & 0 deletions docs/spec/v1/helmcharts.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ spec:
kind: HelmRepository
name: podinfo
version: '5.*'
minAge: 168h # only promote chart versions published at least 7 days ago
```

In the above example:
Expand Down Expand Up @@ -179,6 +180,48 @@ the latest version of the chart with value `*`.
Version can be a fixed semver, minor or patch semver range of a specific
version (i.e. `4.0.x`) or any semver range (i.e. `>=4.0.0 <5.0.0`).

### Minimum age

`.spec.minAge` is an optional field to specify the minimum age a chart version
must have been published before it is promoted as an Artifact. The value must be
in a [Go recognized duration string format](https://pkg.go.dev/time#ParseDuration),
e.g. `168h` (7 days) or `24h30m`.

When set, the controller compares the chart version's publish timestamp (as
recorded in the Helm repository index) against the current time. If the
elapsed time is less than `minAge`, the Artifact is not published and the
controller requeues the object for the exact remaining duration, so it is
promoted as soon as the requirement is met.

This is useful as a supply chain security measure: delaying the automatic
promotion of a new chart version gives time to detect and respond to a
compromised or malicious release before it reaches your clusters.

```yaml
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: podinfo
namespace: default
spec:
interval: 1h
chart: podinfo
version: ">=6.0.0"
sourceRef:
kind: HelmRepository
name: podinfo
minAge: 168h # only promote chart versions that are at least 7 days old
```

**Note:** `minAge` only applies when the source reference is a `HelmRepository`.
For `GitRepository` and `Bucket` sources the publish timestamp is not available
and the field is ignored.

When a chart version does not yet meet the minimum age requirement, the
controller sets a Condition on the HelmChart — see
[ChartVersionTooNew](#chartversiontoonew) for details.

### Values files

`.spec.valuesFiles` is an optional field to specify an alternative list of
Expand Down Expand Up @@ -812,6 +855,35 @@ configuration issue in the HelmChart spec. When a reconciliation fails, the
reconciliation is performed again after the failure, the reason is updated to
`Progressing`.

#### ChartVersionTooNew

When [`.spec.minAge`](#minimum-age) is set and the resolved chart version has
not yet been published long enough, the controller marks the Artifact as not
ready and sets a Condition with the following attributes in the HelmChart's
`.status.conditions`:

- `type: ArtifactInStorage`
- `status: "False"`
- `reason: ChartVersionTooNew`

The `message` field reports the current age of the chart version and the
configured minimum, for example:

```
chart version 6.12.0 was published 2h30m0s ago, waiting for minimum age of 168h0m0s
```

The controller requeues the object for exactly the remaining duration, so no
manual intervention is required. Once the minimum age is reached the object
reconciles normally and the Artifact is published.

An Event with reason `ChartVersionTooNew` is also emitted:

```console
LAST SEEN TYPE REASON OBJECT MESSAGE
0s Normal ChartVersionTooNew helmchart/podinfo chart version 6.12.0 does not meet minimum age requirement (age: 2h30m0s, minimum: 168h0m0s)
```

#### Stalled HelmChart

The source-controller can mark a HelmChart as _stalled_ when it determines that
Expand Down
27 changes: 27 additions & 0 deletions internal/controller/helmchart_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ func (r *HelmChartReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
reconcilers := []helmChartReconcileFunc{
r.reconcileStorage,
r.reconcileSource,
r.reconcileMinAge,
r.reconcileArtifact,
}
recResult, retErr = r.reconcile(ctx, serialPatcher, obj, reconcilers)
Expand Down Expand Up @@ -817,6 +818,32 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj
return sreconcile.ResultSuccess, nil
}

// reconcileMinAge checks whether the resolved chart version satisfies the
// MinAge requirement on the object. If the chart was published less than
// MinAge ago (using the creation timestamp from the Helm repository index),
// it sets ArtifactInStorageCondition=False and returns a Waiting error so
// that reconcileArtifact is skipped until the age requirement is met.
func (r *HelmChartReconciler) reconcileMinAge(_ context.Context, _ *patch.SerialPatcher, obj *sourcev1.HelmChart, b *chart.Build) (sreconcile.Result, error) {
if obj.Spec.MinAge == nil || b.CreatedAt.IsZero() {
return sreconcile.ResultSuccess, nil
}
elapsed := time.Since(b.CreatedAt)
if elapsed >= obj.Spec.MinAge.Duration {
return sreconcile.ResultSuccess, nil
}
remaining := obj.Spec.MinAge.Duration - elapsed
conditions.MarkFalse(obj, sourcev1.ArtifactInStorageCondition, sourcev1.ChartVersionTooNewReason,
"chart version %s was published %s ago, waiting for minimum age of %s",
b.Version, elapsed.Truncate(time.Second), obj.Spec.MinAge.Duration)
e := serror.NewWaiting(
fmt.Errorf("chart version %s does not meet minimum age requirement (age: %s, minimum: %s)",
b.Version, elapsed.Truncate(time.Second), obj.Spec.MinAge.Duration),
sourcev1.ChartVersionTooNewReason,
)
e.RequeueAfter = remaining
return sreconcile.ResultEmpty, e
}

// reconcileArtifact archives a new Artifact to the Storage, if the current
// (Status) data on the object does not match the given.
//
Expand Down
6 changes: 6 additions & 0 deletions internal/helm/chart/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"path/filepath"
"regexp"
"strings"
"time"

sourcefs "github.com/fluxcd/pkg/oci"
helmchart "helm.sh/helm/v4/pkg/chart/v2"
Expand Down Expand Up @@ -156,6 +157,11 @@ type Build struct {
// VerifiedResult indicates the results of verifying the chart.
// If no verification was performed, this field should be VerificationResultIgnored.
VerifiedResult oci.VerificationResult
// CreatedAt is the timestamp at which the chart version was published in
// the source repository, as reported by the Helm repository index.
// It is zero when the source does not provide a creation timestamp
// (e.g. OCI registries without creation annotations).
CreatedAt time.Time
}

// Summary returns a human-readable summary of the Build.
Expand Down
1 change: 1 addition & 0 deletions internal/helm/chart/builder_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ func generateBuildResult(cv *repo.ChartVersion, opts BuildOptions) (*Build, bool
result.Version = cv.Version
result.Name = cv.Name
result.VerifiedResult = oci.VerificationResultIgnored
result.CreatedAt = cv.Created

// Set build specific metadata if instructed
if opts.VersionMetadata != "" {
Expand Down