Skip to content
Merged
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
12 changes: 12 additions & 0 deletions cmd/flux/build_kustomization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,18 @@ spec:
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build helmrelease with sops metadata",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/sops-helmrelease",
resultFile: "./testdata/build-kustomization/sops-helmrelease-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build configmap with sops metadata",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/sops-configmap",
resultFile: "./testdata/build-kustomization/sops-configmap-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}

tmpl := map[string]string{
Expand Down
14 changes: 14 additions & 0 deletions cmd/flux/create_kustomization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ func TestCreateKustomization(t *testing.T) {
args: "create kustomization my-app --path=./deploy --export",
assert: assertError("source is required"),
},
{
// Verify that --decryption-provider and --decryption-secret produce the
// expected Kustomization YAML with a spec.decryption block.
name: "with sops decryption",
args: "create kustomization mysql " +
"--source=GitRepository/apps " +
"--path=./apps " +
"--decryption-provider=sops " +
"--decryption-secret=sops-age " +
"--namespace=flux-system " +
"--interval=1m " +
"--export",
assert: assertGoldenFile("testdata/create_kustomization/with-sops-decryption.yaml"),
},
}

for _, tt := range tests {
Expand Down
11 changes: 11 additions & 0 deletions cmd/flux/testdata/build-kustomization/sops-configmap-result.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
data:
api-key: ENC[AES256_GCM,data:abc123,iv:xyz,tag:tag,type:str]
kind: ConfigMap
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: app-config
namespace: default
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
data:
api-key: ENC[AES256_GCM,data:abc123,iv:xyz,tag:tag,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
abc
-----END AGE ENCRYPTED FILE-----
lastmodified: "2023-07-15T00:00:00Z"
mac: ENC[AES256_GCM,data:mac,iv:iv,tag:tag,type:str]
encrypted_regex: ^(data)$
version: 3.7.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./configmap.yaml
19 changes: 19 additions & 0 deletions cmd/flux/testdata/build-kustomization/sops-helmrelease-result.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: mysql
namespace: default
spec:
chart:
spec:
chart: mysql
sourceRef:
kind: HelmRepository
name: bitnami
values:
mysql:
rootPassword: ENC[AES256_GCM,data:abc123,iv:xyz,tag:tag,type:str]
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mysql
namespace: default
spec:
chart:
spec:
chart: mysql
sourceRef:
kind: HelmRepository
name: bitnami
values:
mysql:
rootPassword: ENC[AES256_GCM,data:abc123,iv:xyz,tag:tag,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
abc
-----END AGE ENCRYPTED FILE-----
lastmodified: "2023-07-15T00:00:00Z"
mac: ENC[AES256_GCM,data:mac,iv:iv,tag:tag,type:str]
encrypted_regex: ^(values)$
version: 3.7.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
17 changes: 17 additions & 0 deletions cmd/flux/testdata/create_kustomization/with-sops-decryption.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: mysql
namespace: flux-system
spec:
decryption:
provider: sops
secretRef:
name: sops-age
interval: 1m0s
path: ./apps
prune: false
sourceRef:
kind: GitRepository
name: apps
13 changes: 13 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,19 @@ func maskSopsData(res *resource.Resource) error {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
}
} else {
// For non-Secret resources (e.g. HelmRelease), strip the top-level .sops metadata
// block so it is not persisted in the cluster or exposed in build/diff output.
// The kustomize-controller decrypts these resources before apply when
// spec.decryption.provider is set; the .sops field is not part of the CRD schema
// and would cause a server-side apply dry-run failure if left in place.
asYaml, err := res.AsYAML()
if err != nil {
return fmt.Errorf("failed to read %s %s for sops check: %w", res.GetKind(), res.GetName(), err)
}
if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) {
res.PipeE(yaml.FieldClearer{Name: "sops"})
Comment on lines +751 to +756
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-Secret branch detects SOPS by serializing the whole resource to YAML and doing substring checks. This adds per-resource overhead during build/diff and is also brittle (it depends on YAML rendering details like mac: ENC[ formatting). Consider doing a structured lookup on the root node instead (e.g., check for a top-level sops mapping and optionally sops.mac), then clear the sops field based on that, avoiding AsYAML()/bytes.Contains entirely.

Suggested change
asYaml, err := res.AsYAML()
if err != nil {
return fmt.Errorf("failed to read %s %s for sops check: %w", res.GetKind(), res.GetName(), err)
}
if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) {
res.PipeE(yaml.FieldClearer{Name: "sops"})
sopsNode, err := res.Pipe(yaml.Lookup("sops"))
if err != nil {
return fmt.Errorf("failed to read %s %s sops field: %w", res.GetKind(), res.GetName(), err)
}
if sopsNode != nil && sopsNode.YNode() != nil && sopsNode.YNode().Kind == yaml.MappingNode {
macNode, err := res.Pipe(yaml.Lookup("sops", "mac"))
if err != nil {
return fmt.Errorf("failed to read %s %s sops.mac field: %w", res.GetKind(), res.GetName(), err)
}
if macNode != nil && strings.Contains(yaml.GetValue(macNode), "ENC[") {
if err := res.PipeE(yaml.FieldClearer{Name: "sops"}); err != nil {
return fmt.Errorf("failed to clear %s %s sops field: %w", res.GetKind(), res.GetName(), err)
}
}

Copilot uses AI. Check for mistakes.
}
}

return nil
Expand Down
120 changes: 120 additions & 0 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,126 @@ type: kubernetes.io/dockerconfigjson
}
}

func TestMaskSopsDataNonSecret(t *testing.T) {
testCases := []struct {
name string
yamlStr string
expected string
}{
{
// A SOPS-encrypted HelmRelease (values block encrypted) must have its
// .sops metadata stripped so it is safe for build/diff output and does
// not cause a server-side apply schema error.
name: "HelmRelease with sops metadata",
yamlStr: `apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mysql
namespace: default
spec:
chart:
spec:
chart: mysql
sourceRef:
kind: HelmRepository
name: bitnami
values:
mysql:
rootPassword: ENC[AES256_GCM,data:abc123,iv:xyz,tag:tag,type:str]
replicationPassword: ENC[AES256_GCM,data:def456,iv:xyz,tag:tag,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
abc
-----END AGE ENCRYPTED FILE-----
lastmodified: "2023-07-15T00:00:00Z"
mac: ENC[AES256_GCM,data:mac,iv:iv,tag:tag,type:str]
encrypted_regex: ^(values)$
version: 3.7.3
`,
expected: `apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mysql
namespace: default
spec:
chart:
spec:
chart: mysql
sourceRef:
kind: HelmRepository
name: bitnami
values:
mysql:
replicationPassword: ENC[AES256_GCM,data:def456,iv:xyz,tag:tag,type:str]
rootPassword: ENC[AES256_GCM,data:abc123,iv:xyz,tag:tag,type:str]
`,
},
{
// A HelmRelease without any SOPS metadata must pass through unchanged.
name: "HelmRelease without sops metadata",
yamlStr: `apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: default
spec:
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
values:
replicaCount: 2
`,
expected: `apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: default
spec:
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
values:
replicaCount: 2
`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r, err := yaml.Parse(tc.yamlStr)
if err != nil {
t.Fatalf("unable to parse yaml: %v", err)
}

res := &resource.Resource{RNode: *r}
if err := maskSopsData(res); err != nil {
t.Fatalf("maskSopsData returned unexpected error: %v", err)
}

got, err := res.AsYAML()
if err != nil {
t.Fatalf("unable to convert resource to yaml: %v", err)
}
if diff := cmp.Diff(string(got), tc.expected); diff != "" {
t.Errorf("unexpected output (-got +want):\n%v", diff)
}
})
}
}

func Test_unMarshallKustomization(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading