brew.py 10 KB

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