Skip to content

Commit 09d8098

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

File tree

6 files changed

+856
-556
lines changed

6 files changed

+856
-556
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: 157 additions & 23 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
@@ -28,9 +29,11 @@
2829
from oras.provider import Registry
2930
from oras.schemas import manifest as oras_manifest_schema
3031

31-
from ..constants import OCI_ANNOTATION_SIGNATURE_KEY, OCI_ANNOTATION_SIGNED_STRING_KEY
32+
from ..constants import (
33+
OCI_ANNOTATION_SIGNATURE_KEY,
34+
OCI_ANNOTATION_SIGNED_STRING_KEY
35+
)
3236
from ..features import CName
33-
3437
from .checksum import (
3538
calculate_sha256,
3639
verify_sha256,
@@ -673,38 +676,73 @@ def push_from_dir(
673676
cname: str,
674677
directory: str,
675678
manifest_file: str,
676-
commit: Optional[str] = None,
679+
additional_tags: list = None,
677680
):
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)
681+
"""
682+
Push artifacts from a directory to a registry
683+
684+
Args:
685+
architecture: Target architecture of the image
686+
version: Version tag for the image
687+
cname: Canonical name of the image
688+
directory: Directory containing the artifacts
689+
manifest_file: File to write the manifest index entry to
690+
additional_tags: Additional tags to push the manifest with
691+
692+
Returns:
693+
The digest of the pushed manifest
694+
"""
695+
if additional_tags is None:
696+
additional_tags = []
689697

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

695716
features = ""
717+
commit = ""
696718
for artifact in oci_metadata:
697719
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="
720+
try:
721+
file_path = f"{directory}/{artifact['file_name']}"
722+
723+
config = configparser.ConfigParser(allow_unnamed_section=True)
724+
config.read(file_path)
725+
726+
if config.has_option(
727+
configparser.UNNAMED_SECTION, "GARDENLINUX_FEATURES"
728+
):
729+
features = config.get(
730+
configparser.UNNAMED_SECTION, "GARDENLINUX_FEATURES"
731+
)
732+
if config.has_option(
733+
configparser.UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID"
734+
):
735+
commit = config.get(
736+
configparser.UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID"
704737
)
705-
break
706-
file.close()
707738

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

0 commit comments

Comments
 (0)