github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/tools/dependencies/list_versions.py (about) 1 #!/usr/bin/env python 2 3 # List all available versions of Weave Net's dependencies: 4 # - Go 5 # - Docker 6 # - Kubernetes 7 # 8 # Depending on the parameters passed, it can gather the equivalent of the below 9 # bash one-liners: 10 # git ls-remote --tags https://github.com/golang/go \ 11 # | grep -oP '(?<=refs/tags/go)[\.\d]+$' \ 12 # | sort --version-sort 13 # git ls-remote --tags https://github.com/golang/go \ 14 # | grep -oP '(?<=refs/tags/go)[\.\d]+rc\d+$' \ 15 # | sort --version-sort \ 16 # | tail -n 1 17 # git ls-remote --tags https://github.com/docker/docker \ 18 # | grep -oP '(?<=refs/tags/v)\d+\.\d+\.\d+$' \ 19 # | sort --version-sort 20 # git ls-remote --tags https://github.com/docker/docker \ 21 # | grep -oP '(?<=refs/tags/v)\d+\.\d+\.\d+\-rc\d*$' \ 22 # | sort --version-sort \ 23 # | tail -n 1 24 # git ls-remote --tags https://github.com/kubernetes/kubernetes \ 25 # | grep -oP '(?<=refs/tags/v)\d+\.\d+\.\d+$' \ 26 # | sort --version-sort 27 # git ls-remote --tags https://github.com/kubernetes/kubernetes \ 28 # | grep -oP '(?<=refs/tags/v)\d+\.\d+\.\d+\-beta\.\d+$' \ 29 # | sort --version-sort | tail -n 1 30 # 31 # Dependencies: 32 # - python 33 # - git 34 # 35 # Testing: 36 # $ python -m doctest -v list_versions.py 37 38 from os import linesep, path 39 from sys import argv, exit, stdout, stderr 40 from getopt import getopt, GetoptError 41 from subprocess import Popen, PIPE 42 from pkg_resources import parse_version 43 from itertools import groupby 44 from six.moves import filter 45 import shlex 46 import re 47 48 # See also: /usr/include/sysexits.h 49 _ERROR_RUNTIME = 1 50 _ERROR_ILLEGAL_ARGS = 64 51 52 _TAG_REGEX = '^[0-9a-f]{40}\s+refs/tags/%s$' 53 _VERSION = 'version' 54 DEPS = { 55 'go': { 56 'url': 'https://github.com/golang/go', 57 're': 'go(?P<%s>[\d\.]+(?:rc\d)*)' % _VERSION, 58 'min': None 59 }, 60 'docker': { 61 'url': 'https://github.com/docker/docker', 62 're': 'v(?P<%s>\d+\.\d+\.\d+(?:\-rc\d)*)' % _VERSION, 63 # Weave Net only works with Docker from 1.10.0 onwards, so we ignore 64 # all previous versions: 65 'min': '1.10.0', 66 }, 67 'kubernetes': { 68 'url': 'https://github.com/kubernetes/kubernetes', 69 're': 'v(?P<%s>\d+\.\d+\.\d+(?:\-beta\.\d)*)' % _VERSION, 70 # Weave Kube requires Kubernetes 1.4.2+, so we ignore all previous 71 # versions: 72 'min': '1.4.2', 73 } 74 } 75 76 77 class Version(object): 78 ''' Helper class to parse and manipulate (sort, filter, group) software 79 versions. ''' 80 81 def __init__(self, version): 82 self.version = version 83 self.digits = [ 84 int(x) if x else 0 85 for x in re.match('(\d*)\.?(\d*)\.?(\d*).*?', version).groups() 86 ] 87 self.major, self.minor, self.patch = self.digits 88 self.__parsed = parse_version(version) 89 self.is_rc = self.__parsed.is_prerelease 90 91 def __lt__(self, other): 92 return self.__parsed.__lt__(other.__parsed) 93 94 def __gt__(self, other): 95 return self.__parsed.__gt__(other.__parsed) 96 97 def __le__(self, other): 98 return self.__parsed.__le__(other.__parsed) 99 100 def __ge__(self, other): 101 return self.__parsed.__ge__(other.__parsed) 102 103 def __eq__(self, other): 104 return self.__parsed.__eq__(other.__parsed) 105 106 def __ne__(self, other): 107 return self.__parsed.__ne__(other.__parsed) 108 109 def __str__(self): 110 return self.version 111 112 def __repr__(self): 113 return self.version 114 115 116 def _read_go_version_from_dockerfile(): 117 # Read Go version from weave/build/Dockerfile 118 dockerfile_path = path.join( 119 path.dirname(path.dirname(path.dirname(path.realpath(__file__)))), 120 'build', 'Dockerfile') 121 with open(dockerfile_path, 'r') as f: 122 for line in f: 123 m = re.match('^FROM golang:(\S*)$', line) 124 if m: 125 return m.group(1) 126 raise RuntimeError( 127 "Failed to read Go version from weave/build/Dockerfile." 128 " You may be running this script from somewhere else than weave/tools." 129 ) 130 131 132 def _try_set_min_go_version(): 133 ''' Set the current version of Go used to build Weave Net's containers as 134 the minimum version. ''' 135 try: 136 DEPS['go']['min'] = _read_go_version_from_dockerfile() 137 except IOError as e: 138 stderr.write('WARNING: No minimum Go version set. Root cause: %s%s' % 139 (e, linesep)) 140 141 142 def _sanitize(out): 143 return out.decode('ascii').strip().split(linesep) 144 145 146 def _parse_tag(tag, version_pattern, debug=False): 147 ''' Parse Git tag output's line using the provided `version_pattern`, e.g.: 148 >>> _parse_tag( 149 '915b77eb4efd68916427caf8c7f0b53218c5ea4a refs/tags/v1.4.6', 150 'v(?P<version>\d+\.\d+\.\d+(?:\-beta\.\d)*)') 151 '1.4.6' 152 ''' 153 pattern = _TAG_REGEX % version_pattern 154 m = re.match(pattern, tag) 155 if m: 156 return m.group(_VERSION) 157 elif debug: 158 stderr.write( 159 'ERROR: Failed to parse version out of tag [%s] using [%s].%s' % 160 (tag, pattern, linesep)) 161 162 163 def get_versions_from(git_repo_url, version_pattern): 164 ''' Get release and release candidates' versions from the provided Git 165 repository. ''' 166 git = Popen( 167 shlex.split('git ls-remote --tags %s' % git_repo_url), stdout=PIPE) 168 out, err = git.communicate() 169 status_code = git.returncode 170 if status_code != 0: 171 raise RuntimeError('Failed to retrieve git tags from %s. ' 172 'Status code: %s. Output: %s. Error: %s' % 173 (git_repo_url, status_code, out, err)) 174 return list( 175 filter(None, (_parse_tag(line, version_pattern) 176 for line in _sanitize(out)))) 177 178 179 def _tree(versions, level=0): 180 ''' Group versions by major, minor and patch version digits. ''' 181 if not versions or level >= len(versions[0].digits): 182 return # Empty versions or no more digits to group by. 183 versions_tree = [] 184 for _, versions_group in groupby(versions, lambda v: v.digits[level]): 185 subtree = _tree(list(versions_group), level + 1) 186 if subtree: 187 versions_tree.append(subtree) 188 # Return the current subtree if non-empty, or the list of "leaf" versions: 189 return versions_tree if versions_tree else versions 190 191 192 def _is_iterable(obj): 193 ''' 194 Check if the provided object is an iterable collection, i.e. not a string, 195 e.g. a list, a generator: 196 >>> _is_iterable('string') 197 False 198 >>> _is_iterable([1, 2, 3]) 199 True 200 >>> _is_iterable((x for x in [1, 2, 3])) 201 True 202 ''' 203 return hasattr(obj, '__iter__') and not isinstance(obj, str) 204 205 206 def _leaf_versions(tree, rc): 207 ''' 208 Recursively traverse the versions tree in a depth-first fashion, 209 and collect the last node of each branch, i.e. leaf versions. 210 ''' 211 versions = [] 212 if _is_iterable(tree): 213 for subtree in tree: 214 versions.extend(_leaf_versions(subtree, rc)) 215 if not versions: 216 if rc: 217 last_rc = next(filter(lambda v: v.is_rc, reversed(tree)), None) 218 last_prod = next( 219 filter(lambda v: not v.is_rc, reversed(tree)), None) 220 if last_rc and last_prod and (last_prod < last_rc): 221 versions.extend([last_prod, last_rc]) 222 elif not last_prod: 223 versions.append(last_rc) 224 else: 225 # Either there is no RC, or we ignore the RC as older than 226 # the latest production version: 227 versions.append(last_prod) 228 else: 229 versions.append(tree[-1]) 230 return versions 231 232 233 def filter_versions(versions, min_version=None, rc=False, latest=False): 234 ''' Filter provided versions 235 236 >>> filter_versions( 237 ['1.0.0-beta.1', '1.0.0', '1.0.1', '1.1.1', '1.1.2-rc1', '2.0.0'], 238 min_version=None, latest=False, rc=False) 239 [1.0.0, 1.0.1, 1.1.1, 2.0.0] 240 241 >>> filter_versions( 242 ['1.0.0-beta.1', '1.0.0', '1.0.1', '1.1.1', '1.1.2-rc1', '2.0.0'], 243 min_version=None, latest=True, rc=False) 244 [1.0.1, 1.1.1, 2.0.0] 245 246 >>> filter_versions( 247 ['1.0.0-beta.1', '1.0.0', '1.0.1', '1.1.1', '1.1.2-rc1', '2.0.0'], 248 min_version=None, latest=False, rc=True) 249 [1.0.0-beta.1, 1.0.0, 1.0.1, 1.1.1, 1.1.2-rc1, 2.0.0] 250 251 >>> filter_versions( 252 ['1.0.0-beta.1', '1.0.0', '1.0.1', '1.1.1', '1.1.2-rc1', '2.0.0'], 253 min_version='1.1.0', latest=False, rc=True) 254 [1.1.1, 1.1.2-rc1, 2.0.0] 255 256 >>> filter_versions( 257 ['1.0.0-beta.1', '1.0.0', '1.0.1', '1.1.1', '1.1.2-rc1', '2.0.0'], 258 min_version=None, latest=True, rc=True) 259 [1.0.1, 1.1.1, 1.1.2-rc1, 2.0.0] 260 261 >>> filter_versions( 262 ['1.0.0-beta.1', '1.0.0', '1.0.1', '1.1.1', '1.1.2-rc1', '2.0.0'], 263 min_version='1.1.0', latest=True, rc=True) 264 [1.1.1, 1.1.2-rc1, 2.0.0] 265 ''' 266 versions = sorted([Version(v) for v in versions]) 267 if min_version: 268 min_version = Version(min_version) 269 versions = [v for v in versions if v >= min_version] 270 if not rc: 271 versions = [v for v in versions if not v.is_rc] 272 if latest: 273 versions_tree = _tree(versions) 274 return _leaf_versions(versions_tree, rc) 275 else: 276 return versions 277 278 279 def _usage(error_message=None): 280 if error_message: 281 stderr.write('ERROR: ' + error_message + linesep) 282 stdout.write( 283 linesep.join([ 284 'Usage:', ' list_versions.py [OPTION]... [DEPENDENCY]', 285 'Examples:', ' list_versions.py go', 286 ' list_versions.py -r docker', 287 ' list_versions.py --rc docker', 288 ' list_versions.py -l kubernetes', 289 ' list_versions.py --latest kubernetes', 'Options:', 290 '-l/--latest Include only the latest version of each major and' 291 ' minor versions sub-tree.', 292 '-r/--rc Include release candidate versions.', 293 '-h/--help Prints this!', '' 294 ])) 295 296 297 def _validate_input(argv): 298 try: 299 config = {'rc': False, 'latest': False} 300 opts, args = getopt(argv, 'hlr', ['help', 'latest', 'rc']) 301 for opt, value in opts: 302 if opt in ('-h', '--help'): 303 _usage() 304 exit() 305 if opt in ('-l', '--latest'): 306 config['latest'] = True 307 if opt in ('-r', '--rc'): 308 config['rc'] = True 309 if len(args) != 1: 310 raise ValueError('Please provide a dependency to get versions of.' 311 ' Expected 1 argument but got %s: %s.' % 312 (len(args), args)) 313 dependency = args[0].lower() 314 if dependency not in DEPS.keys(): 315 raise ValueError( 316 'Please provide a valid dependency.' 317 ' Supported one dependency among {%s} but got: %s.' % 318 (', '.join(DEPS.keys()), dependency)) 319 return dependency, config 320 except GetoptError as e: 321 _usage(str(e)) 322 exit(_ERROR_ILLEGAL_ARGS) 323 except ValueError as e: 324 _usage(str(e)) 325 exit(_ERROR_ILLEGAL_ARGS) 326 327 328 def main(argv): 329 try: 330 dependency, config = _validate_input(argv) 331 if dependency == 'go': 332 _try_set_min_go_version() 333 versions = get_versions_from(DEPS[dependency]['url'], 334 DEPS[dependency]['re']) 335 versions = filter_versions(versions, DEPS[dependency]['min'], **config) 336 print(linesep.join(map(str, versions))) 337 except Exception as e: 338 print(str(e)) 339 exit(_ERROR_RUNTIME) 340 341 342 if __name__ == '__main__': 343 main(argv[1:])