brew.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import hashlib
  2. import logging
  3. import os
  4. import random
  5. from shutil import rmtree
  6. import string
  7. import docker
  8. import git
  9. from summary import Summary
  10. DEFAULT_REPOSITORY = 'git://github.com/shin-/brew'
  11. DEFAULT_BRANCH = 'master'
  12. client = docker.Client(timeout=10000)
  13. processed = {}
  14. processed_folders = []
  15. def build_library(repository=None, branch=None, namespace=None, push=False,
  16. debug=False, prefill=True, registry=None, targetlist=None,
  17. repos_folder=None, logger=None):
  18. ''' Entrypoint method build_library.
  19. repository: Repository containing a library/ folder. Can be a
  20. local path or git repository
  21. branch: If repository is a git repository, checkout this branch
  22. (default: DEFAULT_BRANCH)
  23. namespace: Created repositories will use the following namespace.
  24. (default: no namespace)
  25. push: If set to true, push images to the repository
  26. debug: Enables debug logging if set to True
  27. prefill: Retrieve images from public repository before building.
  28. Serves to prefill the builder cache.
  29. registry: URL to the private registry where results should be
  30. pushed. (only if push=True)
  31. targetlist: String indicating which library files are targeted by
  32. this build. Entries should be comma-separated. Default
  33. is all files.
  34. repos_folder: Fixed location where cloned repositories should be
  35. stored. Default is None, meaning folders are temporary
  36. and cleaned up after the build finishes.
  37. logger: Logger instance to use. Default is None, in which case
  38. build_library will create its own logger.
  39. '''
  40. dst_folder = None
  41. summary = Summary()
  42. if logger is None:
  43. logger = logging.getLogger(__name__)
  44. logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
  45. level='INFO')
  46. if repository is None:
  47. repository = DEFAULT_REPOSITORY
  48. if branch is None:
  49. branch = DEFAULT_BRANCH
  50. if debug:
  51. logger.setLevel('DEBUG')
  52. if targetlist is not None:
  53. targetlist = targetlist.split(',')
  54. if not repository.startswith(('https://', 'git://')):
  55. logger.info('Repository provided assumed to be a local path')
  56. dst_folder = repository
  57. try:
  58. client.version()
  59. except Exception as e:
  60. logger.error('Could not reach the docker daemon. Please make sure it '
  61. 'is running.')
  62. logger.warning('Also make sure you have access to the docker UNIX '
  63. 'socket (use sudo)')
  64. return
  65. if not dst_folder:
  66. logger.info('Cloning docker repo from {0}, branch: {1}'.format(
  67. repository, branch))
  68. try:
  69. rep, dst_folder = git.clone_branch(repository, branch)
  70. except Exception as e:
  71. logger.exception(e)
  72. logger.error('Source repository could not be fetched. Check '
  73. 'that the address is correct and the branch exists.')
  74. return
  75. try:
  76. dirlist = os.listdir(os.path.join(dst_folder, 'library'))
  77. except OSError as e:
  78. logger.error('The path provided ({0}) could not be found or didn\'t'
  79. 'contain a library/ folder.'.format(dst_folder))
  80. return
  81. for buildfile in dirlist:
  82. if buildfile == 'MAINTAINERS':
  83. continue
  84. if (targetlist and buildfile not in targetlist):
  85. continue
  86. f = open(os.path.join(dst_folder, 'library', buildfile))
  87. linecnt = 0
  88. for line in f:
  89. linecnt += 1
  90. if not line or line.strip() == '':
  91. continue
  92. elif line.lstrip().startswith('#'): # # It's a comment!
  93. continue
  94. logger.debug('{0} ---> {1}'.format(buildfile, line))
  95. try:
  96. tag, url, ref, dfile = parse_line(line, logger)
  97. if prefill:
  98. logger.debug('Pulling {0} from official repository (cache '
  99. 'fill)'.format(buildfile))
  100. try:
  101. client.pull(buildfile)
  102. except:
  103. # Image is not on official repository, ignore prefill
  104. pass
  105. img, commit = build_repo(url, ref, buildfile, dfile, tag,
  106. namespace, push, registry,
  107. repos_folder, logger)
  108. summary.add_success(buildfile, (linecnt, line), img, commit)
  109. except Exception as e:
  110. logger.exception(e)
  111. summary.add_exception(buildfile, (linecnt, line), e)
  112. f.close()
  113. cleanup(dst_folder, dst_folder != repository, repos_folder is None)
  114. summary.print_summary(logger)
  115. return summary
  116. def parse_line(line, logger):
  117. df_folder = '.'
  118. args = line.split(':', 1)
  119. if len(args) != 2:
  120. logger.debug("Invalid line: {0}".format(line))
  121. raise RuntimeError('Incorrect line format, please refer to the docs')
  122. try:
  123. repo = args[1].strip().split()
  124. if len(repo) == 2:
  125. df_folder = repo[1].strip()
  126. url, ref = repo[0].strip().rsplit('@', 1)
  127. return (args[0].strip(), url, ref, df_folder)
  128. except ValueError:
  129. logger.debug("Invalid line: {0}".format(line))
  130. raise RuntimeError('Incorrect line format, please refer to the docs')
  131. def cleanup(libfolder, clean_libfolder=False, clean_repos=True):
  132. ''' Cleanup method called at the end of build_library.
  133. libfolder: Folder containing the library definition.
  134. clean_libfolder: If set to True, libfolder will be removed.
  135. Only if libfolder was temporary
  136. clean_repos: Remove library repos. Also resets module variables
  137. "processed" and "processed_folders" if set to true.
  138. '''
  139. global processed_folders
  140. global processed
  141. if clean_libfolder:
  142. rmtree(libfolder, True)
  143. if clean_repos:
  144. for d in processed_folders:
  145. rmtree(d, True)
  146. processed_folders = []
  147. processed = {}
  148. def _random_suffix():
  149. return ''.join([
  150. random.choice(string.ascii_letters + string.digits) for i in xrange(6)
  151. ])
  152. def get_repo_hash(repo_url, ref, df_location):
  153. h = hashlib.md5(repo_url)
  154. h.update(ref)
  155. h.update(df_location)
  156. return h.hexdigest()
  157. def build_repo(repository, ref, docker_repo, dockerfile_location,
  158. docker_tag, namespace, push, registry, repos_folder, logger):
  159. ''' Builds one line of a library file.
  160. repository: URL of the git repository that needs to be built
  161. ref: Git reference (or commit ID) that needs to be built
  162. docker_repo: Name of the docker repository where the image will
  163. end up.
  164. dockerfile_location: Folder containing the Dockerfile
  165. docker_tag: Tag for the image in the docker repository.
  166. namespace: Namespace for the docker repository.
  167. push: If the image should be pushed at the end of the build
  168. registry: URL to private registry where image should be pushed
  169. repos_folder: Directory where repositories should be cloned
  170. logger: Logger instance
  171. '''
  172. dst_folder = None
  173. img_id = None
  174. commit_id = None
  175. repo_hash = get_repo_hash(repository, ref, dockerfile_location)
  176. if repos_folder:
  177. # Repositories are stored in a fixed location and can be reused
  178. dst_folder = os.path.join(repos_folder, docker_repo + _random_suffix())
  179. docker_repo = '{0}/{1}'.format(namespace or 'library', docker_repo)
  180. if repo_hash in processed.keys():
  181. logger.info('[cache hit] {0}'.format(repo_hash))
  182. logger.info('This ref has already been built, reusing image ID')
  183. img_id = processed[repo_hash]
  184. if ref.startswith('refs/'):
  185. commit_id = processed[repository].ref(ref)
  186. else:
  187. commit_id = ref
  188. else:
  189. # Not already built
  190. logger.info('[cache miss] {0}'.format(repo_hash))
  191. rep = None
  192. logger.info('Cloning {0} (ref: {1})'.format(repository, ref))
  193. if repository not in processed: # Repository not cloned yet
  194. try:
  195. rep, dst_folder = git.clone(repository, ref, dst_folder)
  196. except Exception:
  197. if dst_folder:
  198. rmtree(dst_folder)
  199. ref = 'refs/tags/' + ref
  200. rep, dst_folder = git.clone(repository, ref, dst_folder)
  201. processed[repository] = rep
  202. processed_folders.append(dst_folder)
  203. else:
  204. rep = processed[repository]
  205. if ref in rep.refs:
  206. # The ref already exists, we just need to checkout
  207. dst_folder = git.checkout(rep, ref)
  208. elif 'refs/tags/' + ref in rep.refs:
  209. ref = 'refs/tags/' + ref
  210. dst_folder = git.checkout(rep, ref)
  211. else: # ref is not present, try pulling it from the remote origin
  212. try:
  213. rep, dst_folder = git.pull(repository, rep, ref)
  214. except Exception:
  215. ref = 'refs/tags/' + ref
  216. rep, dst_folder = git.pull(repository, rep, ref)
  217. dockerfile_location = os.path.join(dst_folder, dockerfile_location)
  218. if not 'Dockerfile' in os.listdir(dockerfile_location):
  219. raise RuntimeError('Dockerfile not found in cloned repository')
  220. commit_id = rep.head()
  221. logger.info('Building using dockerfile...')
  222. img_id, logs = client.build(path=dockerfile_location, quiet=True)
  223. if img_id is None:
  224. logger.error('Image ID not found. Printing build logs...')
  225. logger.debug(logs)
  226. raise RuntimeError('Build failed')
  227. logger.info('Committing to {0}:{1}'.format(docker_repo,
  228. docker_tag or 'latest'))
  229. client.tag(img_id, docker_repo, docker_tag)
  230. logger.info("Registering as processed: {0}".format(repo_hash))
  231. processed[repo_hash] = img_id
  232. if push:
  233. logger.info('Pushing result to registry {0}'.format(
  234. registry or "default"))
  235. push_repo(img_id, docker_repo, registry=registry, logger=logger)
  236. return img_id, commit_id
  237. def push_repo(img_id, repo, registry=None, docker_tag=None, logger=None):
  238. ''' Pushes a repository to a registry
  239. img_id: Image ID to push
  240. repo: Repository name where img_id should be tagged
  241. registry: Private registry where image needs to be pushed
  242. docker_tag: Tag to be applied to the image in docker repo
  243. logger: Logger instance
  244. '''
  245. exc = None
  246. if registry is not None:
  247. repo = '{0}/{1}'.format(registry, repo)
  248. logger.info('Also tagging {0}'.format(repo))
  249. client.tag(img_id, repo, docker_tag)
  250. for i in xrange(4):
  251. try:
  252. pushlog = client.push(repo)
  253. if '"error":"' in pushlog:
  254. raise RuntimeError('Error while pushing: {0}'.format(pushlog))
  255. except Exception as e:
  256. exc = e
  257. continue
  258. return
  259. raise exc