Kubernetes.core.k8s - Issue creating a specific configmap when value is json and contains Jinja2 variable

SUMMARY

When applying a particular ConfigMap where the data is JSON and includes Jinja2 variables I get the following error:
"json: cannot unmarshal object into Go struct field ConfigMap.data of type string, reason:BadRequest, code:400.

The JSON validates when fed to json_pp.
Executing the playbook with ANSIBLE_KEEP_REMOTE_FILES=1 and feeding the args file to json_pp also shows valid.
In the args file all Jinja2 variables are substituted with the expected values.

ISSUE TYPE
  • Bug Report
COMPONENT NAME

kubernetes.core.k8s

ANSIBLE VERSION
  ansible [core 2.14.9]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/kubeadmin/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.9/site-packages/ansible
  ansible collection location = /home/kubeadmin/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.9.18 (main, Sep  7 2023, 00:00:00) [GCC 11.4.1 20230605 (Red Hat 11.4.1-2)] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True
COLLECTION VERSION
Collection      Version
--------------- -------
kubernetes.core 3.0.0
CONFIGURATION
CONFIG_FILE() = /etc/ansible/ansible.cfg
OS / ENVIRONMENT

RHEL 9.2
RKE2 1.25.12

STEPS TO REPRODUCE

I run the following playbook with:
ansible-playbook -vvv -i inventory setup_app1.yaml
The failed message:
ā€œmsgā€: ā€œFailed to create object: b’{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"ConfigMap in version \\"v1\\" cannot be handled as a ConfigMap: json: cannot unmarshal object into Go struct field ConfigMap.data of type string","reason":"BadRequest","code":400}\nā€™ā€

If I add two back slashes ā€˜\’ before the first line in the json file ā€œConnectionStringsā€: I get no error but that line is missing in the mounted file in the container after deployment.
I can use kubectl to create a ConfigMap using the same JSON. The only change I make is substituting Jinja2 variables with their corresponding values.

When running the playbook with ANSIBLE_KEEP_REMOTE_FILES=1, exploding Ansiballz_k8s.py, then inspecting the args file, I can verify the data is valid JSON.
The true/false values in the yaml JSON are not quoted. I suspected they were being interpreted as boolean so I added single quotes in the yaml JSON. The output args file on the target host has double quotes around the true/false values but I still get the same error.

No issues with variable values. This is the playbook with relevant tasks. Some names and details have been changed to protect the innocent

---
- hosts: m1
  become: true

  environment:
    APPSNAMESPACE: "{{ apps_namespace }}"
    APPSCONTEXT: "{{ apps_context }}"

  tasks:
  - name: Create the namespace
    kubernetes.core.k8s:
      name: "{{ apps_namespace }}"
      api_version: v1
      kind: Namespace
      state: present

  - name: Create contexts for my namespace
    ansible.builtin.shell: |
      kubectl config set-context $APPSCONTEXT --namespace=$APPSNAMESPACE --cluster=default  --user=default

  - name: Create configmap
    kubernetes.core.k8s:
      state: present
      namespace: "{{ apps_namespace }}"
      context: "{{ apps_context }}"
      definition:
        kind: ConfigMap
        apiVersion: v1
        metadata:
          name: "{{ server_cm_name }}"
        data:
          appsettings.json: |-
            {
              "ConnectionStrings": {
                "DefaultConnection": "User ID=pguser;Password=randompass;Host={{ db_svc_name }};Port=5432;Database=mydb;Pooling=true;"
              },
              "Logging": {
                "IncludeScopes": false,
                "LogLevel": {
                  "Default": "Warning"
                }
              },

              "SenderSettings": {
                "SendFiles": true,
                "CheckIntervalCronExpression": "0/1 * * * *"
              },
              "RemoteSettings": {
                "Enabled": false,
                "BaseUrl": "https://api.example.com/inwx6/0c8af111-e8d6-1c2a-d91c-50699bcdb1ca/",
                "UserName": "482dc608-ebd7-4989-8035-8f11ea8560c9",
                "Secret": "n06He9W9V7cDg1HMf7eoLKKr3ybbJWx3"
              },
              "OtherAppSettings": {
                "Enabled": true,
                "BaseUrl": "https://{{ app_fqdn }}/apis/default/app/",
                "ClientID": "meRBYfhb4iR02nOoqKOvt-JwZTfX7XhnxCJ5t",
                "UserName": "admin",
                "Secret": "pass"
              }, 
              "AppSsoSettings": {
                "Authority": "https://{{ sso_fqdn }}/auth/app/myappIAM",
                "ClientId": "app-sso",
                "ClientSecret": "904ef4c0-0e5e-4ee5-82ae-a38d2b0de46",
                "CallbackPath": "/Home/Index",
                "IgnoreSslErrors": true,
                "LogOutUri": "https://app.local/auth/app/myappIAM/logout?redirect_uri=https://app.local/app/Core"
              },
              "DeployDirLin": "/home/src/bin/myapp",
              "DeployDirWin": "c:\\dev",
              "AllowedHosts": "*",
              "Servicer": {
                "EndPoints": {
                  "Https": {
                    "Url": "https://0.0.0.0:5150",
                    "Certificate": {
                      "Path": "/etc/pki/tls/certs/mycertpak.pfx",
                      "Password": "randompass"
                    }
                  }
                }
              }
            }

EXPECTED RESULTS

I expect a status of changed when the task is run.

ACTUAL RESULTS

I get a failed result for the task

The full traceback is:
  File "/tmp/ansible_kubernetes.core.k8s_payload_kaauh79a/ansible_kubernetes.core.k8s_payload.zip/ansible_collections/kubernetes/core/plugins/module_utils/k8s/runner.py", line 101, in run_module
    result = perform_action(svc, definition, module.params)
  File "/tmp/ansible_kubernetes.core.k8s_payload_kaauh79a/ansible_kubernetes.core.k8s_payload.zip/ansible_collections/kubernetes/core/plugins/module_utils/k8s/runner.py", line 186, in perform_action
    instance = svc.create(resource, definition)
  File "/tmp/ansible_kubernetes.core.k8s_payload_kaauh79a/ansible_kubernetes.core.k8s_payload.zip/ansible_collections/kubernetes/core/plugins/module_utils/k8s/service.py", line 358, in create
    raise CoreException(msg) from e
fatal: [m1]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "api_key": null,
            "api_version": "v1",
            "append_hash": false,
            "apply": false,
            "ca_cert": null,
            "client_cert": null,
            "client_key": null,
            "context": "mycontext",
            "continue_on_error": false,
            "definition": {
                "apiVersion": "v1",
                "data": {
                    "appsettings.json": {
                        "AllowedHosts": "*",
                        "RemoteSettings": {
                            "BaseUrl": "https://api.example.com/inwx6/0c8af111-e8d6-1c2a-d91c-50699bcdb1ca/",
                            "Enabled": "false",
                            "Secret": "n06He9W9V7cDg1HMf7eoLKKr3ybbJWx3",
                            "UserName": "482dc608-ebd7-4989-8035-8f11ea8560c9"
                        },
                        "ConnectionStrings": {
                            "DefaultConnection": "User ID=pguser;Password=randompass;Host=mydb-svc;Port=5432;Database=mydb;Pooling=true;"
                        },
                        "DeployDirNix": "/home/src/bin/myapp",
                        "DeployDirWin": "c:\\dev",
                        "Servicer": {
                            "EndPoints": {
                                "Https": {
                                    "Certificate": {
                                        "Password": "randompass",
                                        "Path": "/etc/pki/tls/certs/mycertpak.pfx"
                                    },
                                    "Url": "https://0.0.0.0:5051"
                                }
                            }
                        },
                        "Logging": {
                            "IncludeScopes": "false",
                            "LogLevel": {
                                "Default": "Warning"
                            }
                        },
                        "AppSsoSettings": {
                            "Authority": "https://sso.example.com/auth/app/myappIAM",
                            "CallbackPath": "/Home/Index",
                            "ClientId": "app-sso",
                            "ClientSecret": "7041f4c0-0e5e-4ee5-82ae-a38d290de46e",
                            "IgnoreSslErrors": "true",
                            "LogOutUri": "https://app.local/auth/app/myappIAM/logout?redirect_uri=https://app.local/app/Core"
                        },
                        "OtherAppSettings": {
                            "BaseUrl": "https://app2.example.com/apis/default/app/",
                            "ClientID": "meRBYfhb4iR02nOoqKOvt-JwZTfX7XhnxCJ5t",
                            "Enabled": "true",
                            "Secret": "pass",
                            "UserName": "admin"
                        },
                        "SenderSettings": {
                            "CheckIntervalCronExpression": "0/1 * * * *",
                            "SendFiles": "true"
                        }
                    }
                },
                "kind": "ConfigMap",
                "metadata": {
                    "name": "server-cm",
                    "namespace": "my-apps"
                }
            },
            "delete_all": false,
            "delete_options": null,
            "force": false,
            "generate_name": null,
            "hidden_fields": null,
            "host": null,
            "impersonate_groups": null,
            "impersonate_user": null,
            "kind": null,
            "kubeconfig": null,
            "label_selectors": null,
            "merge_type": null,
            "name": null,
            "namespace": "my-apps",
            "no_proxy": null,
            "password": null,
            "persist_config": null,
            "proxy": null,
            "proxy_headers": null,
            "resource_definition": {
                "apiVersion": "v1",
                "data": {
                    "appsettings.json": {
                        "AllowedHosts": "*",
                        "RemoteSettings": {
                            "BaseUrl": "https://api.example.com/inwx6/0c8af111-e8d6-1c2a-d91c-50699bcdb1ca/",
                            "Enabled": "false",
                            "Secret": "n06He9W9V7cDg1HMf7eoLKKr3ybbJWx3",
                            "UserName": "482dc608-ebd7-4989-8035-8f11ea8560c9"
                        },
                        "ConnectionStrings": {
                            "DefaultConnection": "User ID=pguser;Password=randompass;Host=mydb-svc;Port=5432;Database=mydb;Pooling=true;"
                        },
                        "DeployDirNix": "/home/src/bin/myapp",
                        "DeployDirWin": "c:\\dev",
                        "Servicer": {
                            "EndPoints": {
                                "Https": {
                                    "Certificate": {
                                        "Password": "randompass",
                                        "Path": "/etc/pki/tls/certs/mycertpak.pfx"
                                    },
                                    "Url": "https://0.0.0.0:5150"
                                }
                            }
                        },
                        "Logging": {
                            "IncludeScopes": "false",
                            "LogLevel": {
                                "Default": "Warning"
                            }
                        },
                        "AppSsoSettings": {
                            "Authority": "https://sso.example.com/auth/app/myappIAM",
                            "CallbackPath": "/Home/Index",
                            "ClientId": "app-sso",
                            "ClientSecret": "7041f4c0-0e5e-4ee5-82ae-a38d290de46e",
                            "IgnoreSslErrors": "true",
                            "LogOutUri": "https://app.local/auth/app/myappIAM/logout?redirect_uri=https://app.local/app/Core"
                        },
                        "OtherAppSettings": {
                            "BaseUrl": "https://app2.example.com/apis/default/app/",
                            "ClientID": "meRBYfhb4iR02nOoqKOvt-JwZTfX7XhnxCJ5t",
                            "Enabled": "true",
                            "Secret": "pass",
                            "UserName": "admin"
                        },
                        "SenderSettings": {
                            "CheckIntervalCronExpression": "0/1 * * * *",
                            "SendFiles": "true"
                        }
                    }
                },
                "kind": "ConfigMap",
                "metadata": {
                    "name": "server-cm",
                    "namespace": "my-apps"
                }
            },
            "server_side_apply": null,
            "src": null,
            "state": "present",
            "template": null,
            "username": null,
            "validate": null,
            "validate_certs": null,
            "wait": false,
            "wait_condition": null,
            "wait_sleep": 5,
            "wait_timeout": 120
        }
    },
    "msg": "Failed to create object: b'{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"ConfigMap in version \\\\\"v1\\\\\" cannot be handled as a ConfigMap: json: cannot unmarshal object into Go struct field ConfigMap.data of type string\",\"reason\":\"BadRequest\",\"code\":400}\\n'",
    "reason": "Bad Request"
}

PLAY RECAP *********************************************************************************************************************************************************************************************************************************************************************************
m1                         : ok=3    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

I’ve cross posted this from the Kubernetes.core Github issue report I created.
There may be a bug this specific ConfigMap is triggering or I am just not able to find the typo/error in it.
https://github.com/ansible-collections/kubernetes.core/issues/682

You might try saving that config map as a jinja2 template file, and using the template parameter of kubernets.core.k8s to create the configmap instead of defining it in the task.

I haven’t used templates before but was reading about that this AM. I figured it might be a good method.
Thanks

It was suggested in the Kubernetes.core Github issues, where I posted the bug report, that I could store the data in a variable and convert it to JSON when using the variable in the ConfigMap.
From gravesm in Github:

- kubernetes.core.k8s:
    definition:
      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: mymap
        namespace: default
      data:
        appsettings.json: "{{ data | to_nice_json(indent=2) }}"
  vars:
    myvar: foobar
    data: |-
      {
        "somesetting": "{{ myvar }}"
      }

To fix the error, convert your JSON object into a string before including it in the ConfigMap. Kubernetes expects each value in the data section to be a string. If you’re using Ansible, you can use the to_nice_json filter for this conversion. Make sure to properly quote and format the string in your configuration.

Cheers,

Tim