File indexing completed on 2024-12-08 08:08:26
0001 """ 0002 This module contains code for the pipeline command 0003 """ 0004 0005 # SPDX-FileCopyrightText: 2021 Leon Morten Richter <github@leonmortenrichter.de> 0006 # 0007 # SPDX-License-Identifier: GPL-2.0-or-later 0008 0009 import argparse 0010 import os 0011 import sys 0012 from enum import Enum 0013 from typing import List, Optional, Dict 0014 0015 from gitlab.exceptions import GitlabGetError 0016 from gitlab.v4.objects import ProjectPipeline 0017 0018 from lab.repositoryconnection import RepositoryConnection 0019 from lab.table import Table 0020 from lab.utils import TextFormatting, Utils, LogType 0021 0022 0023 class PipelineStatus(Enum): 0024 """ 0025 Currently supported Pipeline Status from Gitlab. 0026 0027 Allowed : https://docs.gitlab.com/ce/api/pipelines.html#list-project-pipelines 0028 """ 0029 0030 WAITING = "waiting_for_resource" 0031 PENDING = "pending" 0032 RUNNING = "running" 0033 SUCCESS = "success" 0034 FAILED = "failed" 0035 CANCELED = "canceled" 0036 SKIPPED = "skipped" 0037 SCHEDULED = "scheduled" 0038 0039 @classmethod 0040 def _missing_(cls, value: object) -> None: 0041 """ 0042 Called if a given value does not exist. 0043 Supported in all version of Python since 3.6. 0044 """ 0045 Utils.log(LogType.WARNING, f"Invalid status '{str(value)}'") 0046 sys.exit(1) 0047 0048 @staticmethod 0049 def format(status: str) -> str: 0050 """ 0051 Transform a status into a formatted string, that is a colored string. 0052 If no color is defined for the given status, this is a NO-OP. 0053 """ 0054 formatting = { 0055 "success": TextFormatting.GREEN, 0056 "failed": TextFormatting.RED, 0057 "running": TextFormatting.BLUE, 0058 "canceled": TextFormatting.PURPLE, 0059 "pending": TextFormatting.BLUE, 0060 "waiting_for_resource": TextFormatting.YELLOW, 0061 } 0062 0063 if status in formatting: 0064 # Only format if there is a defined color for the status 0065 return f"{formatting.get(status, '')}{status}{TextFormatting.END}" 0066 0067 # If color is associated with the status, do nothing 0068 return status 0069 0070 @property 0071 def formatted(self) -> str: 0072 """ 0073 Formatted string representation of a status. 0074 """ 0075 return self.format(str(self)) 0076 0077 @property 0078 def finished(self) -> bool: 0079 """ 0080 Is the pipeline finished, that is either skipped, canceled, failed or succeeded. 0081 """ 0082 finished_states = ( 0083 PipelineStatus.SKIPPED, 0084 PipelineStatus.SUCCESS, 0085 PipelineStatus.FAILED, 0086 PipelineStatus.CANCELED, 0087 ) 0088 return self in finished_states 0089 0090 def __str__(self) -> str: 0091 return str(self.value) 0092 0093 def __repr__(self) -> str: 0094 return str(self) 0095 0096 0097 def parser( 0098 subparsers: argparse._SubParsersAction, # pylint: disable=protected-access 0099 ) -> argparse.ArgumentParser: 0100 """ 0101 Subparser for pipeline command 0102 :param subparsers: subparsers object from global parser 0103 :return: search subparser 0104 """ 0105 pipeline_parser: argparse.ArgumentParser = subparsers.add_parser( 0106 "pipelines", help="Fetch pipeline status from GitLab." 0107 ) 0108 0109 # Optionally filter by status. This is None by default. 0110 pipeline_parser.add_argument( 0111 "--status", 0112 help=f"Filter pipelines by status, one of: [{', '.join(str(s) for s in PipelineStatus)}]", 0113 metavar="", 0114 type=PipelineStatus, 0115 default=None, 0116 choices=list(PipelineStatus), 0117 ) 0118 0119 pipeline_parser.add_argument( 0120 "--ref", 0121 help="Filter pipelines by reference, e.g. 'master'", 0122 metavar="ref", 0123 type=str, 0124 nargs="?", 0125 ) 0126 0127 pipeline_parser.add_argument( 0128 "pipeline_id", 0129 help="Show pipeline by id if provided", 0130 metavar="pipeline_id", 0131 type=int, 0132 nargs="?", 0133 ) 0134 0135 return pipeline_parser 0136 0137 0138 def run(args: argparse.Namespace) -> None: 0139 """ 0140 :param args: parsed arguments 0141 """ 0142 if args.pipeline_id is not None: 0143 pipeline: PipelineShow = PipelineShow(args.pipeline_id) 0144 print(pipeline) 0145 0146 else: 0147 lister: PipelineList = PipelineList(args.status, ref=args.ref) 0148 lister.print_formatted_list() 0149 0150 0151 class PipelineShow(RepositoryConnection): 0152 """ 0153 Show single pipeline 0154 """ 0155 0156 def __init__(self, pipeline_id: int) -> None: 0157 RepositoryConnection.__init__(self) 0158 try: 0159 self.pipeline: ProjectPipeline = self._remote_project.pipelines.get( 0160 pipeline_id, lazy=False 0161 ) 0162 except GitlabGetError: 0163 Utils.log(LogType.WARNING, f"No pipeline with ID {pipeline_id}") 0164 sys.exit(1) 0165 0166 def __str__(self) -> str: 0167 """ 0168 Human readable representation of a pipeline. 0169 Output looks like: 0170 "Pipeline #25794 for master triggered by some one(Someone) 2 months ago 0171 finished after 1m 52s with status: success" 0172 """ 0173 status: PipelineStatus = PipelineStatus(self.pipeline.status) 0174 0175 text_buffer: str = "" 0176 text_buffer += ( 0177 f"Pipeline {TextFormatting.BOLD}#{self.pipeline.id}" 0178 + f"{TextFormatting.END} for {self.pipeline.ref} " 0179 + f"triggered by {self.pipeline.user['name']}({self.pipeline.user['username']}) " 0180 + f"{Utils.pretty_date(self.pipeline.created_at)} " 0181 ) 0182 0183 if status.finished: 0184 text_buffer += ( 0185 f"finished after {Utils.pretty_time_delta(self.pipeline.duration or 0)} " 0186 + f"with status: {status.formatted}" 0187 ) 0188 else: 0189 text_buffer += f"is currently {status.formatted}" 0190 0191 text_buffer += os.linesep 0192 return text_buffer 0193 0194 0195 class PipelineList(RepositoryConnection): 0196 """ 0197 Search class 0198 """ 0199 0200 def __init__(self, status: Optional[PipelineStatus] = None, ref: Optional[str] = None) -> None: 0201 RepositoryConnection.__init__(self) 0202 0203 self.status: Optional[PipelineStatus] = status 0204 self.ref: Optional[str] = ref 0205 0206 if ref is not None and ref not in self._local_repo.refs: 0207 # Print a warning, if the ref is not found LOCALLY 0208 # The remote may contain refs, that do not exists inside the local copy, 0209 # therefore only a warning is printed. 0210 Utils.log(LogType.WARNING, f"Ref '{ref}' is not found locally.") 0211 0212 def print_formatted_list(self) -> None: 0213 """ 0214 Print the list of pipelines to terminal formatted as a table 0215 """ 0216 table = Table() 0217 0218 # Compute args that are sent to GitLab 0219 args: Dict[str, str] = {} 0220 if self.status is not None: 0221 # Only yield pipelines that have the specific status 0222 # If empty all pipelines will be returned by GitLab 0223 args["status"] = str(self.status) 0224 0225 if self.ref is not None: 0226 # Only yield pipeline that match a given reference 0227 args["ref"] = self.ref 0228 0229 # Build the printable table 0230 pipelines: List[ProjectPipeline] = self._remote_project.pipelines.list(**args) 0231 for pipeline in pipelines: 0232 row: List[str] = [ 0233 TextFormatting.BOLD + "#" + str(pipeline.id) + TextFormatting.END, 0234 pipeline.ref, 0235 Utils.pretty_date(pipeline.created_at), 0236 PipelineStatus.format(pipeline.status), 0237 ] 0238 table.add_row(row) 0239 0240 table.print()