Skip to content

Commit 20ef0d9

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 19acee6 commit 20ef0d9

File tree

4 files changed

+268
-8
lines changed

4 files changed

+268
-8
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
echo "deb $sources_url/ /" | sudo tee /etc/apt/sources.list.d/devel-kubic-libcontainers-unstable.list
9494
curl -fsSL $key_url | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/devel_kubic_libcontainers_unstable.gpg > /dev/null
9595
sudo apt update
96-
sudo apt install -y podman
96+
sudo apt install -y podman skopeo
9797
- name: Install python test deps
9898
run: |
9999
# make sure test deps are available for root

test/test_build.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
if platform.system() == "Linux" and platform.machine() == "x86_64" and not testutil.has_x86_64_v3_cpu():
3131
pytest.skip("need x86_64-v3 capable CPU", allow_module_level=True)
3232

33+
registry_port = testutil.get_free_port()
3334

3435
class ImageBuildResult(NamedTuple):
3536
img_type: str
@@ -113,11 +114,20 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload):
113114
password = "password"
114115
kargs = "systemd.journald.forward_to_console=1"
115116

117+
container_ref = tc.container_ref
118+
119+
# We cannot use localhost as we need to access the registry from both
120+
# the host system and the bootc-image-builder container.
121+
default_ip = testutil.get_ip_from_default_route()
122+
local_registry = f"{default_ip}:{registry_port}"
123+
if tc.sign:
124+
container_ref = testutil.get_signed_container_ref(local_registry, tc.container_ref)
125+
116126
# params can be long and the qmp socket (that has a limit of 100ish
117127
# AF_UNIX) is derived from the path
118128
# hash the container_ref+target_arch, but exclude the image_type so that the output path is shared between calls to
119129
# different image type combinations
120-
output_path = shared_tmpdir / format(abs(hash(tc.container_ref + str(tc.target_arch))), "x")
130+
output_path = shared_tmpdir / format(abs(hash(container_ref + str(tc.target_arch))), "x")
121131
output_path.mkdir(exist_ok=True)
122132

123133
# make sure that the test store exists, because podman refuses to start if the source directory for a volume
@@ -164,7 +174,7 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload):
164174
bib_output = bib_output_path.read_text(encoding="utf8")
165175
results.append(ImageBuildResult(
166176
image_type, generated_img, tc.target_arch, tc.osinfo_template,
167-
tc.container_ref, tc.rootfs, username, password,
177+
container_ref, tc.rootfs, username, password,
168178
ssh_keyfile_private_path, kargs, bib_output, journal_output))
169179

170180
# generate new keyfile
@@ -257,15 +267,35 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload):
257267
if tc.local:
258268
cmd.extend(["-v", "/var/lib/containers/storage:/var/lib/containers/storage"])
259269

270+
if tc.sign:
271+
registry_conf = testutil.RegistryConf()
272+
registry_conf.local_registry = local_registry
273+
registry_conf.base_dir = output_path
274+
gpg_conf = testutil.GPGConf()
275+
gpg_conf.base_dir = output_path
276+
testutil.sign_container_image(gpg_conf, registry_conf, tc.container_ref)
277+
policy_file = registry_conf.policy_file
278+
lookaside_conf_file = registry_conf.lookaside_conf_file
279+
sigstore_dir = registry_conf.sigstore_dir
280+
pub_key_file = gpg_conf.pub_key_file
281+
signed_image_args = [
282+
"-v", f"{policy_file}:/etc/containers/policy.json",
283+
"-v", f"{lookaside_conf_file}:/etc/containers/registries.d/bib-tests.yaml",
284+
"-v", f"{sigstore_dir}:{sigstore_dir}",
285+
"-v", f"{pub_key_file}:{pub_key_file}",
286+
]
287+
cmd.extend(signed_image_args)
288+
260289
cmd.extend([
261290
*creds_args,
262291
build_container,
263-
tc.container_ref,
292+
container_ref,
264293
*types_arg,
265294
*upload_args,
266295
*target_arch_args,
267296
*tc.bib_rootfs_args(),
268297
"--local" if tc.local else "--local=false",
298+
"--tls-verify=false" if tc.sign else "--tls-verify=true"
269299
])
270300

271301
# print the build command for easier tracing
@@ -299,7 +329,7 @@ def del_ami():
299329
for image_type in image_types:
300330
results.append(ImageBuildResult(
301331
image_type, artifact[image_type], tc.target_arch, tc.osinfo_template,
302-
tc.container_ref, tc.rootfs, username, password,
332+
container_ref, tc.rootfs, username, password,
303333
ssh_keyfile_private_path, kargs, bib_output, journal_output, metadata))
304334
yield results
305335

@@ -316,7 +346,7 @@ def del_ami():
316346
img.unlink()
317347
else:
318348
print("does not exist")
319-
subprocess.run(["podman", "rmi", tc.container_ref], check=False)
349+
subprocess.run(["podman", "rmi", container_ref], check=False)
320350
return
321351

322352

test/testcases.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ 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+
# Sign the container_ref and use the new signed image instead of the original one
27+
sign: bool = False
2628

2729
def bib_rootfs_args(self):
2830
if self.rootfs:
@@ -31,7 +33,7 @@ def bib_rootfs_args(self):
3133

3234
def __str__(self):
3335
return ",".join([
34-
attr
36+
f"{name}={attr}"
3537
for name, attr in inspect.getmembers(self)
3638
if not name.startswith("_") and not callable(attr) and attr
3739
])
@@ -68,7 +70,11 @@ def gen_testcases(what): # pylint: disable=too-many-return-statements
6870
if what == "ami-boot":
6971
return [TestCaseCentos(image="ami"), TestCaseFedora(image="ami")]
7072
if what == "anaconda-iso":
71-
return [TestCaseCentos(image="anaconda-iso"), TestCaseFedora(image="anaconda-iso")]
73+
return [
74+
TestCaseFedora(image="anaconda-iso", sign=True),
75+
TestCaseCentos(image="anaconda-iso"),
76+
TestCaseFedora(image="anaconda-iso"),
77+
]
7278
if what == "qemu-boot":
7379
test_cases = [
7480
klass(image=img)

test/testutil.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dataclasses
2+
import json
13
import os
24
import pathlib
35
import platform
@@ -147,3 +149,225 @@ def create_filesystem_customizations(rootfs: str):
147149
"-v", "/var/lib/containers/storage:/var/lib/containers/storage",
148150
"--security-opt", "label=type:unconfined_t",
149151
]
152+
153+
154+
def get_ip_from_default_route():
155+
default_route = subprocess.run([
156+
"ip",
157+
"route",
158+
"list",
159+
"default"
160+
], check=True, capture_output=True, text=True).stdout
161+
return default_route.split()[8]
162+
163+
164+
@dataclasses.dataclass
165+
class GPGConf:
166+
_key_params_tmpl: str = """
167+
%no-protection
168+
Key-Type: RSA
169+
Key-Length: {key_length}
170+
Key-Usage: sign
171+
Name-Real: Bootc Image Builder Tests
172+
Name-Email: {email}
173+
Expire-Date: 0
174+
"""
175+
_base_dir: str = "/tmp/bib-tests"
176+
_email: str = "[email protected]"
177+
_key_length: str = "3072"
178+
home_dir: str = f"{_base_dir}/.gnupg"
179+
pub_key_file: str = f"{_base_dir}/GPG-KEY-bib-tests"
180+
key_params: str = _key_params_tmpl.format(key_length=_key_length, email=_email)
181+
182+
@property
183+
def base_dir(self) -> str:
184+
return self._base_dir
185+
186+
@base_dir.setter
187+
def base_dir(self, base_dir: str) -> None:
188+
self._base_dir = base_dir
189+
self.home_dir = f"{base_dir}/.gnupg"
190+
self.pub_key_file = f"{base_dir}/GPG-KEY-bib-tests"
191+
192+
@property
193+
def email(self) -> str:
194+
return self._email
195+
196+
@email.setter
197+
def email(self, email: str) -> None:
198+
self._email = email
199+
self.key_params = self._key_params_tmpl.format(
200+
email=self._email,
201+
key_length=self._key_length
202+
)
203+
204+
@property
205+
def key_length(self) -> str:
206+
return self._key_length
207+
208+
@key_length.setter
209+
def key_length(self, length: str) -> None:
210+
self._key_length = length
211+
self._key_params = self._key_params_tmpl.format(
212+
email=self._email,
213+
key_length=self._key_length
214+
)
215+
216+
def gpg_gen_key(gpg_conf: GPGConf):
217+
if os.path.exists(gpg_conf.home_dir):
218+
return
219+
220+
os.makedirs(gpg_conf.home_dir, mode=0o700, exist_ok=False)
221+
222+
subprocess.run(
223+
["gpg", "--gen-key", "--batch"],
224+
check=True, env={"GNUPGHOME": gpg_conf.home_dir},
225+
input=gpg_conf.key_params,
226+
text=True)
227+
228+
subprocess.run(
229+
["gpg", "--output", gpg_conf.pub_key_file,
230+
"--armor", "--export", gpg_conf.email],
231+
check=True, env={"GNUPGHOME": gpg_conf.home_dir})
232+
233+
234+
@dataclasses.dataclass
235+
class RegistryConf():
236+
_lookaside_conf_tmpl: str = """
237+
docker:
238+
{local_registry}:
239+
lookaside: file:///{sigstore_dir}
240+
"""
241+
_local_registry: str = "localhost:5000"
242+
_base_dir: str = "/tmp/bib-tests"
243+
_sigstore_dir: str = f"{_base_dir}/sigstore"
244+
_registries_d_dir: str = f"{_base_dir}/registries.d"
245+
policy_file: str = f"{_base_dir}/policy.json"
246+
lookaside_conf_file: str = f"{_registries_d_dir}/lookaside.yml"
247+
lookaside_conf: str = _lookaside_conf_tmpl.format(
248+
local_registry=_local_registry,
249+
sigstore_dir=_sigstore_dir
250+
)
251+
252+
@property
253+
def local_registry(self) -> str:
254+
return self._local_registry
255+
256+
@local_registry.setter
257+
def local_registry(self, registry: str) -> None:
258+
self._local_registry = registry
259+
self.lookaside_conf = self._lookaside_conf_tmpl.format(
260+
local_registry=self._local_registry,
261+
sigstore_dir=self._sigstore_dir
262+
)
263+
264+
@property
265+
def base_dir(self) -> str:
266+
return self._base_dir
267+
268+
@base_dir.setter
269+
def base_dir(self, base_dir: str) -> None:
270+
self._base_dir = base_dir
271+
self._sigstore_dir = f"{base_dir}/sigstore"
272+
self.policy_file = f"{base_dir}/policy.json"
273+
self._registries_d_dir = f"{base_dir}/registries.d"
274+
self.lookaside_conf_file = f"{self._registries_d_dir}/lookaside.yaml"
275+
self.lookaside_conf = self._lookaside_conf_tmpl.format(
276+
local_registry=self._local_registry,
277+
sigstore_dir=self._sigstore_dir
278+
)
279+
280+
@property
281+
def sigstore_dir(self) -> str:
282+
return self._sigstore_dir
283+
284+
@sigstore_dir.setter
285+
def sigstore_dir(self, sigstore_dir: str) -> None:
286+
self._sigstore_dir = sigstore_dir
287+
self.lookaside_conf = self._lookaside_conf_tmpl.format(
288+
local_registry=self._local_registry,
289+
sigstore_dir=self._sigstore_dir
290+
)
291+
292+
@property
293+
def registries_d_dir(self) -> str:
294+
return self._registries_d_dir
295+
296+
@registries_d_dir.setter
297+
def registries_d_dir(self, dir: str) -> None:
298+
self._registries_d_dir= dir
299+
self.lookaside_conf_file = f"{self._registries_d_dir}/lookaside.yaml"
300+
301+
def ensure_registry(registry_conf: RegistryConf):
302+
registry_container_name = subprocess.run([
303+
"podman", "ps", "-a", "--filter", "name=registry", "--format", "{{.Names}}"
304+
], check=True, capture_output=True, text=True).stdout.strip()
305+
306+
registry_port = registry_conf.local_registry.split(":")[1]
307+
if registry_container_name != "registry":
308+
subprocess.run([
309+
"podman", "run", "-d",
310+
"-p", f"{registry_port}:5000",
311+
"--restart", "always",
312+
"--name", "registry",
313+
"registry:2"
314+
], check=True)
315+
316+
registry_container_state = subprocess.run([
317+
"podman", "ps", "-a", "--filter", "name=registry", "--format", "{{.State}}"
318+
], check=True, capture_output=True, text=True).stdout.strip()
319+
320+
if registry_container_state in ("paused", "exited"):
321+
subprocess.run([
322+
"podman", "start", "registry"
323+
], check=True)
324+
325+
326+
def get_signed_container_ref(local_registry: str, container_ref: str):
327+
container_ref_path = container_ref[container_ref.index('/'):]
328+
return f"{local_registry}{container_ref_path}"
329+
330+
331+
def sign_container_image(gpg_conf: GPGConf, registry_conf: RegistryConf, container_ref):
332+
gpg_gen_key(gpg_conf)
333+
ensure_registry(registry_conf)
334+
local_registry = registry_conf.local_registry
335+
policy_file = registry_conf.policy_file
336+
registries_d_dir = registry_conf.registries_d_dir
337+
lookaside_conf_file = registry_conf.lookaside_conf_file
338+
lookaside_conf = registry_conf.lookaside_conf
339+
pub_key_file = gpg_conf.pub_key_file
340+
home_dir = gpg_conf.home_dir
341+
email = gpg_conf.email
342+
registry_policy = {
343+
"default": [{"type": "insecureAcceptAnything"}],
344+
"transports": {
345+
"docker": {
346+
f"{local_registry}": [
347+
{
348+
"type": "signedBy",
349+
"keyType": "GPGKeys",
350+
"keyPath": f"{pub_key_file}"
351+
}
352+
]
353+
},
354+
"docker-daemon": {
355+
"": [{"type": "insecureAcceptAnything"}]
356+
}
357+
}
358+
}
359+
with open(policy_file, mode="w", encoding="utf-8") as f:
360+
f.write(json.dumps(registry_policy))
361+
362+
os.makedirs(os.path.dirname(lookaside_conf_file), mode=0o700, exist_ok=False)
363+
with open(lookaside_conf_file, mode="w", encoding="utf-8") as f:
364+
f.write(lookaside_conf)
365+
366+
signed_container_ref = get_signed_container_ref(local_registry, container_ref)
367+
subprocess.run([
368+
"skopeo", "--registries.d", f"{registries_d_dir}",
369+
"copy", "--dest-tls-verify=false", "--remove-signatures",
370+
"--sign-by", email,
371+
f"docker://{container_ref}",
372+
f"docker://{signed_container_ref}",
373+
], check=True, env={"GNUPGHOME": home_dir})

0 commit comments

Comments
 (0)