Skip to content

Commit d46802a

Browse files
authored
[ISV-5277] Create helper functions for container images (#769)
Signed-off-by: Maurizio Porrato <[email protected]>
1 parent ec74189 commit d46802a

File tree

2 files changed

+122
-28
lines changed

2 files changed

+122
-28
lines changed

operator-pipeline-images/operatorcert/integration/external_tools.py

+94-25
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import tempfile
1010
from os import PathLike
1111
from pathlib import Path
12-
from typing import Mapping, Optional, Sequence, TypeAlias
12+
from typing import Mapping, Optional, Sequence, TypeAlias, IO
1313

1414
LOGGER = logging.getLogger("operator-cert")
1515

@@ -118,18 +118,13 @@ def run_playbook(
118118
run(*command, cwd=self.path)
119119

120120

121-
class Podman:
121+
class RegistryAuthMixin:
122122
"""
123-
Utility class to interact with Podman.
123+
Mixin class to help running the tools in the podman family (podman, buildah, skopeo)
124+
with a given set of authentication credentials for the container registries
124125
"""
125126

126127
def __init__(self, auth: Optional[Mapping[str, tuple[str, str]]] = None):
127-
"""
128-
Initialize the Podman instance
129-
130-
Args:
131-
auth: The authentication credentials for registries
132-
"""
133128
self._auth = {
134129
"auths": {
135130
registry: {
@@ -142,26 +137,52 @@ def __init__(self, auth: Optional[Mapping[str, tuple[str, str]]] = None):
142137
}
143138
}
144139

145-
def _run(self, *args: CommandArg) -> None:
140+
def save_auth(self, dest_file: IO[str]) -> None:
141+
"""
142+
Dump the auth credentials to a json file
143+
Args:
144+
dest_file: destination json file
145+
"""
146+
json.dump(self._auth, dest_file)
147+
148+
def run(self, *command: CommandArg) -> None:
149+
"""
150+
Run the given command with the REGISTRY_AUTH_FILE environment variable pointing
151+
to a temporary file containing a json representation of the credentials in a format
152+
compatible with podman, buildah and skopeo
153+
Args:
154+
*command: command line to execute
155+
"""
156+
157+
if self._auth["auths"]:
158+
with tempfile.NamedTemporaryFile(
159+
mode="w",
160+
encoding="utf-8",
161+
suffix=".json",
162+
delete=True,
163+
delete_on_close=False,
164+
) as tmp:
165+
self.save_auth(tmp)
166+
tmp.close()
167+
LOGGER.debug("Using auth file: %s", tmp.name)
168+
run(*command, env={"REGISTRY_AUTH_FILE": tmp.name})
169+
else:
170+
run(*command)
171+
172+
173+
class Podman(RegistryAuthMixin):
174+
"""
175+
Utility class to interact with Podman.
176+
"""
177+
178+
def run(self, *args: CommandArg) -> None:
146179
"""
147180
Run a podman subcommand
148181
149182
Args:
150183
*args: The podman subcommand and its arguments
151184
"""
152-
command: list[CommandArg] = ["podman"]
153-
command.extend(args)
154-
with tempfile.NamedTemporaryFile(
155-
mode="w",
156-
encoding="utf-8",
157-
suffix=".json",
158-
delete=True,
159-
delete_on_close=False,
160-
) as tmp:
161-
json.dump(self._auth, tmp)
162-
tmp.close()
163-
LOGGER.debug("Using podman auth file: %s", tmp.name)
164-
run(*command, env={"REGISTRY_AUTH_FILE": tmp.name})
185+
super().run("podman", *args)
165186

166187
def build(
167188
self,
@@ -185,7 +206,7 @@ def build(
185206
command.extend(["-f", containerfile])
186207
if extra_args:
187208
command.extend(extra_args)
188-
self._run(*command)
209+
self.run(*command)
189210

190211
def push(self, image: str) -> None:
191212
"""
@@ -194,4 +215,52 @@ def push(self, image: str) -> None:
194215
Args:
195216
image: The name of the image to push.
196217
"""
197-
self._run("push", image)
218+
self.run("push", image)
219+
220+
221+
class Skopeo(RegistryAuthMixin):
222+
"""
223+
Utility class to interact with Skopeo.
224+
"""
225+
226+
def run(self, *args: CommandArg) -> None:
227+
"""
228+
Run a skopeo subcommand
229+
230+
Args:
231+
*args: The skopeo subcommand and its arguments
232+
"""
233+
super().run("skopeo", *args)
234+
235+
def copy(
236+
self,
237+
from_image: str,
238+
to_image: str,
239+
extra_args: Optional[Sequence[CommandArg]] = None,
240+
) -> None:
241+
"""
242+
Copy a container image
243+
244+
Args:
245+
from_image: source container image ref
246+
to_image: destination image ref
247+
extra_args: optional args to add to the skopeo command line
248+
"""
249+
250+
command: list[CommandArg] = ["copy", from_image, to_image]
251+
if extra_args:
252+
command.extend(extra_args)
253+
self.run(*command)
254+
255+
def delete(
256+
self,
257+
image: str,
258+
) -> None:
259+
"""
260+
Delete a container image
261+
262+
Args:
263+
image: container image ref
264+
"""
265+
266+
self.run("delete", image)

operator-pipeline-images/tests/integration/test_external_tools.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import subprocess
22
from pathlib import Path
3-
from unittest.mock import ANY, MagicMock, patch
3+
from unittest.mock import MagicMock, patch
44

55
import pytest
6-
from operatorcert.integration.external_tools import Ansible, Podman, Secret, run
6+
from operatorcert.integration.external_tools import Ansible, Podman, Secret, Skopeo, run
77

88

99
def test_Secret() -> None:
@@ -81,7 +81,6 @@ def test_Podman_build(mock_run: MagicMock) -> None:
8181
"-f",
8282
Path("/foo/Dockerfile"),
8383
"-q",
84-
env=ANY,
8584
)
8685

8786

@@ -92,3 +91,29 @@ def test_Podman_push(mock_run: MagicMock) -> None:
9291
mock_run.assert_called_once()
9392
assert mock_run.mock_calls[0].args == ("podman", "push", "quay.io/foo/bar")
9493
assert "REGISTRY_AUTH_FILE" in mock_run.mock_calls[0].kwargs["env"]
94+
95+
96+
@patch("operatorcert.integration.external_tools.run")
97+
def test_Skopeo_copy(mock_run: MagicMock) -> None:
98+
skopeo = Skopeo()
99+
skopeo.copy(
100+
"docker://quay.io/foo/bar:abc", "docker://quay.io/foo/baz:latest", ["-q"]
101+
)
102+
mock_run.assert_called_once_with(
103+
"skopeo",
104+
"copy",
105+
"docker://quay.io/foo/bar:abc",
106+
"docker://quay.io/foo/baz:latest",
107+
"-q",
108+
)
109+
110+
111+
@patch("operatorcert.integration.external_tools.run")
112+
def test_Skopeo_delete(mock_run: MagicMock) -> None:
113+
skopeo = Skopeo()
114+
skopeo.delete("docker://quay.io/foo/bar:abc")
115+
mock_run.assert_called_once_with(
116+
"skopeo",
117+
"delete",
118+
"docker://quay.io/foo/bar:abc",
119+
)

0 commit comments

Comments
 (0)