Mirror of an open source Kubernetes-native API gateway for microservices built on the Envoy Proxy https://www.getambassador.io
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ambscout.py 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. from typing import ClassVar, Dict, List, Optional, Tuple, Union
  2. import datetime
  3. import json
  4. import logging
  5. import re
  6. import os
  7. import semantic_version
  8. from scout import Scout
  9. # Import version stuff directly from ambassador.VERSION to avoid a circular import.
  10. from .VERSION import Version, Build, BuildInfo
  11. ScoutNotice = Union[str, Dict[str, str]]
  12. class AmbScout:
  13. reTaggedBranch: ClassVar = re.compile(r'^(\d+\.\d+\.\d+)(-[a-zA-Z][a-zA-Z]\d+)?$')
  14. reGitDescription: ClassVar = re.compile(r'-(\d+)-g([0-9a-f]+)$')
  15. install_id: str
  16. runtime: str
  17. namespace: str
  18. version: str
  19. semver: Optional[semantic_version.Version]
  20. _scout: Optional[Scout]
  21. _scout_error: Optional[str]
  22. _notices: Optional[List[ScoutNotice]]
  23. _last_result: Optional[dict]
  24. _last_update: Optional[datetime.datetime]
  25. _update_frequency: Optional[datetime.timedelta]
  26. _latest_version: Optional[str] = None
  27. _latest_semver: Optional[semantic_version.Version] = None
  28. def __init__(self) -> None:
  29. self.install_id = os.environ.get('AMBASSADOR_SCOUT_ID', "00000000-0000-0000-0000-000000000000")
  30. self.runtime = "kubernetes" if os.environ.get('KUBERNETES_SERVICE_HOST', None) else "docker"
  31. self.namespace = os.environ.get('AMBASSADOR_NAMESPACE', 'default')
  32. self.version = self.parse_git_description(Version, Build)
  33. self.semver = self.get_semver(self.version)
  34. self.logger = logging.getLogger("ambassador.scout")
  35. self.logger.setLevel(logging.DEBUG)
  36. self.logger.debug("Ambassador version %s" % Version)
  37. self.logger.debug("Scout version %s%s" % (self.version, " - BAD SEMVER" if not self.semver else ""))
  38. self.logger.debug("Runtime %s" % self.runtime)
  39. self.logger.debug("Namespace %s" % self.namespace)
  40. self._scout = None
  41. self._scout_error = None
  42. self._notices = None
  43. self._last_result = None
  44. self._last_update = datetime.datetime.now() - datetime.timedelta(hours=24)
  45. self._update_frequency = datetime.timedelta(hours=4)
  46. self._latest_version = None
  47. self._latest_semver = None
  48. def __str__(self) -> str:
  49. return ("%s: %s" % ("OK" if self._scout else "??",
  50. self._scout_error if self._scout_error else "OK"))
  51. @property
  52. def scout(self) -> Optional[Scout]:
  53. if not self._scout:
  54. try:
  55. self._scout = Scout(app="ambassador", version=self.version, install_id=self.install_id)
  56. self._scout_error = None
  57. self.logger.debug("Scout connection established")
  58. except OSError as e:
  59. self._scout = None
  60. self._scout_error = e
  61. self.logger.debug("Scout connection failed, will retry later: %s" % self._scout_error)
  62. return self._scout
  63. def report(self, force_result: Optional[dict]=None, **kwargs) -> Tuple[dict, List[ScoutNotice]]:
  64. _notices: List[ScoutNotice] = []
  65. env_result = os.environ.get("AMBASSADOR_SCOUT_RESULT", None)
  66. if env_result:
  67. force_result = json.loads(env_result)
  68. result: Optional[dict] = force_result
  69. result_was_cached: bool = False
  70. if not result:
  71. if 'runtime' not in kwargs:
  72. kwargs['runtime'] = self.runtime
  73. if 'namespace' not in kwargs:
  74. kwargs['namespace'] = self.namespace
  75. # How long since the last Scout update? If it's been more than an hour,
  76. # check Scout again.
  77. now = datetime.datetime.now()
  78. if (now - self._last_update) > self._update_frequency:
  79. if self.scout:
  80. result = self.scout.report(**kwargs)
  81. self._last_update = now
  82. self._last_result = dict(**result)
  83. else:
  84. result = { "scout": "unavailable: %s" % self._scout_error }
  85. _notices.append({ "level": "debug",
  86. "message": "scout temporarily unavailable: %s" % self._scout_error })
  87. # Whether we could talk to Scout or not, update the timestamp so we don't
  88. # try again too soon.
  89. result_timestamp = datetime.datetime.now()
  90. else:
  91. _notices.append({ "level": "debug", "message": "Returning cached result" })
  92. result = dict(**self._last_result)
  93. result_was_cached = True
  94. result_timestamp = self._last_update
  95. else:
  96. _notices.append({ "level": "debug", "message": "Returning forced result" })
  97. result_timestamp = datetime.datetime.now()
  98. if not self.semver:
  99. _notices.append({
  100. "level": "warning",
  101. "message": "Ambassador has invalid version '%s'??!" % self.version
  102. })
  103. result['cached'] = result_was_cached
  104. result['timestamp'] = result_timestamp.timestamp()
  105. # Do version & notices stuff.
  106. if 'latest_version' in result:
  107. latest_version = result['latest_version']
  108. latest_semver = self.get_semver(latest_version)
  109. if latest_semver:
  110. self._latest_version = latest_version
  111. self._latest_semver = latest_semver
  112. else:
  113. _notices.append({
  114. "level": "warning",
  115. "message": "Scout returned invalid version '%s'??!" % latest_version
  116. })
  117. if (self._latest_semver and ((not self.semver) or
  118. (self._latest_semver > self.semver))):
  119. _notices.append({
  120. "level": "info",
  121. "message": "Upgrade available! to Ambassador version %s" % self._latest_semver
  122. })
  123. if 'notices' in result:
  124. _notices.extend(result['notices'])
  125. self._notices = _notices
  126. return result, self._notices
  127. @staticmethod
  128. def get_semver(version_string: str) -> Optional[semantic_version.Version]:
  129. semver = None
  130. try:
  131. semver = semantic_version.Version(version_string)
  132. except ValueError:
  133. pass
  134. return semver
  135. @staticmethod
  136. def parse_git_description(version: str, build: BuildInfo) -> str:
  137. # Here's what we get for various kinds of builds:
  138. #
  139. # Random (clean):
  140. # Version: shared-dev-tgr606-f60229d
  141. # build.git.branch: shared/dev/tgr606
  142. # build.git.commit: f60229d
  143. # build.git.dirty: False
  144. # build.git.description: 0.50.0-tt2-1-gf60229d
  145. # --> 0.50.0-local+gf60229d
  146. #
  147. # Random (dirty):
  148. # Version: shared-dev-tgr606-f60229d-dirty
  149. # build.git.branch: shared/dev/tgr606
  150. # build.git.commit: f60229d
  151. # build.git.dirty: True
  152. # build.git.description: 0.50.0-tt2-1-gf60229d
  153. # --> 0.50.0-local+gf60229d.dirty
  154. #
  155. # EA:
  156. # Version: 0.50.0
  157. # build.git.branch: 0.50.0-ea2
  158. # build.git.commit: 05aefd5
  159. # build.git.dirty: False
  160. # build.git.description: 0.50.0-ea2
  161. # --> 0.50.0-ea2
  162. #
  163. # RC
  164. # Version: 0.40.0
  165. # build.git.branch: 0.40.0-rc1
  166. # build.git.commit: d450dca
  167. # build.git.dirty: False
  168. # build.git.description: 0.40.0-rc1
  169. # --> 0.40.0-rc1
  170. #
  171. # GA
  172. # Version: 0.40.0
  173. # build.git.branch: 0.40.0
  174. # build.git.commit: e301a90
  175. # build.git.dirty: False
  176. # build.git.description: 0.40.0
  177. # --> 0.40.0
  178. # Start by assuming that the version is sane and we needn't
  179. # tack any build metadata onto it.
  180. build_elements = []
  181. if not AmbScout.reTaggedBranch.search(version):
  182. # This isn't a proper sane version. It must be a local build. Per
  183. # Scout's rules, anything with semver build metadata is treated as a
  184. # dev version, so we need to make sure our returned version has some.
  185. #
  186. # Start by assuming that we won't find a useable version in the
  187. # description, and must fall back to 0.0.0.
  188. build_version = "0.0.0"
  189. desc_delta = None
  190. # OK. Can we find a version from what the git description starts
  191. # with? If so, the upgrade logic in the diagnostics will work more
  192. # sanely.
  193. m = AmbScout.reGitDescription.search(build.git.description)
  194. if m:
  195. # OK, the description ends with -$delta-g$commit at least, so
  196. # it may start with a version. Strip off the matching text and
  197. # remember the delta and commit.
  198. desc_delta = m.group(1)
  199. desc = build.git.description[0:m.start()]
  200. # Does the remaining description match a sane version?
  201. m = AmbScout.reTaggedBranch.search(desc)
  202. if m:
  203. # Yes. Use it as the base version.
  204. build_version = m.group(1)
  205. # We'll use prerelease "local", and include the branch and such
  206. # in the build metadata.
  207. version = '%s-local' % build_version
  208. # Does the commit not appear in a build element?
  209. build_elements = []
  210. if desc_delta:
  211. build_elements.append(desc_delta)
  212. build_elements.append("g%s" % build.git.commit)
  213. # If this branch is dirty, append a build element of "dirty".
  214. if build.git.dirty:
  215. build_elements.append('dirty')
  216. # Finally, put it all together.
  217. if build_elements:
  218. version += "+%s" % ('.'.join(build_elements))
  219. return version