brew.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 = 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('stackbrew/' + buildfile)
  101. except:
  102. # Image is not on official repository, ignore prefill
  103. pass
  104. img, commit = build_repo(url, ref, buildfile, tag, namespace,
  105. push, registry, repos_folder, logger)
  106. summary.add_success(buildfile, (linecnt, line), img, commit)
  107. processed['{0}@{1}'.format(url, ref)] = img
  108. except Exception as e:
  109. logger.exception(e)
  110. summary.add_exception(buildfile, (linecnt, line), e)
  111. f.close()
  112. cleanup(dst_folder, dst_folder != repository, repos_folder is None)
  113. summary.print_summary(logger)
  114. return summary
  115. def parse_line(line, logger):
  116. args = line.split(':', 1)
  117. if len(args) != 2:
  118. logger.debug("Invalid line: {0}".format(line))
  119. raise RuntimeError('Incorrect line format, please refer to the docs')
  120. try:
  121. url, ref = args[1].strip().rsplit('@', 1)
  122. return (args[0].strip(), url, ref)
  123. except ValueError:
  124. logger.debug("Invalid line: {0}".format(line))
  125. raise RuntimeError('Incorrect line format, please refer to the docs')
  126. def cleanup(libfolder, clean_libfolder=False, clean_repos=True):
  127. ''' Cleanup method called at the end of build_library.
  128. libfolder: Folder containing the library definition.
  129. clean_libfolder: If set to True, libfolder will be removed.
  130. Only if libfolder was temporary
  131. clean_repos: Remove library repos. Also resets module variables
  132. "processed" and "processed_folders" if set to true.
  133. '''
  134. global processed_folders
  135. global processed
  136. if clean_libfolder:
  137. rmtree(libfolder, True)
  138. if clean_repos:
  139. for d in processed_folders:
  140. rmtree(d, True)
  141. processed_folders = []
  142. processed = {}
  143. def _random_suffix():
  144. return ''.join([
  145. random.choice(string.ascii_letters + string.digits) for i in xrange(6)
  146. ])
  147. def build_repo(repository, ref, docker_repo, docker_tag, namespace, push,
  148. registry, repos_folder, logger):
  149. ''' Builds one line of a library file.
  150. repository: URL of the git repository that needs to be built
  151. ref: Git reference (or commit ID) that needs to be built
  152. docker_repo: Name of the docker repository where the image will
  153. end up.
  154. docker_tag: Tag for the image in the docker repository.
  155. namespace: Namespace for the docker repository.
  156. push: If the image should be pushed at the end of the build
  157. registry: URL to private registry where image should be pushed
  158. repos_folder: Directory where repositories should be cloned
  159. logger: Logger instance
  160. '''
  161. dst_folder = None
  162. img_id = None
  163. commit_id = None
  164. if repos_folder:
  165. # Repositories are stored in a fixed location and can be reused
  166. dst_folder = os.path.join(repos_folder, docker_repo + _random_suffix())
  167. docker_repo = '{0}/{1}'.format(namespace or 'library', docker_repo)
  168. if '{0}@{1}'.format(repository, ref) in processed.keys() or\
  169. '{0}@{1}'.format(repository, 'refs/tags' + ref) in processed.keys():
  170. if '{0}@{1}'.format(repository, ref) not in processed.keys():
  171. ref = 'refs/tags/' + ref
  172. logger.info('This ref has already been built, reusing image ID')
  173. img_id = processed['{0}@{1}'.format(repository, ref)]
  174. if ref.startswith('refs/'):
  175. commit_id = processed[repository].ref(ref)
  176. else:
  177. commit_id = ref
  178. else:
  179. # Not already built
  180. rep = None
  181. logger.info('Cloning {0} (ref: {1})'.format(repository, ref))
  182. if repository not in processed: # Repository not cloned yet
  183. try:
  184. rep, dst_folder = git.clone(repository, ref, dst_folder)
  185. except Exception:
  186. if dst_folder:
  187. rmtree(dst_folder)
  188. ref = 'refs/tags/' + ref
  189. rep, dst_folder = git.clone(repository, ref, dst_folder)
  190. processed[repository] = rep
  191. processed_folders.append(dst_folder)
  192. else:
  193. rep = processed[repository]
  194. if ref in rep.refs:
  195. # The ref already exists, we just need to checkout
  196. dst_folder = git.checkout(rep, ref)
  197. elif 'refs/tags/' + ref in rep.refs:
  198. ref = 'refs/tags/' + ref
  199. dst_folder = git.checkout(rep, ref)
  200. else: # ref is not present, try pulling it from the remote origin
  201. try:
  202. rep, dst_folder = git.pull(repository, rep, ref)
  203. except Exception:
  204. ref = 'refs/tags/' + ref
  205. rep, dst_folder = git.pull(repository, rep, ref)
  206. if not 'Dockerfile' in os.listdir(dst_folder):
  207. raise RuntimeError('Dockerfile not found in cloned repository')
  208. commit_id = rep.head()
  209. logger.info('Building using dockerfile...')
  210. img_id, logs = client.build(path=dst_folder, quiet=True)
  211. if img_id is None:
  212. logger.error('Image ID not found. Printing build logs...')
  213. logger.debug(logs)
  214. raise RuntimeError('Build failed')
  215. logger.info('Committing to {0}:{1}'.format(docker_repo,
  216. docker_tag or 'latest'))
  217. client.tag(img_id, docker_repo, docker_tag)
  218. if push:
  219. logger.info('Pushing result to registry {0}'.format(
  220. registry or "default"))
  221. push_repo(img_id, docker_repo, registry=registry, logger=logger)
  222. return img_id, commit_id
  223. def push_repo(img_id, repo, registry=None, docker_tag=None, logger=None):
  224. ''' Pushes a repository to a registry
  225. img_id: Image ID to push
  226. repo: Repository name where img_id should be tagged
  227. registry: Private registry where image needs to be pushed
  228. docker_tag: Tag to be applied to the image in docker repo
  229. logger: Logger instance
  230. '''
  231. exc = None
  232. if registry is not None:
  233. repo = '{0}/{1}'.format(registry, repo)
  234. logger.info('Also tagging {0}'.format(repo))
  235. client.tag(img_id, repo, docker_tag)
  236. for i in xrange(4):
  237. try:
  238. pushlog = client.push(repo)
  239. if '"error":"' in pushlog:
  240. raise RuntimeError('Error while pushing: {0}'.format(pushlog))
  241. except Exception as e:
  242. exc = e
  243. continue
  244. return
  245. raise exc