v2.0.4 processing of Secrets not compliant with API and breaks expected behaviour
joaocc opened this issue · comments
As per Secret API spec (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#secret-v1-core):
stringData (...)It is provided as a write-only input field for convenience. All keys and values are merged into the data field on write, overwriting any existing values. The stringData field is never output when reading from the API.
However, it seems the implementation of ca6ec65 (to avoid plan issues) seems to overwrite data
with stringData
if stringData
exists, instead of merging them as per the spec.
This can cause subtle issues as the provider is breaking the expected behaviour from the user point of view.
A suggestion would be to convert this to a merge as per the spec.
This is related to #120 but they are not the same
Testing it now but it looks like I can't reproduce it...
Given initial manifest:
resource "kubectl_manifest" "example" {
yaml_body = <<EOF
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
type: Opaque
stringData:
username: admin # required field for kubernetes.io/basic-auth
password: t0p-Secret # required field for kubernetes.io/basic-auth
EOF
}
We get a new secret created
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
uid: 0449fb6c-6bbe-4a2f-be2d-642dfaa9f163
resourceVersion: '904359108'
creationTimestamp: '2024-04-06T18:42:51Z'
data:
password: dDBwLVNlY3JldA==
username: YWRtaW4=
type: Opaque
Adding a new field manually via kubectl edit
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
uid: 0449fb6c-6bbe-4a2f-be2d-642dfaa9f163
resourceVersion: '904359522'
creationTimestamp: '2024-04-06T18:42:51Z'
labels:
k8slens-edit-resource-version: v1
annotations:
kubectl.kubernetes.io/last-applied-configuration: >
{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"secret-basic-auth","namespace":"default"},"stringData":{"password":"t0p-Secret","username":"admin"},"type":"Opaque"}
managedFields:
- manager: HashiCorp
operation: Update
apiVersion: v1
time: '2024-04-06T18:42:51Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:password: {}
f:username: {}
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/last-applied-configuration: {}
f:type: {}
- manager: node-fetch
operation: Update
apiVersion: v1
time: '2024-04-06T18:43:53Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
f:foo: {}
f:metadata:
f:labels:
.: {}
f:k8slens-edit-resource-version: {}
selfLink: /api/v1/namespaces/default/secrets/secret-basic-auth
data:
foo: YWRtaW4=
password: dDBwLVNlY3JldA==
username: YWRtaW4=
type: Opaque
Running terraform plan
doesn't show any changes.
Changing one of the values
resource "kubectl_manifest" "example" {
yaml_body = <<EOF
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
type: Opaque
stringData:
username: admin1 # required field for kubernetes.io/basic-auth
password: t0p-Secret # required field for kubernetes.io/basic-auth
EOF
}
manual field is still maintained
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
uid: 0449fb6c-6bbe-4a2f-be2d-642dfaa9f163
resourceVersion: '904360377'
creationTimestamp: '2024-04-06T18:42:51Z'
labels:
k8slens-edit-resource-version: v1
annotations:
kubectl.kubernetes.io/last-applied-configuration: >
{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"secret-basic-auth","namespace":"default"},"stringData":{"password":"t0p-Secret","username":"admin1"},"type":"Opaque"}
managedFields:
- manager: node-fetch
operation: Update
apiVersion: v1
time: '2024-04-06T18:43:53Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
f:foo: {}
f:metadata:
f:labels:
.: {}
f:k8slens-edit-resource-version: {}
- manager: HashiCorp
operation: Update
apiVersion: v1
time: '2024-04-06T18:45:26Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:password: {}
f:username: {}
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/last-applied-configuration: {}
f:type: {}
selfLink: /api/v1/namespaces/default/secrets/secret-basic-auth
data:
foo: YWRtaW4=
password: dDBwLVNlY3JldA==
username: YWRtaW4x
type: Opaque
adding a new field in data on manifest
resource "kubectl_manifest" "example" {
yaml_body = <<EOF
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
type: Opaque
data:
bar: YmFyZS1hdXRoCg==
stringData:
username: admin1 # required field for kubernetes.io/basic-auth
password: t0p-Secret # required field for kubernetes.io/basic-auth
EOF
}
All 4 fields are maintained
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
uid: 0449fb6c-6bbe-4a2f-be2d-642dfaa9f163
resourceVersion: '904360882'
creationTimestamp: '2024-04-06T18:42:51Z'
labels:
k8slens-edit-resource-version: v1
annotations:
kubectl.kubernetes.io/last-applied-configuration: >
{"apiVersion":"v1","data":{"bar":"YmFyZS1hdXRoCg=="},"kind":"Secret","metadata":{"annotations":{},"name":"secret-basic-auth","namespace":"default"},"stringData":{"password":"t0p-Secret","username":"admin1"},"type":"Opaque"}
managedFields:
- manager: node-fetch
operation: Update
apiVersion: v1
time: '2024-04-06T18:43:53Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
f:foo: {}
f:metadata:
f:labels:
.: {}
f:k8slens-edit-resource-version: {}
- manager: HashiCorp
operation: Update
apiVersion: v1
time: '2024-04-06T18:46:42Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:bar: {}
f:password: {}
f:username: {}
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/last-applied-configuration: {}
f:type: {}
selfLink: /api/v1/namespaces/default/secrets/secret-basic-auth
data:
bar: YmFyZS1hdXRoCg==
foo: YWRtaW4=
password: dDBwLVNlY3JldA==
username: YWRtaW4x
type: Opaque
Did I miss some scenario?
I believe the issue is with something like this (sorry for the succinctness, but don't have time for much more today) in yaml pseudo-code
data:
field1: in-data-1 # assume base 64 encoded
field2 in-data-2 # assume base 64 encoded
stringData:
field1: in-stringdata-1
field3: in-stringdata-2
As per https://kubernetes.io/docs/concepts/configuration/secret/#restriction-names-data (the second source, in addition to the API link on original post),
The keys of data and stringData must consist of alphanumeric characters, -, _ or .. All key-value pairs in the stringData field are internally merged into the data field. If a key appears in both the data and the stringData field, the value specified in the stringData field takes precedence.
So the resulting equivalent if using only data should be:
data:
field1: in-stringdata-1 # assume base 64 encoded
field2 in-data-2 # assume base 64 encoded
field3: in-stringdata-2
When I looked at the code it seemed this would not be the case, but I may have seen it incorrectly.
On this topic of consistency with the API, while looking for the extra quote above, found another line in kubernetes docs on (https://kubernetes.io/docs/concepts/configuration/secret/#bootstrap-token-secrets)
Note: The stringData field for a Secret does not work well with server-side apply.
To avoid some errors, this should prob be enforced at the level of kube_manifest (either by overriding or aborting if set).
Thanks
It's how it's working now
resource "kubectl_manifest" "example" {
yaml_body = <<EOF
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
type: Opaque
data:
username: bm90YWRtaW4K # base64 encoded string for "notadmin"
stringData:
username: admin # required field for kubernetes.io/basic-auth
password: t0p-Secret # required field for kubernetes.io/basic-auth
EOF
}
resulting manifest
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
namespace: default
uid: 938ccfee-a03d-46b3-930a-73e78f23be06
resourceVersion: '904817565'
creationTimestamp: '2024-04-07T10:48:10Z'
annotations:
kubectl.kubernetes.io/last-applied-configuration: >
{"apiVersion":"v1","data":{"username":"bm90YWRtaW4K"},"kind":"Secret","metadata":{"annotations":{},"name":"secret-basic-auth","namespace":"default"},"stringData":{"password":"t0p-Secret","username":"admin"},"type":"Opaque"}
managedFields:
- manager: HashiCorp
operation: Update
apiVersion: v1
time: '2024-04-07T10:48:10Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:password: {}
f:username: {}
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/last-applied-configuration: {}
f:type: {}
selfLink: /api/v1/namespaces/default/secrets/secret-basic-auth
data:
password: dDBwLVNlY3JldA==
username: YWRtaW4=
type: Opaque
As for the serverside apply, it doesn't matter. As you can see
if stringData, found := userProvided.Raw.Object["stringData"]; found {
// there is an edge case where stringData might be nil and not a map[string]interface{}
// in this case we will just ignore it
if stringData, ok := stringData.(map[string]interface{}); ok {
// move all stringdata values to the data
for k, v := range stringData {
encodedString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v", v)))
meta_v1_unstruct.SetNestedField(userProvided.Raw.Object, encodedString, "data", k)
}
// and unset the stringData entirely
meta_v1_unstruct.RemoveNestedField(userProvided.Raw.Object, "stringData")
}
}
Once I've moved stringdata into data, I remove stringData from the manifest via meta_v1_unstruct.RemoveNestedField(userProvided.Raw.Object, "stringData")
Since it looks like the provider is working correctly, I am going to close this one. Feel free to comment for reopening in case I've missed something
Hi. You are correct. My go understanding was not as good as to let me see the setting of data
. Sorry for that. Thanks