Skip to content

Commit d09c4a5

Browse files
committed
Add support to analyze OCI image layers with Podman.
Signed-off-by: Tobias Wolf <wolf@b1-systems.de> On-behalf-of: SAP <tobias.wolf@sap.com>
1 parent ba4cfd4 commit d09c4a5

File tree

4 files changed

+280
-4
lines changed

4 files changed

+280
-4
lines changed

src/gardenlinux/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@
167167
GLVD_BASE_URL = "https://security.gardenlinux.org/v1"
168168

169169
PODMAN_CONNECTION_MAX_IDLE_SECONDS = 3
170+
PODMAN_FS_CHANGE_ADDED = "added"
171+
PODMAN_FS_CHANGE_DELETED = "deleted"
172+
PODMAN_FS_CHANGE_MODIFIED = "modified"
173+
PODMAN_FS_CHANGE_UNSUPPORTED = "unsupported"
170174

171175
# https://github.com/gardenlinux/gardenlinux/issues/3044
172176
# Empty string is the 'legacy' variant with traditional root fs and still needed/supported

src/gardenlinux/oci/image.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
OCI podman
5+
"""
6+
7+
import logging
8+
from urllib.parse import urlencode
9+
from typing import Any, Dict, List, Optional
10+
11+
from podman.domain.images import Image as _Image
12+
13+
from ..constants import (
14+
PODMAN_FS_CHANGE_ADDED,
15+
PODMAN_FS_CHANGE_DELETED,
16+
PODMAN_FS_CHANGE_MODIFIED,
17+
PODMAN_FS_CHANGE_UNSUPPORTED,
18+
)
19+
from .podman_context import PodmanContext
20+
from .podman_object_context import PodmanObjectContext
21+
22+
PODMAN_CHANGES_KINDS = {
23+
0: PODMAN_FS_CHANGE_MODIFIED,
24+
1: PODMAN_FS_CHANGE_ADDED,
25+
2: PODMAN_FS_CHANGE_DELETED,
26+
}
27+
28+
29+
class Image(PodmanObjectContext):
30+
"""
31+
Podman image class with extended API features support.
32+
33+
:author: Garden Linux Maintainers
34+
:copyright: Copyright 2024 SAP SE
35+
:package: gardenlinux
36+
:subpackage: oci
37+
:since: 1.0.0
38+
:license: https://www.apache.org/licenses/LICENSE-2.0
39+
Apache License, Version 2.0
40+
"""
41+
42+
def __init__(self, image: _Image, logger: Optional[logging.Logger] = None):
43+
"""
44+
Constructor __init__(Image)
45+
46+
:since: 1.0.0
47+
"""
48+
49+
PodmanObjectContext.__init__(self, logger)
50+
self._image_id = image.id
51+
52+
@property
53+
def id(self) -> str:
54+
"""
55+
podman-py.readthedocs.io: Returns the identifier for the object.
56+
57+
:return: (str) Identifier for the object
58+
:since: 1.0.0
59+
"""
60+
61+
return self._image_id # type: ignore[no-any-return]
62+
63+
@property
64+
@PodmanContext.wrap
65+
def labels(self, podman: PodmanContext) -> Dict[str, str]:
66+
"""
67+
podman-py.readthedocs.io: Returns the identifier for the object.
68+
69+
:return: (str) Identifier for the object
70+
:since: 1.0.0
71+
"""
72+
73+
return self._get(podman=podman).labels # type: ignore[no-any-return]
74+
75+
@property
76+
@PodmanContext.wrap
77+
def layer_image_ids(self, podman: PodmanContext) -> List[str]:
78+
"""
79+
Returns the podman image IDs of all parent layers.
80+
81+
:param podman: Podman context
82+
83+
:return: (list) Podman layer image IDs
84+
:since: 1.0.0
85+
"""
86+
87+
return [
88+
image_data["Id"]
89+
for image_data in self.history(podman=podman)
90+
if len(image_data["Id"]) == 64
91+
]
92+
93+
def __getattr__(
94+
self,
95+
name: str,
96+
) -> Any:
97+
"""
98+
python.org: Called when an attribute lookup has not found the attribute in
99+
the usual places (i.e. it is not an instance attribute nor is it found in the
100+
class tree for self).
101+
102+
:param name: Attribute name
103+
104+
:return: (mixed) Attribute
105+
:since: 1.0.0
106+
"""
107+
108+
@PodmanObjectContext.wrap
109+
def wrapped_context(podman: PodmanContext, *args: Any, **kwargs: Any) -> Any:
110+
"""
111+
Wrapping function to use the podman context.
112+
"""
113+
114+
py_attr = getattr(self._get(podman=podman), name)
115+
return py_attr(*args, **kwargs)
116+
117+
return wrapped_context
118+
119+
def _get(self, podman: PodmanContext) -> _Image:
120+
"""
121+
Returns the underlying podman image object.
122+
123+
:param podman: Podman context
124+
125+
:return: (podman.domains.images.Image) Podman image object
126+
:since: 1.0.0
127+
"""
128+
129+
return podman.images.get(self._image_id)
130+
131+
@PodmanContext.wrap
132+
def get_filesystem_changes(
133+
self, podman: PodmanContext, parent_layer_image_id: Optional[str] = None
134+
) -> Dict[str, List[str]]:
135+
"""
136+
Returns the underlying podman image object.
137+
138+
:param podman: Podman context
139+
140+
:return: (_Image) Podman image object
141+
:since: 1.0.0
142+
"""
143+
144+
changes: Dict[str, List[str]] = {
145+
PODMAN_FS_CHANGE_ADDED: [],
146+
PODMAN_FS_CHANGE_DELETED: [],
147+
PODMAN_FS_CHANGE_MODIFIED: [],
148+
PODMAN_FS_CHANGE_UNSUPPORTED: [],
149+
}
150+
151+
query = ""
152+
153+
if parent_layer_image_id is not None:
154+
query = urlencode({"parent": parent_layer_image_id})
155+
156+
resp = self._raw_request(
157+
"get", f"/images/{self._image_id}/changes?{query}", podman=podman
158+
)
159+
160+
resp.raise_for_status()
161+
162+
for entry in resp.json():
163+
changes[
164+
PODMAN_CHANGES_KINDS.get(entry["Kind"], PODMAN_FS_CHANGE_UNSUPPORTED)
165+
].append(entry["Path"])
166+
167+
return changes

src/gardenlinux/oci/podman.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from pathlib import Path
1212
from typing import Any, Dict, List, Optional
1313

14+
1415
from ..logger import LoggerSetup
16+
from .image import Image
1517
from .podman_context import PodmanContext
1618

1719

@@ -116,12 +118,12 @@ def build_and_save_oci_archive(
116118
return {oci_archive_file_name.name: image_id}
117119

118120
@PodmanContext.wrap
119-
def get_image_id(
121+
def get_image(
120122
self,
121123
container: str,
122124
podman: PodmanContext,
123125
oci_tag: Optional[str] = None,
124-
) -> str:
126+
) -> Image:
125127
"""
126128
Returns the Podman image ID for a given OCI container tag.
127129
@@ -136,7 +138,22 @@ def get_image_id(
136138
else:
137139
container_tag += f":{oci_tag}"
138140

139-
image = podman.images.get(container_tag)
141+
return Image(podman.images.get(container_tag))
142+
143+
@PodmanContext.wrap
144+
def get_image_id(
145+
self,
146+
container: str,
147+
podman: PodmanContext,
148+
oci_tag: Optional[str] = None,
149+
) -> str:
150+
"""
151+
Returns the Podman image ID for a given OCI container tag.
152+
153+
:since: 1.0.0
154+
"""
155+
156+
image = self.get_image(container, oci_tag=oci_tag, podman=podman)
140157
return image.id # type: ignore[no-any-return]
141158

142159
@PodmanContext.wrap
@@ -202,7 +219,7 @@ def pull(
202219
kwargs["tag"] = oci_tag
203220

204221
image = podman.images.pull(container, **kwargs)
205-
return image.id
222+
return image.id # type: ignore[no-any-return]
206223

207224
@PodmanContext.wrap
208225
def push(
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
OCI podman context
5+
"""
6+
7+
import logging
8+
from functools import wraps
9+
from typing import Any, Optional
10+
11+
from requests import Response
12+
13+
from ..logger import LoggerSetup
14+
from .podman_context import PodmanContext
15+
16+
17+
class PodmanObjectContext(object):
18+
"""
19+
Podman object context handles access to the podman context for API calls.
20+
21+
:author: Garden Linux Maintainers
22+
:copyright: Copyright 2024 SAP SE
23+
:package: gardenlinux
24+
:subpackage: oci
25+
:since: 1.0.0
26+
:license: https://www.apache.org/licenses/LICENSE-2.0
27+
Apache License, Version 2.0
28+
"""
29+
30+
def __init__(self, logger: Optional[logging.Logger] = None):
31+
"""
32+
Constructor __init__(PodmanObjectContext)
33+
34+
:since: 1.0.0
35+
"""
36+
37+
if logger is None or not logger.hasHandlers():
38+
logger = LoggerSetup.get_logger("gardenlinux.oci")
39+
40+
self._logger = logger
41+
42+
@PodmanContext.wrap
43+
def _raw_request(
44+
self,
45+
method: str,
46+
path_and_parameters: str,
47+
podman: PodmanContext,
48+
**kwargs: Any,
49+
) -> Response:
50+
"""
51+
Returns the podman API response for the request given.
52+
53+
:param method: Podman API method
54+
:param path_and_parameters: Podman API path and query parameters
55+
:param podman: Podman context
56+
57+
:return: (Response) Podman API response
58+
:since: 1.0.0
59+
"""
60+
61+
method_callable = getattr(podman.api, method)
62+
return method_callable(path_and_parameters, **kwargs) # type: ignore[no-any-return]
63+
64+
@staticmethod
65+
def wrap(f: Any) -> Any:
66+
"""
67+
Wraps the given function to provide access to a podman client.
68+
69+
:since: 1.0.0
70+
"""
71+
72+
@wraps(f)
73+
@PodmanContext.wrap
74+
def decorator(*args: Any, **kwargs: Any) -> Any:
75+
"""
76+
Decorator for wrapping a function or method with a call context.
77+
"""
78+
79+
podman = kwargs.get("podman")
80+
81+
if podman is None:
82+
raise RuntimeError("Podman context not ready")
83+
84+
del kwargs["podman"]
85+
86+
return f(podman=podman, *args, **kwargs)
87+
88+
return decorator

0 commit comments

Comments
 (0)