Skip to content

Commit 92d7632

Browse files
committed
WIP: OCI tagging
1 parent 43f8e8d commit 92d7632

File tree

11 files changed

+535
-230
lines changed

11 files changed

+535
-230
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: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ 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"
2223

2324
[tool.poetry.group.dev.dependencies]
2425
bandit = "^1.8.3"

src/gardenlinux/features/__main__.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +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
237247

248+
print(f"Extracted flavor: {flavor}")
249+
return flavor
238250

239251
if __name__ == "__main__":
240252
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_tags",
56+
required=False,
57+
multiple=True,
58+
help="Additional tags 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_tags,
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_tags
8180
)
8281
if cosign_file:
8382
print(digest, file=open(cosign_file, "w"))

src/gardenlinux/oci/registry.py

Lines changed: 186 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -651,40 +651,59 @@ def push_from_dir(
651651
cname: str,
652652
directory: str,
653653
manifest_file: str,
654-
commit: Optional[str] = None,
654+
additional_tags: list = None,
655655
):
656-
# Step 1 scan and extract nested artifacts:
657-
for file in os.listdir(directory):
658-
try:
659-
if file.endswith(".pxe.tar.gz"):
660-
logger.info(f"Found nested artifact {file}")
661-
nested_tar_obj = tarfile.open(f"{directory}/{file}")
662-
nested_tar_obj.extractall(filter="data", path=directory)
663-
nested_tar_obj.close()
664-
except (OSError, tarfile.FilterError, tarfile.TarError) as e:
665-
print(f"Failed to extract nested artifact {file}", e)
666-
exit(1)
656+
"""
657+
Push artifacts from a directory to a registry
658+
659+
Args:
660+
architecture: Target architecture of the image
661+
version: Version tag for the image
662+
cname: Canonical name of the image
663+
directory: Directory containing the artifacts
664+
manifest_file: File to write the manifest index entry to
665+
additional_tags: Additional tags to push the manifest with
666+
667+
Returns:
668+
The digest of the pushed manifest
669+
"""
670+
if additional_tags is None:
671+
additional_tags = []
667672

668673
try:
674+
# Step 1: scan and extract nested artifacts
675+
for file in os.listdir(directory):
676+
try:
677+
if file.endswith(".pxe.tar.gz"):
678+
logger.info(f"Found nested artifact {file}")
679+
nested_tar_obj = tarfile.open(f"{directory}/{file}")
680+
nested_tar_obj.extractall(filter="data", path=directory)
681+
nested_tar_obj.close()
682+
except (OSError, tarfile.FilterError, tarfile.TarError) as e:
683+
print(f"Failed to extract nested artifact {file}", e)
684+
exit(1)
685+
686+
# Step 2: Get metadata from files
669687
oci_metadata = get_oci_metadata_from_fileset(
670688
os.listdir(directory), architecture
671689
)
672690

673691
features = ""
692+
commit = ""
674693
for artifact in oci_metadata:
675694
if artifact["media_type"] == "application/io.gardenlinux.release":
676-
file = open(f"{directory}/{artifact["file_name"]}", "r")
677-
lines = file.readlines()
678-
for line in lines:
679-
if line.strip().startswith("GARDENLINUX_FEATURES="):
680-
features = line.strip().removeprefix(
681-
"GARDENLINUX_FEATURES="
682-
)
683-
break
684-
file.close()
685-
686-
flavor = get_flavor_from_cname(cname, get_arch=True)
687-
695+
with open(f"{directory}/{artifact["file_name"]}", "r") as file:
696+
for line in file:
697+
line = line.strip()
698+
if line.startswith("GARDENLINUX_FEATURES="):
699+
features = line.removeprefix("GARDENLINUX_FEATURES=")
700+
elif line.startswith("GARDENLINUX_COMMIT_ID="):
701+
commit = line.removeprefix("GARDENLINUX_COMMIT_ID=")
702+
if features and commit: # Break if both values are found
703+
break
704+
break # Break after processing the release file
705+
706+
# Step 3: Push the image manifest
688707
digest = self.push_image_manifest(
689708
architecture,
690709
cname,
@@ -695,11 +714,153 @@ def push_from_dir(
695714
manifest_file,
696715
commit=commit,
697716
)
717+
718+
# Step 4: Process additional tags if provided
719+
if additional_tags and len(additional_tags) > 0:
720+
print(f"DEBUG: Processing {len(additional_tags)} additional tags")
721+
logger.info(f"Processing {len(additional_tags)} additional tags")
722+
723+
# Call push_additional_tags with repository information
724+
self.push_additional_tags(
725+
architecture,
726+
cname,
727+
version,
728+
additional_tags,
729+
repo_name=None,
730+
registry_url=None
731+
)
732+
733+
return digest
698734
except Exception as e:
699735
print("Error: ", e)
700736
exit(1)
701-
return digest
702737

738+
def push_additional_tags(self, architecture, cname, version, additional_tags, repo_name=None, registry_url=None):
739+
"""
740+
Push additional tags for an existing manifest using requests directly
741+
742+
Args:
743+
architecture: Target architecture of the image
744+
cname: Canonical name of the image
745+
version: Version tag for the image
746+
additional_tags: List of additional tags to push
747+
repo_name: Repository name (optional, will be determined if not provided)
748+
registry_url: Registry URL (optional, will be determined if not provided)
749+
"""
750+
try:
751+
print(f"DEBUG: Processing {len(additional_tags)} additional tags")
752+
753+
# Source tag is version-cname-architecture
754+
source_tag = f"{version}-{cname}-{architecture}"
755+
756+
# Use provided repo_name or determine it from container_name
757+
if repo_name is None:
758+
# Get container parts
759+
container_parts = self.container_name.split(':')
760+
container_repo = container_parts[0]
761+
762+
# Parse repository name
763+
if '/' in container_repo:
764+
# Production environment
765+
repo_parts = container_repo.split('/')
766+
repo_name = '/'.join(repo_parts[1:])
767+
else:
768+
# Test environment
769+
repo_name = "gardenlinux-example"
770+
771+
# Use provided registry_url or get from class
772+
if registry_url is None:
773+
# Check if we have a registry_url property
774+
if hasattr(self, 'registry_url') and self.registry_url:
775+
registry_url = self.registry_url
776+
else:
777+
# Determine if we should use HTTP or HTTPS
778+
use_insecure = getattr(self, 'insecure', True) # Default to True for tests
779+
protocol = "http" if use_insecure else "https"
780+
781+
# Get registry host
782+
container_parts = self.container_name.split(':')
783+
container_repo = container_parts[0]
784+
785+
if '/' in container_repo:
786+
registry_host = container_repo.split('/')[0]
787+
else:
788+
registry_host = "127.0.0.1:18081" # Default for test environment
789+
790+
registry_url = f"{protocol}://{registry_host}"
791+
792+
# Ensure registry_url has protocol prefix
793+
if not registry_url.startswith("http://") and not registry_url.startswith("https://"):
794+
use_insecure = getattr(self, 'insecure', True) # Default to True for tests
795+
protocol = "http" if use_insecure else "https"
796+
registry_url = f"{protocol}://{registry_url}"
797+
798+
print(f"DEBUG: Using source tag: {source_tag}")
799+
print(f"DEBUG: Using repository: {repo_name}")
800+
print(f"DEBUG: Using registry URL: {registry_url}")
801+
802+
# Import requests for HTTP operations
803+
import requests
804+
805+
# Set up authentication headers if needed
806+
headers = {}
807+
if hasattr(self, 'token') and self.token:
808+
headers["Authorization"] = f"Bearer {self.token}"
809+
810+
# Add accept headers for manifest formats
811+
headers.update({
812+
"Accept": "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json"
813+
})
814+
815+
# Step 1: Get the manifest from the source tag
816+
source_url = f"{registry_url}/v2/{repo_name}/manifests/{source_tag}"
817+
verify = not getattr(self, 'insecure', True) # Default to not verify for tests
818+
819+
# Get the source manifest
820+
source_resp = requests.get(source_url, headers=headers, verify=verify)
821+
822+
if source_resp.status_code != 200:
823+
print(f"DEBUG: Failed to get source manifest: {source_resp.status_code}")
824+
logger.error(f"Failed to get source manifest: {source_resp.status_code}")
825+
return
826+
827+
# Get the manifest content and content-type
828+
manifest_content = source_resp.text
829+
content_type = source_resp.headers.get("Content-Type", "application/vnd.oci.image.manifest.v1+json")
830+
831+
print(f"DEBUG: Successfully retrieved manifest with content type: {content_type}")
832+
833+
# Step 2: For each additional tag, push the manifest
834+
for tag in additional_tags:
835+
try:
836+
print(f"DEBUG: Pushing additional tag: {tag}")
837+
838+
# Push using requests
839+
push_headers = headers.copy()
840+
push_headers.update({
841+
"Content-Type": content_type
842+
})
843+
844+
# Create push URL for the new tag
845+
tag_url = f"{registry_url}/v2/{repo_name}/manifests/{tag}"
846+
847+
# Push the manifest using requests
848+
tag_resp = requests.put(tag_url, data=manifest_content, headers=push_headers, verify=verify)
849+
850+
if tag_resp.status_code in [200, 201]:
851+
print(f"DEBUG: Successfully pushed tag {tag}")
852+
logger.info(f"Successfully pushed tag {tag}")
853+
else:
854+
print(f"DEBUG: Failed to push tag {tag}: {tag_resp.status_code}")
855+
logger.error(f"Failed to push tag {tag}: {tag_resp.status_code}")
856+
857+
except Exception as e:
858+
print(f"DEBUG: Error pushing tag {tag}: {str(e)}")
859+
logger.error(f"Error pushing tag {tag}: {str(e)}")
860+
861+
except Exception as e:
862+
print(f"DEBUG: Error in push_additional_tags: {str(e)}")
863+
logger.error(f"Error in push_additional_tags: {str(e)}")
703864

704865
def extract_tar(tar: str, tmpdir: str):
705866
"""

0 commit comments

Comments
 (0)