build/diff: strip SOPS metadata on non-Secrets

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Assisted-by: GitHub Copilot/GPT-5.3-Codex
This commit is contained in:
Sebastien Tardif 2026-04-27 06:55:13 -07:00
parent 4e78a9d7e0
commit a485d0ec60
11 changed files with 266 additions and 0 deletions

View file

@ -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{

View file

@ -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 {

View file

@ -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
---

View file

@ -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

View file

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./configmap.yaml

View file

@ -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]
---

View file

@ -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

View file

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml

View file

@ -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

View file

@ -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"})
}
}
return nil

View file

@ -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