Installer un package R depuis un GitLab privé : le guide de survie

Auteur : Vincent Guyader
Tags : Ressources
Date :

Avez-vous déjà rencontré des difficultés à installer un package R depuis ses sources hébergées sur un GitLab privé ?

Vous avez trop bataillé avec {remotes}, {gert}, {git2r} et obtenu des :

Error: Failed to install 'unknown package' from Git:
Error in 'git2r_remote_ls': unexpected http status code: 403

Que vous avez résolu (ou pas), sans trop comprendre comment vous avez fait ? (Et parfois en mettant en dur des tokens sur-puissants dans vos codes…)

Si oui, cet article devrait vous intéresser. Faisons le point sur les différentes options possibles.

Je ne vais me concentrer que sur les cas problématiques, c’est-à-dire ceux où votre package n’est pas ouvert en lecture au monde entier ,autrement dit, les cas qui nécessitent de batailler avec des tokens. Je pars du principe que votre GitLab privé est à jour (au moins en version 17.x).

Spoiler : il faudra bien distinguer le cas d’une installation en local sur votre ordinateur de celui d’une chaîne de CI. Et il faut avoir les idées claires sur les dépendances nécessaires à votre package, en particulier si ces dépendances sont elles aussi sur votre GitLab privé.

Token, kézako ?

Un token (ou jeton d’accès) est une chaîne de caractères qui remplace votre mot de passe pour l’authentification automatisée. Contrairement à votre mot de passe, un token peut :

  • Avoir des droits limités (lecture seule, accès à certains projets uniquement…)
  • Avoir une date d’expiration configurable
  • Être révoqué individuellement sans changer votre mot de passe

Pour générer un token GitLab, rendez-vous sur https://votre.gitlab.fr/-/user_settings/personal_access_tokens`. Sélectionnez au minimum les scopesread_apietread_repository` pour pouvoir installer des packages.

⚠️ Règle d’or : n’utilisez jamais votre mot de passe en lieu et place d’un token ! Si votre token fuite (dans un script, un dépôt public, un log de CI…), vous pouvez le révoquer sans compromettre l’accès à votre compte. Avec un mot de passe, c’est une autre histoire… pensez a utiliser des token différents en fonction de votre usage.

En local : la base avec remotes::install_gitlab

Notre premier réflexe serait d’utiliser remotes::install_gitlab. Celui-ci utilise par défaut gitlab.com en tant que host. Il est possible de surcharger le paramètre host pour pointer vers votre GitLab privé comme ceci :

remotes::install_gitlab(
    repo = "vincent/monpackage@main",
    host = "https://gitlab.thinkr.fr",
    auth_token = "glpat-XXX35"
)

Cette approche ne fonctionnera en l’état que si :

  • Le token que vous utilisez a accès à l’API et accès en lecture au dépôt
  • Votre package monpackage ne dépend pas lui-même d’un autre package privé de votre GitLab

Et oui, même si les droits associés au token permettraient de récupérer la dépendance, le auth_token n’est pas transmis aux « sous-processus » mis en œuvre par {remotes} pour installer les dépendances.

Donc ce n’est pas pleinement satisfaisant comme approche.

Gérer les dépendances privées : configurer les credentials

Pour pouvoir faire une installation propre avec des dépendances privées, il faut aussi définir votre token dans les options de {remotes} :

options(remotes.git_credentials = git2r::cred_user_pass("oauth2", "glpat-XXX35"))

Ou plus proprement, en passant par une variable d’environnement. Éditez votre .Renviron (avec usethis::edit_r_environ()), puis redémarrez R :

GITLAB_PAT=glpat-XXX35

Ensuite :

options(remotes.git_credentials = git2r::cred_user_pass("oauth2", Sys.getenv("GITLAB_PAT")))

À noter que install_gitlab() utilisera par défaut la variable d’environnement GITLAB_PAT si elle existe, ce qui permet de lancer directement :

remotes::install_gitlab(
    repo = "vincent/monpackage@main",
    host = "https://gitlab.thinkr.fr"
)

Pour des raisons de maintenance et d’explicitation, j’ai plutôt tendance à faire ceci, c’est plus clair :

remotes::install_gitlab(
    repo = "vincent/monpackage@main",
    host = "https://gitlab.thinkr.fr",
    auth_token = Sys.getenv("GITLAB_PAT")
)

Ou à utiliser l’approche « généraliste » de {remotes} :

remotes::install_git(
    url = "https://gitlab.thinkr.fr/vincent/monpackage.git",
    credentials = git2r::cred_user_pass("oauth2", Sys.getenv("GITLAB_PAT"))
)

Déclarer une dépendance privée dans votre package

Maintenant, regardons comment indiquer dans le code source de votre package que celui-ci dépend d’un autre package de votre GitLab privé.

Ça se définit au niveau du fichier DESCRIPTION de votre package : il faut expliciter dans le champ Imports le nom du package en dépendance, puis dans le champ Remotes indiquer où se trouve ce package.

Package: monpackage
...
Imports:
    unedependance
Remotes:
    git::https://gitlab.thinkr.fr/thinkr/bakacode/unedependance.git@main

Vous pourrez aussi trouver des écritures de la forme :
[email protected]::thinkr/bakacode/unedependance.git@main ou
gitlab::thinkr/bakacode/[email protected] dans le champ Remotes.

C’est très spécifique à GitLab et impose l’usage d’un GITLAB_PAT. Ça gère mal les hôtes autres que gitlab.com et en pratique ça n’apporte aucun confort d’usage, donc restons sur des Remotes de la forme `git::https://gitlab.thinkr.fr` . (Mais si je suis passé à côté d’un avantage de cette syntaxe, je suis preneur de vos retours !)

Le cas particulier de la CI

Si vous êtes arrivé jusqu’ici, vous savez comment installer un package depuis un GitLab privé. La seule condition : disposer d’un token avec les droits suffisants sur le package à installer et ses dépendances.

Parlons maintenant du cas particulier de la CI.

L’approche naïve (qui fonctionne mais…)

Techniquement, il est possible d’appliquer exactement ce que vous avez compris de la partie précédente dans la chaîne de CI. Il « suffit » de définir puis utiliser un token suffisamment puissant que vous passerez en variable à votre chaîne de CI comme ceci:

building:
  stage: analyse
  script:
    - R -e "options(remotes.git_credentials = git2r::cred_user_pass('oauth2', Sys.getenv('GITLAB_PAT')));devtools::install_git(url = 'https://gitlab.thinkr.fr/vincent/monpackage.git', credentials = git2r::cred_user_pass('oauth2', Sys.getenv('GITLAB_PAT')))"

Cela va fonctionner dans votre gitlab-ci.yml, puisque rien ne vous en empêche.

Mais d’expérience :

  • La génération du token avec exactement les bons droits est complexe
  • Le token expire au bout d’un moment, ca demande de la maintenance
  • Et on finit par mettre le token d’un admin à l’échelle du groupe dans GitLab… ce qui n’est pas optimal niveau sécurité

La bonne approche : le CI_JOB_TOKEN

Quand un runner se lance pour exécuter une chaîne de CI, celui-ci embarque son propre token, accessible via la variable d’environnement CI_JOB_TOKEN. C’est ce token qui est utilisé pour le git clone qui récupère le code de votre dépôt.

Ce token est :

  • Temporaire : il expire à la fin du job
  • Limité : il ne dispose (normalement) des droits de lecture que sur le dépôt qui lance la CI

La question devient : comment s’assurer que le CI_JOB_TOKEN dispose bien des droits pour récupérer les packages en dépendance ?

Configurer les autorisations inter-projets

Cela se gère au niveau de GitLab dans Settings > CI/CD > Job token permissions des projets en dépendance.

Prenons un exemple concret : vous souhaitez installer le package A, qui dépend du package B, qui lui-même dépend du package C (A, B et C étant privés sur votre GitLab).

Vous devez :

  1. Dans les options du package C : autoriser les packages A et B dans la « CI/CD job token allowlist »
  2. Dans les options du package B : autoriser le package A
┌─────────┐ dépend de         ┌─────────┐ dépend de         ┌─────────┐
│ A       │ ───────────────▶ │ B        │ ───────────────▶ │ C       │
└─────────┘                   └─────────┘                   └─────────┘
                                │                               │
                               autorise A                 autorise A, B

Astuce : si vous avez pris l’habitude de ranger vos projets dans des groupes, vous pouvez par exemple autoriser tous les projets du groupe « production » à utiliser tous les projets du groupe « outils ». Ça simplifie grandement la maintenance !

Le gitlab-ci.yml fonctionnel

Dans ce contexte, on utilisera cette option :

options(remotes.git_credentials = git2r::cred_user_pass("gitlab-ci-token", Sys.getenv("CI_JOB_TOKEN")))

Voici un exemple de gitlab-ci.yml fonctionnel pour un check de package avec des dépendances internes :

image: rocker/geospatial:4.5.0

stages:
  - build-and-check-package

build-and-check-package:
  stage: build-and-check-package
  script:
    - Rscript -e 'install.packages("remotes")'
    - Rscript -e 'install.packages("git2r")'    
    - Rscript -e 'options(remotes.git_credentials = git2r::cred_user_pass("gitlab-ci-token", Sys.getenv("CI_JOB_TOKEN")));remotes::install_deps(upgrade = "never")'
    - R -e 'rcmdcheck::rcmdcheck(args = c("--no-manual"), error_on = "warning", check_dir = "check")'

Récapitulatif : GITLAB_PAT vs CI_JOB_TOKEN

Ne pas confondre ces deux tokens, ils n’ont pas la même portée ni le même usage :

GITLAB_PAT CI_JOB_TOKEN
Type Jeton personnel Jeton automatique
Lié à Un utilisateur Un job CI
Droits Hérités du compte utilisateur Limités via allowlist
Durée de vie Longue (configurable) Expire à la fin du job
Usage principal Installation locale CI/CD

{remotes} peut utiliser l’un ou l’autre :

# Jeton CI
options(remotes.git_credentials = git2r::cred_user_pass("gitlab-ci-token", Sys.getenv("CI_JOB_TOKEN")))

# Jeton personnel
options(remotes.git_credentials = git2r::cred_user_pass("oauth2", Sys.getenv("GITLAB_PAT")))

Checklist de dépannage

Votre installation échoue avec une erreur 403 ? Voici les points à vérifier :

En local

  • [ ] Votre GITLAB_PAT est-il défini dans .Renviron ?
  • [ ] Le token a-t-il les scopes read_api et read_repository ?
  • [ ] Avez-vous redémarré R après avoir modifié .Renviron ?
  • [ ] L’option remotes.git_credentials est-elle bien définie avant l’appel à install_* ?
  • [ ] Le token a-t-il accès à toutes les dépendances privées (pas seulement au package principal) ?

En CI

  • [ ] Les projets en dépendance autorisent-ils votre projet dans leur « Job token allowlist » ?
  • [ ] Utilisez-vous bien "gitlab-ci-token" (et non "oauth2") avec CI_JOB_TOKEN ?
  • [ ] Le champ Remotes de votre DESCRIPTION utilise-t-il la syntaxe `git::https://…` ?

Pour debugger

Vous pouvez tester l’accès à l’API GitLab directement :

# Tester votre token
httr2::request("https://gitlab.thinkr.fr/api/v4/projects") |>
httr2::req_headers("PRIVATE-TOKEN" = Sys.getenv("GITLAB_PAT")) |>
httr2::req_perform() |>
httr2::resp_status()

Un code 200, c’est bon. Un 401 ou 403, c’est que votre token n’a pas les bons droits.

En résumé

Installer un package R depuis un GitLab privé, ce n’est pas sorcier, mais ça demande de comprendre quelques subtilités :

  1. En local : utilisez GITLAB_PAT dans votre .Renviron et configurez remotes.git_credentials
  2. Pour vos dépendances : déclarez-les proprement dans le champ Remotes avec la syntaxe `git::https://…`
  3. En CI : privilégiez le CI_JOB_TOKEN et configurez les autorisations inter-projets dans GitLab

Le plus important ? Ne jamais mettre de token en dur dans votre code. Votre futur vous (et vos collègues) vous remercieront.


Vosu êtes un humain et non une IA qui passe par là ? Vous avez des questions ou des retours d’expérience sur le sujet ? Les commentaires sont ouverts !


À propos de l'auteur

Vincent Guyader

Vincent Guyader

Codeur fou, formateur et expert logiciel R


Commentaires


À lire également