1616from typing import Optional , Tuple
1717
1818import jsonschema
19+ import opencontainers .distribution .reggie as reggie
1920import oras .auth
2021import oras .client
2122import oras .defaults
2930from oras .schemas import manifest as oras_manifest_schema
3031
3132from 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+ )
3338from .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,161 @@ 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+ # Get repository and registry information from container
731+ repo_name = self .container .repository
732+ registry_url = self .container .registry
733+
734+ # Call push_additional_tags_manifest with repository information
735+ self .push_additional_tags_manifest (
736+ architecture ,
737+ cname ,
738+ version ,
739+ additional_tags ,
740+ repo_name = repo_name ,
741+ registry_url = registry_url ,
742+ )
743+
744+ return digest
700745 except Exception as e :
701746 print ("Error: " , e )
702747 exit (1 )
703- return digest
748+
749+ def push_additional_tags_manifest (
750+ self , architecture , cname , version , additional_tags , repo_name , registry_url
751+ ):
752+ """
753+ Push additional tags for an existing manifest using Reggie client
754+
755+ Args:
756+ architecture: Target architecture of the image
757+ cname: Canonical name of the image
758+ version: Version tag for the image
759+ additional_tags: List of additional tags to push
760+ repo_name: Repository name
761+ registry_url: Registry URL
762+ """
763+ try :
764+ print (
765+ f"DEBUG: Processing { len (additional_tags )} additional tags for manifest"
766+ )
767+
768+ # Source tag is version-cname-architecture as pushed by push_image_manifest
769+ source_tag = f"{ version } -{ cname } -{ architecture } "
770+
771+ # Ensure registry_url has protocol prefix
772+ if not registry_url .startswith ("http://" ) and not registry_url .startswith (
773+ "https://"
774+ ):
775+ use_insecure = getattr (
776+ self , "insecure" , True
777+ ) # Default to True if attribute doesn't exist
778+ protocol = "http" if use_insecure else "https"
779+ registry_url = f"{ protocol } ://{ registry_url } "
780+
781+ print (f"DEBUG: Using source tag: { source_tag } " )
782+ print (f"DEBUG: Using repository: { repo_name } " )
783+ print (f"DEBUG: Using registry URL: { registry_url } " )
784+ logger .info (f"Using source tag: { source_tag } " )
785+ logger .info (f"Using repository: { repo_name } " )
786+ logger .info (f"Using registry URL: { registry_url } " )
787+
788+ # Configure client options
789+ client_options = [reggie .WithUserAgent (GL_USER_AGENT_REGISTRY )]
790+
791+ # Initialize reggie client
792+ client = reggie .NewClient (registry_url , * client_options )
793+
794+ # Set authentication token if provided
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+ # Step 1: Get the manifest from the source tag using Reggie
800+ get_manifest_req = client .NewRequest (
801+ "GET" , f"/v2/{ repo_name } /manifests/{ source_tag } "
802+ )
803+ get_manifest_req .headers .update (
804+ {
805+ "Accept" : "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json"
806+ }
807+ )
808+
809+ # Set authentication for the GET request
810+ if token :
811+ get_manifest_req .SetAuthToken (token )
812+ elif username and password :
813+ get_manifest_req .SetBasicAuth (username , password )
814+
815+ get_manifest_resp = client .Do (get_manifest_req )
816+
817+ if get_manifest_resp .status_code != 200 :
818+ print (
819+ f"DEBUG: Failed to get source manifest: { get_manifest_resp .status_code } "
820+ )
821+ logger .error (
822+ f"Failed to get source manifest: { get_manifest_resp .status_code } "
823+ )
824+ return
825+
826+ # Get the manifest content and content-type
827+ manifest_content = get_manifest_resp .text
828+ content_type = get_manifest_resp .headers .get (
829+ "Content-Type" , "application/vnd.oci.image.manifest.v1+json"
830+ )
831+
832+ print (
833+ f"DEBUG: Successfully retrieved manifest with content type: { content_type } "
834+ )
835+ logger .info (
836+ f"Successfully retrieved manifest with content type: { content_type } "
837+ )
838+
839+ # Step 2: For each additional tag, push the manifest using Reggie
840+ for tag in additional_tags :
841+ try :
842+ print (f"DEBUG: Pushing additional tag: { tag } " )
843+
844+ # Create a new PUT request for the tag
845+ put_manifest_req = client .NewRequest (
846+ "PUT" , f"/v2/{ repo_name } /manifests/{ tag } "
847+ )
848+
849+ # Set the content type header to match the original manifest
850+ put_manifest_req .headers .update ({"Content-Type" : content_type })
851+
852+ # Set authentication for the PUT request
853+ if token :
854+ put_manifest_req .SetAuthToken (token )
855+ elif username and password :
856+ put_manifest_req .SetBasicAuth (username , password )
857+
858+ # Set the body to the manifest content
859+ put_manifest_req .SetBody (manifest_content )
860+
861+ # Use the reggie client to make the request
862+ put_manifest_resp = client .Do (put_manifest_req )
863+
864+ if put_manifest_resp .status_code in [200 , 201 ]:
865+ print (f"DEBUG: Successfully pushed tag { tag } for manifest" )
866+ logger .info (f"Successfully pushed tag { tag } for manifest" )
867+ else :
868+ print (
869+ f"DEBUG: Failed to push tag { tag } for manifest: { put_manifest_resp .status_code } "
870+ )
871+ logger .error (
872+ f"Failed to push tag { tag } for manifest: { put_manifest_resp .status_code } "
873+ )
874+
875+ except Exception as e :
876+ print (f"DEBUG: Error pushing tag { tag } for manifest: { str (e )} " )
877+ logger .error (f"Error pushing tag { tag } for manifest: { str (e )} " )
878+
879+ except Exception as e :
880+ print (f"DEBUG: Error in push_additional_tags_manifest: { str (e )} " )
881+ logger .error (f"Error in push_additional_tags_manifest: { str (e )} " )
0 commit comments