gitlab_api.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import argparse
  2. import os
  3. import re
  4. import tarfile
  5. import tempfile
  6. import zipfile
  7. from functools import wraps
  8. import gitlab
  9. class Gitlab(object):
  10. JOB_NAME_PATTERN = re.compile(r'(\w+)(\s+(\d+)/(\d+))?')
  11. DOWNLOAD_ERROR_MAX_RETRIES = 3
  12. def __init__(self, project_id=None):
  13. config_data_from_env = os.getenv('PYTHON_GITLAB_CONFIG')
  14. if config_data_from_env:
  15. # prefer to load config from env variable
  16. with tempfile.NamedTemporaryFile('w', delete=False) as temp_file:
  17. temp_file.write(config_data_from_env)
  18. config_files = [temp_file.name]
  19. else:
  20. # otherwise try to use config file at local filesystem
  21. config_files = None
  22. gitlab_id = os.getenv('LOCAL_GITLAB_HTTPS_HOST') # if None, will use the default gitlab server
  23. self.gitlab_inst = gitlab.Gitlab.from_config(gitlab_id=gitlab_id, config_files=config_files)
  24. self.gitlab_inst.auth()
  25. if project_id:
  26. self.project = self.gitlab_inst.projects.get(project_id)
  27. else:
  28. self.project = None
  29. def get_project_id(self, name, namespace=None):
  30. """
  31. search project ID by name
  32. :param name: project name
  33. :param namespace: namespace to match when we have multiple project with same name
  34. :return: project ID
  35. """
  36. projects = self.gitlab_inst.projects.list(search=name)
  37. res = []
  38. for project in projects:
  39. if namespace is None:
  40. if len(projects) == 1:
  41. res.append(project.id)
  42. break
  43. if project.namespace['path'] == namespace:
  44. if project.name == name:
  45. res.insert(0, project.id)
  46. else:
  47. res.append(project.id)
  48. if not res:
  49. raise ValueError("Can't find project")
  50. return res[0]
  51. def download_artifacts(self, job_id, destination):
  52. """
  53. download full job artifacts and extract to destination.
  54. :param job_id: Gitlab CI job ID
  55. :param destination: extract artifacts to path.
  56. """
  57. job = self.project.jobs.get(job_id)
  58. with tempfile.NamedTemporaryFile(delete=False) as temp_file:
  59. job.artifacts(streamed=True, action=temp_file.write)
  60. with zipfile.ZipFile(temp_file.name, 'r') as archive_file:
  61. archive_file.extractall(destination)
  62. def retry_download(func):
  63. """
  64. This wrapper will only catch IOError and retry the whole function.
  65. So only use it with download functions, read() inside and atomic
  66. functions
  67. """
  68. @wraps(func)
  69. def wrapper(self, *args, **kwargs):
  70. retried = 0
  71. while True:
  72. try:
  73. res = func(self, *args, **kwargs)
  74. except (IOError, EOFError) as e:
  75. retried += 1
  76. if retried > self.DOWNLOAD_ERROR_MAX_RETRIES:
  77. raise e # get out of the loop
  78. else:
  79. print('Retried for the {} time'.format(retried))
  80. continue
  81. else:
  82. break
  83. return res
  84. return wrapper
  85. @retry_download # type: ignore
  86. def download_artifact(self, job_id, artifact_path, destination=None):
  87. """
  88. download specific path of job artifacts and extract to destination.
  89. :param job_id: Gitlab CI job ID
  90. :param artifact_path: list of path in artifacts (relative path to artifact root path)
  91. :param destination: destination of artifact. Do not save to file if destination is None
  92. :return: A list of artifact file raw data.
  93. """
  94. job = self.project.jobs.get(job_id)
  95. raw_data_list = []
  96. for a_path in artifact_path:
  97. try:
  98. data = job.artifact(a_path)
  99. except gitlab.GitlabGetError as e:
  100. print("Failed to download '{}' from job {}".format(a_path, job_id))
  101. raise e
  102. raw_data_list.append(data)
  103. if destination:
  104. file_path = os.path.join(destination, a_path)
  105. try:
  106. os.makedirs(os.path.dirname(file_path))
  107. except OSError:
  108. # already exists
  109. pass
  110. with open(file_path, 'wb') as f:
  111. f.write(data)
  112. return raw_data_list
  113. def find_job_id(self, job_name, pipeline_id=None, job_status='success'):
  114. """
  115. Get Job ID from job name of specific pipeline
  116. :param job_name: job name
  117. :param pipeline_id: If None, will get pipeline id from CI pre-defined variable.
  118. :param job_status: status of job. One pipeline could have multiple jobs with same name after retry.
  119. job_status is used to filter these jobs.
  120. :return: a list of job IDs (parallel job will generate multiple jobs)
  121. """
  122. job_id_list = []
  123. if pipeline_id is None:
  124. pipeline_id = os.getenv('CI_PIPELINE_ID')
  125. pipeline = self.project.pipelines.get(pipeline_id)
  126. jobs = pipeline.jobs.list(all=True)
  127. for job in jobs:
  128. match = self.JOB_NAME_PATTERN.match(job.name)
  129. if match:
  130. if match.group(1) == job_name and job.status == job_status:
  131. job_id_list.append({'id': job.id, 'parallel_num': match.group(3)})
  132. return job_id_list
  133. @retry_download # type: ignore
  134. def download_archive(self, ref, destination, project_id=None):
  135. """
  136. Download archive of certain commit of a repository and extract to destination path
  137. :param ref: commit or branch name
  138. :param destination: destination path of extracted archive file
  139. :param project_id: download project of current instance if project_id is None
  140. :return: root path name of archive file
  141. """
  142. if project_id is None:
  143. project = self.project
  144. else:
  145. project = self.gitlab_inst.projects.get(project_id)
  146. with tempfile.NamedTemporaryFile(delete=False) as temp_file:
  147. try:
  148. project.repository_archive(sha=ref, streamed=True, action=temp_file.write)
  149. except gitlab.GitlabGetError as e:
  150. print('Failed to archive from project {}'.format(project_id))
  151. raise e
  152. print('archive size: {:.03f}MB'.format(float(os.path.getsize(temp_file.name)) / (1024 * 1024)))
  153. with tarfile.open(temp_file.name, 'r') as archive_file:
  154. root_name = archive_file.getnames()[0]
  155. archive_file.extractall(destination)
  156. return os.path.join(os.path.realpath(destination), root_name)
  157. if __name__ == '__main__':
  158. parser = argparse.ArgumentParser()
  159. parser.add_argument('action')
  160. parser.add_argument('project_id', type=int)
  161. parser.add_argument('--pipeline_id', '-i', type=int, default=None)
  162. parser.add_argument('--ref', '-r', default='master')
  163. parser.add_argument('--job_id', '-j', type=int, default=None)
  164. parser.add_argument('--job_name', '-n', default=None)
  165. parser.add_argument('--project_name', '-m', default=None)
  166. parser.add_argument('--destination', '-d', default=None)
  167. parser.add_argument('--artifact_path', '-a', nargs='*', default=None)
  168. args = parser.parse_args()
  169. gitlab_inst = Gitlab(args.project_id)
  170. if args.action == 'download_artifacts':
  171. gitlab_inst.download_artifacts(args.job_id, args.destination)
  172. if args.action == 'download_artifact':
  173. gitlab_inst.download_artifact(args.job_id, args.artifact_path, args.destination)
  174. elif args.action == 'find_job_id':
  175. job_ids = gitlab_inst.find_job_id(args.job_name, args.pipeline_id)
  176. print(';'.join([','.join([str(j['id']), j['parallel_num']]) for j in job_ids]))
  177. elif args.action == 'download_archive':
  178. gitlab_inst.download_archive(args.ref, args.destination)
  179. elif args.action == 'get_project_id':
  180. ret = gitlab_inst.get_project_id(args.project_name)
  181. print('project id: {}'.format(ret))