brew.py 11 KB

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