Search code examples
kubernetesansiblejinja2ansible-runner

Return integer value without quote when using variable


I'm new to Ansible (about 1 week experience). I have this playbook that creates a deployment in my Kubernetes cluster using the module kubernetes.core.k8s.

...
  vars:
    worker_count: "{{ lookup('ansible.builtin.env', 'worker_count') }}"
  tasks:
    - name: Create a deployment
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: worker
            namespace: default
            labels:
              app: worker
          spec:
            replicas: "{{ worker_count }}"  <--- the problem
            selector:
              matchLabels:
                app: worker
...

In the vars section, I retrieve worker_count from env var, and use it in the replicas field.

When I tried to run it with Python package ansible_runner:

import ansible_runner
ansible_runner.run(playbook="path/to/pb", envvars={"worker_count": 5})

I got the error (I tried running the playbook with ansible-playbook CLI and got the same error).

fatal: [localhost]: FAILED! => {"changed": false, "error": 400, "msg": "Deployment worker: Failed to create object: b'{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Deployment in version \\"v1\\" cannot be handled as a Deployment: json: cannot unmarshal string into Go struct field DeploymentSpec.spec.replicas of type int32","reason":"BadRequest","code":400}\n'", "reason": "Bad Request", "status": 400}

So I tried to change the replicas line to:

replicas: "{{ worker_count | int }}"

But it still gives the same error. What I understand is by using the additional | int, it is cast to integer but then returned as string again. You can do integer operations (addition, division, etc.) to the variable, but it will still return the result in String with the quotes " (source1, source2). That's why it still gives the same error.

So I figured I need to return the variable without the quote, but I can't find a way for this after hours looking on the internet.


Solution

  • Note: In the below examples, Ansible callback yaml is used to render stdout.

    See:

    ansible-doc -t callback yaml
    

    The below playbook explains what's going on

    shell> cat pb2.yml
    - hosts: localhost
    
      vars:
    
        w1: 5
        w2: '5'
        w3: "{{ w1 }}"
        w4: "{{ w1 + 1 - 1 }}"
    
      tasks:
    
        - debug:
            msg: |
              w1: {{ w1 }}
              w1|type_debug: {{ w1|type_debug }}
              w2: {{ w2 }}
              w2|type_debug: {{ w2|type_debug }}
              w3: {{ w3 }}
              w3|type_debug: {{ w3|type_debug }}
              w4: {{ w4 }}
              w4|type_debug: {{ w4|type_debug }}
    

    gives (abridged)

    shell> ansible-playbook pb2.yml
      ...
      msg: |-
        w1: 5
        w1|type_debug: int
        w2: 5
        w2|type_debug: AnsibleUnicode
        w3: 5
        w3|type_debug: int
        w4: 5
        w4|type_debug: str
    

    The type of w1 is an integer and the type of w2 is a string. The type of w3 is also an integer because Jinja is not used in this simple assignment. But Jinja is used as long as there are any operations in the template. This is the reason for w4 being a string.

    The result of a Jinja template is a string. You can change it by using DEFAULT_JINJA2_NATIVE:

    This option preserves variable types during template operations.

    This makes the type of w4 an integer.

    shell> ANSIBLE_JINJA2_NATIVE=true ansible-playbook pb2.yml
      ...
        w4|type_debug: int
    

    The below playbook will test the environment variable worker_count

    - hosts: localhost
    
      vars:
    
        worker_count: "{{ lookup('env', 'worker_count') }}"
        
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: worker
            namespace: default
            labels:
              app: worker
          spec:
            replicas: "{{ worker_count }}"
            selector:
              matchLabels:
                app: worker
    
      tasks:
    
        - debug:
            msg: |
              worker_count: {{ worker_count }}
              worker_count|type_debug: {{ worker_count|type_debug }}
              definition.spec.replicas: {{ definition.spec.replicas }}
              definition.spec.replicas|type_debug: {{ definition.spec.replicas|type_debug }}
    

    gives (abridged)

    shell> worker_count=5 ansible-playbook pb.yml
      ...
      msg: |-
        worker_count: 5
        worker_count|type_debug: AnsibleUnsafeText
        definition.spec.replicas: 5
        definition.spec.replicas|type_debug: AnsibleUnsafeText
    

    You'll get strings unless you set ANSIBLE_JINJA2_NATIVE=true

    shell> ANSIBLE_JINJA2_NATIVE=true worker_count=5 ansible-playbook pb.yml
      ...
      msg: |-
        worker_count: 5
        worker_count|type_debug: int
        definition.spec.replicas: 5
        definition.spec.replicas|type_debug: int
    

    Ansible Runner recognizes this option too. The Python script

    shell> cat pb.py
    import ansible_runner
    r = ansible_runner.run(private_data_dir="/scratch/tmp7/test-565",
                           playbook="pb.yml",
                           envvars={"worker_count": 5})
    

    gives (abridged)

    shell> python3 pb.py
    
    PLAY [localhost] ***************************************************************
    
    TASK [debug] *******************************************************************
    ok: [localhost] => {
        "msg": "worker_count: 5\nworker_count|type_debug: AnsibleUnsafeText\ndefinition.spec.replicas: 5\ndefinition.spec.replicas|type_debug: AnsibleUnsafeText\n"
    }
      ...
    

    You'll also get strings unless you set ANSIBLE_JINJA2_NATIVE=true

    shell> ANSIBLE_JINJA2_NATIVE=true python3 pb.py
    
    PLAY [localhost] ***************************************************************
    
    TASK [debug] *******************************************************************
    ok: [localhost] => {
        "msg": "worker_count: 5\nworker_count|type_debug: int\ndefinition.spec.replicas: 5\ndefinition.spec.replicas|type_debug: int\n"
    }
      ...