Skip to content

Commit 917a23c

Browse files
authored
Merge pull request #33 from gardenlinux/no-yaml-required
oci push can be used without any modifications to gardenlinux features. This commit adds feature to automatically deduce the build artifacts from the cname. THe pytests automatically create dummy artifacts, so we do not need to re-build gardenlinux in all flavors to test this library.
2 parents 50cdf19 + a5a406f commit 917a23c

File tree

18 files changed

+731
-46
lines changed

18 files changed

+731
-46
lines changed

.github/workflows/pytests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ jobs:
1313
steps:
1414
- name: Checkout code
1515
uses: actions/checkout@v4
16+
with:
17+
submodules: 'true'
1618
- name: Set up Python
1719
uses: actions/setup-python@v5
1820
with:

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
gardenlinux
21
_build
32

43
# Byte-compiled / optimized / DLL files

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "test-data/gardenlinux"]
2+
path = test-data/gardenlinux
3+
url = https://github.com/gardenlinux/gardenlinux

cert/gencert.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
4+
CERT_NAME="$SCRIPT_DIR/oci-sign"
5+
6+
7+
openssl req -x509 -newkey rsa -pkeyopt rsa_keygen_bits:4096 -days 3650 -nodes -keyout $CERT_NAME.key -out $CERT_NAME.crt -subj "/CN=Garden Linux test signing key for oci"
8+

poetry.lock

Lines changed: 360 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "python_gardenlinux_lib"
33
version = "0.1.0"
44
description = "Contains tools to work with the features directory of gardenlinux, for example deducting dependencies from feature sets or validating cnames"
5-
authors = ["Malte Münch <[email protected]>"]
5+
authors = ["Garden Linux Maintainers <[email protected]>"]
66
license = "Apache-2.0"
77
readme = "README.md"
88
packages = [{include = "python_gardenlinux_lib", from="src"}]
@@ -15,6 +15,11 @@ pytest = "^8.3.2"
1515
gitpython = "^3.1.43"
1616
sphinx-rtd-theme = "^2.0.0"
1717
apt-repo = "^0.5"
18+
jsonschema = "^4.23.0"
19+
oras = { git = "https://github.com/oras-project/oras-py.git", rev="caf8db5b279382335fbb1f6d7402ed9b73618d37" }
20+
python-dotenv = "^1.0.1"
21+
cryptography = "^43.0.0"
22+
1823

1924
[tool.poetry.group.dev.dependencies]
2025
black = "^24.8.0"
@@ -23,6 +28,7 @@ black = "^24.8.0"
2328
pythonpath = [
2429
"src"
2530
]
31+
norecursedirs = "test-data"
2632

2733
[build-system]
2834
requires = ["poetry-core"]

src/python_gardenlinux_lib/features/__init__.py

Whitespace-only changes.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from apt_repo import APTRepository
2+
from typing import Optional
3+
4+
5+
class GardenLinuxRepo(APTRepository):
6+
def __init__(
7+
self,
8+
dist: str,
9+
url: Optional[str] = "http://packages.gardenlinux.io/gardenlinux",
10+
components: Optional[list[str]] = ["main"],
11+
) -> None:
12+
self.components = components
13+
self.url = url
14+
self.dist = dist
15+
self.repo = APTRepository(self.url, self.dist, self.components)
16+
17+
def get_package_version_by_name(self, name: str) -> list[tuple[str, str]]:
18+
"""
19+
:param str name: name of package to find
20+
:returns: packages matching the input name
21+
"""
22+
return [
23+
(package.package, package.version)
24+
for package in self.repo.get_packages_by_name(name)
25+
]
26+
27+
def get_packages_versions(self):
28+
"""
29+
Returns list of (package, version) tuples
30+
"""
31+
return [(p.package, p.version) for p in self.repo.packages]
32+
33+
34+
def compare_gardenlinux_repo_version(version_a: str, version_b: str):
35+
"""
36+
:param str version_a: Version of first Garden Linux repo
37+
:param str version_b: Version of first Garden Linux repo
38+
39+
Example: print(compare_gardenlinux_repo_version("1443.2", "1443.1"))
40+
"""
41+
return compare_repo(GardenLinuxRepo(version_a), GardenLinuxRepo(version_b))
42+
43+
44+
def compare_repo(
45+
a: GardenLinuxRepo, b: GardenLinuxRepo, available_in_both: Optional[bool] = False
46+
):
47+
"""
48+
:param a GardenLinuxRepo: first repo to compare
49+
:param b GardenLinuxRepo: second repo to compare
50+
:returns: differences between repo a and repo b
51+
"""
52+
53+
packages_a = dict(a.get_packages_versions())
54+
packages_b = dict(b.get_packages_versions())
55+
if available_in_both:
56+
all_names = set(packages_a.keys()).intersection(set(packages_b.keys()))
57+
else:
58+
all_names = set(packages_a.keys()).union(set(packages_b.keys()))
59+
60+
return [
61+
(name, packages_a.get(name), packages_b.get(name))
62+
for name in all_names
63+
if (
64+
name in packages_a
65+
and name in packages_b
66+
and packages_a[name] != packages_b[name]
67+
)
68+
or (name not in packages_b or name not in packages_a)
69+
]
70+
71+
72+
# EXAMPLE USAGE.
73+
# print(compare_gardenlinux_repo_version("1443.2", "1443.1"))
74+
75+
# gl_repo = GardenLinuxRepo("today")
76+
# gl_repo_1592 = GardenLinuxRepo("1592.0")
77+
# deb_testing = GardenLinuxRepo("testing", "https://deb.debian.org/debian/")
78+
# print(compare_repo(gl_repo, gl_repo_1592, available_in_both=True))
79+
# print(compare_repo(gl_repo, deb_testing, available_in_both=True))
80+
# # print(gl_repo.get_packages_versions())
81+
# print(gl_repo.get_package_version_by_name("wget"))

src/python_gardenlinux_lib/parse_features.py renamed to src/python_gardenlinux_lib/features/parse_features.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
GL_MEDIA_TYPE_LOOKUP = {
1111
"tar": "application/io.gardenlinux.image.archive.format.tar",
12+
"tar.gz": "application/io.gardenlinux.image.archive.format.tar.gz",
1213
"pxe.tar.gz": "application/io.gardenlinux.image.archive.format.pxe.tar.gz",
1314
"iso": "application/io.gardenlinux.image.archive.format.iso",
1415
"oci": "application/io.gardenlinux.image.archive.format.oci",
@@ -85,7 +86,7 @@ def construct_layer_metadata(
8586
"""
8687
media_type = lookup_media_type_for_filetype(filetype)
8788
return {
88-
"filename": f"{cname}-{version}-{arch}-{commit}.{filetype}",
89+
"file_name": f"{cname}-{version}-{arch}-{commit}.{filetype}",
8990
"media_type": media_type,
9091
}
9192

src/python_gardenlinux_lib/oras/registry.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import uuid
99
from enum import Enum, auto
1010
from typing import Optional, Tuple
11-
from python_gardenlinux_lib.parse_features import read_feature_files
1211

1312
import jsonschema
1413
import oras.auth
@@ -17,8 +16,8 @@
1716
import oras.oci
1817
import oras.provider
1918
import oras.utils
19+
from python_gardenlinux_lib.features.parse_features import get_oci_metadata
2020
import requests
21-
import yaml
2221
from oras.container import Container as OrasContainer
2322
from oras.decorator import ensure_container
2423
from oras.provider import Registry
@@ -126,6 +125,29 @@ def construct_layer_signed_data_string(
126125
return data_to_sign
127126

128127

128+
def setup_registry(
129+
container_name: str,
130+
private_key: Optional[str] = None,
131+
insecure: bool = False,
132+
public_key: Optional[str] = None,
133+
):
134+
username = os.getenv("GLOCI_REGISTRY_USERNAME")
135+
token = os.getenv("GLOCI_REGISTRY_TOKEN")
136+
if username is None:
137+
logger.error("No username")
138+
raise ValueError("No Username provided when setting up registry")
139+
if token is None:
140+
logger.error("No token")
141+
raise ValueError("No token provided when setting up registry")
142+
return GlociRegistry(
143+
container_name,
144+
token,
145+
insecure=insecure,
146+
private_key=private_key,
147+
public_key=public_key,
148+
)
149+
150+
129151
class GlociRegistry(Registry):
130152
def __init__(
131153
self,
@@ -533,27 +555,35 @@ def upload_index(self, index: dict) -> requests.Response:
533555
return response
534556

535557
def push_image_manifest(
536-
self, architecture: str, cname: str, version: str, info_yaml: str
558+
self,
559+
architecture: str,
560+
cname: str,
561+
version: str,
562+
gardenlinux_root: str,
563+
build_artifacts_dir: str,
537564
):
538565
"""
539566
creates and pushes an image manifest
567+
568+
:param str architecture: target architecture of the image
569+
:param str cname: canonical name of the target image
570+
:param str build_artifacts_dir: directory where the build artifacts are located
540571
"""
541-
# container = OrasContainer(container_name)
542-
with open(info_yaml, "r") as f:
543-
info_data = yaml.safe_load(f)
544-
base_path = os.path.join(os.path.dirname(info_yaml))
572+
573+
# TODO: construct oci_artifacts default data
574+
575+
oci_metadata = get_oci_metadata(cname, version, gardenlinux_root)
545576

546577
manifest_image = oras.oci.NewManifest()
547578
total_size = 0
548579

549-
for artifact in info_data["oci_artifacts"]:
580+
for artifact in oci_metadata:
550581
annotations_input = artifact["annotations"]
551582
media_type = artifact["media_type"]
552-
file_path = os.path.join(base_path, artifact["file_name"])
583+
file_path = os.path.join(build_artifacts_dir, artifact["file_name"])
553584

554585
if not os.path.exists(file_path):
555-
logger.error(f"{file_path} does not exist.")
556-
continue
586+
raise ValueError(f"{file_path} does not exist.")
557587

558588
cleanup_blob = False
559589
if os.path.isdir(file_path):
@@ -573,25 +603,26 @@ def push_image_manifest(
573603
self._check_200_response(response)
574604
if cleanup_blob and os.path.exists(file_path):
575605
os.remove(file_path)
576-
layer = self.create_layer(
577-
info_yaml,
578-
cname,
579-
version,
580-
architecture,
581-
"application/io.gardenlinux.oci.info-yaml",
582-
)
583-
total_size += int(layer["size"])
584-
manifest_image["layers"].append(layer)
606+
# layer = self.create_layer(
607+
# info_yaml,
608+
# cname,
609+
# version,
610+
# architecture,
611+
# "application/io.gardenlinux.oci.info-yaml",
612+
# )
613+
# total_size += int(layer["size"])
614+
# manifest_image["layers"].append(layer)
615+
585616
manifest_image["annotations"] = {}
586617
manifest_image["annotations"]["version"] = version
587618
manifest_image["annotations"]["cname"] = cname
588619
manifest_image["annotations"]["architecture"] = architecture
589620
attach_state(manifest_image["annotations"], "UNTESTED")
590621

591-
if layer is None:
592-
raise ValueError("error: layer is none")
593-
response = self.upload_blob(info_yaml, self.container, layer)
594-
self._check_200_response(response)
622+
# if layer is None:
623+
# raise ValueError("error: layer is none")
624+
# response = self.upload_blob(info_yaml, self.container, layer)
625+
# self._check_200_response(response)
595626

596627
config_annotations = {"cname": cname, "architecture": architecture}
597628
conf, config_file = create_config_from_dict(dict(), config_annotations)

0 commit comments

Comments
 (0)