Blog Ingénierie Utiliser l'API python-gitlab pour améliorer vos workflows DevSecOps
Date de la publication : February 1, 2023
Lecture : 29 min

Utiliser l'API python-gitlab pour améliorer vos workflows DevSecOps

Vous souhaitez améliorer vos workflows DevSecOps ? Découvrez dans ce tutoriel des exemples et bonnes pratiques d’utilisation de l’API python-gitlab.

post-cover-image.jpg

Un jour, un ami m’a dit : « Le travail manuel est un bug ». Depuis, face à des tâches répétitives, j’ai pris l’habitude de les automatiser autant que possible.

Par exemple, en interrogeant une API REST pour faire un inventaire des paramètres, ou en effectuant des appels d’API pour créer de nouveaux commentaires dans les tickets ou les merge requests de GitLab. L'interaction avec l'API REST de GitLab peut se faire de différentes manières, en utilisant des requêtes HTTP avec curl (ou hurl) en ligne de commande, ou en écrivant un script dans un langage de programmation.

Dans ce dernier cas, il faut effectuer des tâches fastidieuses avec le code brut des requêtes HTTP et l'analyse des réponses JSON. Grâce à la communauté GitLab, de nombreux langages sont pris en charge par les bibliothèques d'abstraction d'API. Elles prennent en charge tous les attributs de l’API, ajoutent des fonctions d'aide pour obtenir, créer et supprimer des objets, et facilitent ainsi la tâche des équipes de développement. La bibliothèque python-gitlab est une bibliothèque écrite en Python, riche en fonctionnalités et facile à utiliser.

Dans cet article, vous apprendrez les bases de l’utilisation de la bibliothèque python-gitlab en vous familiarisant avec les objets de l’API, les attributs, la pagination et les ensembles de résultats. Vous découvrirez également des cas d'utilisation concrets tels que la collecte de données, la génération de synthèses et l’écriture de données dans l'API pour créer des commentaires et des validations.

Il y a encore beaucoup à apprendre, avec de nombreux cas d'utilisation inspirés des questions posées par la communauté sur le forum, Hacker News, des tickets, et bien plus encore.

Premiers pas

La documentation python-gitlab est une excellente ressource pour débuter. Elle offre un aperçu des types d'objets et de leurs méthodes, ainsi que des exemples de workflows combinés. Cette ressource est idéale pour faire vos premiers pas, en plus de la documentation sur les ressources de l'API GitLab qui fournit les attributs d'objet associés.

Les exemples de code présentés dans cet article nécessitent Python 3.8+ et la bibliothèque python-gitlab. Des exigences supplémentaires sont spécifiées dans le fichier requirements.txt. Un exemple nécessite pyyaml pour l'analyse de la configuration YAML. Pour suivre et mettre en pratique le code des cas d'utilisation, il est recommandé de cloner le projet, d'installer les prérequis et d'exécuter les scripts.

Exemple avec Homebrew sur macOS :

git clone https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-api-python.git

cd gitlab-api-python

brew install python

pip3 install -r requirements.txt

python3 <scriptname>.py

Les scripts n'utilisent pas de bibliothèque partagée commune fournissant des fonctions génériques pour la lecture des paramètres ou d’autres fonctionnalités d'aide supplémentaires. L’objectif est de montrer des exemples faciles à suivre, qui peuvent être utilisés de manière autonome pour des tests, et qui nécessitent uniquement l'installation de la bibliothèque python-gitlab.

Nous recommandons d'améliorer le code pour une utilisation en production. Cela vous aidera à créer un projet d’API tooling maintenu, incluant par exemple des images de conteneurs et des modèles CI/CD que les équipes de développement peuvent utiliser au sein d'une plateforme DevSecOps.

Configuration

Sans configuration, python-gitlab exécutera des requêtes non authentifiées sur le serveur par défaut : https://gitlab.com. Les paramètres de configuration les plus courants concernent l'instance GitLab à laquelle se connecter, et la méthode d'authentification en spécifiant les jetons d'accès. Python-gitlab prend en charge différents types de configuration : un fichier de configuration ou des variables d'environnement.

Le fichier de configuration est disponible pour les liaisons de bibliothèque d’API et pour l'interface de ligne de commande (que nous n’aborderons pas dans cet article). Le fichier de configuration prend en charge les credential helpers pour accéder directement aux jetons.

Les variables d'environnement, en tant que méthode de configuration alternative, vous offrent un moyen simple d'exécuter le script dans un terminal, de l'intégrer dans des images de conteneurs et de le préparer à être exécuté dans des pipelines CI/CD.

Vous devez lancer la configuration dans le contexte du script Python. Importez la bibliothèque os pour récupérer les variables d'environnement à l'aide de la méthode os.environ.get(). Le premier paramètre spécifie la clé, le second paramètre définit la valeur par défaut lorsque la variable n'est pas disponible dans l'environnement.

import os

gl_server = os.environ.get('GL_SERVER', 'https://gitlab.com')

print(gl_server)

Le paramétrage dans le terminal peut se faire directement pour la commande uniquement, ou être exporté dans l'environnement shell.

$ GL_SERVER=’https://gitlab.company.com’ python3 script.py

$ export GL_SERVER=’https://gitlab.company.com’
$ python3 script.py

Nous recommandons d'ajouter des contrôles de sécurité pour s’assurer que toutes les variables sont définies avant de continuer l'exécution du programme. L'extrait de code suivant importe les bibliothèques requises, lit la variable d'environnement GL_SERVER, et attend de l'utilisateur qu'il définisse la variable GL_TOKEN. Si ce n'est pas le cas, le script affiche et génère des erreurs, puis appelle sys.exit(1), pour indiquer un statut d’erreur.

import gitlab
import os
import sys

GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')
GITLAB_TOKEN = os.environ.get('GL_TOKEN')

if not GITLAB_TOKEN:
    print("Please set the GL_TOKEN env variable.")
    sys.exit(1)

Examinons maintenant un exemple plus détaillé, qui crée une connexion à l'API et effectue une requête de données.

Gestion des objets : l'objet GitLab

Toute interaction avec l'API nécessite une instanciation de l'objet GitLab. C'est le point d'entrée pour configurer le serveur GitLab auquel se connecter et s'authentifier à l'aide de jetons d'accès, et définir d’autres paramètres globaux pour la pagination, le chargement d’objets, et plus encore.

L'exemple suivant exécute une requête non authentifiée sur GitLab.com. Il est possible d'accéder aux points de terminaison d’API publique, et d'obtenir par exemple un modèle .gitignore pour Python.

python_gitlab_object_unauthenticated.py

import gitlab

gl = gitlab.Gitlab()

# Get .gitignore templates without authentication
gitignore_templates = gl.gitignores.get('Python')

print(gitignore_templates.content)

Dans les sections suivantes, nous vous partageons des informations détaillées sur :

  • La gestion et le chargement des objets,
  • La pagination des résultats,
  • Le travail avec les relations entre objets,
  • Le travail avec différents scopes de collection d'objets.

La gestion et le chargement des objets

La bibliothèque python-gitlab donne accès aux ressources GitLab en utilisant ce que l’on appelle des « Gestionnaires ». Chaque type de gestionnaire implémente des méthodes pour travailler avec les ensembles de données (list, get, etc.).

Le script ci-dessous montre comment accéder aux sous-groupes, aux projets directs et à tous les projets, y compris les sous-groupes, aux tickets, aux epics et aux tâches. Une authentification est nécessaire pour accéder à tous les attributs. L'extrait de code utilise donc des variables pour obtenir le jeton d'authentification, et utilise également la variable GROUP_ID pour spécifier un groupe principal à partir duquel il faut commencer la recherche.

#!/usr/bin/env python

import gitlab
import os
import sys

GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')
# https://gitlab.com/gitlab-de/use-cases/
GROUP_ID = os.environ.get('GL_GROUP_ID', 16058698)
GITLAB_TOKEN = os.environ.get('GL_TOKEN')

if not GITLAB_TOKEN:
    print("Please set the GL_TOKEN env variable.")
    sys.exit(1)

gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)

# Main
main_group = gl.groups.get(GROUP_ID)

print("Sub groups")
for sg in main_group.subgroups.list():
    print("Subgroup name: {sg}".format(sg=sg.name))

print("Projects (direct)")
for p in main_group.projects.list():
    print("Project name: {p}".format(p=p.name))

print("Projects (including subgroups)")
for p in main_group.projects.list(include_subgroups=True, all=True):
     print("Project name: {p}".format(p=p.name))

print("Issues")
for i in main_group.issues.list(state='opened'):
    print("Issue title: {t}".format(t=i.title))

print("Epics")
for e in main_group.issues.list():
    print("Epic title: {t}".format(t=e.title))

print("Todos")
for t in gl.todos.list(state='pending'):
    print("Todo: {t} url: {u}".format(t=t.body, u=t.target_url

Vous pouvez exécuter le script python_gitlab_object_manager_methods.py en remplaçant la variable GROUP_ID sur GitLab.com SaaS pour analyser votre propre groupe. Vous devez spécifier la variable GL_SERVER pour les instances auto-gérées. GL_TOKEN doit fournir le jeton d'accès personnel.

export GL_TOKEN=xxx

export GL_SERVER=”https://gitlab.company.com”

export GL_SERVER=”https://gitlab.com”

export GL_GROUP_ID=1234

python3 python_gitlab_object_manager_methods.py

À partir de maintenant, les exemples n'affichent plus les en-têtes Python et l'analyse des variables d'environnement, afin de se concentrer sur l'algorithme et les fonctionnalités. Tous les scripts sont open source sous licence MIT, et sont disponibles dans ce projet.

La pagination des résultats

Par défaut, l’API GitLab ne renvoie pas tous les ensembles de résultats et exige que les clients utilisent la pagination pour parcourir toutes les pages de résultats. La bibliothèque python-gitlab permet aux utilisateurs de définir les paramètres globalement dans l'objet GitLab, ou sur chaque appel list(). Cela permet d'éviter que tous les ensembles de résultats déclenchent des requêtes API, ce qui peut ralentir l'exécution du script. Utilisez iterator=True, et les appels d'API sont déclenchés à la demande lors de l'accès à l'objet.

L'exemple suivant recherche le nom de groupe everyonecancontribute et utilise la pagination du jeu de clés pour afficher 100 résultats sur chaque page. L'itérateur est défini sur true dans gl.groups.list(iterator=True) pour récupérer de nouveaux ensembles de résultats à la demande. Si le nom du groupe recherché est trouvé, la boucle s'interrompt et affiche un résumé, incluant la mesure de la durée totale de la requête de recherche.

SEARCH_GROUP_NAME="everyonecancontribute"

# Use keyset pagination
# https://python-gitlab.readthedocs.io/en/stable/api-usage.html#pagination
gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN,
    pagination="keyset", order_by="id", per_page=100)

# Iterate over the list, and fire new API calls in case the result set does not match yet
groups = gl.groups.list(iterator=True)

found_page = 0
start = timer()

for group in groups:
    if SEARCH_GROUP_NAME == group.name:
        # print(group) # debug
        found_page = groups.current_page
        break

end = timer()

duration = f'{end-start:.2f}'

if found_page > 0:
    print("Pagination API example for Python with GitLab{desc} - found group {g} on page {p}, duration {d}s".format(
        desc=", the DevSecOps platform", g=SEARCH_GROUP_NAME, p=found_page, d=duration))
else:
    print("Could not find group name '{g}', duration {d}".format(g=SEARCH_GROUP_NAME, d=duration))

L'exécution de python_gitlab_pagination.py a permis de trouver le groupe everyonecancontribute à la page 5.

$ ​​python3 python_gitlab_pagination.py
Pagination API example for Python with GitLab, the DevSecOps platform - found group everyonecancontribute on page 5, duration 8.51s

Le travail avec les relations entre objets

Lorsque vous travaillez avec des relations entre objets, par exemple pour collecter tous les projets dans un groupe donné, vous devez envisager des étapes supplémentaires. Par défaut, les objets de projet renvoyés présentent des attributs limités. Les objets gérables nécessitent un appel supplémentaire get() pour obtenir l'objet de projet complet de l'API en arrière-plan. Ce workflow permet de réduire les temps d’attente et le trafic en limitant les attributs immédiatement renvoyés.

L'exemple suivant illustre le problème en parcourant tous les projets d'un groupe et en essayant d'appeler la fonction project.branches.list(). Cela génère une exception dans le flux try/except. Le deuxième exemple obtient un objet de projet gérable et tente à nouveau d'appeler la fonction.

# Main
group = gl.groups.get(GROUP_ID)

# Collect all projects in group and subgroups
projects = group.projects.list(include_subgroups=True, all=True)

for project in projects:
    # Try running a method on a weak object
    try:
       print("🤔 Project: {pn} 💡 Branches: {b}\n".format(
        pn=project.name,
        b=", ".join([x.name for x in project.branches.list()])))
    except Exception as e:
        print("Got exception: {e} \n ===================================== \n".format(e=e))

    # Retrieve a full manageable project object
    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples
    manageable_project = gl.projects.get(project.id)

    # Print a method available on a manageable object
    print("🤔 Project: {pn} 💡 Branches: {b}\n".format(
        pn=manageable_project.name,
        b=", ".join([x.name for x in manageable_project.branches.list()])))

Le gestionnaire d'exceptions dans la bibliothèque python-gitlab affiche le message d'erreur et renvoie à la documentation. Pour le débogage, notez que les objets peuvent ne pas être disponibles pour la gestion lorsque vous ne pouvez pas accéder aux attributs de l'objet ou aux appels de fonction.

$ python3 python_gitlab_manageable_objects.py

🤔 Project: GitLab API Playground 💡 Branches: cicd-demo-automated-comments, docs-mr-approval-settings, main

Got exception: 'GroupProject' object has no attribute 'branches'

<class 'gitlab.v4.objects.projects.GroupProject'> was created via a
list() call and only a subset of the data may be present. To ensure
all data is present get the object using a get(object.id) call. For
more details, see:

https://python-gitlab.readthedocs.io/en/v3.8.1/faq.html#attribute-error-list
 =====================================

Consultez le script complet.

Le travail avec différents scopes de collection d'objets

Parfois, le script doit collecter tous les projets d'une instance auto-gérée, d'un groupe avec des sous-groupes, ou d'un projet unique. Ce dernier cas est utile pour accélérer les tests sur les attributs requis, et la récupération du groupe facilite les tests à grande échelle par la suite. L'extrait de code suivant collecte tous les objets de projet dans la liste projects et y ajoute les objets provenant des différentes configurations entrantes. Vous retrouverez également à nouveau le modèle d'objet gérable pour le projet dans les groupes.

    # Collect all projects, or prefer projects from a group id, or a project id
    projects = []

    # Direct project ID
    if PROJECT_ID:
        projects.append(gl.projects.get(PROJECT_ID))

    # Groups and projects inside
    elif GROUP_ID:
        group = gl.groups.get(GROUP_ID)

        for project in group.projects.list(include_subgroups=True, all=True):
            # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples
            manageable_project = gl.projects.get(project.id)
            projects.append(manageable_project)

    # All projects on the instance (may take a while to process)
    else:
        projects = gl.projects.list(get_all=True)

L'exemple complet se trouve dans ce script pour lister les paramètres des règles d'approbation des merge requests pour les cibles de projet spécifiées.

Utilisation de l’approche DevSecOps pour les actions de lecture API

Le jeton d'accès authentifié nécessite un scope read_api.

Les cas d’utilisation suivants seront abordés :

  • Lister les branches par état de fusion,
  • Afficher les paramètres du projet pour révision : règles d'approbation des merge requests,
  • Inventaire : obtenir toutes les variables CI/CD protégées ou masquées,
  • Télécharger un fichier depuis le dépôt,
  • Aide à la migration : lister tous les clusters Kubernetes basés sur des certificats,
  • Productivité des équipes : vérifier si les merge requests existantes nécessitent un rebase après avoir fusionné une merge request de refactorisation majeure.

Lister les branches par état de fusion

Pour nettoyer un projet Git, il est courant d'évaluer le nombre de branches fusionnées et non fusionnées. En réponse à une question sur le forum de la communauté GitLab concernant le filtrage des listes de branches, j'ai écrit un script aidant à cela. La méthode branches.list() renvoie tous les objets de branche stockés dans une liste temporaire, pour un traitement ultérieur en deux boucles : la collecte des noms de branches fusionnées, et celle des noms de branches non fusionnées. L'attribut merged sur l'objet branch est une valeur booléenne qui indique si la branche a été fusionnée ou non.

project = gl.projects.get(PROJECT_ID, lazy=False, pagination="keyset", order_by="updated_at", per_page=100)

# Get all branches
real_branches = []
for branch in project.branches.list():
    real_branches.append(branch)

print("All branches")
for rb in real_branches:
    print("Branch: {b}".format(b=rb.name))

# Get all merged branches
merged_branches_names = []
for branch in real_branches:
    if branch.default:
        continue # ignore the default branch for merge status

    if branch.merged:
        merged_branches_names.append(branch.name)

print("Branches merged: {b}".format(b=", ".join(merged_branches_names)))

# Get un-merged branches
not_merged_branches_names = []
for branch in real_branches:
    if branch.default:
        continue # ignore the default branch for merge status

    if not branch.merged:
        not_merged_branches_names.append(branch.name)

print("Branches not merged: {b}".format(b=", ".join(not_merged_branches_names)))

Le workflow est destiné à être lu étape par étape. Vous pouvez vous entraîner à optimiser le code Python pour la collecte conditionnelle des noms de branches.

Afficher les paramètres du projet pour examen : règles d'approbation des merge requests

Le script suivant parcourt tous les objets de projet collectés et vérifie si des règles d'approbation sont spécifiées. Si la longueur de la liste est supérieure à zéro, il parcourt la liste en boucle et affiche les paramètres avec la méthode JSON pretty print.

    # Loop over projects and print the settings
    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/merge_request_approvals.html
    for project in projects:
        if len(project.approvalrules.list()) > 0:
            #print(project) #debug
            print("# Project: {name}, ID: {id}\n\n".format(name=project.name_with_namespace, id=project.id))
            print("[MR Approval settings]({url}/-/settings/merge_requests)\n\n".format(url=project.web_url))

            for ar in project.approvalrules.list():
                print("## Approval rule: {name}, ID: {id}".format(name=ar.name, id=ar.id))
                print("\n```json\n")
                print(json.dumps(ar.attributes, indent=2)) # TODO: can be more beautiful, but serves its purpose with pretty print JSON
                print("\n```\n")

Inventaire : obtenir toutes les variables CI/CD protégées ou masquées

Les variables CI/CD sont utiles au paramétrage des pipelines et peuvent être configurées globalement sur l'instance, dans les groupes et dans les projets. Nous pouvons aussi y stocker des informations confidentielles, des mots de passe ou encore des secrets. Il peut parfois être nécessaire d’avoir une vue d'ensemble de toutes les variables CI/CD protégées ou masquées pour estimer le nombre de variables à actualiser, lors de la rotation des jetons par exemple.

Le script suivant récupère tous les groupes et projets, puis tente de collecter les variables CI/CD de l'instance globale (cela nécessite des autorisations d'administrateur), des groupes et des projets (cela nécessite des autorisations de chargé de maintenance/propriétaire). Il affiche toutes les variables CI/CD qui sont soit protégées, soit masquées, en précisant qu'une valeur potentiellement secrète y est stockée.

#!/usr/bin/env python

import gitlab
import os
import sys

# Helper function to evaluate secrets and print the variables
def eval_print_var(var):
    if var.protected or var.masked:
        print("🛡️🛡️🛡️ Potential secret: Variable '{name}', protected {p}, masked: {m}".format(name=var.key,p=var.protected,m=var.masked))

GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')
GITLAB_TOKEN = os.environ.get('GL_TOKEN') # token requires maintainer+ permissions. Instance variables require admin access.
PROJECT_ID = os.environ.get('GL_PROJECT_ID') #optional
GROUP_ID = os.environ.get('GL_GROUP_ID', 8034603) # https://gitlab.com/everyonecancontribute

if not GITLAB_TOKEN:
    print("🤔 Please set the GL_TOKEN env variable.")
    sys.exit(1)

gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)

# Collect all projects, or prefer projects from a group id, or a project id
projects = []
# Collect all groups, or prefer group from a group id
groups = []

# Direct project ID
if PROJECT_ID:
    projects.append(gl.projects.get(PROJECT_ID))

# Groups and projects inside
elif GROUP_ID:
    group = gl.groups.get(GROUP_ID)

    for project in group.projects.list(include_subgroups=True, all=True):
        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples
        manageable_project = gl.projects.get(project.id)
        projects.append(manageable_project)

    groups.append(group)

# All projects/groups on the instance (may take a while to process, use iterators to fetch on-demand).
else:
    projects = gl.projects.list(iterator=True)
    groups = gl.groups.list(iterator=True)

print("# List of all CI/CD variables marked as secret (instance, groups, projects)")

# https://python-gitlab.readthedocs.io/en/stable/gl_objects/variables.html

# Instance variables (if the token has permissions)
print("Instance variables, if accessible")
try:
    for i_var in gl.variables.list(iterator=True):
        eval_print_var(i_var)
except:
    print("No permission to fetch global instance variables, continueing without.")
    print("\n")

# group variables (maintainer permissions for groups required)
for group in groups:
    print("Group {n}, URL: {u}".format(n=group.full_path, u=group.web_url))
    for g_var in group.variables.list(iterator=True):
        eval_print_var(g_var)

    print("\n")

# Loop over projects and print the settings
for project in projects:
    # skip archived projects, they throw 403 errors
    if project.archived:
        continue

    print("Project {n}, URL: {u}".format(n=project.path_with_namespace, u=project.web_url))
    for p_var in project.variables.list(iterator=True):
        eval_print_var(p_var)

    print("\n")

Le script n’affiche pas les valeurs des variables, cela étant réservé comme exercice pour les environnements sécurisés. Pour stocker des secrets, faites plutôt appel à des fournisseurs externes.

Télécharger un fichier depuis le dépôt

L'objectif de ce script est de télécharger un fichier à partir d’un chemin spécifié dans une branche donnée, et de stocker son contenu dans un nouveau fichier.

# Goal: Try to download README.md from https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-api-python/-/blob/main/README.md
FILE_NAME = 'README.md'
BRANCH_NAME = 'main'

# Search the file in the repository tree and get the raw blob
for f in project.repository_tree():
    print("File path '{name}' with id '{id}'".format(name=f['name'], id=f['id']))

    if f['name'] == FILE_NAME:
        f_content = project.repository_raw_blob(f['id'])
        print(f_content)

# Alternative approach: Get the raw file from the main branch
raw_content = project.files.raw(file_path=FILE_NAME, ref=BRANCH_NAME)
print(raw_content)

# Store the file on disk
with open('raw_README.md', 'wb') as f:
    project.files.raw(file_path=FILE_NAME, ref=BRANCH_NAME, streamed=True, action=f.write)

Aide à la migration : lister tous les clusters Kubernetes basés sur des certificats

L'intégration des clusters Kubernetes basée sur des certificats dans GitLab a été dépréciée. Pour faciliter les plans de migration, l'inventaire des groupes et projets existants peut être automatisé à l'aide de l'API GitLab.

groups = [ ]

# get GROUP_ID group
groups.append(gl.groups.get(GROUP_ID))

for group in groups:
    for sg in group.subgroups.list(include_subgroups=True, all=True):
        real_group = gl.groups.get(sg.id)
        groups.append(real_group)

group_clusters = {}
project_clusters = {}

for group in groups:
    #Collect group clusters
    g_clusters = group.clusters.list()

    if len(g_clusters) > 0:
        group_clusters[group.id] = g_clusters

    # Collect all projects in group and subgroups and their clusters
    projects = group.projects.list(include_subgroups=True, all=True)

    for project in projects:
        # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples
        manageable_project = gl.projects.get(project.id)

        # skip archived projects
        if project.archived:
            continue

        p_clusters = manageable_project.clusters.list()

        if len(p_clusters) > 0:
            project_clusters[project.id] = p_clusters

# Print summary
print("## Group clusters\n\n")
for g_id, g_clusters in group_clusters.items():
    url = gl.groups.get(g_id).web_url
    print("Group ID {g_id}: {u}\n\n".format(g_id=g_id, u=url))
    print_clusters(g_clusters)

print("## Project clusters\n\n")
for p_id, p_clusters in project_clusters.items():
    url = gl.projects.get(p_id).web_url
    print("Project ID {p_id}: {u}\n\n".format(p_id=p_id, u=url))
    print_clusters(p_clusters)

Consultez le script complet.

Productivité des équipes : vérifier si les merge requests existantes nécessitent un rebase après avoir fusionné une merge request de refactorisation majeure

Le dépôt du manuel GitLab est un large monorepo qui contient de nombreuses merge requests créées, examinées, approuvées et fusionnées. Certaines revues prennent plus de temps que d'autres, et certaines merge requests impactent plusieurs pages, lorsqu'il s'agit par exemple de renommer une chaîne de caractères ou toutes les pages du manuel. Le manuel Marketing avait besoin d’une restructuration (pensez à une refactorisation du code), et de nombreux répertoires et chemins d'accès ont été déplacés ou renommés.

Les tâches liées aux tickets ont augmenté au fil du temps, et nous craignons que des conflits sur d'autres merge requests apparaissent après avoir fusionné des changements importants. Avec python-gitlab vous pouvez récupérer toutes les merge requests dans un projet donné, y compris les détails sur la branche Git, sur les chemins sources modifiés, et bien plus encore.

Le script résultant configure une liste des sources touchées par toutes les merge requests, vérifie si la merge request diffère avec mr.diffs.list(), et si un modèle correspond à la valeur dans old_path. Si une correspondance est trouvée, le script l'enregistre et sauvegarde la merge request dans le dictionnaire seen_mr, pour un résumé ultérieur. Des attributs supplémentaires sont collectés pour afficher une liste de tâches en Markdown contenant des URL, afin de faciliter le copier-coller dans les descriptions des tickets. Consultez le script complet.

PATH_PATTERNS = [
    'path/to/handbook/source/page.md',
]

# Only list opened MRs
# https://python-gitlab.readthedocs.io/en/stable/gl_objects/merge_requests.html#project-merge-requests
mrs = project.mergerequests.list(state='opened', iterator=True)

seen_mr = {}

for mr in mrs:
    # https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-request-diffs
    real_mr = project.mergerequests.get(mr.get_id())
    real_mr_id = real_mr.attributes['iid']
    real_mr_url = real_mr.attributes['web_url']

    for diff in real_mr.diffs.list(iterator=True):
        real_diff = real_mr.diffs.get(diff.id)

        for d in real_diff.attributes['diffs']:
            for p in PATH_PATTERNS:
                if p in d['old_path']:
                    print("MATCH: {p} in MR {mr_id}, status '{s}', title '{t}' - URL: {mr_url}".format(
                        p=p,
                        mr_id=real_mr_id,
                        s=mr_status,
                        t=real_mr.attributes['title'],
                        mr_url=real_mr_url))

                    if not real_mr_id in seen_mr:
                        seen_mr[real_mr_id] = real_mr

print("\n# MRs to update\n")

for id, real_mr in seen_mr.items():
    print("- [ ] !{mr_id} - {mr_url}+ Status: {s}, Title: {t}".format(
        mr_id=id,
        mr_url=real_mr.attributes['web_url'],
        s=real_mr.attributes['detailed_merge_status'],
        t=real_mr.attributes['title']))

Cas d’utilisation DevSecOps pour les actions d'écriture de l’API

Le jeton d'accès authentifié nécessite une portée d’autorisation complète de l’API.

Les cas d’utilisation suivants sont abordés :

  • Déplacer des epics d’un groupe à l’autre,
  • Conformité : vérifier que les paramètres du projet ne sont pas remplacés,
  • Prendre des notes, générer un aperçu de la date d'échéance,

Déplacer des epics d’un groupe à l’autre

Vous devez parfois déplacer des epics dans un autre groupe. Une question posée dans le Slack de GitLab nous a incité à examiner une proposition de fonctionnalité pour l'interface utilisateur, pour plus tard écrire un script API permettant d'automatiser ces étapes. L'idée consiste à déplacer une epic d'un groupe source vers un groupe cible, et de copier son titre, sa description et ses labels. Puisque les epics permettent de regrouper les tickets, elles doivent également être réaffectées à l'epic cible. Il faut aussi prendre en compte les relations parent-enfant des epics, toutes les epics enfants des epics sources devant être réaffectées à l'epic cible.

Le script suivant recherche d'abord tous les attributs de l'epic source, puis crée une nouvelle epic cible avec des attributs minimaux : titre et description. La liste des labels est copiée et les modifications sont conservées grâce à l'appel save(). Les tickets attribués à l'epic doivent être recréés dans l'epic cible. L'appel create() crée l'élément de relation, et non un nouvel objet de ticket en tant que tel. Le déplacement des epics enfants nécessite une approche différente, car la relation est inversée : le parent_id de l'epic enfant doit être comparé à l'identifiant de l'epic source et, s'il correspond, mis à jour vers l'identifiant de l'epic cible. Après avoir tout copié avec succès, l'epic source doit être passée à l'état closed.

#!/usr/bin/env python

# Description: Show how epics can be moved between groups, including title, description, labels, child epics and issues.
# Requirements: python-gitlab Python libraries. GitLab API write access, and maintainer access to all configured groups/projects.
# Author: Michael Friedrich <[email protected]>
# License: MIT, (c) 2023-present GitLab B.V.

import gitlab
import os
import sys

GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')
# https://gitlab.com/gitlab-da/use-cases/gitlab-api
SOURCE_GROUP_ID = os.environ.get('GL_SOURCE_GROUP_ID', 62378643)
# https://gitlab.com/gitlab-da/use-cases/gitlab-api/epic-move-target
TARGET_GROUP_ID = os.environ.get('GL_TARGET_GROUP_ID', 62742177)
# https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1
EPIC_ID = os.environ.get('GL_EPIC_ID', 1)
GITLAB_TOKEN = os.environ.get('GL_TOKEN')

if not GITLAB_TOKEN:
    print("Please set the GL_TOKEN env variable.")
    sys.exit(1)

gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)

# Main
# Goal: Move epic to target group, including title, body, labels, and child epics and issues.
source_group = gl.groups.get(SOURCE_GROUP_ID)
target_group = gl.groups.get(TARGET_GROUP_ID)

# Create a new target epic and copy all its items, then close the source epic.
source_epic = source_group.epics.get(EPIC_ID)
# print(source_epic) #debug

epic_title = source_epic.title
epic_description = source_epic.description
epic_labels = source_epic.labels
epic_issues = source_epic.issues.list()

# Create the epic with minimal attributes
target_epic = target_group.epics.create({
    'title': epic_title,
    'description': epic_description,
})

# Assign the list
target_epic.labels = epic_labels

# Persist the changes in the new epic
target_epic.save()

# Epic issues need to be re-assigned in a loop
for epic_issue in epic_issues:
    ei = target_epic.issues.create({'issue_id': epic_issue.id})

# Child epics need to update their parent_id to the new epic
# Need to search in all epics, use lazy object loading
for sge in source_group.epics.list(lazy=True):
    # this epic has the source epic as parent epic?
    if sge.parent_id == source_epic.id:
        # Update the parent id
        sge.parent_id = target_epic.id
        sge.save()

print("Copied source epic {source_id} ({source_url}) to target epic {target_id} ({target_url})".format(
    source_id=source_epic.id, source_url=source_epic.web_url,
    target_id=target_epic.id, target_url=target_epic.web_url))

# Close the old epic
source_epic.state_event = 'close'
source_epic.save()
print("Closed source epic {source_id} ({source_url})".format(
    source_id=source_epic.id, source_url=source_epic.web_url))
$  python3 move_epic_between_groups.py
Copied source epic 725341 (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1) to target epic 725358 (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/epic-move-target/-/epics/6)
Closed source epic 725341 (https://gitlab.com/groups/gitlab-da/use-cases/gitlab-api/-/epics/1)

L'epic cible a été créée et affiche le résultat attendu : même titre, description, labels, epic enfant et tickets.

Tutoriel sur l'API GitLab : déplacer des epics

Exercice : le script ne copie pas encore les commentaires et les fils de discussion. Faites des recherches et aidez-nous à mettre à jour le script. Les merge requests sont les bienvenues !

Conformité : vérifier que les paramètres du projet ne sont pas remplacés

Les paramètres des projets et des groupes peuvent être modifiés accidentellement par des membres de l'équipe. Les exigences de conformité doivent être respectées. Autre cas d’utilisation : gérer la configuration avec des outils d’Infrastructure as Code et s'assurer que la configuration de GitLab reste la même au niveau du groupe, du projet et autres. Des outils comme Ansible ou Terraform peuvent invoquer un script API ou utiliser la bibliothèque python-gitlab pour effectuer des tâches de gestion des paramètres.

Dans l'exemple suivant, seule la branche main est protégée.

API python-gitlab : protection des branches

Supposons qu'une nouvelle branche production a été ajoutée et qu’elle doit également être protégée. Le script suivant définit le dictionnaire des branches protégées et leurs niveaux d'accès pour les autorisations de push et de fusion au niveau du chargé de maintenance. Il établit la logique de comparaison de la documentation python-gitlab sur les branches protégées.

#!/usr/bin/env python

import gitlab
import os
import sys

GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')
# https://gitlab.com/gitlab-da/use-cases/
GROUP_ID = os.environ.get('GL_GROUP_ID', 16058698)
GITLAB_TOKEN = os.environ.get('GL_TOKEN')

PROTECTED_BRANCHES = {
    'main': {
        'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,
        'push_access_level': gitlab.const.AccessLevel.MAINTAINER
    },
    'production': {
        'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,
        'push_access_level': gitlab.const.AccessLevel.MAINTAINER
    },
}

if not GITLAB_TOKEN:
    print("Please set the GL_TOKEN env variable.")
    sys.exit(1)

gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN)

# Main
group = gl.groups.get(GROUP_ID)

# Collect all projects in group and subgroups
projects = group.projects.list(include_subgroups=True, all=True)

for project in projects:
    # Retrieve a full manageable project object
    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples
    manageable_project = gl.projects.get(project.id)

    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/protected_branches.html
    protected_branch_names = []

    for pb in manageable_project.protectedbranches.list():
        manageable_protected_branch = manageable_project.protectedbranches.get(pb.name)
        print("Protected branch name: {n}, merge_access_level: {mal}, push_access_level: {pal}".format(
            n=manageable_protected_branch.name,
            mal=manageable_protected_branch.merge_access_levels,
            pal=manageable_protected_branch.push_access_levels
        ))

        protected_branch_names.append(manageable_protected_branch.name)

    for branch_to_protect, levels in PROTECTED_BRANCHES.items():
        # Fix missing protected branches
        if branch_to_protect not in protected_branch_names:
            print("Adding branch {n} to protected branches settings".format(n=branch_to_protect))
            p_branch = manageable_project.protectedbranches.create({
                'name': branch_to_protect,
                'merge_access_level': gitlab.const.AccessLevel.MAINTAINER,
                'push_access_level': gitlab.const.AccessLevel.MAINTAINER
            })

L'exécution du script affiche la branche main existante, ainsi qu’une note indiquant que la branche production sera mise à jour. La capture d'écran des paramètres du dépôt démontre cette action.

$ python3 enforce_protected_branches.py                                                ─╯
Protected branch name: main, merge_access_level: [{'id': 67294702, 'access_level': 40, 'access_level_description': 'Maintainers', 'user_id': None, 'group_id': None}], push_access_level: [{'id': 68546039, 'access_level': 40, 'access_level_description': 'Maintainers', 'user_id': None, 'group_id': None}]
Adding branch production to protected branches settings

Capture d'écran de code en Python avec GitLab

Prise de notes : générer un aperçu de la date d'échéance

Une discussion de Hacker News sur les outils de prise de notes nous a inspiré la création d'un tableau Markdown, extrait de fichiers de prise de notes, et trié par date d'échéance. Le script est plus complexe à comprendre.

Votre avis nous intéresse

Cet article de blog vous a plu ou vous avez des questions ou des commentaires ? Partagez vos réflexions en créant un nouveau sujet dans le forum de la communauté GitLab. Partager votre expérience

Lancez-vous dès maintenant

Découvrez comment la plateforme DevSecOps unifiée de GitLab peut aider votre équipe.

Commencer un essai gratuit

Découvrez le forfait qui convient le mieux à votre équipe

En savoir plus sur la tarification

Découvrez ce que GitLab peut offrir à votre équipe

Échanger avec un expert