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