v2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import json
  2. import logging
  3. import os
  4. import random
  5. import re
  6. from shutil import rmtree
  7. import string
  8. import docker
  9. import git
  10. DEFAULT_REPOSITORY = 'git://github.com/dotcloud/stackbrew'
  11. DEFAULT_BRANCH = 'master'
  12. logger = logging.getLogger(__name__)
  13. logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
  14. level='INFO')
  15. def set_loglevel(level):
  16. logger.setLevel(level)
  17. git.logger.setLevel(level)
  18. class StackbrewError(Exception):
  19. def __init__(self, message, cause=None):
  20. super(StackbrewError, self).__init__(message)
  21. self.cause = cause
  22. def log(self, logger):
  23. logger.exception(self)
  24. if self.cause:
  25. logger.error('The cause of this error is the following:')
  26. logger.exception(self.cause)
  27. class StackbrewLibrary(object):
  28. def __init__(self, repository, branch=None):
  29. self.logger = logging.getLogger(__name__)
  30. logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
  31. level='INFO')
  32. self.branch = branch or DEFAULT_BRANCH
  33. self.repository = repository
  34. self.library = None
  35. if not self.repository.startswith(('https://', 'git://')):
  36. self.logger.info('Repository provided assumed to be a local path')
  37. self.library = self.repository
  38. def clone_library(self):
  39. if self.library:
  40. return self.library
  41. try:
  42. rep, library = git.clone_branch(self.repository, self.branch)
  43. self.library = library
  44. except git.GitException as e:
  45. raise StackbrewError(
  46. 'Source repository could not be fetched. Ensure '
  47. 'the address is correct and the branch exists.',
  48. e
  49. )
  50. def list_repositories(self):
  51. if not self.library:
  52. self.clone_library()
  53. try:
  54. return [e for e in os.listdir(
  55. os.path.join(self.library, 'library')) if e != 'MAINTAINERS']
  56. except OSError as e:
  57. raise StackbrewError(
  58. 'The path provided ({0}) could not be found or '
  59. 'didn\'t contain a library/ folder'.format(self.library),
  60. e
  61. )
  62. class StackbrewRepo(object):
  63. def __init__(self, name, definition_file):
  64. self.buildlist = {}
  65. self.git_folders = {}
  66. self.name = name
  67. for line in definition_file:
  68. if not line or line.strip() == '':
  69. continue
  70. elif line.lstrip().startswith('#'): # # It's a comment!
  71. continue
  72. logger.debug(line)
  73. tag, url, ref, dfile = self._parse_line(line)
  74. if (url, ref, dfile) in self.buildlist:
  75. self.buildlist[(url, ref, dfile)].append(tag)
  76. else:
  77. self.buildlist[(url, ref, dfile)] = [tag]
  78. def _parse_line(self, line):
  79. df_folder = '.'
  80. args = line.split(':', 1)
  81. if len(args) != 2:
  82. logger.debug("Invalid line: {0}".format(line))
  83. raise StackbrewError(
  84. 'Incorrect line format, please refer to the docs'
  85. )
  86. try:
  87. repo = args[1].strip().split()
  88. if len(repo) == 2:
  89. df_folder = repo[1].strip()
  90. url, ref = repo[0].strip().rsplit('@', 1)
  91. return (args[0].strip(), url, ref, df_folder)
  92. except ValueError:
  93. logger.debug("Invalid line: {0}".format(line))
  94. raise StackbrewError(
  95. 'Incorrect line format, please refer to the docs'
  96. )
  97. def list_versions(self):
  98. return self.buildlist.keys()
  99. def get_associated_tags(self, repo):
  100. return self.buildlist.get(repo, None)
  101. def add_git_repo(self, url, repo):
  102. self.git_folders[url] = repo
  103. def get_git_repo(self, url):
  104. return self.git_folders.get(url, (None, None))
  105. class StackbrewBuilder(object):
  106. def __init__(self, library, namespaces=None, targetlist=None,
  107. repo_cache=None):
  108. self.lib = library
  109. if not hasattr(self.lib, 'list_repositories'):
  110. raise StackbrewError('Invalid library passed to StackbrewBuilder')
  111. self.namespaces = namespaces or ['stackbrew']
  112. self.targetlist = targetlist
  113. self.repo_cache = repo_cache
  114. self.history = {}
  115. def build_repo_list(self):
  116. self.repos = []
  117. for repo in self.lib.list_repositories():
  118. if self.targetlist and repo not in self.targetlist:
  119. continue
  120. try:
  121. with open(os.path.join(self.lib.library, 'library', repo)) as f:
  122. self.repos.append(StackbrewRepo(repo, f))
  123. except IOError as e:
  124. raise StackbrewError(
  125. 'Failed to read definition file for {0}'.format(repo),
  126. e
  127. )
  128. for repo in self.repos:
  129. for version in repo.list_versions():
  130. logger.debug('{0}: {1}'.format(
  131. repo.name,
  132. ','.join(repo.get_associated_tags(version))
  133. ))
  134. return self.repos
  135. def build_all(self, continue_on_error=True, callback=None):
  136. self.pushlist = []
  137. for repo in self.repos:
  138. self.build_repo(repo, continue_on_error, callback)
  139. for namespace in self.namespaces:
  140. self.pushlist.append('/'.join([namespace, repo.name]))
  141. def build_repo(self, repo, continue_on_error=True, callback=None):
  142. for version in repo.list_versions():
  143. try:
  144. self.build_version(repo, version, callback)
  145. except StackbrewError as e:
  146. if not continue_on_error:
  147. raise e
  148. e.log(logger)
  149. def build_version(self, repo, version, callback=None):
  150. if version in self.history:
  151. return self.history[version], None
  152. url, ref, dfile = version
  153. try:
  154. rep, dst_folder = self.clone_version(repo, version)
  155. except StackbrewError as exc:
  156. if callback:
  157. callback(exc, repo, version, None, None)
  158. raise exc
  159. dockerfile_location = os.path.join(dst_folder, dfile)
  160. if not 'Dockerfile' in os.listdir(dockerfile_location):
  161. exc = StackbrewError('Dockerfile not found in cloned repository')
  162. if callback:
  163. callback(exc, repo, version, None, None)
  164. raise exc
  165. img_id, build_result = self.do_build(
  166. repo, version, dockerfile_location, callback
  167. )
  168. self.history[version] = img_id
  169. return img_id, build_result
  170. def do_build(self, repo, version, dockerfile_location, callback=None):
  171. raise NotImplementedError
  172. def _clone_or_checkout(self, url, ref, dst_folder, rep):
  173. if rep:
  174. try:
  175. # The ref already exists, we just need to checkout
  176. dst_folder = git.checkout(rep, ref)
  177. except git.GitException:
  178. # ref is not present, try pulling it from the remote origin
  179. rep, dst_folder = git.pull(url, rep, ref)
  180. return rep, dst_folder
  181. if dst_folder:
  182. rmtree(dst_folder)
  183. return git.clone(url, ref, dst_folder)
  184. def clone_version(self, repo, version):
  185. url, ref, dfile = version
  186. rep, dst_folder = repo.get_git_repo(url)
  187. if not dst_folder and self.repo_cache:
  188. dst_folder = os.path.join(
  189. self.repo_cache, repo.name + _random_suffix()
  190. )
  191. os.mkdir(dst_folder)
  192. try:
  193. rep, dst_folder = self._clone_or_checkout(
  194. url, ref, dst_folder, rep
  195. )
  196. except Exception as e:
  197. raise StackbrewError(
  198. 'Failed to clone repository {0}@{1}'.format(url, ref),
  199. e
  200. )
  201. repo.add_git_repo(url, (rep, dst_folder))
  202. return rep, dst_folder
  203. def get_pushlist(self):
  204. return self.pushlist
  205. def push_all(self, callback=None):
  206. for repo in self.pushlist:
  207. self.do_push(repo, callback)
  208. def do_push(self, repo_name, callback=None):
  209. raise NotImplementedError
  210. def _random_suffix():
  211. return ''.join([
  212. random.choice(string.ascii_letters + string.digits) for i in xrange(6)
  213. ])
  214. class LocalBuilder(StackbrewBuilder):
  215. def __init__(self, library, namespaces=None, targetlist=None,
  216. repo_cache=None):
  217. super(LocalBuilder, self).__init__(
  218. library, namespaces, targetlist, repo_cache
  219. )
  220. self.client = docker.Client(version='1.9', timeout=10000)
  221. self.build_success_re = r'^Successfully built ([a-f0-9]+)\n$'
  222. def do_build(self, repo, version, dockerfile_location, callback=None):
  223. logger.info(
  224. 'Build start: {0} {1}'.format(repo.name, version)
  225. )
  226. build_result = self.client.build(path=dockerfile_location, rm=True,
  227. stream=True, quiet=True)
  228. img_id = self._parse_result(build_result)
  229. if not img_id:
  230. exc = StackbrewError(
  231. 'Build failed for {0} ({1})'.format(repo.name, version)
  232. )
  233. if callback:
  234. callback(exc, repo, version, None, build_result)
  235. raise exc
  236. for tag in repo.get_associated_tags(version):
  237. logger.info(
  238. 'Build success: {0} ({1}:{2})'.format(img_id, repo.name, tag)
  239. )
  240. for namespace in self.namespaces:
  241. self.client.tag(img_id, '/'.join([namespace, repo.name]), tag)
  242. if callback:
  243. callback(None, repo, version, img_id, build_result)
  244. return img_id, build_result
  245. def _parse_result(self, build_result):
  246. if isinstance(build_result, tuple):
  247. img_id, logs = build_result
  248. return img_id
  249. else:
  250. lines = [line for line in build_result]
  251. try:
  252. parsed_lines = [json.loads(e).get('stream', '') for e in lines]
  253. except ValueError:
  254. # sometimes all the data is sent on a single line ????
  255. #
  256. # ValueError: Extra data: line 1 column 87 - line 1 column
  257. # 33268 (char 86 - 33267)
  258. line = lines[0]
  259. # This ONLY works because every line is formatted as
  260. # {"stream": STRING}
  261. parsed_lines = [
  262. json.loads(obj).get('stream', '') for obj in
  263. re.findall('{\s*"stream"\s*:\s*"[^"]*"\s*}', line)
  264. ]
  265. for line in parsed_lines:
  266. match = re.match(self.build_success_re, line)
  267. if match:
  268. return match.group(1)
  269. return None
  270. def do_push(self, repo_name, callback=None):
  271. exc = None
  272. for i in xrange(4):
  273. try:
  274. pushlog = self.client.push(repo_name)
  275. if '"error":"' in pushlog:
  276. raise RuntimeError(
  277. 'Error while pushing: {0}'.format(pushlog)
  278. )
  279. except Exception as e:
  280. exc = e
  281. continue
  282. if callback:
  283. callback(None, repo_name, pushlog)
  284. return
  285. if not callback:
  286. raise StackbrewError(
  287. 'Error while pushing {0}'.format(repo_name),
  288. exc
  289. )
  290. else:
  291. return callback(exc, repo_name, None)