1010import os
1111import re
1212import sys
13- from functools import total_ordering
1413
1514from invoke .exceptions import Exit
1615from invoke .tasks import task
2726except ImportError :
2827 yaml = None
2928
29+ try :
30+ import semver
31+ except ImportError :
32+ semver = None
3033
3134DOCKER_HUB_API_URL = "https://hub.docker.com/v2/repositories/kindest/node/tags"
3235VERSIONS_FILE = "k8s_versions.json"
3336E2E_YAML_PATH = ".gitlab/e2e/e2e.yml"
3437
3538
36- @total_ordering
37- class Version :
38- """Kubernetes version with proper semantic version comparison.
39-
40- Handles both final releases and RC (release candidate) versions:
41- - v1.35.0-rc.1 < v1.35.0-rc.2 < v1.35.0
42- """
43-
44- def __init__ (self , major : int , minor : int , patch : int , rc : int | None = None ):
45- self .major = major
46- self .minor = minor
47- self .patch = patch
48- self .rc = rc # None for final releases, number for RC versions
49-
50- def __eq__ (self , other ):
51- if not isinstance (other , Version ):
52- return NotImplemented
53- return (self .major , self .minor , self .patch , self .rc ) == (
54- other .major ,
55- other .minor ,
56- other .patch ,
57- other .rc ,
58- )
59-
60- def __lt__ (self , other ):
61- if not isinstance (other , Version ):
62- return NotImplemented
63-
64- # Compare major, minor, patch first
65- # If they aren't equal, then return the tuple comparison
66- if (self .major , self .minor , self .patch ) != (other .major , other .minor , other .patch ):
67- return (self .major , self .minor , self .patch ) < (other .major , other .minor , other .patch )
68-
69- # If base versions are equal, compare RC status
70- # RC versions are less than final releases: v1.35.0-rc.1 < v1.35.0
71- if self .rc is None :
72- return False # self is final (>= any RC or final with same base version)
73- return other .rc is None or self .rc < other .rc # self is RC, so less than final OR compare RC numbers
74-
75- def __repr__ (self ):
76- if self .rc is None :
77- return f"Version({ self .major } .{ self .minor } .{ self .patch } )"
78- return f"Version({ self .major } .{ self .minor } .{ self .patch } -rc.{ self .rc } )"
79-
80-
8139def _check_dependencies ():
8240 """Check if required dependencies are installed."""
8341 missing = []
8442 if requests is None :
8543 missing .append ('requests' )
8644 if yaml is None :
8745 missing .append ('pyyaml' )
46+ if semver is None :
47+ missing .append ('semver' )
8848
8949 if missing :
9050 raise Exit (
@@ -93,28 +53,26 @@ def _check_dependencies():
9353 )
9454
9555
96- def _parse_version (version_str : str ) -> Version | None :
56+ def _parse_version (version_str : str ) -> semver . VersionInfo | None :
9757 """
98- Parse a Kubernetes version string into a Version object.
58+ Parse a Kubernetes version string into a semver VersionInfo object.
59+
60+ Semver naturally handles RC versions correctly:
61+ - v1.35.0-rc.1 < v1.35.0-rc.2 < v1.35.0
9962
10063 Examples:
101- 'v1.34.0' -> Version (1, 34, 0)
102- 'v1.35.0-rc.1' -> Version (1, 35, 0, rc=1 )
64+ 'v1.34.0' -> VersionInfo (1, 34, 0)
65+ 'v1.35.0-rc.1' -> VersionInfo (1, 35, 0, prerelease='rc.1' )
10366
10467 Returns None if the version string is invalid.
10568 """
106- # Match a release version
107- match = re .match (r'^v?(\d+)\.(\d+)\.(\d+)$' , version_str )
108- if match :
109- major , minor , patch = map (int , match .groups ())
110- return Version (major , minor , patch )
111- # Match an RC version
112- rc_match = re .match (r'^v?(\d+)\.(\d+)\.(\d+)\-rc\.(\d+)$' , version_str )
113- if rc_match :
114- major , minor , patch , rc = map (int , rc_match .groups ())
115- return Version (major , minor , patch , rc = rc )
69+ # Remove leading 'v' if present
70+ clean_version = version_str .lstrip ('v' )
11671
117- return None
72+ try :
73+ return semver .VersionInfo .parse (clean_version )
74+ except (ValueError , AttributeError ):
75+ return None
11876
11977
12078def _get_docker_hub_tags () -> list [dict ]:
@@ -158,8 +116,7 @@ def _get_latest_k8s_versions() -> dict[str, dict[str, str]]:
158116 version_tags = []
159117
160118 # Final release Kubernetes version tags
161- _ = _get_docker_hub_tags () # TODO: add this back when testing of RCs is done
162- for tag in []:
119+ for tag in _get_docker_hub_tags ():
163120 tag_name = tag .get ('name' , '' )
164121 version = _parse_version (tag_name )
165122
@@ -171,11 +128,10 @@ def _get_latest_k8s_versions() -> dict[str, dict[str, str]]:
171128 # RC Kubernetes version tags
172129 for tag in get_github_rc_releases ():
173130 tag_name = tag .get ('tag_name' , '' )
174- tarball = tag .get ('tarball_url' )
175131 version = _parse_version (tag_name )
176- if version and tag_name and tarball :
132+ if version and tag_name :
177133 # Hardcode 'rc' to True because get_github_rc_releases() only returns rc releases
178- version_tags .append ({'version' : version , 'tag' : tag_name , 'rc' : True , 'tarball' : tarball })
134+ version_tags .append ({'version' : version , 'tag' : tag_name , 'rc' : True })
179135
180136 # Sort by version (major, minor, patch)
181137 version_tags .sort (key = lambda x : x ['version' ], reverse = True )
@@ -188,19 +144,16 @@ def _get_latest_k8s_versions() -> dict[str, dict[str, str]]:
188144 tag = latest .get ('tag' )
189145 digest = latest .get ('digest' )
190146 rc = latest .get ('rc' )
191- tarball = latest .get ('tarball' )
192147
193148 # Build return dictionary
194- # Structure: {tag_name: {'tag': tag_name, 'digest': digest?, 'rc': bool?, 'tarball': url? }}
195- # Final releases include 'digest', RC releases include 'rc' and 'tarball'
149+ # Structure: {tag_name: {'tag': tag_name, 'digest': digest?, 'rc': bool?}}
150+ # Final releases include 'digest', RC releases include 'rc'
196151 if tag :
197152 result = {tag : {'tag' : tag }}
198153 if digest :
199154 result [tag ]['digest' ] = digest
200155 if rc :
201156 result [tag ]['rc' ] = rc
202- if tarball :
203- result [tag ]['tarball' ] = tarball
204157 return result
205158
206159 return {}
@@ -226,11 +179,29 @@ def _save_versions(versions: dict[str, dict[str, str]], versions_file: str) -> N
226179def _find_new_versions (
227180 current : dict [str , dict [str , str ]], previous : dict [str , dict [str , str ]]
228181) -> dict [str , dict [str , str ]]:
229- """Find versions that are new or have different digests."""
182+ """Find versions that are new or have different digests.
183+
184+ Notes:
185+ - RC versions from GitHub won't have digests initially
186+ - Only compare digests if BOTH current and previous have them
187+ - If current has no digest (RC from GitHub) and version exists, don't mark as new
188+ """
230189 new_versions = {}
231190
232191 for version , data in current .items ():
233- if version not in previous or previous [version ].get ('digest' ) != data .get ('digest' ):
192+ # Version doesn't exist in previous - it's new
193+ if version not in previous :
194+ new_versions [version ] = data
195+ continue
196+
197+ # Version exists - check if digest changed
198+ current_digest = data .get ('digest' )
199+ previous_digest = previous [version ].get ('digest' )
200+
201+ # Only compare digests if BOTH have them
202+ # This prevents RC versions (no digest from GitHub) from being marked as new
203+ # when they already exist in the saved file (with digest from build)
204+ if current_digest and previous_digest and current_digest != previous_digest :
234205 new_versions [version ] = data
235206
236207 return new_versions
@@ -436,10 +407,7 @@ def fetch_versions(_, output_file=VERSIONS_FILE):
436407 latest_data = current_versions [latest_version ]
437408
438409 print (f"Latest Kubernetes version: { latest_version } " )
439- if latest_data .get ('tarball' ):
440- print (f" Tarball: { latest_data ['tarball' ]} " )
441- else :
442- print (f" Digest: { latest_data ['digest' ]} " )
410+ print (f" Digest: { latest_data .get ('digest' , 'Digest unknown' )} " )
443411
444412 # Load previous versions and compare
445413 previous_versions = _load_existing_versions (output_file )
@@ -448,10 +416,7 @@ def fetch_versions(_, output_file=VERSIONS_FILE):
448416 if new_versions :
449417 print ("\n New version(s) found!" )
450418 for version , data in new_versions .items ():
451- if data .get ('tarball' ):
452- print (f" { version } : { data ['tarball' ]} " )
453- else :
454- print (f" { version } : { data ['digest' ]} " )
419+ print (f" { version } : { data .get ('digest' , 'Digest unknown' )} " )
455420
456421 # Set GitHub Actions outputs
457422 _set_github_output ('has_new_versions' , 'true' )
0 commit comments