Skip to content

Commit cf377b1

Browse files
committed
port oci cli back from python_gardenlinux_cli
1 parent e68538b commit cf377b1

File tree

14 files changed

+899
-76
lines changed

14 files changed

+899
-76
lines changed

README.md

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,33 @@
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: parse features
20+
1521
**Inclusion via poetry**:
1622

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

2128
if __name__ == "__main__":
2229
# Step 1: parse the "features directory" and get the full graph containing all features
23-
all_features = parse_features_lib.read_feature_files("features")
30+
all_features = parse_features.read_feature_files("features")
2431

2532
# Step 2: supply desired features and get all their dependencies
26-
dependencies = parse_features_lib.filter_graph(all_features, {"gardener", "_prod", "server", "ociExample"})
33+
dependencies = parse_features.filter_graph(all_features, {"gardener", "_prod", "server", "ociExample"})
2734

2835
# Step 3: play with the retrieved data.
2936
for feature, info in dependencies.nodes(data="content"):
@@ -32,5 +39,47 @@ if __name__ == "__main__":
3239
```
3340

3441
## 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/).
3642

43+
The library is documented with docstrings, which are used to generate the developer documentation available [here](https://gardenlinux.github.io/python-gardenlinux-lib/).
44+
45+
## Push OCI artifacts to a registry
46+
47+
this tool helps you to push oci artifacts.
48+
49+
### Installation
50+
51+
```bash
52+
git clone https://github.com/gardenlinux/python-gardenlinux-lib.git
53+
mkdir venv
54+
python -m venv venv
55+
source venv/bin/activate.sh
56+
poetry install
57+
gl-oci --help
58+
```
59+
60+
### Usage
61+
62+
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.
63+
64+
#### 1. Push layers + manifest
65+
66+
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 GLOCI_REGISTRY_TOKEN to authenticate against the registry. Below is an example of a full program call of `push-manifest`
67+
68+
```bash
69+
GLOCI_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
70+
```
71+
72+
#### 2. Update index with manifest entry
73+
74+
Parameters that are the same as for `push-manifest`:
75+
76+
- env-var `GLOCI_REGISTRY_TOKEN`
77+
- `--version`
78+
- `--container`
79+
- `--manifest-file` this time this parameter adjusts the manifest entry file to be read from instead of being written to
80+
81+
A full example looks like this:
82+
83+
```bash
84+
GLOCI_REGISTRY_TOKEN=asdf123 gl-oci update-index --container ghcr.io/gardenlinux/gl-oci --version 1592.1 --manifest_file oci_manifest_entry_metal.json
85+
```

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ black = "^24.8.0"
2929
gl-cname = "python_gardenlinux_lib.cname:main"
3030
gl-flavors-parse = "python_gardenlinux_lib.flavors.parse_flavors:main"
3131
flavors-parse = "python_gardenlinux_lib.flavors.parse_flavors:main"
32+
gl-oci = "python_gardenlinux_lib.oci.cli:main"
3233

3334
[tool.pytest.ini_options]
3435
pythonpath = [

src/python_gardenlinux_lib/cname.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def main():
2121

2222
re_match = re.match(
2323
"([a-zA-Z0-9]+(-[a-zA-Z0-9\\_\\-]*?)?)(-([a-z0-9]+)(-([a-z0-9.]+)-([a-z0-9]+))*)?$",
24-
args.cname
24+
args.cname,
2525
)
2626

2727
assert re_match, f"not a valid cname {args.cname}"
@@ -72,11 +72,13 @@ def main():
7272

7373
print(cname)
7474

75+
7576
def get_cname_base(sorted_features):
7677
return reduce(
77-
lambda a, b : a + ("-" if not b.startswith("_") else "") + b, sorted_features
78+
lambda a, b: a + ("-" if not b.startswith("_") else "") + b, sorted_features
7879
)
7980

81+
8082
def get_minimal_feature_set(graph):
8183
return set([node for (node, degree) in graph.in_degree() if degree == 0])
8284

src/python_gardenlinux_lib/constants.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,19 @@
22

33
# It is important that this list is sorted in descending length of the entries
44
GL_MEDIA_TYPES = [
5+
"secureboot.aws-efivars",
6+
"secureboot.kek.auth",
57
"gcpimage.tar.gz.log",
8+
"secureboot.pk.auth",
9+
"secureboot.kek.crt",
10+
"secureboot.kek.der",
11+
"secureboot.db.auth",
612
"firecracker.tar.gz",
13+
"secureboot.pk.crt",
14+
"secureboot.pk.der",
15+
"secureboot.db.crt",
16+
"secureboot.db.der",
17+
"secureboot.db.arn",
718
"platform.test.log",
819
"platform.test.xml",
920
"gcpimage.tar.gz",
@@ -12,11 +23,15 @@
1223
"pxe.tar.gz.log",
1324
"root.squashfs",
1425
"manifest.log",
26+
"squashfs.log",
1527
"release.log",
28+
"vmlinuz.log",
29+
"initrd.log",
1630
"pxe.tar.gz",
1731
"qcow2.log",
1832
"test-log",
1933
"boot.efi",
34+
"squashfs",
2035
"manifest",
2136
"vmdk.log",
2237
"tar.log",
@@ -69,12 +84,27 @@
6984
"vhd.log": "application/io.gardenlinux.log",
7085
"ova.log": "application/io.gardenlinux.log",
7186
"vmlinuz": "application/io.gardenlinux.kernel",
87+
"vmlinuz.log": "application/io.gardenlinux.log",
7288
"initrd": "application/io.gardenlinux.initrd",
89+
"initrd.log": "application/io.gardenlinux.log",
7390
"root.squashfs": "application/io.gardenlinux.squashfs",
91+
"squashfs": "application/io.gardenlinux.squashfs",
92+
"squashfs.log": "application/io.gardenlinux.log",
7493
"boot.efi": "application/io.gardenlinux.efi",
7594
"platform.test.log": "application/io.gardenlinux.io.platform.test.log",
7695
"platform.test.xml": "application/io.gardenlinux.io.platform.test.xml",
7796
"chroot.test.log": "application/io.gardenlinux.io.chroot.test.log",
7897
"chroot.test.xml": "application/io.gardenlinux.io.chroot.test.xml",
7998
"oci.log": "application/io.gardenlinux.log",
99+
"secureboot.pk.crt": "application/io.gardenlinux.cert.secureboot.pk.crt",
100+
"secureboot.pk.der": "application/io.gardenlinux.cert.secureboot.pk.der",
101+
"secureboot.pk.auth": "application/io.gardenlinux.cert.secureboot.pk.auth",
102+
"secureboot.kek.crt": "application/io.gardenlinux.cert.secureboot.kek.crt",
103+
"secureboot.kek.der": "application/io.gardenlinux.cert.secureboot.kek.der",
104+
"secureboot.kek.auth": "application/io.gardenlinux.cert.secureboot.kek.auth",
105+
"secureboot.db.crt": "application/io.gardenlinux.cert.secureboot.db.crt",
106+
"secureboot.db.der": "application/io.gardenlinux.cert.secureboot.db.der",
107+
"secureboot.db.auth": "application/io.gardenlinux.cert.secureboot.db.auth",
108+
"secureboot.db.arn": "application/io.gardenlinux.cert.secureboot.db.arn",
109+
"secureboot.aws-efivars": "application/io.gardenlinux.cert.secureboot.aws-efivars",
80110
}

src/python_gardenlinux_lib/features/parse_features.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def get_gardenlinux_commit(gardenlinux_root: str, limit: Optional[int] = None) -
3838
else:
3939
return commit_str
4040

41+
4142
def get_features_dict(
4243
cname: str, gardenlinux_root: str, feature_dir_name: str = "features"
4344
) -> dict:
@@ -61,6 +62,7 @@ def get_features_dict(
6162

6263
return features_by_type
6364

65+
6466
def get_features_graph(
6567
cname: str, gardenlinux_root: str, feature_dir_name: str = "features"
6668
) -> networkx.graph:
@@ -78,6 +80,7 @@ def get_features_graph(
7880

7981
return graph
8082

83+
8184
def get_features_list(
8285
cname: str, gardenlinux_root: str, feature_dir_name: str = "features"
8386
) -> list:
@@ -93,6 +96,7 @@ def get_features_list(
9396

9497
return features
9598

99+
96100
def get_features(
97101
cname: str, gardenlinux_root: str, feature_dir_name: str = "features"
98102
) -> str:
@@ -107,6 +111,7 @@ def get_features(
107111

108112
return ",".join(features)
109113

114+
110115
def construct_layer_metadata(
111116
filetype: str, cname: str, version: str, arch: str, commit: str
112117
) -> dict:
@@ -125,6 +130,7 @@ def construct_layer_metadata(
125130
"annotations": {"io.gardenlinux.image.layer.architecture": arch},
126131
}
127132

133+
128134
def construct_layer_metadata_from_filename(filename: str, arch: str) -> dict:
129135
"""
130136
:param str filename: filename of the blob
@@ -138,6 +144,7 @@ def construct_layer_metadata_from_filename(filename: str, arch: str) -> dict:
138144
"annotations": {"io.gardenlinux.image.layer.architecture": arch},
139145
}
140146

147+
141148
def get_file_set_from_cname(cname: str, version: str, arch: str, gardenlinux_root: str):
142149
"""
143150
:param str cname: the target cname of the image
@@ -160,6 +167,7 @@ def get_file_set_from_cname(cname: str, version: str, arch: str, gardenlinux_roo
160167
)
161168
return file_set
162169

170+
163171
def get_oci_metadata_from_fileset(fileset: list, arch: str):
164172
"""
165173
:param str arch: arch of the target image
@@ -175,6 +183,7 @@ def get_oci_metadata_from_fileset(fileset: list, arch: str):
175183

176184
return oci_layer_metadata_list
177185

186+
178187
def get_oci_metadata(cname: str, version: str, arch: str, gardenlinux_root: str):
179188
"""
180189
:param str cname: the target cname of the image
@@ -197,6 +206,7 @@ def get_oci_metadata(cname: str, version: str, arch: str, gardenlinux_root: str)
197206

198207
return oci_layer_metadata_list
199208

209+
200210
def lookup_media_type_for_filetype(filetype: str) -> str:
201211
"""
202212
:param str filetype: filetype of the target layer
@@ -209,6 +219,7 @@ def lookup_media_type_for_filetype(filetype: str) -> str:
209219
f"media type for {filetype} is not defined. You may want to add the definition to parse_features_lib"
210220
)
211221

222+
212223
def lookup_media_type_for_file(filename: str) -> str:
213224
"""
214225
:param str filename: filename of the target layer
@@ -222,6 +233,7 @@ def lookup_media_type_for_file(filename: str) -> str:
222233
f"media type for {filename} is not defined. You may want to add the definition to parse_features_lib"
223234
)
224235

236+
225237
def deduce_feature_name(feature_dir: str):
226238
"""
227239
:param str feature_dir: Directory of single Feature
@@ -232,20 +244,23 @@ def deduce_feature_name(feature_dir: str):
232244
raise ValueError("Expected name from parse_feature_yaml function to be set")
233245
return parsed["name"]
234246

247+
235248
def deduce_archive_filetypes(feature_dir):
236249
"""
237250
:param str feature_dir: Directory of single Feature
238251
:return: str list of filetype for archive
239252
"""
240253
return deduce_filetypes_from_string(feature_dir, "image")
241254

255+
242256
def deduce_image_filetypes(feature_dir):
243257
"""
244258
:param str feature_dir: Directory of single Feature
245259
:return: str list of filetype for image
246260
"""
247261
return deduce_filetypes_from_string(feature_dir, "convert")
248262

263+
249264
def deduce_filetypes(feature_dir):
250265
"""
251266
:param str feature_dir: Directory of single Feature
@@ -260,6 +275,7 @@ def deduce_filetypes(feature_dir):
260275
image_file_types.extend(archive_file_types)
261276
return image_file_types
262277

278+
263279
def deduce_filetypes_from_string(feature_dir: str, script_base_name: str):
264280
"""
265281
Garden Linux features can optionally have an image.<filetype> or convert.<filetype> script,
@@ -283,6 +299,7 @@ def deduce_filetypes_from_string(feature_dir: str, script_base_name: str):
283299

284300
return sorted(result)
285301

302+
286303
def read_feature_files(feature_dir):
287304
"""
288305
Legacy function copied from gardenlinux/builder
@@ -312,6 +329,7 @@ def read_feature_files(feature_dir):
312329
raise ValueError("Graph is not directed acyclic graph")
313330
return feature_graph
314331

332+
315333
def parse_feature_yaml(feature_yaml_file: str):
316334
"""
317335
Legacy function copied from gardenlinux/builder
@@ -328,9 +346,11 @@ def parse_feature_yaml(feature_yaml_file: str):
328346
content = yaml.safe_load(f)
329347
return {"name": name, "content": content}
330348

349+
331350
def __get_node_features(node):
332351
return node.get("content", {}).get("features", {})
333352

353+
334354
def filter_graph(feature_graph, feature_set, ignore_excludes=False):
335355
filter_set = set(feature_graph.nodes())
336356

@@ -369,29 +389,35 @@ def filter_func(node):
369389
raise ValueError("Including explicitly excluded feature")
370390
return graph
371391

392+
372393
def sort_set(input_set, order_list):
373394
return [item for item in order_list if item in input_set]
374395

396+
375397
def __sort_key(graph, node):
376398
prefix_map = {"platform": "0", "element": "1", "flag": "2"}
377399
node_type = __get_node_type(graph.nodes.get(node, {}))
378400
prefix = prefix_map[node_type]
379401
return f"{prefix}-{node}"
380402

403+
381404
def sort_nodes(graph):
382405
def key_function(node):
383406
return __sort_key(graph, node)
384407

385408
return list(networkx.lexicographical_topological_sort(graph, key=key_function))
386409

410+
387411
def __reverse_cname_base(cname):
388412
cname = cname.replace("_", "-_")
389413
return set(cname.split("-"))
390414

415+
391416
def __reverse_sort_nodes(graph):
392417
reverse_graph = graph.reverse()
393418
assert networkx.is_directed_acyclic_graph(reverse_graph)
394419
return sort_nodes(reverse_graph)
395420

421+
396422
def __get_node_type(node):
397423
return node.get("content", {}).get("type")

0 commit comments

Comments
 (0)