Skip to content

Commit c50c3ad

Browse files
committed
port OCI module back from python_gardenlinux_cli
1 parent 6714f06 commit c50c3ad

19 files changed

+1222
-27
lines changed

README.md

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,75 @@
44
![security check](https://github.com/gardenlinux/parse_features_lib/actions/workflows/bandit.yml/badge.svg)
55

66
# Parse features lib
7-
This library helps you to work with the gardenlinux/features folder. It parses all info.yamls and builds a tree.
87

9-
Features (planned):
10-
* validate CNAMEs
11-
* validate info.yamls
12-
* Deduct dependencies from cname_base
8+
This library includes tooling to build and distribute [Garden Linux](https://github.com/gardenlinux/gardenlinux).
9+
10+
Features:
11+
12+
- compare APT repositories
13+
- parse features
14+
- parse flavors
15+
- push OCI artifacts to a registry
1316

1417
## Quickstart
18+
19+
### Example: get a list of features for a given cname
20+
1521
**Inclusion via poetry**:
1622

17-
`parse_features_lib = { git = "https://github.com/gardenlinux/parse_features_lib", rev="main" }`
23+
`gardenlinux = { git = "https://github.com/gardenlinux/python_gardenlinux_lib", rev="0.6.0" }`
24+
1825
```python
19-
import parse_features_lib
26+
import gardenlinux.features as features
2027

2128
if __name__ == "__main__":
22-
# Step 1: parse the "features directory" and get the full graph containing all features
23-
all_features = parse_features_lib.read_feature_files("features")
29+
# Step 1: parse the "features directory" and get a list of features
30+
features_list = features.filter_as_list("aws-gardener_prod")
31+
print(features_list)
32+
```
33+
34+
## Developer Documentation
35+
36+
The library is documented with docstrings, which are used to generate the developer documentation available [here](https://gardenlinux.github.io/python-gardenlinux-lib/).
37+
38+
## Push OCI artifacts to a registry
39+
40+
this tool helps you to push oci artifacts.
2441

25-
# Step 2: supply desired features and get all their dependencies
26-
dependencies = parse_features_lib.filter_graph(all_features, {"gardener", "_prod", "server", "ociExample"})
42+
### Installation
2743

28-
# Step 3: play with the retrieved data.
29-
for feature, info in dependencies.nodes(data="content"):
30-
if "oci_artifacts" in info:
31-
print(feature, info["oci_artifacts"])
44+
```bash
45+
git clone https://github.com/gardenlinux/python-gardenlinux-lib.git
46+
mkdir venv
47+
python -m venv venv
48+
source venv/bin/activate.sh
49+
poetry install
50+
gl-oci --help
3251
```
3352

34-
## Developer Documentation
35-
The library is documented with docstrings, which are used to generate the developer documentation available [here](https://gardenlinux.github.io/python-gardenlinux-lib/).
53+
### Usage
54+
55+
The process to push a Gardenlinux build-output folder to an OCI registry is split into two steps: In the first step all files are pushed to the registry and a manifest that includes all those pushed files (layers) is created and pushed as well. An index entry that links to this manifest is created offline and written to a local file but not pushed to any index. This push to an index can be done in the second step where the local file containing the index entry is read and pushed to an index. The seperation into two steps was done because pushing of manifests takes long and writes to dedicated resources (possible to run in parallel). Updating the index on the other hand is quick but writes to a share resource (not possible to run in parallel). By splitting the process up into two steps it is possible to run the slow part in parallel and the quick part sequentially.
56+
57+
#### 1. Push layers + manifest
58+
59+
To push layers you have to supply the directory with the build outputs `--dir`. Also you have to supply cname (`--cname`), architecture `--arch` and version `--version` of the build. This information will be included in the manifest. You have to supply an endpoint where the artifacts shall be pushed to `--container`, for example `ghcr.io/gardenlinux/gardenlinux`. You can disable enforced HTTPS connections to your registry with `--insecure True`. You can supply `--cosign_file <filename>` if you want to have the hash saved in `<filename>`. This can be handy to read the hash later to sign the manifest with cosign. With `--manifest_file <filename>` you tell the program in which file to store the manifests index entry. This is the file that can be used in the next step to update the index. You can use the environment variable GL_CLI_REGISTRY_TOKEN to authenticate against the registry. Below is an example of a full program call of `push-manifest`
60+
61+
```bash
62+
GL_CLI_REGISTRY_TOKEN=asdf123 gl-oci push-manifest --dir build-metal-gardener_prod --container ghcr.io/gardenlinux/gl-oci --arch amd64 --version 1592.1 --cname metal-gardener_prod --cosign_file digest --manifest_file oci_manifest_entry_metal.json
63+
```
3664

65+
#### 2. Update index with manifest entry
66+
67+
Parameters that are the same as for `push-manifest`:
68+
69+
- env-var `GL_CLI_REGISTRY_TOKEN`
70+
- `--version`
71+
- `--container`
72+
- `--manifest-file` this time this parameter adjusts the manifest entry file to be read from instead of being written to
73+
74+
A full example looks like this:
75+
76+
```bash
77+
GL_CLI_REGISTRY_TOKEN=asdf123 gl-oci update-index --container ghcr.io/gardenlinux/gl-oci --version 1592.1 --manifest_file oci_manifest_entry_metal.json
78+
```

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ boto3 = "*"
2323
[tool.poetry.group.dev.dependencies]
2424
bandit = "^1.8.3"
2525
black = "^24.8.0"
26+
opencontainers = "^0.0.14"
2627

2728
[tool.poetry.group.docs.dependencies]
2829
sphinx-rtd-theme = "^2.0.0"
@@ -31,6 +32,7 @@ sphinx-rtd-theme = "^2.0.0"
3132
gl-cname = "gardenlinux.features.cname_main:main"
3233
gl-features-parse = "gardenlinux.features.__main__:main"
3334
gl-flavors-parse = "gardenlinux.flavors.__main__:main"
35+
gl-oci = "gardenlinux.oci.__main__:main"
3436
flavors-parse = "gardenlinux.flavors.__main__:main"
3537

3638
[tool.pytest.ini_options]

src/gardenlinux/constants.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,19 @@
5555

5656
# It is important that this list is sorted in descending length of the entries
5757
GL_MEDIA_TYPES = [
58+
"secureboot.aws-efivars",
59+
"secureboot.kek.auth",
5860
"gcpimage.tar.gz.log",
61+
"secureboot.pk.auth",
62+
"secureboot.kek.crt",
63+
"secureboot.kek.der",
64+
"secureboot.db.auth",
5965
"firecracker.tar.gz",
66+
"secureboot.pk.crt",
67+
"secureboot.pk.der",
68+
"secureboot.db.crt",
69+
"secureboot.db.der",
70+
"secureboot.db.arn",
6071
"platform.test.log",
6172
"platform.test.xml",
6273
"gcpimage.tar.gz",
@@ -65,11 +76,15 @@
6576
"pxe.tar.gz.log",
6677
"root.squashfs",
6778
"manifest.log",
79+
"squashfs.log",
6880
"release.log",
81+
"vmlinuz.log",
82+
"initrd.log",
6983
"pxe.tar.gz",
7084
"qcow2.log",
7185
"test-log",
7286
"boot.efi",
87+
"squashfs",
7388
"manifest",
7489
"vmdk.log",
7590
"tar.log",
@@ -122,12 +137,30 @@
122137
"vhd.log": "application/io.gardenlinux.log",
123138
"ova.log": "application/io.gardenlinux.log",
124139
"vmlinuz": "application/io.gardenlinux.kernel",
140+
"vmlinuz.log": "application/io.gardenlinux.log",
125141
"initrd": "application/io.gardenlinux.initrd",
142+
"initrd.log": "application/io.gardenlinux.log",
126143
"root.squashfs": "application/io.gardenlinux.squashfs",
144+
"squashfs": "application/io.gardenlinux.squashfs",
145+
"squashfs.log": "application/io.gardenlinux.log",
127146
"boot.efi": "application/io.gardenlinux.efi",
128147
"platform.test.log": "application/io.gardenlinux.io.platform.test.log",
129148
"platform.test.xml": "application/io.gardenlinux.io.platform.test.xml",
130149
"chroot.test.log": "application/io.gardenlinux.io.chroot.test.log",
131150
"chroot.test.xml": "application/io.gardenlinux.io.chroot.test.xml",
132151
"oci.log": "application/io.gardenlinux.log",
152+
"secureboot.pk.crt": "application/io.gardenlinux.cert.secureboot.pk.crt",
153+
"secureboot.pk.der": "application/io.gardenlinux.cert.secureboot.pk.der",
154+
"secureboot.pk.auth": "application/io.gardenlinux.cert.secureboot.pk.auth",
155+
"secureboot.kek.crt": "application/io.gardenlinux.cert.secureboot.kek.crt",
156+
"secureboot.kek.der": "application/io.gardenlinux.cert.secureboot.kek.der",
157+
"secureboot.kek.auth": "application/io.gardenlinux.cert.secureboot.kek.auth",
158+
"secureboot.db.crt": "application/io.gardenlinux.cert.secureboot.db.crt",
159+
"secureboot.db.der": "application/io.gardenlinux.cert.secureboot.db.der",
160+
"secureboot.db.auth": "application/io.gardenlinux.cert.secureboot.db.auth",
161+
"secureboot.db.arn": "application/io.gardenlinux.cert.secureboot.db.arn",
162+
"secureboot.aws-efivars": "application/io.gardenlinux.cert.secureboot.aws-efivars",
133163
}
164+
165+
OCI_ANNOTATION_SIGNATURE_KEY = "io.gardenlinux.oci.signature"
166+
OCI_ANNOTATION_SIGNED_STRING_KEY = "io.gardenlinux.oci.signed-string"

src/gardenlinux/features/__main__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,29 @@ def sort_subset(input_set, order_list):
212212
return [item for item in order_list if item in input_set]
213213

214214

215+
def get_flavor_from_cname(cname: str, get_arch: bool = True) -> str:
216+
"""
217+
Extracts the flavor from a canonical name.
218+
219+
:param str cname: Canonical name of an image
220+
:param bool get_arch: Whether to include the architecture in the flavor
221+
:return: Flavor string
222+
"""
223+
224+
# cname:
225+
# azure-gardener_prod_tpm2_trustedboot-amd64-1312.2-80ffcc87
226+
# transform to flavor:
227+
# azure-gardener_prod_tpm2_trustedboot-amd64
228+
229+
platform = cname.split("-")[0]
230+
features = cname.split("-")[1:-1]
231+
arch = cname.split("-")[-1]
232+
233+
if get_arch:
234+
return f"{platform}-{features}-{arch}"
235+
else:
236+
return f"{platform}-{features}"
237+
238+
215239
if __name__ == "__main__":
216240
main()

src/gardenlinux/oci/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# from .__main__ import Cli
4+
5+
# __all__ = ["Cli"]

src/gardenlinux/oci/__main__.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import click
5+
6+
from pygments.lexer import default
7+
8+
from .registry import GlociRegistry
9+
10+
11+
@click.group()
12+
def cli():
13+
pass
14+
15+
16+
@cli.command()
17+
@click.option(
18+
"--container",
19+
required=True,
20+
type=click.Path(),
21+
help="Container Name",
22+
)
23+
@click.option(
24+
"--version",
25+
required=True,
26+
type=click.Path(),
27+
help="Version of image",
28+
)
29+
@click.option(
30+
"--commit",
31+
required=False,
32+
type=click.Path(),
33+
default=None,
34+
help="Commit of image",
35+
)
36+
@click.option(
37+
"--arch",
38+
required=True,
39+
type=click.Path(),
40+
help="Target Image CPU Architecture",
41+
)
42+
@click.option(
43+
"--cname", required=True, type=click.Path(), help="Canonical Name of Image"
44+
)
45+
@click.option("--dir", "directory", required=True, help="path to the build artifacts")
46+
@click.option(
47+
"--cosign_file",
48+
required=False,
49+
help="A file where the pushed manifests digests is written to. The content can be used by an external tool (e.g. cosign) to sign the manifests contents",
50+
)
51+
@click.option(
52+
"--manifest_file",
53+
default="manifests/manifest.json",
54+
help="A file where the index entry for the pushed manifest is written to.",
55+
)
56+
@click.option(
57+
"--insecure",
58+
default=False,
59+
help="Use HTTP to communicate with the registry",
60+
)
61+
def push_manifest(
62+
container,
63+
version,
64+
commit,
65+
arch,
66+
cname,
67+
directory,
68+
cosign_file,
69+
manifest_file,
70+
insecure,
71+
):
72+
"""push artifacts from a dir to a registry, get the index-entry for the manifest in return"""
73+
container_name = f"{container}:{version}"
74+
registry = GlociRegistry(
75+
container_name=container_name,
76+
token=os.getenv("GL_CLI_REGISTRY_TOKEN"),
77+
insecure=insecure,
78+
)
79+
digest = registry.push_from_dir(
80+
arch, version, cname, directory, manifest_file, commit=commit
81+
)
82+
if cosign_file:
83+
print(digest, file=open(cosign_file, "w"))
84+
85+
86+
@cli.command()
87+
@click.option(
88+
"--container",
89+
"container",
90+
required=True,
91+
type=click.Path(),
92+
help="Container Name",
93+
)
94+
@click.option(
95+
"--version",
96+
"version",
97+
required=True,
98+
type=click.Path(),
99+
help="Version of image",
100+
)
101+
@click.option(
102+
"--manifest_folder",
103+
default="manifests",
104+
help="A folder where the index entries are read from.",
105+
)
106+
@click.option(
107+
"--insecure",
108+
default=False,
109+
help="Use HTTP to communicate with the registry",
110+
)
111+
def update_index(container, version, manifest_folder, insecure):
112+
"""push a index entry from a list of files to an index"""
113+
container_name = f"{container}:{version}"
114+
registry = GlociRegistry(
115+
container_name=container_name,
116+
token=os.getenv("GL_CLI_REGISTRY_TOKEN"),
117+
insecure=insecure,
118+
)
119+
registry.update_index(manifest_folder)
120+
121+
122+
def main():
123+
"""Entry point for the gl-oci command."""
124+
cli()
125+
126+
127+
if __name__ == "__main__":
128+
cli()

src/gardenlinux/oci/crypto.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import hashlib
2+
3+
4+
def verify_sha256(checksum: str, data: bytes):
5+
data_checksum = f"sha256:{hashlib.sha256(data).hexdigest()}"
6+
if checksum != data_checksum:
7+
raise ValueError(f"Invalid checksum. {checksum} != {data_checksum}")
8+
9+
10+
def calculate_sha256(file_path: str) -> str:
11+
"""Calculate the SHA256 checksum of a file."""
12+
sha256_hash = hashlib.sha256()
13+
with open(file_path, "rb") as f:
14+
for byte_block in iter(lambda: f.read(4096), b""):
15+
sha256_hash.update(byte_block)
16+
return sha256_hash.hexdigest()

src/gardenlinux/oci/defaults.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
annotation_signature_key = "io.gardenlinux.oci.signature"
2+
annotation_signed_string_key = "io.gardenlinux.oci.signed-string"

src/gardenlinux/oci/helper.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import json
2+
import os
3+
import re
4+
5+
6+
def write_dict_to_json_file(input, output_path):
7+
if os.path.exists(output_path):
8+
raise ValueError(f"{output_path} already exists")
9+
with open(output_path, "w") as fp:
10+
json.dump(input, fp)
11+
12+
13+
def get_uri_for_digest(uri, digest):
14+
"""
15+
Given a URI for an image, return a URI for the related digest.
16+
17+
URI may be in any of the following forms:
18+
19+
ghcr.io/homebrew/core/hello
20+
ghcr.io/homebrew/core/hello:2.10
21+
ghcr.io/homebrew/core/hello@sha256:ff81...47a
22+
"""
23+
base_uri = re.split(r"[@:]", uri, maxsplit=1)[0]
24+
return f"{base_uri}@{digest}"

0 commit comments

Comments
 (0)