xref: /aosp_15_r20/external/mesa3d/bin/ci/gitlab_common.py (revision 6104692788411f58d303aa86923a9ff6ecaded22)
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