Open jmazzitelli opened 3 years ago
Here's a description of the real-world problem that I am having that is caused by this k8s issue: https://github.com/kiali/kiali-operator/pull/348#issuecomment-873191750
@jmazzitelli thanks for taking the time to report and fully described the issue, I will work on this.
@jmazzitelli I just check how this will work using kubectl
and it seems working as ansible does
Namespace yaml definition
apiVersion: v1
kind: Namespace
metadata:
name: kubectl-ns
labels:
one: "value1-A"
two: "value2"
three: null
create namespace using kubectl create -f ./namespace.yaml --validate=false
and describe it
apiVersion: v1
kind: Namespace
metadata:
creationTimestamp: "2021-07-06T16:59:45Z"
labels:
kubernetes.io/metadata.name: kubectl-ns
one: value1-A
three: ""
two: value2
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
(...)
Then when running apply command with the same definition the namespace is updated as on ansible
apiVersion: v1
kind: Namespace
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{},"labels":{"one":"value1-A","three":null,"two":"value2"},"name":"kubectl-ns"}}
creationTimestamp: "2021-07-06T16:59:45Z"
labels:
kubernetes.io/metadata.name: kubectl-ns
one: value1-A
two: value2
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
(...)
Regardless of the behavior of kubectl, I would think a simple solution would be for the k8s task to remove any fields with a value of "null" if it knows it is going to create a resource (I assume k8s knows if it is going to create a new resource as opposed to updating an existing one??). In this way, ansible will work better than kubectl :)
Just to shed some more light on this. The null
(or None
by the time the yaml gets loaded into python) value is being passed to the K8S api, but converting that to an empty string is just how the api works as best I can tell. Fixing this is a bit complicated because null
values are specifically allowed in CRDs. If we just removed null
fields we'd break CRDs. I think CRDs are the only resource that allows nullable fields, but I'm honestly not sure. I see a few possible solutions, but feel free to suggest others:
null
fields on everything except CRDs. This assumes we can determine if a resources is a CRD. Can't think of how to do this off the top of my head, but it's likely possible. We'd need to confirm first that only CRDs allow nullable fields.validate
param.BTW: to me this is a result of having k8s
support this state
field with "present". Granted, the nice thing with that feature is that the ansible developer is not required to know (well, is not "supposed to be" required to know) if the resource exists or not -- k8s is going to create a new object or just patch an existing one. The burden is lifted off the developer. But, the dev needs to prepare the yaml so it can support either create or update. But in this case, I can't find out how to do that. There is no way for me to give a yaml to k8s that supports both creating a new resource AND updating an existing one because of the different treatment of the "null" fields. I fear I am almost forced to do something like this in my ansible template:
{% if ...lookup to see if resource exists... %}
some yaml that supports updates
{% else %}
some other yaml that supports creates
{% endif %}
ewwww... if someone can propose another workaround that is less hacky, please let me know.
FWIW: here's what I am going to have to do in my ansible template in order to support upgrades in my ansible operator where a previous version of the operand's service had one set of selector fields but a new version of the operand service has a new set of selector fields -- the old ones, if they exist, need to be removed. See the {% if... endif %}
, from https://github.com/kiali/kiali-operator/pull/348/files
apiVersion: v1
kind: Service
metadata:
name: {{ kiali_vars.deployment.instance_name }}
namespace: {{ kiali_vars.deployment.namespace }}
...
spec:
...
selector:
{% if query('k8s', kind='Service', resource_name=kiali_vars.deployment.instance_name, namespace=kiali_vars.deployment.namespace) | length > 0 %}
app: null
version: null
{% endif %}
app.kubernetes.io/name: kiali
app.kubernetes.io/instance: {{ kiali_vars.deployment.instance_name }}
I believe I'm running into the same problem. With this task:
- kubernetes.core.k8s:
state: present
definition:
kind: ResourceQuota
apiVersion: v1
metadata:
name: compute-resources
namespace: deleteme
spec:
hard:
limits.cpu: null
Before running it, no quotas are set
$ kubectl describe quota
No resources found in deleteme namespace.
After running it the first time, the property is created with a value of 0:
$ kubectl describe quota
Name: compute-resources
Namespace: deleteme
Resource Used Hard
-------- ---- ----
limits.cpu 0 0
After running it again, the value is removed:
$ kubectl describe quota
Name: compute-resources
Namespace: deleteme
Resource Used Hard
-------- ---- ----
So it's not idempotent.
... and as @abikouo mentioned, this is the same behaviour as kubectl:
$ kubectl describe quota
No resources found in deleteme namespace.
$ kubectl apply -f - <<EOF
kind: ResourceQuota
apiVersion: v1
metadata:
name: compute-resources
namespace: deleteme
spec:
hard:
limits.cpu: null
EOF
resourcequota/compute-resources created
$ kubectl describe quota
Name: compute-resources
Namespace: deleteme
Resource Used Hard
-------- ---- ----
limits.cpu 0 0
$ kubectl apply -f - <<EOF
kind: ResourceQuota
apiVersion: v1
metadata:
name: compute-resources
namespace: deleteme
spec:
hard:
limits.cpu: null
EOF
resourcequota/compute-resources configured
$ kubectl describe quota
Name: compute-resources
Namespace: deleteme
Resource Used Hard
-------- ---- ----
So I guess it's a k8s API bug?
In my case since I'm setting limits.cpu
directly in an Ansible task, I had to replace my task with something like this:
- kubernetes.core.k8s:
state: present
definition:
kind: ResourceQuota
apiVersion: v1
metadata:
name: compute-resources
namespace: "{{ k8s_namespace }}"
spec:
hard: "{{ spec_hard }}"
vars:
_spec_hard:
# Other quotas set as needed here
spec_hard: "{{ _spec_hard | combine({'limits.cpu': None if (query('k8s', kind='ResourceQuota', resource_name='compute-resources', namespace=k8s_namespace ) | length > 0) else omit}) }}"
SUMMARY
A field (such as a metadata label, or service selector) may or may not exist on a resource - I want to make sure that field does NOT exist (i.e. the field should be removed if it does exist; if it does not exist, do nothing).
A
null
value should tellk8s
to remove the field if it exists but do nothing if it does not exist. If a resource itself does not exist, you would expect the behavior to be the same as if the field does not already exist (hence, do nothing).However, if
k8s
is creating the resource, anull
value is translated to an empty string ("") and the field is erroneously created. This does not happen if the resource exists -k8s
behaves correctly in this case.ISSUE TYPE
COMPONENT NAME
k8s
ANSIBLE VERSION
COLLECTION VERSION
CONFIGURATION
OS / ENVIRONMENT
RHEL 8.4
STEPS TO REPRODUCE
ansible-test.yaml:
EXPECTED RESULTS
When a field is set to
null
in thek8s
task config, that means that field should not exist on the resource (this is true regardless if the resource exists and is being updated, or if the resource does not already exist andk8s
will create it).For any field whose value is specified as
null
in thek8s
task config, that field should be removed from an existing resource. If the field does NOT exist on the resource (or the resource itself does not exist), that field should not be created.ACTUAL RESULTS
This works UNLESS
k8s
creates the resource. Ifk8s
hasstate: present
and the resource does not exist, it will create that resource and any field set tonull
will be created and its value will be set to empty string (""
). If, however, the resource already does exist (i.e.k8s
will not create the resource but will instead update/patch it) then it behaves as expected - if the field exists, it is removed; if the field does not exist,k8s
does nothing.So, in short,
k8s
behaves differently if the resource needs to be created versus updated. If a field is set tonull
andk8s
creates the resource, it will create the field with empty string (""
). This is unexpected.