Skip to content

Commit 1bcdff0

Browse files
authored
Add Git, Version and LoggerSetup Classes (#94)
* add logging * add git module * add version * add get_features_list * black lint * bump version
1 parent cac2454 commit 1bcdff0

File tree

7 files changed

+257
-1
lines changed

7 files changed

+257
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python_gardenlinux_lib"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "Contains tools to work with the features directory of gardenlinux, for example deducting dependencies from feature sets or validating cnames"
55
authors = ["Garden Linux Maintainers <[email protected]>"]
66
license = "Apache-2.0"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .git import Git
2+
from .version import Version
3+
4+
__all__ = ["Git", "Version"]

src/python_gardenlinux_lib/features/parse_features.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ def get_features_dict(cname: str, gardenlinux_root: str) -> dict:
140140
return features_by_type
141141

142142

143+
def get_features_list(cname: str, gardenlinux_root: str) -> list:
144+
"""
145+
:param str cname: the target cname to get the feature dict for
146+
:param str gardenlinux_root: path of garden linux src root
147+
:return: list of features for a given cname
148+
149+
"""
150+
feature_base_dir = f"{gardenlinux_root}/features"
151+
input_features = __reverse_cname_base(cname)
152+
feature_graph = read_feature_files(feature_base_dir)
153+
graph = filter_graph(feature_graph, input_features)
154+
features = __reverse_sort_nodes(graph)
155+
return features
156+
157+
143158
def get_features(cname: str, gardenlinux_root: str) -> str:
144159
"""
145160
:param str cname: the target cname to get the feature set for
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .git import Git
2+
3+
__all__ = ["Git"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import subprocess
2+
from pathlib import Path
3+
import sys
4+
5+
from ..logger import LoggerSetup
6+
7+
8+
class Git:
9+
"""Git operations handler."""
10+
11+
def __init__(self, logger=None):
12+
"""Initialize Git handler.
13+
14+
Args:
15+
logger: Optional logger instance
16+
"""
17+
self.log = logger or LoggerSetup.get_logger("gardenlinux.git")
18+
19+
def get_root(self):
20+
"""Get the root directory of the current Git repository."""
21+
try:
22+
root_dir = subprocess.check_output(
23+
["git", "rev-parse", "--show-toplevel"], text=True
24+
).strip()
25+
self.log.debug(f"Git root directory: {root_dir}")
26+
return Path(root_dir)
27+
except subprocess.CalledProcessError as e:
28+
self.log.error(
29+
"Not a git repository or unable to determine root directory."
30+
)
31+
self.log.debug(f"Git command failed with: {e}")
32+
sys.exit(1)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import logging
2+
3+
4+
class LoggerSetup:
5+
"""Handles logging configuration for the gardenlinux library."""
6+
7+
@staticmethod
8+
def get_logger(name, level=None):
9+
"""Create and configure a logger.
10+
11+
Args:
12+
name: Name for the logger, typically in format 'gardenlinux.module'
13+
level: Logging level, defaults to INFO if not specified
14+
15+
Returns:
16+
Configured logger instance
17+
"""
18+
logger = logging.getLogger(name)
19+
20+
# Only add handler if none exists to prevent duplicate handlers
21+
if not logger.handlers:
22+
handler = logging.StreamHandler()
23+
formatter = logging.Formatter("%(levelname)s: %(message)s")
24+
handler.setFormatter(formatter)
25+
logger.addHandler(handler)
26+
27+
# Set default level if specified
28+
if level is not None:
29+
logger.setLevel(level)
30+
else:
31+
logger.setLevel(logging.INFO)
32+
33+
return logger
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import re
2+
import subprocess
3+
from datetime import datetime, timezone
4+
import requests
5+
from pathlib import Path
6+
7+
from .logger import LoggerSetup
8+
from .features.parse_features import get_features
9+
10+
11+
class Version:
12+
"""Handles version-related operations for Garden Linux."""
13+
14+
def __init__(self, git_root: Path, logger=None):
15+
"""Initialize Version handler.
16+
17+
Args:
18+
git_root: Path to the Git repository root
19+
logger: Optional logger instance
20+
"""
21+
self.git_root = git_root
22+
self.log = logger or LoggerSetup.get_logger("gardenlinux.version")
23+
self.start_date = "Mar 31 00:00:00 UTC 2020"
24+
25+
def get_minor_from_repo(self, major):
26+
"""Check repo.gardenlinux.io for highest available suite minor for given major.
27+
28+
Args:
29+
major: major version
30+
Returns:
31+
minor version
32+
"""
33+
minor = 0
34+
limit = 100 # Hard limit the search
35+
repo_url = f"https://repo.gardenlinux.io/gardenlinux/dists/{major}.{{}}/Release"
36+
37+
while minor <= limit:
38+
try:
39+
check_url = repo_url.format(minor)
40+
response = requests.get(check_url)
41+
if response.status_code != 200:
42+
# No more versions found, return last successful minor
43+
return minor - 1
44+
minor += 1
45+
except requests.RequestException as e:
46+
self.log.debug(f"Error checking repo URL {check_url}: {e}")
47+
return minor - 1
48+
49+
# If we hit the limit, return the last minor
50+
return minor - 1
51+
52+
def get_version(self):
53+
"""Get version using same logic as garden-version bash script.
54+
55+
Args:
56+
version: version string
57+
Returns:
58+
version string
59+
"""
60+
61+
try:
62+
# Check VERSION file
63+
version_file = self.git_root / "VERSION"
64+
if version_file.exists():
65+
version = version_file.read_text().strip()
66+
# Remove comments and empty lines
67+
version = re.sub(r"#.*$", "", version, flags=re.MULTILINE)
68+
version = "\n".join(
69+
line for line in version.splitlines() if line.strip()
70+
)
71+
version = version.strip()
72+
else:
73+
version = "today"
74+
75+
if not version:
76+
version = "today"
77+
78+
# Handle numeric versions (e.g., "27.1")
79+
if re.match(r"^[0-9\.]*$", version):
80+
major = version.split(".")[0]
81+
if int(major) < 10000000: # Sanity check for major version
82+
if "." in version:
83+
return version # Return full version if minor is specified
84+
else:
85+
# Get latest minor version from repo
86+
minor = self.get_minor_from_repo(major)
87+
return f"{major}.{minor}"
88+
89+
# Handle 'today' or 'experimental'
90+
if version in ["today", "experimental"]:
91+
# Calculate days since start date
92+
start_timestamp = datetime.strptime(
93+
self.start_date, "%b %d %H:%M:%S %Z %Y"
94+
).timestamp()
95+
today_timestamp = datetime.now(timezone.utc).timestamp()
96+
major = int((today_timestamp - start_timestamp) / (24 * 60 * 60))
97+
return version
98+
99+
# Handle date input
100+
try:
101+
# Try to parse as date
102+
input_date = datetime.strptime(version, "%Y%m%d")
103+
start_date = datetime.strptime(self.start_date, "%b %d %H:%M:%S %Z %Y")
104+
days_diff = (input_date - start_date).days
105+
return f"{days_diff}.0"
106+
except ValueError:
107+
pass
108+
109+
return version
110+
111+
except Exception as e:
112+
self.log.error(f"Error determining version: {e}")
113+
return "local"
114+
115+
def get_short_commit(self):
116+
"""Get short commit using same logic as the get_commit bash script.
117+
118+
Returns:
119+
short commit string
120+
"""
121+
try:
122+
# Check if COMMIT file exists in git root
123+
commit_file = self.git_root / "COMMIT"
124+
if commit_file.exists():
125+
return commit_file.read_text().strip()
126+
127+
# Check if git repo is clean
128+
status_output = (
129+
subprocess.check_output(
130+
["git", "status", "--porcelain"], stderr=subprocess.DEVNULL
131+
)
132+
.decode()
133+
.strip()
134+
)
135+
136+
if status_output:
137+
self.log.info(f"git status:\n {status_output}")
138+
# Dirty repo or not a git repo
139+
return "local"
140+
else:
141+
# Clean repo - use git commit hash
142+
return (
143+
subprocess.check_output(
144+
["git", "rev-parse", "--short", "HEAD"],
145+
stderr=subprocess.DEVNULL,
146+
)
147+
.decode()
148+
.strip()
149+
)
150+
151+
except subprocess.CalledProcessError:
152+
return "local"
153+
154+
def get_cname(self, platform, features, arch):
155+
"""Get canonical name (cname) for Garden Linux image.
156+
157+
Args:
158+
platform: Platform identifier (e.g., 'kvm', 'aws')
159+
features: List of features
160+
arch: Architecture ('amd64' or 'arm64')
161+
162+
Returns:
163+
Generated cname string
164+
"""
165+
# Get version and commit
166+
version = self.get_version()
167+
commit = self.get_short_commit()
168+
169+
return f"{platform}-{features}-{arch}-{version}-{commit}"

0 commit comments

Comments
 (0)