Skip to content

Commit ab8e6d2

Browse files
committed
test: add anaconda-iso build tests with signed containers
Add anaconda-iso iso build tests with signed containers. The rest of the images can be also added to the test once [1] and [2] are merged [1] osbuild/images#990 [2] osbuild/osbuild#1906 Signed-off-by: Miguel Martín <[email protected]>
1 parent cfb33fd commit ab8e6d2

File tree

3 files changed

+139
-7
lines changed

3 files changed

+139
-7
lines changed

test/test_build.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,20 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload):
113113
password = "password"
114114
kargs = "systemd.journald.forward_to_console=1"
115115

116+
default_ip = testutil.get_ip_from_default_route()
117+
118+
gpg_config = testutil.GPGConfig()
119+
registry_config = testutil.RegistryConfig(local_registry=f"{default_ip}:5000")
120+
container_ref = tc.container_ref
121+
122+
if tc.sign:
123+
container_ref = testutil.sign_container_image(gpg_config, registry_config, container_ref)
124+
116125
# params can be long and the qmp socket (that has a limit of 100ish
117126
# AF_UNIX) is derived from the path
118127
# hash the container_ref+target_arch, but exclude the image_type so that the output path is shared between calls to
119128
# different image type combinations
120-
output_path = shared_tmpdir / format(abs(hash(tc.container_ref + str(tc.target_arch))), "x")
129+
output_path = shared_tmpdir / format(abs(hash(container_ref + str(tc.target_arch))), "x")
121130
output_path.mkdir(exist_ok=True)
122131

123132
# make sure that the test store exists, because podman refuses to start if the source directory for a volume
@@ -164,7 +173,7 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload):
164173
bib_output = bib_output_path.read_text(encoding="utf8")
165174
results.append(ImageBuildResult(
166175
image_type, generated_img, tc.target_arch, tc.osinfo_template,
167-
tc.container_ref, tc.rootfs, username, password,
176+
container_ref, tc.rootfs, username, password,
168177
ssh_keyfile_private_path, kargs, bib_output, journal_output))
169178

170179
# generate new keyfile
@@ -257,15 +266,28 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload):
257266
if tc.local:
258267
cmd.extend(["-v", "/var/lib/containers/storage:/var/lib/containers/storage"])
259268

269+
if tc.sign:
270+
lookaside_config = registry_config.lookaside_config
271+
gpg_pub_key = gpg_config.pub_key
272+
sigstore_dir = registry_config.sigstore_dir
273+
signed_image_args = [
274+
"-v", "/etc/containers/policy.json:/etc/containers/policy.json",
275+
"-v", f"{gpg_pub_key}:{gpg_pub_key}",
276+
"-v", f"{lookaside_config}:{lookaside_config}",
277+
"-v", f"{sigstore_dir}:{sigstore_dir}",
278+
]
279+
cmd.extend(signed_image_args)
280+
260281
cmd.extend([
261282
*creds_args,
262283
build_container,
263-
tc.container_ref,
284+
container_ref,
264285
*types_arg,
265286
*upload_args,
266287
*target_arch_args,
267288
*tc.bib_rootfs_args(),
268289
"--local" if tc.local else "--local=false",
290+
"--tls-verify=false" if tc.sign else "--tls-verify=true"
269291
])
270292

271293
# print the build command for easier tracing
@@ -299,7 +321,7 @@ def del_ami():
299321
for image_type in image_types:
300322
results.append(ImageBuildResult(
301323
image_type, artifact[image_type], tc.target_arch, tc.osinfo_template,
302-
tc.container_ref, tc.rootfs, username, password,
324+
container_ref, tc.rootfs, username, password,
303325
ssh_keyfile_private_path, kargs, bib_output, journal_output, metadata))
304326
yield results
305327

@@ -316,7 +338,7 @@ def del_ami():
316338
img.unlink()
317339
else:
318340
print("does not exist")
319-
subprocess.run(["podman", "rmi", tc.container_ref], check=False)
341+
subprocess.run(["podman", "rmi", container_ref], check=False)
320342
return
321343

322344

test/testcases.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ class TestCase:
2323
# rootfs to use (e.g. ext4), some containers like fedora do not
2424
# have a default rootfs. If unset the container default is used.
2525
rootfs: str = ""
26+
# osinfo_template is a string template describing the OS detected by
27+
# 'osinfo-detect'. It can contain '{arch}' that will be replaced with the
28+
# actual container image arch
29+
osinfo_template: str = ""
30+
# Sign the container_ref and use the new signed image instead of the original one
31+
sign: bool = False
2632

2733
def bib_rootfs_args(self):
2834
if self.rootfs:
@@ -31,7 +37,7 @@ def bib_rootfs_args(self):
3137

3238
def __str__(self):
3339
return ",".join([
34-
attr
40+
f"{name}={attr}"
3541
for name, attr in inspect.getmembers(self)
3642
if not name.startswith("_") and not callable(attr) and attr
3743
])
@@ -68,7 +74,11 @@ def gen_testcases(what): # pylint: disable=too-many-return-statements
6874
if what == "ami-boot":
6975
return [TestCaseCentos(image="ami"), TestCaseFedora(image="ami")]
7076
if what == "anaconda-iso":
71-
return [TestCaseCentos(image="anaconda-iso"), TestCaseFedora(image="anaconda-iso")]
77+
return [
78+
klass(image="anaconda-iso", sign=sign, local=False)
79+
for klass in (TestCaseCentos, TestCaseFedora)
80+
for sign in [True, False]
81+
]
7282
if what == "qemu-boot":
7383
test_cases = [
7484
klass(image=img)

test/testutil.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dataclasses
12
import os
23
import pathlib
34
import platform
@@ -147,3 +148,102 @@ def create_filesystem_customizations(rootfs: str):
147148
"-v", "/var/lib/containers/storage:/var/lib/containers/storage",
148149
"--security-opt", "label=type:unconfined_t",
149150
]
151+
152+
153+
def get_ip_from_default_route():
154+
default_route = subprocess.run([
155+
"ip",
156+
"route",
157+
"list",
158+
"default"
159+
], check=True, capture_output=True).stdout
160+
return default_route.split()[8].decode("utf-8")
161+
162+
163+
@dataclasses.dataclass
164+
class GPGConfig():
165+
email: str = "[email protected]"
166+
pub_key: str = "/etc/pki/rpm-gpg/RPM-GPG-KEY-booc-image-builder"
167+
key_length = "3072"
168+
input: str = f"""
169+
%no-protection
170+
Key-Type: RSA
171+
Key-Length: {key_length}
172+
Key-Usage: sign
173+
Name-Real: Bootc Image Builder
174+
Name-Email: {email}
175+
Expire-Date: 0
176+
"""
177+
178+
179+
@dataclasses.dataclass
180+
class RegistryConfig():
181+
local_registry: str = "localhost:5000"
182+
sigstore_dir: str = "/var/lib/containers/sigstore"
183+
lookaside_config: str = "/etc/containers/registries.d/bib.yaml"
184+
185+
186+
def sign_container_image(gpg_config: GPGConfig, registry_config: RegistryConfig, container_ref):
187+
if not os.path.exists(gpg_config.pub_key):
188+
subprocess.run([
189+
"gpg", "--gen-key", "--batch"
190+
], check=True, capture_output=True,
191+
input=gpg_config.input,
192+
text=True)
193+
194+
subprocess.run([
195+
"gpg",
196+
"--output", gpg_config.pub_key,
197+
"--armor",
198+
"--export",
199+
gpg_config.email
200+
], check=True, capture_output=True)
201+
202+
subprocess.run([
203+
"podman", "image", "trust", "set",
204+
"--pubkeysfile", gpg_config.pub_key,
205+
"--type", "signedBy",
206+
registry_config.local_registry
207+
], check=True, capture_output=True)
208+
209+
registry_lookaside_config = f"""docker:
210+
{registry_config.local_registry}:
211+
lookaside: file:///{registry_config.sigstore_dir}
212+
"""
213+
with open(registry_config.lookaside_config, mode="w", encoding="utf-8") as f:
214+
f.write(registry_lookaside_config)
215+
216+
registry_container_name = subprocess.run([
217+
"podman", "ps", "-a", "--filter", "name=registry", "--format", "{{.Names}}"
218+
], check=True, capture_output=True).stdout.decode("utf-8").strip()
219+
220+
if registry_container_name != "registry":
221+
subprocess.run([
222+
"podman", "run", "-d",
223+
"-p", "5000:5000",
224+
"--restart", "always",
225+
"--name", "registry",
226+
"registry:2"
227+
], check=True, capture_output=True)
228+
229+
registry_container_state = subprocess.run([
230+
"podman", "ps", "-a", "--filter", "name=registry", "--format", "{{.State}}"
231+
], check=True, capture_output=True).stdout.decode("utf-8").strip()
232+
233+
if registry_container_state in ("paused", "exited"):
234+
subprocess.run([
235+
"podman", "start", "registry"
236+
], check=True, capture_output=True)
237+
238+
container_ref_path = container_ref[container_ref.index('/'):]
239+
signed_container_ref = f"{registry_config.local_registry}{container_ref_path}"
240+
subprocess.run([
241+
"skopeo", "copy",
242+
"--dest-tls-verify=false",
243+
"--remove-signatures",
244+
"--sign-by", gpg_config.email,
245+
f"docker://{container_ref}",
246+
f"docker://{signed_container_ref}",
247+
], check=True, capture_output=True)
248+
249+
return signed_container_ref

0 commit comments

Comments
 (0)