Skip to content

tests: fix _get_container_ip for docker 29.0#4606

Merged
georglauterbach merged 3 commits intodocker-mailserver:masterfrom
ap-wtioit:master-fix_tests_docker_29
Nov 15, 2025
Merged

tests: fix _get_container_ip for docker 29.0#4606
georglauterbach merged 3 commits intodocker-mailserver:masterfrom
ap-wtioit:master-fix_tests_docker_29

Conversation

@ap-wtioit
Copy link
Copy Markdown
Contributor

@ap-wtioit ap-wtioit commented Nov 13, 2025

Description

Docker 29 removes NetworkSettings.IPAddress

Docker 28.4.0 inspect of container using bridge

docker inspect ...

[
    {
        "Id": "2a426fc9f069d3849efbca89c661d41f4ae06687efd35883c32053590538cd91",
        # ...
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "9528a9bf4462ab058779d206bc3072263b4351cb7870305a4d6cac504acff6cb",
            "SandboxKey": "/var/run/docker/netns/9528a9bf4462",
            "Ports": {},
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "b915d5a4fd3bdb5fa0c7144dc19dc32c8714ff6be44e9e45f2b1537dc7da7baa",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "76:74:ff:bd:af:0c",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "MacAddress": "76:74:ff:bd:af:0c",
                    "DriverOpts": null,
                    "GwPriority": 0,
                    "NetworkID": "2bcc680edb64388426905c0ad51dd2d3d6bd4da0974afad5a0924fdb0ac99367",
                    "EndpointID": "b915d5a4fd3bdb5fa0c7144dc19dc32c8714ff6be44e9e45f2b1537dc7da7baa",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "DNSNames": null
                }
            }
        }
    }
]
Docker 29.0.0 inspect of container using bridge

docker inspect ...

[
    {
        "Id": "bf1d685dda76fd70336dcfb03eadd84b6ef9df84b5091c836c48124c7ebdd4ac",
        # ...
        "NetworkSettings": {
            "SandboxID": "abf13e6108cc7eb63b18ae1543522d1c323eb0f67b0f298dae1885cd58ae161d",
            "SandboxKey": "/var/run/docker/netns/abf13e6108cc",
            "Ports": {},
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "DriverOpts": null,
                    "GwPriority": 0,
                    "NetworkID": "a236919fbac09468275c1fa155752a5065208ed35eba0735c21f48438aa06892",
                    "EndpointID": "f51e9b253ee4f00cda4a6834c55cd1c7df506b7d7e772ec9fa0929f6998a6419",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "MacAddress": "ee:96:61:cd:b0:ff",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "DNSNames": null
                }
            }
        }
    }
]

Test error on our CI when having a docker 29.0.0 runner:

...
ok 54 [Fail2Ban] fail2ban-jail.cf overrides in 997ms
not ok 55 [Fail2Ban] ban ip on multiple failed login in 26ms
# (in test file test/tests/parallel/set1/spam_virus/fail2ban.bats, line 73)
#   `CONTAINER1_IP=$(_get_container_ip "${CONTAINER1_NAME}")' failed
# template parsing error: template: :1:19: executing "" at <.NetworkSettings.IPAddress>: map has no entry for key "IPAddress"
not ok 56 [Fail2Ban] unban ip works in 27ms
# (in test file test/tests/parallel/set1/spam_virus/fail2ban.bats, line 101)
#   `CONTAINER2_IP=$(_get_container_ip "${CONTAINER2_NAME}")' failed
# template parsing error: template: :1:19: executing "" at <.NetworkSettings.IPAddress>: map has no entry for key "IPAddress"
ok 57 [Fail2Ban] bans work properly (single IP) in 1231ms
...
ok 66 [Postgrey] (enabled) should whitelist recipient '[email protected]' in 161ms
not ok 67 [Postscreen] should fail send when talking out of turn in 33ms
# (from function `setup' in test file test/tests/parallel/set1/spam_virus/postscreen.bats, line 9)
#   `CONTAINER1_IP=$(_get_container_ip "${CONTAINER1_NAME}")' failed
# template parsing error: template: :1:19: executing "" at <.NetworkSettings.IPAddress>: map has no entry for key "IPAddress"
not ok 68 [Postscreen] should successfully pass postscreen and get postfix greeting message (respecting postscreen_greet_wait time) in 27ms
# (from function `setup' in test file test/tests/parallel/set1/spam_virus/postscreen.bats, line 9)
#   `CONTAINER1_IP=$(_get_container_ip "${CONTAINER1_NAME}")' failed
# template parsing error: template: :1:19: executing "" at <.NetworkSettings.IPAddress>: map has no entry for key "IPAddress"
ok 69 [Rspamd] (DKIM) log level is applied correctly in 573ms
...

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (README.md or the documentation under docs/)
  • If necessary, I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have added information about changes made in this PR to CHANGELOG.md

Info @wt-io-it

Copy link
Copy Markdown
Member

@polarathene polarathene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for spotting this and contributing a fix ❤️

I think we'd be fine avoiding the conditional as the information still seems to be there in older releases for some time? Since we already use jq elsewhere, might be better to adopt that here than all the extra commentary explaining the Go template syntax 😅 (I think that's what it is?)

Comment thread test/helper/common.bash Outdated
Comment on lines +372 to +376
# use .NetworkSettings.IPAddress when available (docker < 29 and container uses bridge)
# else use .NetworkSettings.Networks.bridge.IPAddress (docker > 29 and container uses bride)
# else empty string
# with newline at the end
docker inspect --format '{{with (index .NetworkSettings "IPAddress")}}{{.}}{{else}}{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}{{end}}' "${TARGET_CONTAINER_NAME}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do it with the --format template, but since it's JSON we might be better off with just using jq?

Suggested change
# use .NetworkSettings.IPAddress when available (docker < 29 and container uses bridge)
# else use .NetworkSettings.Networks.bridge.IPAddress (docker > 29 and container uses bride)
# else empty string
# with newline at the end
docker inspect --format '{{with (index .NetworkSettings "IPAddress")}}{{.}}{{else}}{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}{{end}}' "${TARGET_CONTAINER_NAME}"
docker inspect --format '{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}' "${TARGET_CONTAINER_NAME}"

vs via jq:

Suggested change
# use .NetworkSettings.IPAddress when available (docker < 29 and container uses bridge)
# else use .NetworkSettings.Networks.bridge.IPAddress (docker > 29 and container uses bride)
# else empty string
# with newline at the end
docker inspect --format '{{with (index .NetworkSettings "IPAddress")}}{{.}}{{else}}{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}{{end}}' "${TARGET_CONTAINER_NAME}"
docker inspect "${TARGET_CONTAINER_NAME}" | jq '.[].NetworkSettings.Networks["bridge"].IPAddress'

Latter looks simpler to grok to me 🤔

Copy link
Copy Markdown
Contributor Author

@ap-wtioit ap-wtioit Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, jq could work, the comment tries actually explaining of the fallbacks:

  • jq needs -r and // ""
    docker inspect container_with_bridge | jq '.[].NetworkSettings.Networks["bridge"].IPAddress'
    # "172.17.0.2"
    docker inspect container_with_bridge | jq -r '.[].NetworkSettings.Networks["bridge"].IPAddress'
    # 172.17.0.2
    docker inspect container_without_bridge | jq -r '.[].NetworkSettings.Networks["bridge"].IPAddress'
    # null
    docker inspect container_without_bridge | jq -r '.[].NetworkSettings.Networks["bridge"].IPAddress // ""'
    # 
    and that would need the adequate comments for explaining the fallback (as i can understand that most people would find that hard to read). i just documented the cases i could think of were the new codes output is the same as the old code with docker < 29.

Should i also change _container_is_running to jq?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also introducing a pipe would mean we should check the error handling:

docker inspect unknown_container | jq -r '.[].NetworkSettings.Networks["bridge"].IPAddress // ""' || echo "FAIL"
#
#error: no such object: unknown_container
docker inspect --format '{{with (index .NetworkSettings "IPAddress")}}{{.}}{{else}}{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}{{end}}' unknown_container || echo "FAIL"
#
#error: no such object: unknown_container
#FAIL

Copy link
Copy Markdown
Member

@polarathene polarathene Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the jq fallback to empty with // "", while the reasoning is good our tests don't seem to use such a fallback when encountering a failure.

# With blank line included (stdout):
$ docker inspect --format '{{ .NetworkSettings.IPAddress }}'

Error: No such object: unknown_container

I'm not sure why your output example differs there with all lowercase 🤔 (Oh, just compared to v29+ where it is lowercase, prior to that it was as my output shows)

that would need the adequate comments for explaining the fallback (as i can understand that most people would find that hard to read).

I'd agree if we had the jq fallback you suggest it would need clarification.

From the looks of it the blank value to stdout probably isn't meaningful to our tests (fail2ban.bats + postscreen.bats that use this utility method), they'd still fail as your shared error output demonstrated with stderr being reported instead.


And also introducing a pipe would mean we should check the error handling:

set -o pipefail will handle that:

$ set -o pipefail
$ docker inspect unknown_container | jq -r '.[].NetworkSettings.Networks["bridge"].IPAddress // ""' || echo "FAIL"
error: no such object: unknown_container

FAIL
$ echo $?
1

That said there's quite a bit of discouragement online for using pipefail broadly/globally, so that's a bit of a bummer.

jq does have an option to output an exit code for invalid input though:

-e / --exit-status: Sets the exit status of jq to

  • 0 if the last output values was neither false nor null
  • 1 if the last output value was either false or null
  • 4 if no valid result was ever produced.
$ docker inspect unknown_container | jq -r --exit-status '.[].NetworkSettings.Networks["bridge"].IPAddress'
error: no such object: unknown_container
$ echo $?
4

Should i also change _container_is_running() to jq?

I don't think that's necessary other than for consistency. My main gripe was the template syntax for docker inspect --format is less intuitive vs the JQ equivalent:

docker inspect --format '{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}' "${TARGET_CONTAINER_NAME}"

vs

docker inspect "${TARGET_CONTAINER_NAME}" | jq -r --exit-status '.[].NetworkSettings.Networks["bridge"].IPAddress'

At least regarding the response is an array of objects, with some nested field access. In this case with jq I've added a bit of extra complexity for the bridge key since it won't work well if there's a hypen in the network name should this change in future (I assume that might be a similar case with your template equivalent syntax having a simpler form?), the jq query could just as easily be .[].NetworkSettings.Networks.bridge.IPAddress.

_container_is_running() is simple enough to grok:

function _container_is_running() {
local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}")
[[ $(docker inspect -f '{{.State.Running}}' "${TARGET_CONTAINER_NAME}") == 'true' ]]
}

vs

[[ $(docker inspect "${TARGET_CONTAINER_NAME}" | jq -re '.[].State.Running') == 'true' ]]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading through the changes and this discussion, I am in favor of not using jq but keeping the changes as they are. They are mostly similar in quality IMHO but what jq gains in terms of lower complexity for its query it loses IMO in terms of error handling and cases with "" etc.

I think the query that this PR introduces is easy enough to understand.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what jq gains in terms of lower complexity for its query it loses IMO in terms of error handling and cases with "" etc.

There is no need to emit a blank output, the default null is fine since the error still can be output via --exit-status (causing the test to fail, while the earlier docker inspect stderr should be captured for the error message).

I am in favor of not using jq but keeping the changes as they are.

I disagree with "changes as they are", but I'll compromise.

We do not need the much longer docker inspect proposed here with two branches based on field detection when the latter is sufficient.

@polarathene polarathene added area/ci kind/bug/fix A fix (PR) for a confirmed bug labels Nov 13, 2025
@polarathene polarathene added this to the v16.0.0 milestone Nov 13, 2025
Copy link
Copy Markdown
Member

@polarathene polarathene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer the jq approach with:

docker inspect "${TARGET_CONTAINER_NAME}" | jq -r --exit-status '.[].NetworkSettings.Networks["bridge"].IPAddress'

I do appreciate the concerns and effort towards replicating the existing behaviour prior to Docker v29 though 👍

I think for the most part it's largely myself and @georglauterbach that work on the test suite, so he may want to weigh in here too, otherwise I think jq approach is preferred unless I've missed addressing any other concern?

Comment thread test/helper/common.bash Outdated
Comment on lines +372 to +376
# use .NetworkSettings.IPAddress when available (docker < 29 and container uses bridge)
# else use .NetworkSettings.Networks.bridge.IPAddress (docker > 29 and container uses bride)
# else empty string
# with newline at the end
docker inspect --format '{{with (index .NetworkSettings "IPAddress")}}{{.}}{{else}}{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}{{end}}' "${TARGET_CONTAINER_NAME}"
Copy link
Copy Markdown
Member

@polarathene polarathene Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the jq fallback to empty with // "", while the reasoning is good our tests don't seem to use such a fallback when encountering a failure.

# With blank line included (stdout):
$ docker inspect --format '{{ .NetworkSettings.IPAddress }}'

Error: No such object: unknown_container

I'm not sure why your output example differs there with all lowercase 🤔 (Oh, just compared to v29+ where it is lowercase, prior to that it was as my output shows)

that would need the adequate comments for explaining the fallback (as i can understand that most people would find that hard to read).

I'd agree if we had the jq fallback you suggest it would need clarification.

From the looks of it the blank value to stdout probably isn't meaningful to our tests (fail2ban.bats + postscreen.bats that use this utility method), they'd still fail as your shared error output demonstrated with stderr being reported instead.


And also introducing a pipe would mean we should check the error handling:

set -o pipefail will handle that:

$ set -o pipefail
$ docker inspect unknown_container | jq -r '.[].NetworkSettings.Networks["bridge"].IPAddress // ""' || echo "FAIL"
error: no such object: unknown_container

FAIL
$ echo $?
1

That said there's quite a bit of discouragement online for using pipefail broadly/globally, so that's a bit of a bummer.

jq does have an option to output an exit code for invalid input though:

-e / --exit-status: Sets the exit status of jq to

  • 0 if the last output values was neither false nor null
  • 1 if the last output value was either false or null
  • 4 if no valid result was ever produced.
$ docker inspect unknown_container | jq -r --exit-status '.[].NetworkSettings.Networks["bridge"].IPAddress'
error: no such object: unknown_container
$ echo $?
4

Should i also change _container_is_running() to jq?

I don't think that's necessary other than for consistency. My main gripe was the template syntax for docker inspect --format is less intuitive vs the JQ equivalent:

docker inspect --format '{{with (index .NetworkSettings.Networks "bridge")}}{{.IPAddress}}{{end}}' "${TARGET_CONTAINER_NAME}"

vs

docker inspect "${TARGET_CONTAINER_NAME}" | jq -r --exit-status '.[].NetworkSettings.Networks["bridge"].IPAddress'

At least regarding the response is an array of objects, with some nested field access. In this case with jq I've added a bit of extra complexity for the bridge key since it won't work well if there's a hypen in the network name should this change in future (I assume that might be a similar case with your template equivalent syntax having a simpler form?), the jq query could just as easily be .[].NetworkSettings.Networks.bridge.IPAddress.

_container_is_running() is simple enough to grok:

function _container_is_running() {
local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}")
[[ $(docker inspect -f '{{.State.Running}}' "${TARGET_CONTAINER_NAME}") == 'true' ]]
}

vs

[[ $(docker inspect "${TARGET_CONTAINER_NAME}" | jq -re '.[].State.Running') == 'true' ]]

@georglauterbach
Copy link
Copy Markdown
Member

Thank you for spottend this @ap-wtioit! It seems like the latest changes with Docker cause havoc wherever I look.. I'll also review this today, please wait with changes (jq vs no jq) until I took a loo :)

Copy link
Copy Markdown
Member

@georglauterbach georglauterbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes work for me. But I'll leave it up to @polarathene to decide whether jq is really necessary.

Comment thread test/helper/common.bash Outdated
Comment thread CHANGELOG.md Outdated
@georglauterbach georglauterbach merged commit 5865f54 into docker-mailserver:master Nov 15, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci kind/bug/fix A fix (PR) for a confirmed bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants