• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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