gitlab_api.py 7.8 KB

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