Skip to content

Commit 0b09dd1

Browse files
committed
WIP: OCI tagging
1 parent 6a7f814 commit 0b09dd1

File tree

13 files changed

+669
-295
lines changed

13 files changed

+669
-295
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ install-test: install-dev
4646
test: install-test
4747
$(POETRY) run pytest -k "not kms"
4848

49+
test-debug: install-test
50+
$(POETRY) run pytest -k "not kms" -vvv -s
51+
52+
test-trace: install-test
53+
$(POETRY) run pytest -k "not kms" -vvv --log-cli-level=DEBUG
54+
4955
format: install-dev
5056
$(POETRY) run black --extend-exclude test-data/gardenlinux .
5157

poetry.lock

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

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ oras = { git = "https://github.com/oras-project/oras-py.git", rev="caf8db5b2793
1919
python-dotenv = "^1.0.1"
2020
cryptography = "^44.0.0"
2121
boto3 = "*"
22+
click = "^8.2.0"
23+
pygments = "^2.19.1"
24+
opencontainers = "^0.0.14"
2225

2326
[tool.poetry.group.dev.dependencies]
2427
bandit = "^1.8.3"

src/gardenlinux/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,5 @@
164164

165165
OCI_ANNOTATION_SIGNATURE_KEY = "io.gardenlinux.oci.signature"
166166
OCI_ANNOTATION_SIGNED_STRING_KEY = "io.gardenlinux.oci.signed-string"
167+
168+
GL_USER_AGENT_REGISTRY = "gardenlinux.oci.registry/1.0"

src/gardenlinux/features/__main__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,27 @@ def get_flavor_from_cname(cname: str, get_arch: bool = True) -> str:
226226
# transform to flavor:
227227
# azure-gardener_prod_tpm2_trustedboot-amd64
228228

229-
platform = cname.split("-")[0]
230-
features = cname.split("-")[1:-1]
231-
arch = cname.split("-")[-1]
229+
parts = cname.split("-")
232230

231+
# Extract platform, features, and architecture
232+
platform = parts[0]
233+
234+
# If there's more than two parts (beyond platform and arch), those are features
235+
if len(parts) > 2:
236+
features = "-".join(parts[1:-1]) # Join all middle parts with hyphens
237+
else:
238+
features = ""
239+
240+
arch = parts[-1]
241+
242+
# Create the flavor string
233243
if get_arch:
234-
return f"{platform}-{features}-{arch}"
244+
flavor = f"{platform}-{features}-{arch}" if features else f"{platform}-{arch}"
235245
else:
236-
return f"{platform}-{features}"
246+
flavor = f"{platform}-{features}" if features else platform
247+
248+
print(f"Extracted flavor: {flavor}")
249+
return flavor
237250

238251

239252
if __name__ == "__main__":

src/gardenlinux/oci/__main__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ def cli():
2626
type=click.Path(),
2727
help="Version of image",
2828
)
29-
@click.option(
30-
"--commit",
31-
required=False,
32-
type=click.Path(),
33-
default=None,
34-
help="Commit of image",
35-
)
3629
@click.option(
3730
"--arch",
3831
required=True,
@@ -58,16 +51,22 @@ def cli():
5851
default=False,
5952
help="Use HTTP to communicate with the registry",
6053
)
54+
@click.option(
55+
"--additional_tag",
56+
required=False,
57+
multiple=True,
58+
help="Additional tag to push the manifest with",
59+
)
6160
def push_manifest(
6261
container,
6362
version,
64-
commit,
6563
arch,
6664
cname,
6765
directory,
6866
cosign_file,
6967
manifest_file,
7068
insecure,
69+
additional_tag,
7170
):
7271
"""push artifacts from a dir to a registry, get the index-entry for the manifest in return"""
7372
container_name = f"{container}:{version}"
@@ -77,7 +76,7 @@ def push_manifest(
7776
insecure=insecure,
7877
)
7978
digest = registry.push_from_dir(
80-
arch, version, cname, directory, manifest_file, commit=commit
79+
arch, version, cname, directory, manifest_file, additional_tag
8180
)
8281
if cosign_file:
8382
print(digest, file=open(cosign_file, "w"))

src/gardenlinux/oci/registry.py

Lines changed: 208 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Optional, Tuple
1717

1818
import jsonschema
19+
import opencontainers.distribution.reggie as reggie
1920
import oras.auth
2021
import oras.client
2122
import oras.defaults
@@ -29,7 +30,11 @@
2930
from oras.schemas import manifest as oras_manifest_schema
3031

3132
from gardenlinux.features import Parser
32-
from ..constants import OCI_ANNOTATION_SIGNATURE_KEY, OCI_ANNOTATION_SIGNED_STRING_KEY
33+
from ..constants import (
34+
OCI_ANNOTATION_SIGNATURE_KEY,
35+
OCI_ANNOTATION_SIGNED_STRING_KEY,
36+
GL_USER_AGENT_REGISTRY,
37+
)
3338
from .checksum import (
3439
calculate_sha256,
3540
verify_sha256,
@@ -653,40 +658,59 @@ def push_from_dir(
653658
cname: str,
654659
directory: str,
655660
manifest_file: str,
656-
commit: Optional[str] = None,
661+
additional_tags: list = None,
657662
):
658-
# Step 1 scan and extract nested artifacts:
659-
for file in os.listdir(directory):
660-
try:
661-
if file.endswith(".pxe.tar.gz"):
662-
logger.info(f"Found nested artifact {file}")
663-
nested_tar_obj = tarfile.open(f"{directory}/{file}")
664-
nested_tar_obj.extractall(filter="data", path=directory)
665-
nested_tar_obj.close()
666-
except (OSError, tarfile.FilterError, tarfile.TarError) as e:
667-
print(f"Failed to extract nested artifact {file}", e)
668-
exit(1)
663+
"""
664+
Push artifacts from a directory to a registry
665+
666+
Args:
667+
architecture: Target architecture of the image
668+
version: Version tag for the image
669+
cname: Canonical name of the image
670+
directory: Directory containing the artifacts
671+
manifest_file: File to write the manifest index entry to
672+
additional_tags: Additional tags to push the manifest with
673+
674+
Returns:
675+
The digest of the pushed manifest
676+
"""
677+
if additional_tags is None:
678+
additional_tags = []
669679

670680
try:
681+
# Step 1: scan and extract nested artifacts
682+
for file in os.listdir(directory):
683+
try:
684+
if file.endswith(".pxe.tar.gz"):
685+
logger.info(f"Found nested artifact {file}")
686+
nested_tar_obj = tarfile.open(f"{directory}/{file}")
687+
nested_tar_obj.extractall(filter="data", path=directory)
688+
nested_tar_obj.close()
689+
except (OSError, tarfile.FilterError, tarfile.TarError) as e:
690+
print(f"Failed to extract nested artifact {file}", e)
691+
exit(1)
692+
693+
# Step 2: Get metadata from files
671694
oci_metadata = get_oci_metadata_from_fileset(
672695
os.listdir(directory), architecture
673696
)
674697

675698
features = ""
699+
commit = ""
676700
for artifact in oci_metadata:
677701
if artifact["media_type"] == "application/io.gardenlinux.release":
678-
file = open(f"{directory}/{artifact["file_name"]}", "r")
679-
lines = file.readlines()
680-
for line in lines:
681-
if line.strip().startswith("GARDENLINUX_FEATURES="):
682-
features = line.strip().removeprefix(
683-
"GARDENLINUX_FEATURES="
684-
)
685-
break
686-
file.close()
687-
688-
flavor = Parser.get_flavor_from_cname(cname, get_arch=True)
689-
702+
with open(f"{directory}/{artifact["file_name"]}", "r") as file:
703+
for line in file:
704+
line = line.strip()
705+
if line.startswith("GARDENLINUX_FEATURES="):
706+
features = line.removeprefix("GARDENLINUX_FEATURES=")
707+
elif line.startswith("GARDENLINUX_COMMIT_ID="):
708+
commit = line.removeprefix("GARDENLINUX_COMMIT_ID=")
709+
if features and commit: # Break if both values are found
710+
break
711+
break # Break after processing the release file
712+
713+
# Step 3: Push the image manifest
690714
digest = self.push_image_manifest(
691715
architecture,
692716
cname,
@@ -697,7 +721,165 @@ def push_from_dir(
697721
manifest_file,
698722
commit=commit,
699723
)
724+
725+
# Step 4: Process additional tags if provided
726+
if additional_tags and len(additional_tags) > 0:
727+
print(f"DEBUG: Processing {len(additional_tags)} additional tags")
728+
logger.info(f"Processing {len(additional_tags)} additional tags")
729+
730+
# Call push_additional_tags_manifest with repository information
731+
self.push_additional_tags_manifest(
732+
architecture,
733+
cname,
734+
version,
735+
additional_tags,
736+
container=self.container,
737+
)
738+
739+
return digest
700740
except Exception as e:
701741
print("Error: ", e)
702742
exit(1)
703-
return digest
743+
744+
def push_additional_tags_manifest(
745+
self, architecture, cname, version, additional_tags, container
746+
):
747+
"""
748+
Push additional tags for an existing manifest using Reggie client
749+
750+
Args:
751+
architecture: Target architecture of the image
752+
cname: Canonical name of the image
753+
version: Version tag for the image
754+
additional_tags: List of additional tags to push
755+
container: Container object
756+
"""
757+
try:
758+
print(
759+
f"DEBUG: Processing {len(additional_tags)} additional tags for manifest"
760+
)
761+
print(f"DEBUG: Container: {container}")
762+
print(f"DEBUG: Container api_prefix: {container.api_prefix}")
763+
print(f"DEBUG: Container uri: {container.uri}")
764+
print(f"DEBUG: Container tag: {container.tag}")
765+
print(f"DEBUG: Container registry: {container.registry}")
766+
print(f"DEBUG: Container repository: {container.repository}")
767+
768+
# Source tag is version-cname-architecture as pushed by push_image_manifest
769+
source_tag = f"{version}-{cname}-{architecture}"
770+
manifest_url = container.manifest_url()
771+
772+
repo_name = manifest_url[manifest_url.find('/v2/')+3:manifest_url.find('/manifests/')]
773+
if repo_name.startswith('/'):
774+
repo_name = repo_name[1:]
775+
776+
# Ensure registry_url and manifest_url have protocol prefix
777+
use_insecure = getattr(
778+
self, "insecure", True
779+
) # Default to True if attribute doesn't exist
780+
protocol = "http" if use_insecure else "https"
781+
registry_url = f"{protocol}://{container.registry}"
782+
783+
print(f"DEBUG: Using source tag: {container.tag}")
784+
print(f"DEBUG: Using registry URL: {registry_url}")
785+
print(f"DEBUG: Using repository name: {repo_name}")
786+
logger.info(f"Using source tag: {container.tag}")
787+
logger.info(f"Using registry URL: {registry_url}")
788+
logger.info(f"Using repository name: {repo_name}")
789+
790+
# Configure client options
791+
client_options = [reggie.WithUserAgent(GL_USER_AGENT_REGISTRY)]
792+
793+
# Initialize reggie client
794+
client = reggie.NewClient(f"{registry_url}/{repo_name}", *client_options)
795+
796+
# Set authentication token if provided
797+
token = os.getenv("GL_CLI_REGISTRY_TOKEN")
798+
username = os.getenv("GL_CLI_REGISTRY_USERNAME")
799+
password = os.getenv("GL_CLI_REGISTRY_PASSWORD")
800+
801+
# Step 1: Get the manifest from the source tag using Reggie
802+
get_manifest_req = client.NewRequest(
803+
"GET", f"/v2/{repo_name}/manifests/{source_tag}"
804+
)
805+
806+
# Set authentication for the GET request
807+
if token:
808+
get_manifest_req.SetAuthToken(token)
809+
elif username and password:
810+
get_manifest_req.SetBasicAuth(username, password)
811+
812+
get_manifest_resp = client.Do(get_manifest_req)
813+
814+
if get_manifest_resp.status_code != 200:
815+
print(
816+
f"DEBUG: Failed to get source manifest: {get_manifest_resp.status_code}"
817+
f"DEBUG: Failed to get source manifest: {get_manifest_resp.text}"
818+
)
819+
logger.error(
820+
f"Failed to get source manifest: {get_manifest_resp.status_code}"
821+
)
822+
return
823+
824+
# Get the manifest content and content-type
825+
manifest_content = get_manifest_resp.content
826+
content_type = get_manifest_resp.headers.get("Content-Type")
827+
828+
print(
829+
f"DEBUG: Successfully retrieved manifest with content: {manifest_content}"
830+
f"DEBUG: Successfully retrieved manifest with content type: {content_type}"
831+
832+
)
833+
logger.info(
834+
f"Successfully retrieved manifest with content: {manifest_content}"
835+
f"Successfully retrieved manifest with content type: {content_type}"
836+
)
837+
838+
# Step 2: For each additional tag, push the manifest using Reggie
839+
for tag in additional_tags:
840+
try:
841+
print(f"DEBUG: Pushing additional tag: {tag}")
842+
843+
# Create a new PUT request for the tag
844+
put_manifest_req = client.NewRequest(
845+
"PUT", f"/v2/{repo_name}/manifests/{tag}"
846+
)
847+
848+
# Set the content type header to match the original manifest
849+
put_manifest_req.SetHeader("Content-Type", content_type)
850+
851+
# Set authentication for the PUT request
852+
if token:
853+
put_manifest_req.SetAuthToken(token)
854+
elif username and password:
855+
put_manifest_req.SetBasicAuth(username, password)
856+
857+
# Set the body to the manifest content
858+
put_manifest_req.SetBody(manifest_content)
859+
860+
print(f"DEBUG: put_manifest_req: {put_manifest_req}")
861+
print(f"DEBUG: put_manifest_req.body: {put_manifest_req.body}")
862+
print(f"DEBUG: put_manifest_req.headers: {put_manifest_req.headers}")
863+
864+
# Use the reggie client to make the request
865+
put_manifest_resp = client.Do(put_manifest_req)
866+
867+
if put_manifest_resp.status_code in [200, 201]:
868+
print(f"DEBUG: Successfully pushed tag {tag} for manifest")
869+
logger.info(f"Successfully pushed tag {tag} for manifest")
870+
else:
871+
print(
872+
f"DEBUG: Failed to push tag {tag} for manifest: {put_manifest_resp.status_code}"
873+
f"DEBUG: Failed to push tag {tag} for manifest: {put_manifest_resp.text}"
874+
)
875+
logger.error(
876+
f"Failed to push tag {tag} for manifest: {put_manifest_resp.status_code}"
877+
)
878+
879+
except Exception as e:
880+
print(f"DEBUG: Error pushing tag {tag} for manifest: {str(e)}")
881+
logger.error(f"Error pushing tag {tag} for manifest: {str(e)}")
882+
883+
except Exception as e:
884+
print(f"DEBUG: Error in push_additional_tags_manifest: {str(e)}")
885+
logger.error(f"Error in push_additional_tags_manifest: {str(e)}")

0 commit comments

Comments
 (0)