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
26 changes: 26 additions & 0 deletions api/v2/helmrelease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ type HelmReleaseSpec struct {
// and information about how they should be merged.
ValuesFrom []ValuesReference `json:"valuesFrom,omitempty"`

// Decryption holds decryption provider configuration for values.
// When set, the controller will attempt to decrypt composed values prior
// to performing Helm actions. The most common provider is SOPS.
// +optional
Decryption *Decryption `json:"decryption,omitempty"`

// Values holds the values for this Helm release.
// +optional
Values *apiextensionsv1.JSON `json:"values,omitempty"`
Expand Down Expand Up @@ -226,6 +232,26 @@ type Kustomize struct {
Images []kustomize.Image `json:"images,omitempty"`
}

// Decryption describes how to decrypt values referenced by the HelmRelease.
// The controller will use this configuration to attempt decryption of composed
// values prior to performing Helm actions.
type Decryption struct {
// Provider specifies the decryption provider (for example: "sops").
// +optional
Provider string `json:"provider,omitempty"`

// SecretRef refers to a Kubernetes Secret containing provider credentials
// when applicable (for example a GCP service account JSON for SOPS KMS).
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`

// ServiceAccountName may be used to indicate a ServiceAccount whose
// credentials should be used for provider access. Controller support
// for this field depends on implementation.
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`
}

// CommonMetadata defines the common labels and annotations.
type CommonMetadata struct {
// Annotations to be added to the object's metadata.
Expand Down
25 changes: 25 additions & 0 deletions api/v2/zz_generated.deepcopy.go

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

28 changes: 28 additions & 0 deletions config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,34 @@ spec:
description: Labels to be added to the object's metadata.
type: object
type: object
decryption:
description: |-
Decryption holds decryption provider configuration for values.
When set, the controller will attempt to decrypt composed values prior
to performing Helm actions. The most common provider is SOPS.
properties:
provider:
description: 'Provider specifies the decryption provider (for
example: "sops").'
type: string
secretRef:
description: |-
SecretRef refers to a Kubernetes Secret containing provider credentials
when applicable (for example a GCP service account JSON for SOPS KMS).
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
serviceAccountName:
description: |-
ServiceAccountName may be used to indicate a ServiceAccount whose
credentials should be used for provider access. Controller support
for this field depends on implementation.
type: string
type: object
dependsOn:
description: |-
DependsOn may contain a DependencyReference slice with
Expand Down
96 changes: 96 additions & 0 deletions docs/api/v2/helm.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,22 @@ and information about how they should be merged.</p>
</tr>
<tr>
<td>
<code>decryption</code><br>
<em>
<a href="#helm.toolkit.fluxcd.io/v2.Decryption">
Decryption
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Decryption holds decryption provider configuration for values.
When set, the controller will attempt to decrypt composed values prior
to performing Helm actions. The most common provider is SOPS.</p>
</td>
</tr>
<tr>
<td>
<code>values</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1?tab=doc#JSON">
Expand Down Expand Up @@ -669,6 +685,70 @@ resource object that contains the reference.</p>
</table>
</div>
</div>
<h3 id="helm.toolkit.fluxcd.io/v2.Decryption">Decryption
</h3>
<p>
(<em>Appears on:</em>
<a href="#helm.toolkit.fluxcd.io/v2.HelmReleaseSpec">HelmReleaseSpec</a>)
</p>
<p>Decryption describes how to decrypt values referenced by the HelmRelease.
The controller will use this configuration to attempt decryption of composed
values prior to performing Helm actions.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>provider</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Provider specifies the decryption provider (for example: &ldquo;sops&rdquo;).</p>
</td>
</tr>
<tr>
<td>
<code>secretRef</code><br>
<em>
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>SecretRef refers to a Kubernetes Secret containing provider credentials
when applicable (for example a GCP service account JSON for SOPS KMS).</p>
</td>
</tr>
<tr>
<td>
<code>serviceAccountName</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ServiceAccountName may be used to indicate a ServiceAccount whose
credentials should be used for provider access. Controller support
for this field depends on implementation.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="helm.toolkit.fluxcd.io/v2.DependencyReference">DependencyReference
</h3>
<p>
Expand Down Expand Up @@ -1548,6 +1628,22 @@ and information about how they should be merged.</p>
</tr>
<tr>
<td>
<code>decryption</code><br>
<em>
<a href="#helm.toolkit.fluxcd.io/v2.Decryption">
Decryption
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Decryption holds decryption provider configuration for values.
When set, the controller will attempt to decrypt composed values prior
to performing Helm actions. The most common provider is SOPS.</p>
</td>
</tr>
<tr>
<td>
<code>values</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1?tab=doc#JSON">
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/wI2L/jsondiff v0.7.0
go.uber.org/zap v1.27.1
golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v4 v4.1.3
k8s.io/api v0.35.2
k8s.io/apiextensions-apiserver v0.35.2
Expand Down Expand Up @@ -223,7 +224,6 @@ require (
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.4.0 // indirect
k8s.io/apiserver v0.35.2 // indirect
k8s.io/component-base v0.35.2 // indirect
Expand Down
82 changes: 82 additions & 0 deletions internal/controller/decrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2023 The Flux authors

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 controller

import (
"bytes"
"context"
"fmt"
"os/exec"

"gopkg.in/yaml.v3"
ctrl "sigs.k8s.io/controller-runtime"

v2 "github.com/fluxcd/helm-controller/api/v2"
)

// runSops performs SOPS decryption of the provided YAML bytes.
// Overridable for unit testing.
var runSops = func(ctx context.Context, in []byte) ([]byte, error) {
cmd := exec.CommandContext(ctx, "sops", "--decrypt", "--output-type", "yaml", "-")
cmd.Stdin = bytes.NewReader(in)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("sops decrypt failed: %w: %s", err, string(out))
}
return out, nil
}

// decryptValues decrypts composed Helm values when `obj.Spec.Decryption` is set.
// It marshals `values` to YAML, pipes through `sops --decrypt` and unmarshals the result.
// For production, prefer reusing fluxcd/pkg/kustomize/kustomize-controller decryption logic.
func (r *HelmReleaseReconciler) decryptValues(ctx context.Context, obj *v2.HelmRelease, values map[string]any) (map[string]any, error) {
log := ctrl.LoggerFrom(ctx)

if values == nil {
return nil, fmt.Errorf("values are nil")
}

in, err := yaml.Marshal(values)
if err != nil {
return nil, fmt.Errorf("marshal values: %w", err)
}

out, err := runSops(ctx, in)
if err != nil {
// avoid exposing secret contents
log.Info("sops decryption failed")
return nil, fmt.Errorf("decryption failed: %w", err)
}

var dec map[string]any
if err := yaml.Unmarshal(out, &dec); err != nil {
return nil, fmt.Errorf("unmarshal decrypted values: %w", err)
}

return dec, nil
}

// DecryptValues is an exported wrapper for testing and external usage that
// delegates to the reconciler's decryptValues method.
func DecryptValues(ctx context.Context, r *HelmReleaseReconciler, obj *v2.HelmRelease, values map[string]any) (map[string]any, error) {
return r.decryptValues(ctx, obj, values)
}

// SetSopsRunner allows tests to override the sops runner implementation.
func SetSopsRunner(runner func(ctx context.Context, in []byte) ([]byte, error)) {
runSops = runner
}
44 changes: 44 additions & 0 deletions internal/controller/decrypt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package controller_test

import (
"context"
"reflect"
"testing"

v2 "github.com/fluxcd/helm-controller/api/v2"
ctrlpkg "github.com/fluxcd/helm-controller/internal/controller"
)

func TestDecryptValues_Success(t *testing.T) {
// Override runSops to return a simple decrypted YAML
ctrlpkg.SetSopsRunner(func(ctx context.Context, in []byte) ([]byte, error) {
return []byte("alpha: 1\nbeta:\n nested: xyz\n"), nil
})

r := &ctrlpkg.HelmReleaseReconciler{}
obj := &v2.HelmRelease{
Spec: v2.HelmReleaseSpec{
Decryption: &v2.Decryption{}, // presence is enough for this test
},
}

input := map[string]any{
"alpha": 123,
}

got, err := ctrlpkg.DecryptValues(context.Background(), r, obj, input)
if err != nil {
t.Fatalf("DecryptValues returned error: %v", err)
}

want := map[string]any{
"alpha": int(1),
"beta": map[string]any{
"nested": "xyz",
},
}

if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected decrypted values.\nwant: %#v\ngot: %#v", want, got)
}
}
15 changes: 15 additions & 0 deletions internal/controller/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}

// If decryption configured, decrypt composed values before loading chart.
if obj.Spec.Decryption != nil {
if obj.Spec.Decryption.Provider == "sops" || obj.Spec.Decryption.Provider == "" {
values, err = r.decryptValues(ctx, obj, values)
if err != nil {
conditions.MarkFalse(obj, meta.ReadyCondition, "DecryptionFailed", "%s", err)
r.Eventf(obj, corev1.EventTypeWarning, "DecryptionFailed", err.Error())
return ctrl.Result{}, err
}
} else {
conditions.MarkFalse(obj, meta.ReadyCondition, "DecryptionError", "unsupported provider %s", obj.Spec.Decryption.Provider)
return ctrl.Result{}, fmt.Errorf("unsupported decryption provider %q", obj.Spec.Decryption.Provider)
}
}

// Load chart from artifact.
loadedChart, err := loader.SecureLoadChartFromURL(loader.NewRetryableHTTPClient(ctx, r.ArtifactFetchRetries, r.ArtifactFetchTimeout), source.GetArtifact().URL, source.GetArtifact().Digest)
if err != nil {
Expand Down
Loading