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:])