Skip to content

Commit 0db589d

Browse files
authored
feat(operations.docker): add login/logout operations (#1694)
1 parent 5698bf1 commit 0db589d

14 files changed

Lines changed: 269 additions & 1 deletion

src/pyinfra/facts/docker.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,27 @@ def command(self, image_id) -> str:
392392
return (
393393
"docker image history --no-trunc --format '{{{{json .}}}}' {0} 2>&- || true"
394394
).format(image_id)
395+
396+
397+
class DockerAuths(FactBase[list[str]]):
398+
"""
399+
Returns the list of registry servers the current user is authenticated
400+
against, read from ``${DOCKER_CONFIG:-$HOME/.docker}/config.json``.
401+
402+
Returns an empty list if no config file exists or no auths are stored.
403+
"""
404+
405+
@override
406+
def command(self) -> str:
407+
return (
408+
'config="${DOCKER_CONFIG:-$HOME/.docker}/config.json"; '
409+
'[ -r "$config" ] && cat "$config" || echo "{}"'
410+
)
411+
412+
@override
413+
def process(self, output: list[str]) -> list[str]:
414+
try:
415+
data = json.loads("".join(output))
416+
except json.JSONDecodeError:
417+
return []
418+
return list(data.get("auths", {}).keys())

src/pyinfra/operations/docker.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@
66

77
from __future__ import annotations
88

9+
from shlex import quote as shlex_quote
10+
911
from pyinfra import host
10-
from pyinfra.api import operation
12+
from pyinfra.api import MaskString, OperationError, QuoteString, StringCommand, operation
1113
from pyinfra.facts.docker import (
14+
DockerAuths,
1215
DockerContainer,
1316
DockerImage,
1417
DockerNetwork,
1518
DockerPlugin,
1619
DockerVolume,
1720
)
1821

22+
DOCKER_HUB_SERVER = "https://index.docker.io/v1/"
23+
1924
from .util.docker import ContainerSpec, handle_docker, parse_image_reference
2025

2126

@@ -554,3 +559,112 @@ def plugin(
554559
command="remove",
555560
plugin=plugin_name,
556561
)
562+
563+
564+
@operation()
565+
def login(
566+
username: str,
567+
password: str,
568+
server: str | None = None,
569+
force: bool = False,
570+
):
571+
"""
572+
Log in to a Docker registry.
573+
574+
+ username: username to authenticate with
575+
+ password: password to authenticate with
576+
+ server: registry server to log in to (defaults to Docker Hub)
577+
+ force: log in even if ``~/.docker/config.json`` already has an entry for the server
578+
579+
Idempotency is checked against the ``auths`` section of
580+
``${DOCKER_CONFIG:-$HOME/.docker}/config.json``: if the server is already
581+
present, the operation is a no-op. Use ``force=True`` to re-run ``docker
582+
login`` (e.g. after rotating credentials).
583+
584+
The password is piped to ``docker login --password-stdin`` so it is not
585+
exposed on the command line, and is masked in pyinfra's command log.
586+
587+
**Examples:**
588+
589+
.. code:: python
590+
591+
from pyinfra.operations import docker
592+
593+
# Log in to a private registry
594+
docker.login(
595+
name="Log in to private registry",
596+
server="myregistry.io:5000",
597+
username="ci",
598+
password="s3cret",
599+
)
600+
601+
# Log in to Docker Hub
602+
docker.login(
603+
name="Log in to Docker Hub",
604+
username="ci",
605+
password="s3cret",
606+
)
607+
"""
608+
if not username:
609+
raise OperationError("docker.login requires a username")
610+
if not password:
611+
raise OperationError("docker.login requires a password")
612+
613+
target_server = server or DOCKER_HUB_SERVER
614+
615+
if not force:
616+
existing_auths = host.get_fact(DockerAuths)
617+
if target_server in existing_auths:
618+
host.noop(f"Already logged in to Docker registry {target_server}")
619+
return
620+
621+
command_bits: list = [
622+
"printf '%s'",
623+
MaskString(shlex_quote(password)),
624+
"| docker login --username",
625+
QuoteString(username),
626+
"--password-stdin",
627+
]
628+
if server:
629+
command_bits.append(QuoteString(server))
630+
631+
yield StringCommand(*command_bits)
632+
633+
634+
@operation()
635+
def logout(server: str | None = None):
636+
"""
637+
Log out of a Docker registry.
638+
639+
+ server: registry server to log out of (defaults to Docker Hub)
640+
641+
No-ops when the server is not present in
642+
``${DOCKER_CONFIG:-$HOME/.docker}/config.json``.
643+
644+
**Examples:**
645+
646+
.. code:: python
647+
648+
from pyinfra.operations import docker
649+
650+
# Log out of a private registry
651+
docker.logout(
652+
name="Log out of private registry",
653+
server="myregistry.io:5000",
654+
)
655+
656+
# Log out of Docker Hub
657+
docker.logout(name="Log out of Docker Hub")
658+
"""
659+
target_server = server or DOCKER_HUB_SERVER
660+
661+
existing_auths = host.get_fact(DockerAuths)
662+
if target_server not in existing_auths:
663+
host.noop(f"Not logged in to Docker registry {target_server}")
664+
return
665+
666+
command_bits: list = ["docker logout"]
667+
if server:
668+
command_bits.append(QuoteString(server))
669+
670+
yield StringCommand(*command_bits)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"command": "config=\"${DOCKER_CONFIG:-$HOME/.docker}/config.json\"; [ -r \"$config\" ] && cat \"$config\" || echo \"{}\"",
3+
"output": ["not json at all"],
4+
"fact": []
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"command": "config=\"${DOCKER_CONFIG:-$HOME/.docker}/config.json\"; [ -r \"$config\" ] && cat \"$config\" || echo \"{}\"",
3+
"output": ["{}"],
4+
"fact": []
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"command": "config=\"${DOCKER_CONFIG:-$HOME/.docker}/config.json\"; [ -r \"$config\" ] && cat \"$config\" || echo \"{}\"",
3+
"output": [
4+
"{\"auths\": {\"https://index.docker.io/v1/\": {\"auth\": \"xxx\"}, \"myregistry.io:5000\": {\"auth\": \"yyy\"}}}"
5+
],
6+
"fact": ["https://index.docker.io/v1/", "myregistry.io:5000"]
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"kwargs": {
3+
"username": "ci",
4+
"password": "newpw",
5+
"server": "myregistry.io:5000",
6+
"force": true
7+
},
8+
"commands": [
9+
{
10+
"raw": "printf '%s' newpw | docker login --username ci --password-stdin myregistry.io:5000",
11+
"masked": "printf '%s' *** | docker login --username ci --password-stdin myregistry.io:5000"
12+
}
13+
]
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"kwargs": {
3+
"username": "ci",
4+
"password": "s3cret"
5+
},
6+
"facts": {
7+
"docker.DockerAuths": ["https://index.docker.io/v1/"]
8+
},
9+
"commands": [],
10+
"noop_description": "Already logged in to Docker registry https://index.docker.io/v1/"
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"kwargs": {
3+
"username": "ci",
4+
"password": "s3cret",
5+
"server": "myregistry.io:5000"
6+
},
7+
"facts": {
8+
"docker.DockerAuths": ["myregistry.io:5000"]
9+
},
10+
"commands": [],
11+
"noop_description": "Already logged in to Docker registry myregistry.io:5000"
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"kwargs": {
3+
"username": "ci",
4+
"password": "s3cret"
5+
},
6+
"facts": {
7+
"docker.DockerAuths": []
8+
},
9+
"commands": [
10+
{
11+
"raw": "printf '%s' s3cret | docker login --username ci --password-stdin",
12+
"masked": "printf '%s' *** | docker login --username ci --password-stdin"
13+
}
14+
]
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"kwargs": {
3+
"username": "ci",
4+
"password": "s3cret",
5+
"server": "myregistry.io:5000"
6+
},
7+
"facts": {
8+
"docker.DockerAuths": []
9+
},
10+
"commands": [
11+
{
12+
"raw": "printf '%s' s3cret | docker login --username ci --password-stdin myregistry.io:5000",
13+
"masked": "printf '%s' *** | docker login --username ci --password-stdin myregistry.io:5000"
14+
}
15+
]
16+
}

0 commit comments

Comments
 (0)