brew.py 10 KB

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