Ein oft zitiertes Sprichwort aus einer Konferenzpräsentation lautet: „Manuelle Arbeit ist ein Fehler“. Bei sich wiederholenden Aufgaben in Arbeitsabläufen ist es ratsam, so viel wie möglich zu automatisieren. Ein Beispiel dafür ist die Nutzung einer REST-API zur Bestandsaufnahme von Einstellungen oder das Erstellen neuer Kommentare in GitLab-Issues und Merge-Anfragen mittels API-Aktionen.
GitLab-API-Interaktion: Möglichkeiten und Unterstützung durch API-Abstraktionsbibliotheken
Die Interaktion mit der REST-API von GitLab kann auf verschiedene Weise erfolgen: entweder durch HTTP-Anfragen mit curl (oder hurl) in der Befehlszeile oder durch das Schreiben von Skripten in einer Programmiersprache. Letzteres kann dazu führen, dass der Code für HTTP-Anfragen und das Parsen der JSON-Antworten neu entwickelt werden muss. Glücklicherweise unterstützt die umfangreiche GitLab-Community viele Programmiersprachen mit API-Abstraktionsbibliotheken.
Diese Bibliotheken bieten Unterstützung für alle API-Attribute, fügen Hilfsfunktionen zum Abrufen, Erstellen und Löschen von Objekten hinzu und helfen Entwicklern dabei, sich auf ihre Kernaufgaben zu konzentrieren. Die python-gitlab Bibliothek ist ein besonders funktionsreiches und einfach zu verwendendes Beispiel für eine solche Bibliothek in Python.
In diesem Blogbeitrag wird die grundlegende Nutzung der python-gitlab-Bibliothek erläutert, einschließlich der Arbeit mit API-Objekten, Attributen, Paginierung und Resultsets. Zudem werden spezifischere Anwendungsfälle vorgestellt, in denen Daten gesammelt, Zusammenfassungen gedruckt und Daten in die API geschrieben werden, um Kommentare und Commits zu erstellen. Viele dieser Anwendungsfälle basieren auf Fragen aus der Community in Foren, auf Hacker News oder in Issues.
Inhaltsverzeichnis
- GitLab-API-Interaktion: Möglichkeiten und Unterstützung durch API-Abstraktionsbibliotheken
- Los geht's: python-gitlab Install
- GitLab API Python: Konfiguration
- Objekte verwalten: das GitLab-Objekt
- DevSecOps-Anwendungsfälle für API-Leseaktionen
- Zweige nach Zusammenführungsstatus auflisten
- Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln
- Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind
- Herunterladen einer Datei aus dem Repository
- Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster
- Team-Effizienz: Prüfe, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen
- DevSecOps-Anwendungsfälle für API-Schreibaktionen
Los geht's: python-gitlab Install
Die Dokumentation von python-gitlab bietet eine umfassende Einführung in die Nutzung, einschließlich Anleitungen für den Einstieg, Informationen zu Objekttypen und deren Methoden sowie kombinierte Workflow-Beispiele. Ergänzend dazu ist die Dokumentation der GitLab-API-Ressourcen hilfreich, da sie die verfügbaren Objektattribute detailliert beschreibt. Zusammen sind diese beiden Dokumentationen die besten Ressourcen für den Einstieg.
Die Code-Beispiele in diesem Blogbeitrag setzen Python 3.8+ und die python-gitlab-Bibliothek voraus. Weitere notwendige Abhängigkeiten sind in der Datei requirements.txt aufgeführt. Ein Beispiel erfordert die Bibliothek pyyaml zum Parsen von YAML-Konfigurationen. Um den Code der Anwendungsfälle nachzuvollziehen und zu üben, empfiehlt es sich, das Projekt zu klonen, die Anforderungen zu installieren und die Skripte auszuführen.
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
Die Skripte verwenden absichtlich keine gemeinsam genutzte Bibliothek, die z. B. generische Funktionen für das Lesen von Parametern oder zusätzliche Hilfsfunktionen bereitstellt. Die Idee ist, einfach zu verstehende Beispiele zu zeigen, die eigenständig zum Testen verwendet werden können und lediglich die Installation der python-gitlab-Bibliothek erfordern.
Es wird empfohlen, den Code für den Produktionseinsatz zu verbessern. Dies kann auch beim Aufbau eines gewarteten API-Tooling-Projekts helfen, das z. B. Container-Images und CI/CD-Vorlagen für Entwickler enthält, die auf einer DevSecOps-Plattform genutzt werden können.
GitLab API Python: Konfiguration
Ohne Konfiguration führt python-gitlab unauthentifizierte Anfragen an den Standardserver https://gitlab.com aus. Die häufigsten Konfigurationseinstellungen beziehen sich auf die GitLab-Instanz, mit der eine Verbindung hergestellt werden soll, und die Authentifizierungsmethode durch Angabe von Zugriffstokens. python-gitlab unterstützt verschiedene Arten der Konfiguration: Eine Konfigurationsdatei oder Umgebungsvariablen.
Die Konfigurationsdatei ist für die API-Bibliotheksbindungen und die CLI verfügbar (die CLI wird in diesem Blogpost nicht erläutert). Die Konfigurationsdatei unterstützt Credential Helpers für den direkten Zugriff auf Token.
Umgebungsvariablen als alternative Konfigurationsmethode bieten eine einfache Möglichkeit, das Skript auf dem Terminal auszuführen, in Container-Images zu integrieren und sie für die Ausführung in CI/CD-Pipelines vorzubereiten.
Die Konfiguration muss in den Kontext des Python-Skripts geladen werden. Beginne mit dem Import der os-Bibliothek, um die Umgebungsvariablen mit der Methode os.environ.get() abzurufen. Der erste Parameter gibt den Schlüssel an, der zweite Parameter legt den Standardwert fest, wenn die Variable in der Umgebung nicht verfügbar ist.
import os
gl_server = os.environ.get('GL_SERVER', 'https://gitlab.com')
print(gl_server)
Die Parametrisierung auf dem Terminal kann direkt nur für den Befehl erfolgen oder in die Shell-Umgebung exportiert werden.
$ GL_SERVER=’https://gitlab.company.com’ python3 script.py
$ export GL_SERVER=’https://gitlab.company.com’
$ python3 script.py
Es wird empfohlen, Sicherheitsprüfungen hinzuzufügen, um sicherzustellen, dass alle Variablen gesetzt sind, bevor das Programm weiter ausgeführt wird. Der folgende Ausschnitt importiert die erforderlichen Bibliotheken, liest die Umgebungsvariable GL_SERVER und erwartet, dass der Benutzer die Variable GL_TOKEN setzt. Wenn dies nicht der Fall ist, gibt das Skript Fehler aus und ruft sys.exit(1) auf, um einen Fehlerstatus anzuzeigen.
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)
Folgend siehst du ein ausführlicheres Beispiel, das eine Verbindung zur API herstellt und eine tatsächliche Datenabfrage durchführt.
Objekte verwalten: das GitLab-Objekt
Für jede Interaktion mit der API muss das GitLab-Objekt instanziiert werden. Dies ist der Einstiegspunkt für die Konfiguration des GitLab-Servers für die Verbindung, die Authentifizierung mithilfe von Zugriffstokens und weitere globale Einstellungen für die Paginierung, das Laden von Objekten und mehr.
Im folgenden Beispiel wird eine nicht authentifizierte Anfrage an gitlab.com ausgeführt. Es ist möglich, auf öffentliche API-Endpunkte zuzugreifen und zum Beispiel eine bestimmte .gitignore Vorlage für Python abzurufen.
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)
Die nächsten Abschnitte geben weitere Einblicke in:
- Objektmanager und Laden von Objekten
- Paginierung von Ergebnissen
- Arbeiten mit Objektbeziehungen
- Arbeiten mit verschiedenen Objektsammlungsbereichen
Objektmanager und Laden von Objekten
Die python-gitlab-Bibliothek ermöglicht den Zugriff auf GitLab-Ressourcen über sogenannte „Manager". Jeder Managertyp implementiert Methoden zur Arbeit mit den Datensätzen (list, get, etc.).
Das Skript zeigt, wie man auf Untergruppen, direkte Projekte, alle Projekte einschließlich Untergruppen, Issues, Epics und To-dos zugreifen kann. Diese Methoden und der API-Endpunkt erfordern eine Authentifizierung für den Zugriff auf alle Attribute. Der Codeschnipsel verwendet daher Variablen, um das Authentifizierungs-Token abzurufen sowie die GROUP_ID-Variable, um eine Hauptgruppe anzugeben, bei der die Suche beginnen soll.
#!/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
Du kannst das Skript python_gitlab_object_manager_methods.py
ausführen, indem du die GROUP_ID-Variable auf GitLab.com SaaS für deine eigene zu analysierende Gruppe überschreibst. Die Variable GL_SERVER muss für selbstverwaltete Instanzziele angegeben werden. GL_TOKEN muss das persönliche Zugriffstoken enthalten.
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
In Zukunft werden die Beispiel-Snippets die Python-Header und das Parsen von Umgebungsvariablen nicht mehr zeigen, um sich auf den Algorithmus und die Funktionalität zu konzentrieren. Alle Skripte sind Open Source unter der MIT-Lizenz und in diesem Projekt verfügbar.
Paginierung der Ergebnisse
Standardmäßig gibt die GitLab-API nicht alle Ergebnissätze zurück und erfordert, dass die Clients die Paginierung verwenden, um durch alle Ergebnisseiten zu iterieren. Mit der python-gitlab-Bibliothek können Benutzer die Einstellungen global im GitLab-Objekt oder bei jedem list()-Aufruf festlegen. Standardmäßig würden alle Ergebnissätze API-Anfragen auslösen, was die Skriptausführung verlangsamen kann. Die empfohlene Methode ist die Verwendung von iterator=True, die ein Generatorobjekt zurückgibt, und API-Aufrufe werden beim Zugriff auf das Objekt bei Bedarf ausgelöst.
Das folgende Beispiel sucht nach dem Gruppennamen everyonecancontribute und verwendet eine Paginierung der Schlüsselsätze mit 100 Ergebnissen auf jeder Seite. Der Iterator wird bei gl.groups.list(iterator=True) auf true gesetzt, um bei Bedarf neue Ergebnissätze abzurufen. Wird der gesuchte Gruppenname gefunden, bricht die Schleife ab und gibt eine Zusammenfassung aus, einschließlich der Messung der Dauer der gesamten Suchanfrage.
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))
Beim Ausführen von python_gitlab_pagination.py wurde die Gruppe everyonecancontribute auf Seite 5 gefunden.
$ python3 python_gitlab_pagination.py
Pagination API example for Python with GitLab, the DevSecOps platform - found group everyonecancontribute on page 5, duration 8.51s
Arbeiten mit Objektbeziehungen
Bei der Arbeit mit Objektbeziehungen – z. B. beim Sammeln aller Projekte in einer bestimmten Gruppe – müssen zusätzliche Schritte unternommen werden. Die zurückgegebenen Projektobjekte enthalten standardmäßig nur begrenzte Attribute. Für verwaltbare Objekte ist ein zusätzlicher get()-Aufruf erforderlich, der das vollständige Projektobjekt von der API im Hintergrund anfordert. Dieser On-Demand-Workflow hilft, Wartezeiten und Datenverkehr zu vermeiden, indem er die sofort zurückgegebenen Attribute reduziert.
Das folgende Beispiel veranschaulicht das Problem, indem es eine Schleife durch alle Projekte in einer Gruppe durchläuft und versucht, die Funktion project.branches.list() aufzurufen, was eine Ausnahme im try/except-Flow auslöst. Im zweiten Beispiel wird ein verwaltbares Projektobjekt ermittelt und der Funktionsaufruf erneut versucht.
# 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()])))
Der Exception-Handler in der python-gitlab-Bibliothek gibt die Fehlermeldung aus und verlinkt auch auf die Dokumentation. Es ist hilfreich, bei der Fehlersuche zu beachten, dass Objekte möglicherweise nicht verwaltet werden können, wenn du nicht auf Objektattribute oder Funktionsaufrufe zugreifen kannst.
$ 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
=====================================
Hier findest du das vollständige Skript.
Arbeiten mit verschiedenen Objektsammlungsbereichen
Manchmal muss das Skript alle Projekte aus einer selbstverwalteten Instanz, aus einer Gruppe mit Untergruppen oder aus einem einzelnen Projekt sammeln. Letzteres ist hilfreich, um die erforderlichen Attribute schneller testen zu können, und der Gruppenabruf hilft später beim Testen im großen Maßstab. Das folgende Snippet sammelt alle Projektobjekte in der projects-Liste und fügt Objekte aus verschiedenen eingehenden Konfigurationen hinzu. Du wirst auch wieder das verwaltbare Objektmuster für Projekte in Gruppen sehen.
# 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)
Das vollständige Beispiel befindet sich in diesem Skript für die Auflistung der Einstellungen der MR-Genehmigungsregeln für bestimmte Projektziele.
DevSecOps-Anwendungsfälle für API-Leseaktionen
Das authentifizierte Zugriffstoken benötigt den Bereich read_api
scope.
Die folgenden Anwendungsfälle werden diskutiert:
- Zweige nach Zusammenführungsstatus auflisten
- Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln
- Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind
- Herunterladen einer Datei aus dem Repository
- Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster
- Team-Effizienz: Prüfen Sie, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen
Zweige nach Zusammenführungsstatus auflisten
Ein häufiges Anliegen ist es, in einem Projekt ein wenig Git-Housekeeping zu betreiben und zu sehen, wie viele zusammengeführte und nicht zusammengeführte Zweige im Umlauf sind. Eine Frage im GitLab-Community-Forum zum Filtern von Zweiglisten hat mich dazu inspiriert, ein Skript zu schreiben, mit dem sich dieses Ziel erreichen lässt. Die Methode branches.list() gibt alle Zweigobjekte zurück, die in einer temporären Liste für die spätere Verarbeitung in zwei Schleifen gespeichert werden: Sammeln der Namen der zusammengeführten Zweige und der Namen der nicht zusammengeführten Zweige. Das Attribut merged des branch-Objekts ist ein boolescher Wert, der angibt, ob der Zweig zusammengeführt wurde.
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)))
Der Arbeitsablauf ist absichtlich schrittweise zu lesen. Du kannst die Optimierung des Python-Codes für die bedingte Zweignamensammlung üben.
Drucken von Projekteinstellungen zur Überprüfung: MR-Genehmigungsregeln
Das folgende Skript geht durch alle gesammelten Projektobjekte und prüft, ob Genehmigungsregeln angegeben sind. Wenn die Länge der Liste größer als Null ist, durchläuft es die Liste in einem Loop und druckt die Einstellungen mit einer JSON-Ph pretty-print-Methode aus.
# 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")
Inventarisierung: Abrufen aller CI/CD-Variablen, die geschützt oder maskiert sind
CI/CD Variablen sind hilfreich für die Pipeline-Parametrisierung und können global auf der Instanz, in Gruppen und in Projekten konfiguriert werden. Auch Daten, Passwörter und andere sensible Informationen können dort gespeichert werden. Manchmal kann es notwendig sein, sich einen Überblick über alle CI/CD-Variablen zu verschaffen, die entweder geschützt oder maskiert sind, um ein Gefühl dafür zu bekommen, wie viele Variablen aktualisiert werden müssen, wenn Token zum Beispiel rotieren.
Das folgende Skript ruft alle Gruppen und Projekte ab und versucht, die CI/CD-Variablen der globalen Instanz (erfordert Admin-Rechte), der Gruppen und Projekte (erfordert Maintainer-/Eigentümer-Rechte) zu sammeln. Es gibt alle CI/CD-Variablen aus, die entweder geschützt oder maskiert sind, und fügt hinzu, dass ein möglicher geheimer Wert gespeichert ist.
#!/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")
Das Skript druckt die Variablenwerte absichtlich nicht aus; dies soll als Übung für sichere Umgebungen dienen. Für die Speicherung von Daten empfiehlt sich die Verwendung externer Anbieter.
Herunterladen einer Datei aus dem Repository
Ziel des Skripts ist es, einen Dateipfad von einem angegebenen Verzweigungsnamen herunterzuladen und dessen Inhalt in einer neuen Datei zu speichern.
# 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)
Hilfe zur Migration: Auflistung aller zertifikatsbasierten Kubernetes-Cluster
Die zertifikatsbasierte Integration von Kubernetes-Clustern in GitLab wurde abgeschafft. Um Migrationspläne zu unterstützen, kann die Erfassung bestehender Gruppen und Projekte mithilfe der GitLab-API automatisiert werden.
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)
Hier findest du das vollständige Skript.
Team-Effizienz: Prüfe, ob bestehende Merge-Requests nach dem Mergen einer großen Refactoring-MR neu gebasht werden müssen
Das GitLab-Handbuch-Repository ist ein großes Monorepo mit zahlreichen Merge-Requests, die erstellt, geprüft, genehmigt und zusammengeführt werden müssen. Einige Prüfungen dauern länger als andere, insbesondere wenn Zusammenführungsanfragen mehrere Seiten betreffen, beispielsweise wenn ein String umbenannt wird oder Änderungen auf alle Handbuchseiten ausgedehnt werden. Das Marketing-Handbuch wurde umstrukturiert, was zu vielen Verschiebungen oder Umbenennungen von Verzeichnissen und Pfaden führte.
Mit der Zeit nahmen die Issues zu, und es bestand die Sorge, dass andere Merge-Anfragen nach dem Zusammenführen der großen Änderungen auf Konflikte stoßen könnten. Es wurde festgestellt, dass Python-Gitlab in der Lage ist, alle Merge-Requests in einem bestimmten Projekt abzurufen, einschließlich Details über den Git-Zweig, geänderte Quellpfade und vieles mehr.
Das daraus resultierende Skript konfiguriert eine Liste von Quellpfaden, die von allen pythongitlab-Merge-Requests berührt werden, und vergleicht die Diffs der Merge-Requests mit mr.diffs.list(), um festzustellen, ob ein Muster mit dem Wert in old_path. übereinstimmt. Bei einer Übereinstimmung protokolliert das Skript diese und speichert die Zusammenführungsanforderung im seen_mr-Wörterbuch für die spätere Zusammenfassung. Zusätzlich werden Attribute gesammelt, um eine Markdown-Aufgabenliste mit URLs zum leichteren Einfügen in Issue-Beschreibungen zu erstellen.
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']))
DevSecOps-Anwendungsfälle für API-Schreibaktionen
Das authentifizierte Zugriffstoken benötigt den vollen Anwendungsbereich der api
.
Die folgenden Anwendungsfälle werden diskutiert:
- Verschieben von Epics zwischen Gruppen
- Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben werden
- Notizen machen, Fälligkeitsübersicht erstellen
Verschieben von Epics zwischen Gruppen
Manchmal ist es erforderlich, Epics, ähnlich wie Issues, in eine andere Gruppe zu verschieben. Eine Frage im GitLab-Marketing-Slack-Kanal hat dazu geführt, einen Funktionsvorschlag für die Benutzeroberfläche und die Schnellaktionen zu prüfen und später über das Schreiben eines API-Skripts nachzudenken, um die Schritte zu automatisieren.
Automatisierung des Verschiebens von Epics
Die Idee ist einfach: Ein Epic wird von einer Quellgruppe in eine Zielgruppe verschoben, wobei Titel, Beschreibung und Labels kopiert werden. Da Epics es erlauben, Themen zu gruppieren, müssen sie auch dem Ziel-Epic neu zugewiesen werden. Parent-Child-Epic-Relationships müssen dabei berücksichtigt werden: Alle Child-Epics der Quell-Epics müssen dem Ziel-Epic neu zugewiesen werden.
Das folgende Skript sucht zunächst alle Attribute des Quellepos und erstellt dann ein neues Zielepos mit den minimalen Attributen: Titel und Beschreibung. Die Liste der Bezeichnungen wird kopiert und die Änderungen werden mit dem save()-Aufruf beibehalten. Die Ausgaben, die dem Epos zugeordnet sind, müssen im Zielepos neu erstellt werden.
Der create()-Aufruf erzeugt das Beziehungselement und nicht ein neues Issue-Objekt selbst. Das Verschieben von Child-Epics erfordert einen anderen Ansatz, da die Beziehung umgekehrt ist: Die parent_id des Child-Epics muss mit der ID des Quell-Epics verglichen und bei Übereinstimmung auf die ID des Ziel-Epics aktualisiert werden. Nachdem alles erfolgreich kopiert wurde, muss das Quell-Epos in den closed-Zustand versetzt werden.
#!/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)
Das Ziel-Epic wurde erstellt und zeigt das erwartete Ergebnis: Derselbe Titel, dieselbe Beschreibung, dieselben Bezeichnungen, dasselbe untergeordnete Epic und dieselben Issues.
Übung: Das Skript kopiert noch keine Kommentare und Diskussionsstränge. Recherchiere und hilf mit, das Skript zu aktualisieren – Merge-Requests willkommen!
Compliance: Sicherstellen, dass Projekteinstellungen nicht überschrieben werden
Projekt- und Gruppeneinstellungen können versehentlich von Teammitgliedern mit Administratorrechten geändert werden. Die Compliance-Anforderungen müssen erfüllt werden. Ein weiterer Anwendungsfall ist die Verwaltung der Konfiguration mit Infrastructure-as-Code-Tools, um sicherzustellen, dass die Konfiguration von GitLab-Instanzen, -Gruppen, -Projekten usw. erhalten bleibt und immer dieselbe ist. Tools wie Ansible oder Terraform können ein API-Skript aufrufen oder die Python-GitLab-Bibliothek verwenden, um Aufgaben zur Verwaltung von Einstellungen auszuführen.
Im folgenden Beispiel ist nur der main-Zweig geschützt.
Nehmen wir an, dass ein neuer production-Zweig hinzugefügt wurde und ebenfalls geschützt werden soll. Das folgende Skript definiert das Wörterbuch der geschützten Zweige und ihre Zugriffsebenen für Push-/Merge-Berechtigungen auf Maintainer-Ebene und baut die Vergleichslogik auf der Grundlage der python-gitlab-Dokumentation zu geschützten Zweigen auf.
#!/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
})
Wenn das Skript ausgeführt wird, werden der bestehende main-Zweig und ein Hinweis, dass die production aktualisiert wird, ausgegeben. Der Screenshot aus den Repository-Einstellungen verdeutlicht diese Aktion.
$ 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
Notizen machen, Fälligkeitsübersicht erstellen
Eine Diskussion auf Hacker News über Tools zum Erstellen von Notizen hat mich dazu inspiriert, eine Übersicht in Form einer Markdown-Tabelle zu erstellen, die aus Dateien, die Notizen aufnehmen, geholt und nach dem geparsten Fälligkeitsdatum sortiert wird. Das Skript ist hier zu finden und etwas komplexer zu verstehen.
Die englischsprachige Originalversion dieses Artikels wurde bereits aktualisiert und erhält einige weitere Tipps, welche wir der deutschen Version beizeiten hinzufügen werden.