Skip to content

Commit 74bf5da

Browse files
committed
add gardenlinux.oci.registry.push_additional_tags_manifest
enables pushing additional tags to OCI manifest
1 parent 91db17e commit 74bf5da

File tree

6 files changed

+852
-555
lines changed

6 files changed

+852
-555
lines changed

poetry.lock

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

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ readme = "README.md"
88
packages = [{include = "gardenlinux", from="src"}, {include = "python_gardenlinux_lib", from="src"}]
99

1010
[tool.poetry.dependencies]
11-
python = "^3.10"
11+
python = "^3.13"
1212
networkx = "^3.3"
1313
PyYAML = "^6.0.2"
1414
pytest = "^8.3.2"
@@ -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/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: 153 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
import base64
4+
import configparser
45
import copy
56
import hashlib
67
import json
@@ -30,7 +31,6 @@
3031

3132
from ..constants import OCI_ANNOTATION_SIGNATURE_KEY, OCI_ANNOTATION_SIGNED_STRING_KEY
3233
from ..features import CName
33-
3434
from .checksum import (
3535
calculate_sha256,
3636
verify_sha256,
@@ -673,38 +673,73 @@ def push_from_dir(
673673
cname: str,
674674
directory: str,
675675
manifest_file: str,
676-
commit: Optional[str] = None,
676+
additional_tags: list = None,
677677
):
678-
# Step 1 scan and extract nested artifacts:
679-
for file in os.listdir(directory):
680-
try:
681-
if file.endswith(".pxe.tar.gz"):
682-
logger.info(f"Found nested artifact {file}")
683-
nested_tar_obj = tarfile.open(f"{directory}/{file}")
684-
nested_tar_obj.extractall(filter="data", path=directory)
685-
nested_tar_obj.close()
686-
except (OSError, tarfile.FilterError, tarfile.TarError) as e:
687-
print(f"Failed to extract nested artifact {file}", e)
688-
exit(1)
678+
"""
679+
Push artifacts from a directory to a registry
680+
681+
Args:
682+
architecture: Target architecture of the image
683+
version: Version tag for the image
684+
cname: Canonical name of the image
685+
directory: Directory containing the artifacts
686+
manifest_file: File to write the manifest index entry to
687+
additional_tags: Additional tags to push the manifest with
688+
689+
Returns:
690+
The digest of the pushed manifest
691+
"""
692+
if additional_tags is None:
693+
additional_tags = []
689694

690695
try:
696+
# scan and extract nested artifacts
697+
for file in os.listdir(directory):
698+
try:
699+
if file.endswith(".pxe.tar.gz"):
700+
logger.info(f"Found nested artifact {file}")
701+
nested_tar_obj = tarfile.open(f"{directory}/{file}")
702+
nested_tar_obj.extractall(filter="data", path=directory)
703+
nested_tar_obj.close()
704+
except (OSError, tarfile.FilterError, tarfile.TarError) as e:
705+
print(f"Failed to extract nested artifact {file}", e)
706+
exit(1)
707+
708+
# Get metadata from files
691709
oci_metadata = get_oci_metadata_from_fileset(
692710
os.listdir(directory), architecture
693711
)
694712

695713
features = ""
714+
commit = ""
696715
for artifact in oci_metadata:
697716
if artifact["media_type"] == "application/io.gardenlinux.release":
698-
file = open(f"{directory}/{artifact["file_name"]}", "r")
699-
lines = file.readlines()
700-
for line in lines:
701-
if line.strip().startswith("GARDENLINUX_FEATURES="):
702-
features = line.strip().removeprefix(
703-
"GARDENLINUX_FEATURES="
717+
try:
718+
file_path = f"{directory}/{artifact['file_name']}"
719+
720+
config = configparser.ConfigParser(allow_unnamed_section=True)
721+
config.read(file_path)
722+
723+
if config.has_option(
724+
configparser.UNNAMED_SECTION, "GARDENLINUX_FEATURES"
725+
):
726+
features = config.get(
727+
configparser.UNNAMED_SECTION, "GARDENLINUX_FEATURES"
704728
)
705-
break
706-
file.close()
729+
if config.has_option(
730+
configparser.UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID"
731+
):
732+
commit = config.get(
733+
configparser.UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID"
734+
)
735+
736+
except (configparser.Error, IOError) as e:
737+
logger.error(
738+
f"Error reading config file {artifact['file_name']}: {e}"
739+
)
740+
break
707741

742+
# Push the image manifest
708743
digest = self.push_image_manifest(
709744
architecture,
710745
cname,
@@ -715,7 +750,103 @@ def push_from_dir(
715750
manifest_file,
716751
commit=commit,
717752
)
753+
754+
# Process additional tags if provided
755+
if additional_tags and len(additional_tags) > 0:
756+
print(f"DEBUG: Processing {len(additional_tags)} additional tags")
757+
logger.info(f"Processing {len(additional_tags)} additional tags")
758+
759+
self.push_additional_tags_manifest(
760+
architecture,
761+
cname,
762+
version,
763+
additional_tags,
764+
container=self.container,
765+
)
766+
767+
return digest
718768
except Exception as e:
719769
print("Error: ", e)
720770
exit(1)
721-
return digest
771+
772+
def push_additional_tags_manifest(
773+
self, architecture, cname, version, additional_tags, container
774+
):
775+
"""
776+
Push additional tags for an existing manifest using ORAS Registry methods
777+
778+
Args:
779+
architecture: Target architecture of the image
780+
cname: Canonical name of the image
781+
version: Version tag for the image
782+
additional_tags: List of additional tags to push
783+
container: Container object
784+
"""
785+
try:
786+
# Source tag is the tag containing the version-cname-architecture combination
787+
source_tag = f"{version}-{cname}-{architecture}"
788+
source_container = copy.deepcopy(container)
789+
source_container.tag = source_tag
790+
791+
# Authentication credentials from environment
792+
token = os.getenv("GL_CLI_REGISTRY_TOKEN")
793+
username = os.getenv("GL_CLI_REGISTRY_USERNAME")
794+
password = os.getenv("GL_CLI_REGISTRY_PASSWORD")
795+
796+
# Login to registry if credentials are provided
797+
if username and password:
798+
logger.debug(f"Logging in with username/password")
799+
try:
800+
self.login(username, password)
801+
except Exception as login_error:
802+
logger.error(f"Login error: {str(login_error)}")
803+
elif token:
804+
# If token is provided, set it directly on the Registry instance
805+
logger.debug(f"Using token authentication")
806+
self.token = base64.b64encode(token.encode("utf-8")).decode("utf-8")
807+
self.auth.set_token_auth(self.token)
808+
809+
# Get the manifest from the source container
810+
try:
811+
logger.debug(f"Getting manifest from {source_container}")
812+
manifest = self.get_manifest(source_container)
813+
if not manifest:
814+
logger.error(f"Failed to get manifest for {source_container}")
815+
return
816+
logger.info(
817+
f"Successfully retrieved manifest: {manifest['mediaType'] if 'mediaType' in manifest else 'unknown'}"
818+
)
819+
except Exception as get_error:
820+
logger.error(f"Error getting manifest: {str(get_error)}")
821+
return
822+
823+
# For each additional tag, push the manifest using Registry.upload_manifest
824+
for tag in additional_tags:
825+
try:
826+
logger.debug(f"Pushing additional tag: {tag}")
827+
828+
# Create a new container for this tag
829+
tag_container = copy.deepcopy(container)
830+
tag_container.tag = tag
831+
832+
logger.debug(f"Pushing to container: {tag_container}")
833+
834+
# Upload the manifest to the new tag
835+
response = self.upload_manifest(manifest, tag_container)
836+
837+
if response and response.status_code in [200, 201]:
838+
logger.info(f"Successfully pushed tag {tag} for manifest")
839+
else:
840+
status_code = getattr(response, "status_code", "unknown")
841+
response_text = getattr(response, "text", "No response text")
842+
logger.error(
843+
f"Failed to push tag {tag} for manifest: {status_code}"
844+
)
845+
846+
except Exception as tag_error:
847+
logger.error(
848+
f"Error pushing tag {tag} for manifest: {str(tag_error)}"
849+
)
850+
851+
except Exception as e:
852+
logger.error(f"Error in push_additional_tags_manifest: {str(e)}")

0 commit comments

Comments
 (0)