1*61046927SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*61046927SAndroid Build Coastguard Worker# Copyright © 2020 - 2022 Collabora Ltd. 3*61046927SAndroid Build Coastguard Worker# Authors: 4*61046927SAndroid Build Coastguard Worker# Tomeu Vizoso <[email protected]> 5*61046927SAndroid Build Coastguard Worker# David Heidelberg <[email protected]> 6*61046927SAndroid Build Coastguard Worker# Guilherme Gallo <[email protected]> 7*61046927SAndroid Build Coastguard Worker# 8*61046927SAndroid Build Coastguard Worker# SPDX-License-Identifier: MIT 9*61046927SAndroid Build Coastguard Worker'''Shared functions between the scripts.''' 10*61046927SAndroid Build Coastguard Worker 11*61046927SAndroid Build Coastguard Workerimport logging 12*61046927SAndroid Build Coastguard Workerimport os 13*61046927SAndroid Build Coastguard Workerimport re 14*61046927SAndroid Build Coastguard Workerimport time 15*61046927SAndroid Build Coastguard Workerfrom functools import cache 16*61046927SAndroid Build Coastguard Workerfrom pathlib import Path 17*61046927SAndroid Build Coastguard Worker 18*61046927SAndroid Build Coastguard WorkerGITLAB_URL = "https://gitlab.freedesktop.org" 19*61046927SAndroid Build Coastguard WorkerTOKEN_DIR = Path(os.getenv("XDG_CONFIG_HOME") or Path.home() / ".config") 20*61046927SAndroid Build Coastguard Worker 21*61046927SAndroid Build Coastguard Worker# Known GitLab token prefixes: https://docs.gitlab.com/ee/security/token_overview.html#token-prefixes 22*61046927SAndroid Build Coastguard WorkerTOKEN_PREFIXES: dict[str, str] = { 23*61046927SAndroid Build Coastguard Worker "Personal access token": "glpat-", 24*61046927SAndroid Build Coastguard Worker "OAuth Application Secret": "gloas-", 25*61046927SAndroid Build Coastguard Worker "Deploy token": "gldt-", 26*61046927SAndroid Build Coastguard Worker "Runner authentication token": "glrt-", 27*61046927SAndroid Build Coastguard Worker "CI/CD Job token": "glcbt-", 28*61046927SAndroid Build Coastguard Worker "Trigger token": "glptt-", 29*61046927SAndroid Build Coastguard Worker "Feed token": "glft-", 30*61046927SAndroid Build Coastguard Worker "Incoming mail token": "glimt-", 31*61046927SAndroid Build Coastguard Worker "GitLab Agent for Kubernetes token": "glagent-", 32*61046927SAndroid Build Coastguard Worker "SCIM Tokens": "glsoat-", 33*61046927SAndroid Build Coastguard Worker} 34*61046927SAndroid Build Coastguard Worker 35*61046927SAndroid Build Coastguard Worker 36*61046927SAndroid Build Coastguard Worker@cache 37*61046927SAndroid Build Coastguard Workerdef print_once(*args, **kwargs): 38*61046927SAndroid Build Coastguard Worker """Print without spamming the output""" 39*61046927SAndroid Build Coastguard Worker print(*args, **kwargs) 40*61046927SAndroid Build Coastguard Worker 41*61046927SAndroid Build Coastguard Worker 42*61046927SAndroid Build Coastguard Workerdef pretty_duration(seconds): 43*61046927SAndroid Build Coastguard Worker """Pretty print duration""" 44*61046927SAndroid Build Coastguard Worker hours, rem = divmod(seconds, 3600) 45*61046927SAndroid Build Coastguard Worker minutes, seconds = divmod(rem, 60) 46*61046927SAndroid Build Coastguard Worker if hours: 47*61046927SAndroid Build Coastguard Worker return f"{hours:0.0f}h{minutes:02.0f}m{seconds:02.0f}s" 48*61046927SAndroid Build Coastguard Worker if minutes: 49*61046927SAndroid Build Coastguard Worker return f"{minutes:0.0f}m{seconds:02.0f}s" 50*61046927SAndroid Build Coastguard Worker return f"{seconds:0.0f}s" 51*61046927SAndroid Build Coastguard Worker 52*61046927SAndroid Build Coastguard Worker 53*61046927SAndroid Build Coastguard Workerdef get_gitlab_pipeline_from_url(gl, pipeline_url) -> tuple: 54*61046927SAndroid Build Coastguard Worker """ 55*61046927SAndroid Build Coastguard Worker Extract the project and pipeline object from the url string 56*61046927SAndroid Build Coastguard Worker :param gl: Gitlab object 57*61046927SAndroid Build Coastguard Worker :param pipeline_url: string with a url to a pipeline 58*61046927SAndroid Build Coastguard Worker :return: ProjectPipeline, Project objects 59*61046927SAndroid Build Coastguard Worker """ 60*61046927SAndroid Build Coastguard Worker pattern = rf"^{re.escape(GITLAB_URL)}/(.*)/-/pipelines/([0-9]+)$" 61*61046927SAndroid Build Coastguard Worker match = re.match(pattern, pipeline_url) 62*61046927SAndroid Build Coastguard Worker if not match: 63*61046927SAndroid Build Coastguard Worker raise AssertionError(f"url {pipeline_url} doesn't follow the pattern {pattern}") 64*61046927SAndroid Build Coastguard Worker namespace_with_project, pipeline_id = match.groups() 65*61046927SAndroid Build Coastguard Worker cur_project = gl.projects.get(namespace_with_project) 66*61046927SAndroid Build Coastguard Worker pipe = cur_project.pipelines.get(pipeline_id) 67*61046927SAndroid Build Coastguard Worker return pipe, cur_project 68*61046927SAndroid Build Coastguard Worker 69*61046927SAndroid Build Coastguard Worker 70*61046927SAndroid Build Coastguard Workerdef get_gitlab_project(glab, name: str): 71*61046927SAndroid Build Coastguard Worker """Finds a specified gitlab project for given user""" 72*61046927SAndroid Build Coastguard Worker if "/" in name: 73*61046927SAndroid Build Coastguard Worker project_path = name 74*61046927SAndroid Build Coastguard Worker else: 75*61046927SAndroid Build Coastguard Worker glab.auth() 76*61046927SAndroid Build Coastguard Worker username = glab.user.username 77*61046927SAndroid Build Coastguard Worker project_path = f"{username}/{name}" 78*61046927SAndroid Build Coastguard Worker return glab.projects.get(project_path) 79*61046927SAndroid Build Coastguard Worker 80*61046927SAndroid Build Coastguard Worker 81*61046927SAndroid Build Coastguard Workerdef get_token_from_default_dir() -> str: 82*61046927SAndroid Build Coastguard Worker """ 83*61046927SAndroid Build Coastguard Worker Retrieves the GitLab token from the default directory. 84*61046927SAndroid Build Coastguard Worker 85*61046927SAndroid Build Coastguard Worker Returns: 86*61046927SAndroid Build Coastguard Worker str: The path to the GitLab token file. 87*61046927SAndroid Build Coastguard Worker 88*61046927SAndroid Build Coastguard Worker Raises: 89*61046927SAndroid Build Coastguard Worker FileNotFoundError: If the token file is not found. 90*61046927SAndroid Build Coastguard Worker """ 91*61046927SAndroid Build Coastguard Worker token_file = TOKEN_DIR / "gitlab-token" 92*61046927SAndroid Build Coastguard Worker try: 93*61046927SAndroid Build Coastguard Worker return str(token_file.resolve()) 94*61046927SAndroid Build Coastguard Worker except FileNotFoundError as ex: 95*61046927SAndroid Build Coastguard Worker print( 96*61046927SAndroid Build Coastguard Worker f"Could not find {token_file}, please provide a token file as an argument" 97*61046927SAndroid Build Coastguard Worker ) 98*61046927SAndroid Build Coastguard Worker raise ex 99*61046927SAndroid Build Coastguard Worker 100*61046927SAndroid Build Coastguard Worker 101*61046927SAndroid Build Coastguard Workerdef validate_gitlab_token(token: str) -> bool: 102*61046927SAndroid Build Coastguard Worker token_suffix = token.split("-")[-1] 103*61046927SAndroid Build Coastguard Worker # Basic validation of the token suffix based on: 104*61046927SAndroid Build Coastguard Worker # https://gitlab.com/gitlab-org/gitlab/-/blob/master/gems/gitlab-secret_detection/lib/gitleaks.toml 105*61046927SAndroid Build Coastguard Worker if not re.match(r"(\w+-)?[0-9a-zA-Z_\-]{20,64}", token_suffix): 106*61046927SAndroid Build Coastguard Worker return False 107*61046927SAndroid Build Coastguard Worker 108*61046927SAndroid Build Coastguard Worker for token_type, token_prefix in TOKEN_PREFIXES.items(): 109*61046927SAndroid Build Coastguard Worker if token.startswith(token_prefix): 110*61046927SAndroid Build Coastguard Worker logging.info(f"Found probable token type: {token_type}") 111*61046927SAndroid Build Coastguard Worker return True 112*61046927SAndroid Build Coastguard Worker 113*61046927SAndroid Build Coastguard Worker # If the token type is not recognized, return False 114*61046927SAndroid Build Coastguard Worker return False 115*61046927SAndroid Build Coastguard Worker 116*61046927SAndroid Build Coastguard Worker 117*61046927SAndroid Build Coastguard Workerdef get_token_from_arg(token_arg: str | Path | None) -> str | None: 118*61046927SAndroid Build Coastguard Worker if not token_arg: 119*61046927SAndroid Build Coastguard Worker logging.info("No token provided.") 120*61046927SAndroid Build Coastguard Worker return None 121*61046927SAndroid Build Coastguard Worker 122*61046927SAndroid Build Coastguard Worker token_path = Path(token_arg) 123*61046927SAndroid Build Coastguard Worker if token_path.is_file(): 124*61046927SAndroid Build Coastguard Worker return read_token_from_file(token_path) 125*61046927SAndroid Build Coastguard Worker 126*61046927SAndroid Build Coastguard Worker return handle_direct_token(token_path, token_arg) 127*61046927SAndroid Build Coastguard Worker 128*61046927SAndroid Build Coastguard Worker 129*61046927SAndroid Build Coastguard Workerdef read_token_from_file(token_path: Path) -> str: 130*61046927SAndroid Build Coastguard Worker token = token_path.read_text().strip() 131*61046927SAndroid Build Coastguard Worker logging.info(f"Token read from file: {token_path}") 132*61046927SAndroid Build Coastguard Worker return token 133*61046927SAndroid Build Coastguard Worker 134*61046927SAndroid Build Coastguard Worker 135*61046927SAndroid Build Coastguard Workerdef handle_direct_token(token_path: Path, token_arg: str | Path) -> str | None: 136*61046927SAndroid Build Coastguard Worker if token_path == Path(get_token_from_default_dir()): 137*61046927SAndroid Build Coastguard Worker logging.warning( 138*61046927SAndroid Build Coastguard Worker f"The default token file {token_path} was not found. " 139*61046927SAndroid Build Coastguard Worker "Please provide a token file or a token directly via --token arg." 140*61046927SAndroid Build Coastguard Worker ) 141*61046927SAndroid Build Coastguard Worker return None 142*61046927SAndroid Build Coastguard Worker logging.info("Token provided directly as an argument.") 143*61046927SAndroid Build Coastguard Worker return str(token_arg) 144*61046927SAndroid Build Coastguard Worker 145*61046927SAndroid Build Coastguard Worker 146*61046927SAndroid Build Coastguard Workerdef read_token(token_arg: str | Path | None) -> str | None: 147*61046927SAndroid Build Coastguard Worker token = get_token_from_arg(token_arg) 148*61046927SAndroid Build Coastguard Worker if token and not validate_gitlab_token(token): 149*61046927SAndroid Build Coastguard Worker logging.warning("The provided token is either an old token or does not seem to " 150*61046927SAndroid Build Coastguard Worker "be a valid token.") 151*61046927SAndroid Build Coastguard Worker logging.warning("Newer tokens are the ones created from a Gitlab 14.5+ instance.") 152*61046927SAndroid Build Coastguard Worker logging.warning("See https://about.gitlab.com/releases/2021/11/22/" 153*61046927SAndroid Build Coastguard Worker "gitlab-14-5-released/" 154*61046927SAndroid Build Coastguard Worker "#new-gitlab-access-token-prefix-and-detection") 155*61046927SAndroid Build Coastguard Worker return token 156*61046927SAndroid Build Coastguard Worker 157*61046927SAndroid Build Coastguard Worker 158*61046927SAndroid Build Coastguard Workerdef wait_for_pipeline(projects, sha: str, timeout=None): 159*61046927SAndroid Build Coastguard Worker """await until pipeline appears in Gitlab""" 160*61046927SAndroid Build Coastguard Worker project_names = [project.path_with_namespace for project in projects] 161*61046927SAndroid Build Coastguard Worker print(f"⏲ for the pipeline to appear in {project_names}..", end="") 162*61046927SAndroid Build Coastguard Worker start_time = time.time() 163*61046927SAndroid Build Coastguard Worker while True: 164*61046927SAndroid Build Coastguard Worker for project in projects: 165*61046927SAndroid Build Coastguard Worker pipelines = project.pipelines.list(sha=sha) 166*61046927SAndroid Build Coastguard Worker if pipelines: 167*61046927SAndroid Build Coastguard Worker print("", flush=True) 168*61046927SAndroid Build Coastguard Worker return (pipelines[0], project) 169*61046927SAndroid Build Coastguard Worker print("", end=".", flush=True) 170*61046927SAndroid Build Coastguard Worker if timeout and time.time() - start_time > timeout: 171*61046927SAndroid Build Coastguard Worker print(" not found", flush=True) 172*61046927SAndroid Build Coastguard Worker return (None, None) 173*61046927SAndroid Build Coastguard Worker time.sleep(1) 174