Interacting with the Docker Hub API using the ansible.builtin.uri module

I trying to get a list of Docker images tags from Docker Hub using Ansible, this is how it works using curl:

curl --silent --get --data-urlencode "service=registry.docker.io" \
--data-urlencode "scope=repository:onlyoffice/documentserver-ee:pull" \
https://auth.docker.io/token | jq  -r '.token'

The above returns a ${TOKEN} than can be used for a second request:

curl -s --header "Authorization: Bearer ${TOKEN}" \
https://registry.hub.docker.com/v2/onlyoffice/documentserver-ee/tags/list \
| jq

The above returns JSON:

{
  "name": "onlyoffice/documentserver-ee",
  "tags": [
    "3.5.0-199",
    "5.6",
    "5.6.4",
    "5.6.4.20",
    "5.6.5",
    "5.6.5.3",
    "6.0",
    "6.0.0",
    "6.0.0.105",
    "6.0.1",
    "6.0.1.32",
    "6.0.2",
    "6.0.2.5",
    "6.1",
    "6.1.0",
    "6.1.0.83",
    "6.1.1",
    "6.1.1.53",
    "6.2",
    "6.2.0",
    "6.2.0.123",
    "6.2.1",
    "6.2.1.24",
    "6.2.2",
    "6.2.2.21",
    "6.3",
    "6.3.0",
    "6.3.0.111",
    "6.3.1",
    "6.3.1.32",
    "6.3.2",
    "6.3.2.2",
    "6.4",
    "6.4.0",
    "6.4.0.121",
    "6.4.1",
    "6.4.1.45",
    "6.4.2",
    "6.4.2.6",
    "7.0",
    "7.0.0",
    "7.0.0.132",
    "7.0.1",
    "7.0.1.37",
    "7.1",
    "7.1.0",
    "7.1.0.215",
    "7.1.1",
    "7.1.1.23",
    "7.2",
    "7.2.0",
    "7.2.0.204",
    "7.2.1",
    "7.2.1.34",
    "7.2.2",
    "7.2.2.56",
    "7.3",
    "7.3.0",
    "7.3.0.184",
    "7.3.2",
    "7.3.2.8",
    "7.3.3",
    "7.3.3.50",
    "7.4",
    "7.4.0",
    "7.4.0.1",
    "7.4.1",
    "7.4.1.1",
    "7.5",
    "7.5.0",
    "7.5.0.1",
    "7.5.1",
    "7.5.1.1",
    "latest",
    "latest-arm64"
  ]
}

However if I try to replicate this using the ansible.builtin.uri module it fails:

- name: Get a Authorization token from auth.docker.io for onlyoffice/documentserver-ee
  ansible.builtin.uri:
    url: https://auth.docker.io/token
    method: GET 
    body_format: form-urlencoded
    body:
      service: "registry.docker.io"
      scope: "repository:onlyoffice/documentserver-ee:pull"
    return_content: true
  check_mode: false
  changed_when: false
  register: workspace_docker_hub_documentserver_ee_auth

- name: Debug workspace_docker_hub_documentserver_ee_auth
  ansible.builtin.debug:
    var: workspace_docker_hub_documentserver_ee_auth
    verbosity: "{% if ansible_check_mode | bool %}0{% else %}1{% endif %}"

- name: Get the tag list for onlyoffice/documentserver-ee
  ansible.builtin.uri:
    url: https://registry.hub.docker.com/v2/onlyoffice/documentserver-ee/tags/list
    method: GET 
    force_basic_auth: false
    use_netrc: false
    headers:
      Authorization: "Bearer {{ workspace_docker_hub_documentserver_ee_auth.json.token }}"
    return_content: true
  check_mode: false
  changed_when: false
  register: workspace_docker_hub_documentserver_ee_tag_list

- name: Debug workspace_docker_hub_documentserver_ee_tag_list
  ansible.builtin.debug:
    var: workspace_docker_hub_documentserver_ee_tag_list
    verbosity: "{% if ansible_check_mode | bool %}0{% else %}1{% endif %}"

The above results in:

TASK [workspace : Get a Authorization token from auth.docker.io for onlyoffice/documentserver-ee] ******************************************************************************************************************************************************************************
ok: [workspace.webarch.org.uk]

TASK [workspace : Debug workspace_docker_hub_documentserver_ee_auth] ***********************************************************************************************************************************************************************************************************
ok: [workspace.webarch.org.uk] => 
  workspace_docker_hub_documentserver_ee_auth:
    changed: false
    connection: close
    content: |-
      {"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFRmpDQ0F2NmdBd0lCQWdJVVZOajJRbU1JWnUzeGl0NUJ1RTlvRWdoVU5KUXdEUVlKS29aSWh2Y05BUUVMQlFBd2dZWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSEV3bFFZV3h2SUVGc2RHOHhGVEFUQmdOVkJBb1RERVJ2WTJ0bGNpd2dTVzVqTGpFVU1CSUdBMVVFQ3hNTFJXNW5hVzVsWlhKcGJtY3hJVEFmQmdOVkJBTVRHRVJ2WTJ0bGNpd2dTVzVqTGlCRmJtY2dVbTl2ZENCRFFUQWVGdzB5TkRBeE1UWXdOak0yTURCYUZ3MHlOVEF4TVRVd05qTTJNREJhTUlHRk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnhNSlVHRnNieUJCYkhSdk1SVXdFd1lEVlFRS0V3eEViMk5yWlhJc0lFbHVZeTR4RkRBU0JnTlZCQXNUQzBWdVoybHVaV1Z5YVc1bk1TQXdIZ1lEVlFRREV4ZEViMk5yWlhJc0lFbHVZeTRnUlc1bklFcFhWQ0JEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTWI4eHR6ZDQ1UWdYekV0bWMxUEJsdWNGUnlzSUF4UUJCN3lSNjdJemdMd05IS24rbUdKTzV5alh6amtLZm5zWm1JRURnZFlraEpBbGNYYTdQa1BFaCtqcTRGNWNaaWtkTmFUQmM3alNkTFJzTVlVa3dwWTl4WUVqYitCYnVGUWVxa0R2RXNqbFJJTzRQK0FsRlhNMDhMYlpIZ3hFWUdkbFk3WFlhT1BLMmE1aUd2eVFRb09GVmZjZDd2ekhaREVBMHZqVmU1M0xLdjVMYmh6TzcxZHRxS0RwNEhnVWR5N1pENDFNN3I1bTd5eE1LeFNpQmJHZTFvem5Wamh1ck5GNHdGSml5bVU4YkhTV2tVTXVLQ3JTbEd4d1NCZFVZNDRyaEh2UW5zYmgzUFF2TUZTWTQ4REdoNFhUUldjSzFWUVlSTnA2ZWFXUVg1RUpJSXVJbjJQOVBzQ0F3RUFBYU43TUhrd0RnWURWUjBQQVFIL0JBUURBZ0dtTUJNR0ExVWRKUVFNTUFvR0NDc0dBUVVGQndNQk1CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRQXdIUVlEVlIwT0JCWUVGSnVRYXZTZHVScm5kRXhLTTAwV2Z2czh5T0RaTUI4R0ExVWRJd1FZTUJhQUZGSGVwRE9ZQ0Y5Qnc5dXNsY0RVUW5CalU3MS9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNDWW0xUVorUUZ1RVhkSWpiNkg4bXNyVFBRSlNnR0JpWDFXSC9QRnpqZlJGeHc3dTdDazBRb0FXZVNqV3JWQWtlVlZQN3J2REpwZ0ZoeUljdzNzMXRPVjN0OGp3cXJTUmc2R285dUd2OG9IWUlLTm9YaDErUFVDTG44b0JwYUJsOUxzSWtsL2FHMG9lY0hrcDVLYmtBNjN6eTFxSUVXNFAzWVJLSk9hSGoxYWFiOXJLc3lRSHF6SUl4TnlDRVVINTMwU1B4RUNMbE53YWVKTDVmNXIxUW5wSi9GM3Q5Vk8xZ0Y2RFpiNitPczdTV29ocGhWZlRCOERkL1VjSk1VOGp2YlF3MWRVREkwelNEdXo2aHNJbGdITk0yak04M0lOS1VqNjNaRDMwRG15ejQvczFFdGgyQmlKK2RHdnFpQkRzaWhaR0tyQnJzUzhWVkRBd3hDeDVRMyJdfQ.eyJhY2Nlc3MiOltdLCJhdWQiOiIiLCJleHAiOjE3MDYyODUyMzYsImlhdCI6MTcwNjI4NDkzNiwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiJkY2tyX2p0aV9IeXZiOVRyV2RxNjdfQTdIa1BGeVFOczY1eHM9IiwibmJmIjoxNzA2Mjg0NjM2LCJzdWIiOiIifQ.oKzlvoISvztrAgvCqywtyNgGOLgGg7g04_2WMYq3t1utSX9XAT_cwrvdY1gSx5oTsgHDCT4hvcPhw9XzzDPBGMHYbmaVuBYv5YDXXImV9A7236KpkEjen1ZlH14TsoiRL3ba9zdo_6whK4_twDZ4KgvvRU7VnApEBAv3aTVgBLTvcYtKlHVrzWHEM0Yy3tj09cF5KHwAFaNrh1PMuFbOy7Dio3iT-ww9QR0dzC_HwHDDlenRjMzeURJdiauqXlVs_iqpb4vacvL0K6WdGKeES7AzzPnlO9CCKsoty1R70EnRs0O3pcJiu-jybHtXZgSq6l58Wyr3_0klmD5Oy9E_PA","access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFRmpDQ0F2NmdBd0lCQWdJVVZOajJRbU1JWnUzeGl0NUJ1RTlvRWdoVU5KUXdEUVlKS29aSWh2Y05BUUVMQlFBd2dZWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSEV3bFFZV3h2SUVGc2RHOHhGVEFUQmdOVkJBb1RERVJ2WTJ0bGNpd2dTVzVqTGpFVU1CSUdBMVVFQ3hNTFJXNW5hVzVsWlhKcGJtY3hJVEFmQmdOVkJBTVRHRVJ2WTJ0bGNpd2dTVzVqTGlCRmJtY2dVbTl2ZENCRFFUQWVGdzB5TkRBeE1UWXdOak0yTURCYUZ3MHlOVEF4TVRVd05qTTJNREJhTUlHRk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnhNSlVHRnNieUJCYkhSdk1SVXdFd1lEVlFRS0V3eEViMk5yWlhJc0lFbHVZeTR4RkRBU0JnTlZCQXNUQzBWdVoybHVaV1Z5YVc1bk1TQXdIZ1lEVlFRREV4ZEViMk5yWlhJc0lFbHVZeTRnUlc1bklFcFhWQ0JEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTWI4eHR6ZDQ1UWdYekV0bWMxUEJsdWNGUnlzSUF4UUJCN3lSNjdJemdMd05IS24rbUdKTzV5alh6amtLZm5zWm1JRURnZFlraEpBbGNYYTdQa1BFaCtqcTRGNWNaaWtkTmFUQmM3alNkTFJzTVlVa3dwWTl4WUVqYitCYnVGUWVxa0R2RXNqbFJJTzRQK0FsRlhNMDhMYlpIZ3hFWUdkbFk3WFlhT1BLMmE1aUd2eVFRb09GVmZjZDd2ekhaREVBMHZqVmU1M0xLdjVMYmh6TzcxZHRxS0RwNEhnVWR5N1pENDFNN3I1bTd5eE1LeFNpQmJHZTFvem5Wamh1ck5GNHdGSml5bVU4YkhTV2tVTXVLQ3JTbEd4d1NCZFVZNDRyaEh2UW5zYmgzUFF2TUZTWTQ4REdoNFhUUldjSzFWUVlSTnA2ZWFXUVg1RUpJSXVJbjJQOVBzQ0F3RUFBYU43TUhrd0RnWURWUjBQQVFIL0JBUURBZ0dtTUJNR0ExVWRKUVFNTUFvR0NDc0dBUVVGQndNQk1CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRQXdIUVlEVlIwT0JCWUVGSnVRYXZTZHVScm5kRXhLTTAwV2Z2czh5T0RaTUI4R0ExVWRJd1FZTUJhQUZGSGVwRE9ZQ0Y5Qnc5dXNsY0RVUW5CalU3MS9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNDWW0xUVorUUZ1RVhkSWpiNkg4bXNyVFBRSlNnR0JpWDFXSC9QRnpqZlJGeHc3dTdDazBRb0FXZVNqV3JWQWtlVlZQN3J2REpwZ0ZoeUljdzNzMXRPVjN0OGp3cXJTUmc2R285dUd2OG9IWUlLTm9YaDErUFVDTG44b0JwYUJsOUxzSWtsL2FHMG9lY0hrcDVLYmtBNjN6eTFxSUVXNFAzWVJLSk9hSGoxYWFiOXJLc3lRSHF6SUl4TnlDRVVINTMwU1B4RUNMbE53YWVKTDVmNXIxUW5wSi9GM3Q5Vk8xZ0Y2RFpiNitPczdTV29ocGhWZlRCOERkL1VjSk1VOGp2YlF3MWRVREkwelNEdXo2aHNJbGdITk0yak04M0lOS1VqNjNaRDMwRG15ejQvczFFdGgyQmlKK2RHdnFpQkRzaWhaR0tyQnJzUzhWVkRBd3hDeDVRMyJdfQ.eyJhY2Nlc3MiOltdLCJhdWQiOiIiLCJleHAiOjE3MDYyODUyMzYsImlhdCI6MTcwNjI4NDkzNiwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiJkY2tyX2p0aV9IeXZiOVRyV2RxNjdfQTdIa1BGeVFOczY1eHM9IiwibmJmIjoxNzA2Mjg0NjM2LCJzdWIiOiIifQ.oKzlvoISvztrAgvCqywtyNgGOLgGg7g04_2WMYq3t1utSX9XAT_cwrvdY1gSx5oTsgHDCT4hvcPhw9XzzDPBGMHYbmaVuBYv5YDXXImV9A7236KpkEjen1ZlH14TsoiRL3ba9zdo_6whK4_twDZ4KgvvRU7VnApEBAv3aTVgBLTvcYtKlHVrzWHEM0Yy3tj09cF5KHwAFaNrh1PMuFbOy7Dio3iT-ww9QR0dzC_HwHDDlenRjMzeURJdiauqXlVs_iqpb4vacvL0K6WdGKeES7AzzPnlO9CCKsoty1R70EnRs0O3pcJiu-jybHtXZgSq6l58Wyr3_0klmD5Oy9E_PA","expires_in":300,"issued_at":"2024-01-26T16:02:16.573922745Z"}
    content_type: application/json
    cookies: {}
    cookies_string: ''
    date: Fri, 26 Jan 2024 16:02:16 GMT
    elapsed: 0
    failed: false
    json:
      access_token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFRmpDQ0F2NmdBd0lCQWdJVVZOajJRbU1JWnUzeGl0NUJ1RTlvRWdoVU5KUXdEUVlKS29aSWh2Y05BUUVMQlFBd2dZWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSEV3bFFZV3h2SUVGc2RHOHhGVEFUQmdOVkJBb1RERVJ2WTJ0bGNpd2dTVzVqTGpFVU1CSUdBMVVFQ3hNTFJXNW5hVzVsWlhKcGJtY3hJVEFmQmdOVkJBTVRHRVJ2WTJ0bGNpd2dTVzVqTGlCRmJtY2dVbTl2ZENCRFFUQWVGdzB5TkRBeE1UWXdOak0yTURCYUZ3MHlOVEF4TVRVd05qTTJNREJhTUlHRk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnhNSlVHRnNieUJCYkhSdk1SVXdFd1lEVlFRS0V3eEViMk5yWlhJc0lFbHVZeTR4RkRBU0JnTlZCQXNUQzBWdVoybHVaV1Z5YVc1bk1TQXdIZ1lEVlFRREV4ZEViMk5yWlhJc0lFbHVZeTRnUlc1bklFcFhWQ0JEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTWI4eHR6ZDQ1UWdYekV0bWMxUEJsdWNGUnlzSUF4UUJCN3lSNjdJemdMd05IS24rbUdKTzV5alh6amtLZm5zWm1JRURnZFlraEpBbGNYYTdQa1BFaCtqcTRGNWNaaWtkTmFUQmM3alNkTFJzTVlVa3dwWTl4WUVqYitCYnVGUWVxa0R2RXNqbFJJTzRQK0FsRlhNMDhMYlpIZ3hFWUdkbFk3WFlhT1BLMmE1aUd2eVFRb09GVmZjZDd2ekhaREVBMHZqVmU1M0xLdjVMYmh6TzcxZHRxS0RwNEhnVWR5N1pENDFNN3I1bTd5eE1LeFNpQmJHZTFvem5Wamh1ck5GNHdGSml5bVU4YkhTV2tVTXVLQ3JTbEd4d1NCZFVZNDRyaEh2UW5zYmgzUFF2TUZTWTQ4REdoNFhUUldjSzFWUVlSTnA2ZWFXUVg1RUpJSXVJbjJQOVBzQ0F3RUFBYU43TUhrd0RnWURWUjBQQVFIL0JBUURBZ0dtTUJNR0ExVWRKUVFNTUFvR0NDc0dBUVVGQndNQk1CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRQXdIUVlEVlIwT0JCWUVGSnVRYXZTZHVScm5kRXhLTTAwV2Z2czh5T0RaTUI4R0ExVWRJd1FZTUJhQUZGSGVwRE9ZQ0Y5Qnc5dXNsY0RVUW5CalU3MS9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNDWW0xUVorUUZ1RVhkSWpiNkg4bXNyVFBRSlNnR0JpWDFXSC9QRnpqZlJGeHc3dTdDazBRb0FXZVNqV3JWQWtlVlZQN3J2REpwZ0ZoeUljdzNzMXRPVjN0OGp3cXJTUmc2R285dUd2OG9IWUlLTm9YaDErUFVDTG44b0JwYUJsOUxzSWtsL2FHMG9lY0hrcDVLYmtBNjN6eTFxSUVXNFAzWVJLSk9hSGoxYWFiOXJLc3lRSHF6SUl4TnlDRVVINTMwU1B4RUNMbE53YWVKTDVmNXIxUW5wSi9GM3Q5Vk8xZ0Y2RFpiNitPczdTV29ocGhWZlRCOERkL1VjSk1VOGp2YlF3MWRVREkwelNEdXo2aHNJbGdITk0yak04M0lOS1VqNjNaRDMwRG15ejQvczFFdGgyQmlKK2RHdnFpQkRzaWhaR0tyQnJzUzhWVkRBd3hDeDVRMyJdfQ.eyJhY2Nlc3MiOltdLCJhdWQiOiIiLCJleHAiOjE3MDYyODUyMzYsImlhdCI6MTcwNjI4NDkzNiwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiJkY2tyX2p0aV9IeXZiOVRyV2RxNjdfQTdIa1BGeVFOczY1eHM9IiwibmJmIjoxNzA2Mjg0NjM2LCJzdWIiOiIifQ.oKzlvoISvztrAgvCqywtyNgGOLgGg7g04_2WMYq3t1utSX9XAT_cwrvdY1gSx5oTsgHDCT4hvcPhw9XzzDPBGMHYbmaVuBYv5YDXXImV9A7236KpkEjen1ZlH14TsoiRL3ba9zdo_6whK4_twDZ4KgvvRU7VnApEBAv3aTVgBLTvcYtKlHVrzWHEM0Yy3tj09cF5KHwAFaNrh1PMuFbOy7Dio3iT-ww9QR0dzC_HwHDDlenRjMzeURJdiauqXlVs_iqpb4vacvL0K6WdGKeES7AzzPnlO9CCKsoty1R70EnRs0O3pcJiu-jybHtXZgSq6l58Wyr3_0klmD5Oy9E_PA
      expires_in: 300
      issued_at: '2024-01-26T16:02:16.573922745Z'
      token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFRmpDQ0F2NmdBd0lCQWdJVVZOajJRbU1JWnUzeGl0NUJ1RTlvRWdoVU5KUXdEUVlKS29aSWh2Y05BUUVMQlFBd2dZWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSEV3bFFZV3h2SUVGc2RHOHhGVEFUQmdOVkJBb1RERVJ2WTJ0bGNpd2dTVzVqTGpFVU1CSUdBMVVFQ3hNTFJXNW5hVzVsWlhKcGJtY3hJVEFmQmdOVkJBTVRHRVJ2WTJ0bGNpd2dTVzVqTGlCRmJtY2dVbTl2ZENCRFFUQWVGdzB5TkRBeE1UWXdOak0yTURCYUZ3MHlOVEF4TVRVd05qTTJNREJhTUlHRk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnhNSlVHRnNieUJCYkhSdk1SVXdFd1lEVlFRS0V3eEViMk5yWlhJc0lFbHVZeTR4RkRBU0JnTlZCQXNUQzBWdVoybHVaV1Z5YVc1bk1TQXdIZ1lEVlFRREV4ZEViMk5yWlhJc0lFbHVZeTRnUlc1bklFcFhWQ0JEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTWI4eHR6ZDQ1UWdYekV0bWMxUEJsdWNGUnlzSUF4UUJCN3lSNjdJemdMd05IS24rbUdKTzV5alh6amtLZm5zWm1JRURnZFlraEpBbGNYYTdQa1BFaCtqcTRGNWNaaWtkTmFUQmM3alNkTFJzTVlVa3dwWTl4WUVqYitCYnVGUWVxa0R2RXNqbFJJTzRQK0FsRlhNMDhMYlpIZ3hFWUdkbFk3WFlhT1BLMmE1aUd2eVFRb09GVmZjZDd2ekhaREVBMHZqVmU1M0xLdjVMYmh6TzcxZHRxS0RwNEhnVWR5N1pENDFNN3I1bTd5eE1LeFNpQmJHZTFvem5Wamh1ck5GNHdGSml5bVU4YkhTV2tVTXVLQ3JTbEd4d1NCZFVZNDRyaEh2UW5zYmgzUFF2TUZTWTQ4REdoNFhUUldjSzFWUVlSTnA2ZWFXUVg1RUpJSXVJbjJQOVBzQ0F3RUFBYU43TUhrd0RnWURWUjBQQVFIL0JBUURBZ0dtTUJNR0ExVWRKUVFNTUFvR0NDc0dBUVVGQndNQk1CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRQXdIUVlEVlIwT0JCWUVGSnVRYXZTZHVScm5kRXhLTTAwV2Z2czh5T0RaTUI4R0ExVWRJd1FZTUJhQUZGSGVwRE9ZQ0Y5Qnc5dXNsY0RVUW5CalU3MS9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNDWW0xUVorUUZ1RVhkSWpiNkg4bXNyVFBRSlNnR0JpWDFXSC9QRnpqZlJGeHc3dTdDazBRb0FXZVNqV3JWQWtlVlZQN3J2REpwZ0ZoeUljdzNzMXRPVjN0OGp3cXJTUmc2R285dUd2OG9IWUlLTm9YaDErUFVDTG44b0JwYUJsOUxzSWtsL2FHMG9lY0hrcDVLYmtBNjN6eTFxSUVXNFAzWVJLSk9hSGoxYWFiOXJLc3lRSHF6SUl4TnlDRVVINTMwU1B4RUNMbE53YWVKTDVmNXIxUW5wSi9GM3Q5Vk8xZ0Y2RFpiNitPczdTV29ocGhWZlRCOERkL1VjSk1VOGp2YlF3MWRVREkwelNEdXo2aHNJbGdITk0yak04M0lOS1VqNjNaRDMwRG15ejQvczFFdGgyQmlKK2RHdnFpQkRzaWhaR0tyQnJzUzhWVkRBd3hDeDVRMyJdfQ.eyJhY2Nlc3MiOltdLCJhdWQiOiIiLCJleHAiOjE3MDYyODUyMzYsImlhdCI6MTcwNjI4NDkzNiwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiJkY2tyX2p0aV9IeXZiOVRyV2RxNjdfQTdIa1BGeVFOczY1eHM9IiwibmJmIjoxNzA2Mjg0NjM2LCJzdWIiOiIifQ.oKzlvoISvztrAgvCqywtyNgGOLgGg7g04_2WMYq3t1utSX9XAT_cwrvdY1gSx5oTsgHDCT4hvcPhw9XzzDPBGMHYbmaVuBYv5YDXXImV9A7236KpkEjen1ZlH14TsoiRL3ba9zdo_6whK4_twDZ4KgvvRU7VnApEBAv3aTVgBLTvcYtKlHVrzWHEM0Yy3tj09cF5KHwAFaNrh1PMuFbOy7Dio3iT-ww9QR0dzC_HwHDDlenRjMzeURJdiauqXlVs_iqpb4vacvL0K6WdGKeES7AzzPnlO9CCKsoty1R70EnRs0O3pcJiu-jybHtXZgSq6l58Wyr3_0klmD5Oy9E_PA
    msg: OK (unknown bytes)
    redirected: false
    status: 200
    strict_transport_security: max-age=31536000
    transfer_encoding: chunked
    url: https://auth.docker.io/token
    x_trace_id: d9c4d8c02d40f8489e9f4d5a7ea206b6

TASK [workspace : Get the tag list for onlyoffice/documentserver-ee] ***********************************************************************************************************************************************************************************************************
fatal: [workspace.webarch.org.uk]: FAILED! => changed=false 
  connection: close
  content: |-
    {"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Class":"","Name":"onlyoffice/documentserver-ee","Action":"pull"}]}]}
  content_length: '171'
  content_type: application/json
  date: Fri, 26 Jan 2024 16:02:18 GMT
  docker_distribution_api_version: registry/2.0
  elapsed: 0
  json:
    errors:
    - code: UNAUTHORIZED
      detail:
      - Action: pull
        Class: ''
        Name: onlyoffice/documentserver-ee
        Type: repository
      message: authentication required
  msg: 'Status code was 401 and not [200]: HTTP Error 401: Unauthorized'
  redirected: false
  status: 401
  strict_transport_security: max-age=31536000
  url: https://registry.hub.docker.com/v2/onlyoffice/documentserver-ee/tags/list
  www_authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:onlyoffice/documentserver-ee:pull",error="invalid_token"

Have I made a stupid mistake or does anyone have a better suggestion than using the command module and curl to get around this issue?

I’m stumped!

This is the version that works using curl:


- name: Get a Authorization token from auth.docker.io for onlyoffice/documentserver-ee
  ansible.builtin.command: >-
    curl --silent --get --data-urlencode "service=registry.docker.io"
    --data-urlencode "scope=repository:onlyoffice/documentserver-ee:pull"
    https://auth.docker.io/token
  check_mode: false
  changed_when: false
  register: workspace_docker_hub_documentserver_ee_curl_auth

- name: Set a fact for the JSON returned from auth.docker.io for onlyoffice/documentserver-ee
  ansible.builtin.set_fact:
    workspace_docker_hub_documentserver_ee_curl_auth_json: "{{ workspace_docker_hub_documentserver_ee_curl_auth.stdout | ansible.builtin.from_json }}"

- name: Get the tag list for onlyoffice/documentserver-ee using curl
  ansible.builtin.command: >-
    curl -s --header "Authorization: Bearer {{ workspace_docker_hub_documentserver_ee_curl_auth_json.token }}"
    https://registry.hub.docker.com/v2/onlyoffice/documentserver-ee/tags/list
  check_mode: false
  changed_when: false
  register: workspace_docker_hub_documentserver_ee_curl_tag_list

- name: Set a fact for the onlyoffice/documentserver-ee tags
  ansible.builtin.set_fact:
    workspace_docker_hub_documentserver_ee_tags: "{{ (workspace_docker_hub_documentserver_ee_curl_tag_list.stdout | ansible.builtin.from_json).tags }}"

- name: Debug workspace_docker_hub_documentserver_ee_tags
  ansible.builtin.debug:
    var: workspace_docker_hub_documentserver_ee_tags
    verbosity: "{% if ansible_check_mode | bool %}0{% else %}1{% endif %}"

The curl version results in:

TASK [workspace : Debug workspace_docker_hub_documentserver_ee_tags] ***********************************************************************************************************************************************************************************************************
ok: [workspace.webarch.org.uk] => 
  workspace_docker_hub_documentserver_ee_tags:
  - 3.5.0-199
  - '5.6'
  - 5.6.4
  - 5.6.4.20
  - 5.6.5
  - 5.6.5.3
  - '6.0'
  - 6.0.0
  - 6.0.0.105
  - 6.0.1
  - 6.0.1.32
  - 6.0.2
  - 6.0.2.5
  - '6.1'
  - 6.1.0
  - 6.1.0.83
  - 6.1.1
  - 6.1.1.53
  - '6.2'
  - 6.2.0
  - 6.2.0.123
  - 6.2.1
  - 6.2.1.24
  - 6.2.2
  - 6.2.2.21
  - '6.3'
  - 6.3.0
  - 6.3.0.111
  - 6.3.1
  - 6.3.1.32
  - 6.3.2
  - 6.3.2.2
  - '6.4'
  - 6.4.0
  - 6.4.0.121
  - 6.4.1
  - 6.4.1.45
  - 6.4.2
  - 6.4.2.6
  - '7.0'
  - 7.0.0
  - 7.0.0.132
  - 7.0.1
  - 7.0.1.37
  - '7.1'
  - 7.1.0
  - 7.1.0.215
  - 7.1.1
  - 7.1.1.23
  - '7.2'
  - 7.2.0
  - 7.2.0.204
  - 7.2.1
  - 7.2.1.34
  - 7.2.2
  - 7.2.2.56
  - '7.3'
  - 7.3.0
  - 7.3.0.184
  - 7.3.2
  - 7.3.2.8
  - 7.3.3
  - 7.3.3.50
  - '7.4'
  - 7.4.0
  - 7.4.0.1
  - 7.4.1
  - 7.4.1.1
  - '7.5'
  - 7.5.0
  - 7.5.0.1
  - 7.5.1
  - 7.5.1.1
  - latest
  - latest-arm64

:roll_eyes:

Hi, try this:

- name: Get a Authorization token from auth.docker.io for onlyoffice/documentserver-ee
  ansible.builtin.uri:
    # url: https://auth.docker.io/token
    url: https://auth.docker.io/token?service=registry.docker.io&scope=repository:onlyoffice/documentserver-ee:pull
    method: GET 
    # body_format: form-urlencoded
    # body:
    #   service: "registry.docker.io"
    #   scope: "repository:onlyoffice/documentserver-ee:pull"
    return_content: true
  check_mode: false
  changed_when: false
  register: workspace_docker_hub_documentserver_ee_auth

This is the HTTP request made by your task. The body is truly used as request body, and is not concatenated into URL.

GET /token HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded
User-Agent: ansible-httpget

service=registry.docker.io&scope=repository%3Aonlyoffice%2Fdocumentserver-ee%3Apull

This is by my task above. auth.docker.io/token requires params as query strings, instead of request body:

GET /token?service=registry.docker.io&scope=repository:onlyoffice/documentserver-ee:pull HTTP/1.1
...
User-Agent: ansible-httpget
2 Likes

Thanks @kurokobo I can’t believe that I appear to have forgotten that a GET doesn’t have a body, do’h! :roll_eyes:

And the final task to complete this is to get the latest item from the list, the strings that are not semantic version number are omitted, it is sorted and the last one picked:

workspace_docker_hub_documentserver_ee_tag_list.json.tags | reject('regex', '[a-zA-Z-]') | community.general.version_sort | last

That took way longer than it should have! But it’ll be quicker next time… :slight_smile:

1 Like

This sounds like something we could add (one or more) module for. While you can do everything with the uri module, having some more specialized UI would make it easier to (re-)use :slight_smile:

2 Likes

If someone does want write a module for this there are lots of posts on StackOverflow with scripts for doing this, for example here are a couple:

I can’t find the one I initially followed…

Yeah, I’ve written some scripts myself doing that ~8 years ago, and they’re still running in the deployment system at $JOB. The official API for container registries is documented here: distribution-spec/spec.md at main · opencontainers/distribution-spec · GitHub, and one easy to spin up registry is the original Docker registry, GitHub - distribution/distribution: The toolkit to pack, ship, store, and deliver container content, which at I think least originally powered the registry part of Docker Hub (not sure whether it still does).

1 Like

For projects that are considered “official” or some such designation, HTTP authentication with a Bearer token is not required, however they paginate the results, so this is the best I have come up with for getting the MySQL image tags:

- name: Loop requests for the tag list for MySQL containers from registry.hub.docker.com
  ansible.builtin.uri:
    url: "https://registry.hub.docker.com/v2/repositories/library/mysql/tags/?page={{ ansible_loop.index0 }}&page_size=10"
    method: GET
    return_content: true
    status_code:
      - 200
      - 404
  check_mode: false
  changed_when: false
  register: docker_hub_mysql_tag_list_loop
  loop: "{{ range(30) }}"
  loop_control:
    label: "{{ ansible_loop.index0 }}"
    extended: true

- name: Set a fact for all the versions of MySQL containers at registry.hub.docker.com
  ansible.builtin.set_fact:
    mysql_docker_all_tags: "{{ docker_hub_mysql_tag_list_loop | community.general.json_query('results[].json[].results[].name') }}"

- name: Debug the all the MySQL container tags at registry.hub.docker.com
  ansible.builtin.debug:
    var: mysql_docker_all_tags
    verbosity: "{% if ansible_check_mode | bool %}1{% else %}2{% endif %}"

- name: Set a fact for the latest version of MySQL containers at registry.hub.docker.com
  ansible.builtin.set_fact:
    mysql_docker_tag_latest: "{{ mysql_docker_all_tags | reject('regex', '[a-zA-Z-]') | community.general.version_sort | last }}"

- name: Debug the latest MySQL container tag at registry.hub.docker.com
  ansible.builtin.debug:
    var: mysql_docker_tag_latest
    verbosity: "{% if ansible_check_mode | bool %}0{% else %}1{% endif %}"

The results:

TASK [workspace : Debug the all the MySQL container tags at registry.hub.docker.com] *******************************************************************************************************************************************************************************************
ok: [workspace.webarch.org.uk] => 
  workspace_mysql_docker_all_tags:
  - latest
  - oraclelinux8
  - oracle
  - innovation-oraclelinux8
  - innovation-oracle
  - innovation
  - 8.3.0-oraclelinux8
  - 8.3.0-oracle
  - 8.3.0
  - 8.3-oraclelinux8
  - latest
  - oraclelinux8
  - oracle
  - innovation-oraclelinux8
  - innovation-oracle
  - innovation
  - 8.3.0-oraclelinux8
  - 8.3.0-oracle
  - 8.3.0
  - 8.3-oraclelinux8
  - 8.3-oracle
  - '8.3'
  - 8.0.36-oraclelinux8
  - 8.0.36-oracle
  - 8.0.36
  - 8.0-oraclelinux8
  - 8.0-oracle
  - '8.0'
  - 8-oraclelinux8
  - 8-oracle
  - '8'
  - 8.0.36-debian
  - 8.0.36-bullseye
  - 8.0-debian
  - 8.0-bullseye
  - 8.2.0-oraclelinux8
  - 8.2.0-oracle
  - 8.2.0
  - 8.2-oraclelinux8
  - 8.2-oracle
  - '8.2'
  - 8.0.35-oraclelinux8
  - 8.0.35-oracle
  - 8.0.35
  - 8.0.35-debian
  - 8.0.35-bullseye
  - 5.7.44-oraclelinux7
  - 5.7-oraclelinux7
  - 5-oraclelinux7
  - 5.7.44-oracle
  - 5.7.44
  - 5.7-oracle
  - '5.7'
  - 5-oracle
  - '5'
  - 8.1.0-oracle
  - 8.1.0
  - 8.1-oracle
  - '8.1'
  - 8.0.34-oracle
  - 8.0.34
  - 5.7.43-oracle
  - 5.7.43
  - 8.0.34-debian
  - debian
  - 8-debian
  - 5.7.42-debian
  - 5.7-debian
  - 5-debian
  - 5.7.42-oracle
  - 5.7.42
  - 8.0.33-oracle
  - 8.0.33
  - 8.0.33-debian
  - 8.0.32-debian
  - 5.7.41-debian
  - 8.0.32-oracle
  - 8.0.32
  - 5.7.41-oracle
  - 5.7.41
  - 8.0.31-debian
  - 5.7.40-debian
  - 8.0.31-oracle
  - 8.0.31
  - 5.7.40-oracle
  - 5.7.40
  - 8.0.30-oracle
  - 8.0.30
  - 5.7.39-oracle
  - 5.7.39
  - 8.0.30-debian
  - 5.7.39-debian
  - 8.0.29-oracle
  - 8.0.29
  - 5.7.38-oracle
  - 5.7.38
  - 8.0.29-debian
  - 5.7.38-debian
  - 8.0.28-debian
  - 8.0.28
  - 5.7.37-debian
  - 5.7.37
  - 8.0.28-oracle
  - 5.7.37-oracle
  - 8.0.27
  - 5.7.36
  - 5.6.51
  - '5.6'
  - 8.0.26
  - 5.7.35
  - 8.0.25
  - 5.7.34
  - 8.0.24
  - 8.0.23
  - 5.7.33
  - 8.0.22
  - 5.7.32
  - 5.6.50
  - 8.0.21
  - 5.7.31
  - 5.6.49
  - 8.0.20
  - 5.7.30
  - 5.6.48
  - 8.0.19
  - 5.7.29
  - 5.6.47
  - 8.0.18
  - 5.7.28
  - 5.6.46
  - 8.0.17
  - 5.7.27
  - 5.6.45
  - 8.0.16
  - 5.7.26
  - 5.6.44
  - 5.5.62
  - '5.5'
  - 5.6.43
  - 5.7.25
  - 8.0.15
  - 8.0.14
  - 5.6.42
  - 5.7.24
  - 8.0.13
  - 5.5.61
  - 5.6.41
  - 5.7.23
  - 8.0.12
  - 5.5.60
  - 5.6.40
  - 5.7.22
  - 8.0.11
  - 5.5.59
  - 5.6.39
  - 5.7.21
  - 8.0.4
  - 8.0.4-rc
  - 8.0.3
  - 5.5.58
  - 5.6.38
  - 5.7.20
  - 5.5.57
  - 5.6.37
  - 5.7.19
  - 8.0.2
  - 5.5.56
  - 5.6.36
  - 5.7.18
  - 8.0.1
  - 5.5.55
  - 5.6.35
  - 5.7.17
  - 8.0.0
  - 5.5.54
  - 5.5.53
  - 5.7.16
  - 5.6.34
  - 5.5.52
  - 5.6.33
  - 5.7.15
  - 5.5.51
  - 5.6.32
  - 5.7.14
  - 5.5.50
  - 5.6.31
  - 5.7.13
  - 5.7.12
  - 5.6.30
  - 5.5.49
  - 5.7.11
  - 5.6.29
  - 5.5.48
  - 5.7.10
  - 5.6.28
  - 5.5.47
  - 5.6.27
  - 5.7.9
  - 5.5.46
  - 5.5.40
  - 5.7.4
  - 5.6.17
  - 5.5.41
  - 5.7.5-m15
  - 5.6.21
  - 5.6.20
  - 5.6.22
  - 5.7.4-m14
  - 5.7.5
  - 5.7.8
  - 5.7.8-rc
  - 5.6.26
  - 5.5.45
  - 5.7.7
  - 5.7.7-rc
  - 5.6.25
  - 5.5.44
  - 5.6.24
  - 5.5.43
  - 5.7.6
  - 5.7.6-m16
  - 5.6.23
  - 5.5.42

TASK [workspace : Debug the latest MySQL container tag at registry.hub.docker.com] *********************************************************************************************************************************************************************************************
ok: [workspace.webarch.org.uk] => 
  workspace_mysql_docker_tag_latest: 8.3.0

The aspect of this I haven’t been able to solve is how to stop when either the next URL in the result is Null or the last result status was a 404, in the absence of a solution for this you need to have the range set to a few more pages than needed to ensure you get all the results… :roll_eyes:

UPDATE: there is the count number in the results, I have tried dividing this by 10 and rounding it up to a whole number but it seems like there is no way to make it an integer, the int filter returns AnsibleUnsafeText and then range won’t accept it because it is not an integer…