Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,097 changes: 1,053 additions & 1,044 deletions poetry.lock

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ packages = [{ include = "gardenlinux", from = "src" }]
[tool.poetry.dependencies]
python = ">=3.13,!=3.14.1"
apt-repo = "^0.5"
boto3 = "^1.42.30"
click = "^8.3.1"
boto3 = "^1.43.10"
click = "^8.4.0"
cryptography = "^46.0.3"
jsonschema = "^4.26.0"
networkx = "^3.6"
Expand All @@ -21,22 +21,22 @@ pygit2 = "^1.19.1"
pygments = "^2.19.2"
PyGithub = "^2.8.1"
PyYAML = "^6.0.2"
gitpython = "^3.1.45"
semver = "^3.0.4"

[tool.poetry.group.dev.dependencies]
bandit = "^1.9.3"
bandit = "^1.9.4"
isort = "^8.0.1"
moto = "^5.1.20"
pre-commit = "^4.5.1"
python-dotenv = "^1.2.1"
pytest = "^9.0.2"
pytest-cov = "^7.0.0"
moto = "^5.2.1"
pre-commit = "^4.6.0"
python-dotenv = "^1.2.2"
pytest = "^9.0.3"
pytest-cov = "^7.1.0"
requests-mock = "^1.12.1"
mypy = "1.20.1"
types-click = "^7.1.8"
types-pyyaml = "^6.0.12.20250915"
types-requests = "^2.32.4.20260107"
boto3-stubs = { extras = ["s3"], version = "^1.42.30" }
types-pyyaml = "^6.0.12.20260518"
types-requests = "^2.33.0.20260518"
boto3-stubs = { extras = ["s3"], version = "^1.42.41" }

[tool.poetry.group.docs.dependencies]
sphinx-rtd-theme = "^3.0.2"
Expand Down
7 changes: 1 addition & 6 deletions src/gardenlinux/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@

GL_BUG_REPORT_URL = "https://github.com/gardenlinux/gardenlinux/issues"
GL_COMMIT_SPECIAL_VALUES = ("local",)
GL_CONTAINER_REGISTRY_BASE_URL = "ghcr.io/gardenlinux/gardenlinux"
GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux"
GL_DISTRIBUTION_NAME = "Garden Linux"
GL_HOME_URL = "https://gardenlinux.io"
Expand All @@ -157,8 +158,6 @@
OCI_ANNOTATION_SIGNED_STRING_KEY = "io.gardenlinux.oci.signed-string"
OCI_IMAGE_INDEX_MEDIA_TYPE = "application/vnd.oci.image.index.v1+json"

RELEASE_ID_FILE = ".github_release_id"

REQUESTS_TIMEOUTS = (5, 60) # connect, read

S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads"
Expand All @@ -167,7 +166,3 @@
GLVD_BASE_URL = "https://security.gardenlinux.org/v1"

PODMAN_CONNECTION_MAX_IDLE_SECONDS = 3

# https://github.com/gardenlinux/gardenlinux/issues/3044
# Empty string is the 'legacy' variant with traditional root fs and still needed/supported
IMAGE_VARIANTS = ["", "_usi", "_tpm2_trustedboot"]
63 changes: 63 additions & 0 deletions src/gardenlinux/distro_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from semver import Version


class UnsupportedDistroVersion(Exception):
pass


class NotAPatchRelease(Exception):
pass


class DistroVersion(Version): # type: ignore[misc]
def __init__(self, version: str | Version):
self._version_format_without_patch_number = False

try:
if isinstance(version, Version):
version_parsed = version
elif len(version.split(".")) == 2:
# Support version strings without patch numbers
version_parsed = Version.parse(f"{version}.0")
self._version_format_without_patch_number = True
else:
version_parsed = Version.parse(version)
except Exception as exc:
raise UnsupportedDistroVersion(exc)

Version.__init__(
self,
version_parsed.major,
version_parsed.minor,
version_parsed.patch,
version_parsed.prerelease,
version_parsed.build,
)

@property
def is_patch_release(self) -> bool:
if self._version_format_without_patch_number:
return self.minor > 0 # type: ignore[no-any-return]

return self.patch > 0 # type: ignore[no-any-return]

@property
def previous_patch_release(self) -> str:
if not self.is_patch_release:
raise NotAPatchRelease(f"{self} is not a patch release")

if self._version_format_without_patch_number:
previous_version = DistroVersion(
Version(self.major, self.minor - 1, self.patch)
)
return f"{previous_version.major}.{previous_version.minor}"

return str(
Version(
self.major,
self.minor,
self.patch - 1,
prerelease=self.prerelease,
build=self.build,
)
)
73 changes: 0 additions & 73 deletions src/gardenlinux/distro_version/__init__.py

This file was deleted.

3 changes: 2 additions & 1 deletion src/gardenlinux/git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Git module
"""

from .credentials import Credentials
from .repository import Repository

__all__ = ["Repository"]
__all__ = ["Credentials", "Repository"]
74 changes: 74 additions & 0 deletions src/gardenlinux/git/credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-

"""
Git credentials provider
"""

import os
from typing import Optional, Union

from pygit2 import KeypairFromAgent, UserPass
from pygit2.enums import CredentialType


class Credentials:
"""
Git credentials provider for pygit2 remote operations.

Handles authentication dynamically based on the server's allowed credential
types. This accounts for git config `url.<base>.insteadOf` rules that may
rewrite HTTPS URLs to SSH at the libgit2 transport layer.

:author: Garden Linux Maintainers
:copyright: Copyright 2024 SAP SE
:package: gardenlinux
:subpackage: git
:since: 0.10.0
:license: https://www.apache.org/licenses/LICENSE-2.0
Apache License, Version 2.0
"""

def __init__(self, token: Optional[str] = None):
"""
Constructor __init__(Credentials)

:param token: GitHub/Git access token for HTTPS authentication.
Falls back to the GITHUB_TOKEN environment variable if not provided.

:since: 0.10.0
"""

self._token = token

if self._token is None:
self._token = os.environ.get("GITHUB_TOKEN")

def __call__(
self,
url: str,
username_from_url: Optional[str],
allowed_types: CredentialType,
) -> Optional[Union[UserPass, KeypairFromAgent]]:
"""
Pygit2 credentials callback.

Called by libgit2 when authentication is required during remote
operations. Returns the appropriate credential object based on what
the server allows.

:param url: The URL being accessed (after any insteadOf rewrites)
:param username_from_url: Username extracted from the URL, if any
:param allowed_types: Bitmask of credential types the server accepts

:return: A pygit2 credential object, or None if no credentials available
:since: 0.10.0
"""

if allowed_types & CredentialType.USERPASS_PLAINTEXT:
if self._token:
return UserPass(self._token, "x-oauth-basic")

if allowed_types & CredentialType.SSH_KEY:
return KeypairFromAgent(username_from_url or "git")

return None
6 changes: 4 additions & 2 deletions src/gardenlinux/git/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from pathlib import Path
from typing import Any, List, Optional

from pygit2 import Oid
from pygit2 import Oid, RemoteCallbacks
from pygit2 import Repository as _Repository
from pygit2 import init_repository

from ..constants import GL_REPOSITORY_URL
from ..logger import LoggerSetup
from .credentials import Credentials


class Repository(_Repository): # type: ignore[misc]
Expand Down Expand Up @@ -135,7 +136,8 @@ def checkout_repo(
)

repo = init_repository(git_directory, origin_url=repo_url)
repo.remotes["origin"].fetch()
callbacks = RemoteCallbacks(credentials=Credentials())
repo.remotes["origin"].fetch(callbacks=callbacks)

if commit is None:
refish = f"origin/{branch}"
Expand Down
22 changes: 3 additions & 19 deletions src/gardenlinux/github/release/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
import logging
import sys

from ...constants import RELEASE_ID_FILE
from ...logger import LoggerSetup
from .deployment_platform import DeploymentPlatform
from .release import Release
from .release_images_metadata import ReleaseImagesMetadata

LOGGER = LoggerSetup.get_logger("gardenlinux.github.release", logging.INFO)


def write_to_release_id_file(release_id: str | int) -> None:
try:
with open(RELEASE_ID_FILE, "w") as file:
file.write(str(release_id))
LOGGER.info(f"Created {RELEASE_ID_FILE} successfully.")
except IOError as e:
LOGGER.error(f"Could not create {RELEASE_ID_FILE} file: {e}")
sys.exit(1)


__all__ = ["Release", "write_to_release_id_file"]
__all__ = ["DeploymentPlatform", "Release", "ReleaseImagesMetadata"]
23 changes: 11 additions & 12 deletions src/gardenlinux/github/release/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from gardenlinux.constants import GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME
from gardenlinux.logger import LoggerSetup

from ..release_notes import create_github_release_notes
from . import write_to_release_id_file
from .notes import MarkdownGenerator
from .release import Release

LOGGER = LoggerSetup.get_logger("gardenlinux.github", logging.INFO)
Expand Down Expand Up @@ -167,24 +166,24 @@ def main() -> None:
release.is_latest = args.latest
release.create()
elif args.command == "create-with-gl-release-notes":
body = create_github_release_notes(
args.tag, args.commit, GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME
)
release = Release(args.repo, args.owner)
release.tag = args.tag
release.commitish = args.commit
release.is_latest = args.latest

generator = MarkdownGenerator(release, GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME)

if args.dry_run:
print("Dry Run ...")
print("This release would be created:")
print(body)
print(str(generator))
else:
release = Release(args.repo, args.owner)
release.tag = args.tag
release.body = body
release.commitish = args.commit
release.is_latest = args.latest
release.body = str(generator)

release_id = release.create()
write_to_release_id_file(f"{release_id}")
LOGGER.info(f"Release created with ID: {release_id}")

print(f"{release_id}")
elif args.command == "upload":
release = Release.get(args.release_id, repo=args.repo, owner=args.owner)

Expand Down
8 changes: 8 additions & 0 deletions src/gardenlinux/github/release/ali_cloud.platform.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"short_name": "ali",
"full_name": "Alibaba Cloud",
"image_extension": "qcow2",

"mapping_type": "regions_list",
"mapping_entry_json": "{\"region\": \"{region_id}\", \"image_id\": \"{image_id}\"}"
}
Loading
Loading