Salut à toi, voyageur égaré !
Tu aimes GitLab, mais tu es frustré par les limitations de gitlab.com ? Ton organisation a besoin de migrer vers une instance autohébergée ?
Enfile tes sous-vêtements coquins et mets-toi sur ton 31, car nous allons dégainer le Python et le code qui tâche !

Nous commencerons par le “pourquoi” et ensuite, je te révélerai le “comment” !
Comme on ne trouve pas vraiment de retour d’expérience sur ce sujet, je me dois de partager avec toi ce que j’ai appris lors de cette migration, et ce, de la manière la plus déjantée possible.
Alors évidemment, j’ai déjà fait ça chez mes clients, mais ce n’est pas le même code, ne pas taper !
Pourquoi ?
Gitlab.com est une très bonne solution pour héberger son code et lancer des jobs de CI/CD, mais certaines limitations peuvent être un peu gênantes quand on commence à être nombreux.
- On n’est pas vraiment admin de l’instance, et c’est un peu désagréable de ne pas pouvoir reset les MFA des utilisateurs en version gratuite.
- On partage l’instance et les runners partagé avec l’ensemble des utilisateurs de gitlab.com
- On peut bien sûr accrocher nos propres runners, mais on subit quand même les indispo et les lenteurs de service quand gitlab.com a un problème
- On ne maîtrise pas les backups de notre instance
- On ne maîtrise pas le réseau quand on a des problématiques de sécurité un peu restrictives.
Si nous n’avez pas ces problématiques, le coût peut devenir un problème à partir du moment ou votre équipe s’aggrandit et qu’il faut payer une licence par utilisateur.
L’argent en vrai, c’est la seule et unique raison !

Une fois n’est pas coutume, on part du principe qu’on n’a pas envie de payer les outils premium pour faire ça !
Donc exclu de payer un mois de premium pour avoir un backup automatisé qui fait tout pour nous.
La doc de gitlab est un peu trompeuse pour le coup :
If you want to bring existing projects to GitLab or copy GitLab projects to a different location, you can:
Import projects from external systems using one of the available importers.
Migrate GitLab projects:
Between two GitLab self-managed instances.
Between a self-managed instance and GitLab.com in both directions.
In the same GitLab instance.
Ça dit qu’on peut migrer, mais quand on creuse un peu le sujet les solutions ne sont pas vraiment automatiques et sans effort.
On se retrouve dans le cas de figure ou on va devoir bricoler avec les APIs.

On ressort le serpent
Si on fouille dans la doc on a un genre de guidance dans l’ordre des objets à migrer.
Autant dire que si on connaît gitlab, c’est complètement évident et parfaitement inutile.
To migrate all data from self-managed to GitLab.com, you can leverage the API. Migrate the assets in this order:
Groups
Projects
Project variables
You must still migrate your Container Registry over a series of Docker pulls and pushes. Re-run any CI pipelines to retrieve any build artifacts.
C’est gentil de nous mettre les liens vers l’API, mais un exemple de script ça aurait pu être vraiment utile.
Pas bien grave, on va utiliser Python Gitlab qui est bien plus indiqué pour faire du scripting et pour nous éviter de construire les requêtes d’API à la main une par une.
https://python-gitlab.readthedocs.io/en/stable/
Et au moins la doc est fournie avec des exemples utiles.

On n’oublie pas un petit truc rigolo aussi, il faudra gérer les permissions des utilisateurs.
Copier les groupes et les projets
On va utiliser des tokens API sur un utilisateur avec les accès maximums des deux côtés.
C’est un peu la faiblesse de cette technique : il faut un utilisateur avec des permissions étendues sur tous les projets …
Mais ce n’est pas grave on est Admin, on assume! On fait attention à ne rien casser et on ne traine pas les yeux là où on en n’a pas besoin :)
https://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html
Petit détail casse pied qui va nous suivre pendant toute la durée de l’article, comme un chewing-gum collé dans les cheveux,
les groupes et les projets peuvent être à tout moment renommés.
Ça n’a l’air de rien comme ça, mais ça veut dire qu’on ne peut pas compter sur le nom technique des objets qu’on migre et ça c’est une vraie galère.
Les groups gitlab fonctionnent par parents-enfants, mais les id ne pourront pas être conservés pendant la migration.
Alors on va faire une structure récursive pour suivre les noms des groupes et si un nouveau groupe et de nouveaux projets sont créés, cette structure suivra le mouvement.
L’avantage de cette solution c’est que l’ordre de création des groupes est bien plus facile et ne dépendra pas des id de projets existant sur le gitlab que vous voulez migrer.
Retour d’expérience négatif, j’ai essayé de faire un code plus simple, en partant d’un tableau en dur, si vous devez plusieurs fois resynchroniser vos groupes et migrations, c’est une mauvaise idée !
Essayer de gérer des mappings de noms est une connerie sans nom (tu l’as celle-ci ?) en plus d’être difficilement gérable, il faut maintenir les exceptions au fur et à mesure où elles apparaissent.
En partant de ces constats, j’en suis arrivé à ce premier script (ChatGPT a un peu aidé).
Pour éviter de te faire vomir ton cassoulet, je vais ajouter des commentaires dans le code (que tu pourras retrouver dans le lien github en fin d’article)

copy_gitlab_groups_and_repository.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
#!/usr/bin/env pipenv-shebang
import os
import gitlab
import pprint
def gitlab_instance(gitlab_url, private_token):
return gitlab.Gitlab(
gitlab_url,
private_token=private_token,
)
def build_group_structure(groups, parent_id=None):
# cette fonction est récursive et construit une structure imbriquée de sous-groupe
groups_structure = {}
for group in groups:
if group.parent_id == parent_id:
groups_structure[group.name] = {
"path": group.path,
"subgroups": build_group_structure(groups, group.id),
}
return groups_structure
def create_group_or_pass(gitlab_instance, group_name, group_path, parent_id=None):
try:
group_data = {"name": group_name, "path": group_path}
if parent_id is not None:
group_data["parent_id"] = parent_id
gitlab_instance.groups.create(group_data)
print(f"Create group : {group_name}")
except gitlab.exceptions.GitlabCreateError as e:
print(f"Group {group_name} already exist, skip...")
def get_group_id_from_group_name(gitlab_instance, group_name):
try:
# hack un peu idiot, on cherche l'id du group par son nom
groups = gitlab_instance.groups.list(search=group_name, get_all=True)
except gitlab.exceptions.GitlabGetError:
print(f"error: group {group_name} not found")
return None
for group in groups:
if group.name == group_name:
return group.id
return None
def create_groups_and_subgroups_recursive(
gitlab_instance, groups_struct, parent_id=None
):
# on créer des sous-groupes tant qu'on à une liste non vide et on récurse sur les sous-niveaux
for group_name, group_info in groups_struct.items():
create_group_or_pass(gitlab_instance, group_name, group_info["path"], parent_id)
group_id = get_group_id_from_group_name(gitlab_instance, group_name)
if group_id is not None:
create_groups_and_subgroups_recursive(
gitlab_instance, group_info["subgroups"], group_id
)
def create_project_from_subgroups_recursive(
gl_com_inst, gl_onprem_inst, groups_struct, parent_id=None
):
# on est obligé de récupérer le projet id et le group id dans le second gitlab pour pouvoir le créer
# ça fait un truc un peu sous optimal
for group_name, group_info in groups_struct.items():
print(f"create projects for group {group_name}")
try:
gl_com_group_id = get_group_id_from_group_name(gl_com_inst, group_name)
gl_com_group = gl_com_inst.groups.get(gl_com_group_id)
gl_com_projects = gl_com_group.projects.list(get_all=True)
gl_onprem_group_id = get_group_id_from_group_name(
gl_onprem_inst, group_name
)
except Exception as e:
print(f"exception during create projects on groups {group_name}, skipping")
break
for project in gl_com_projects:
try:
print(f"create project {project.name}")
gl_onprem_inst.projects.create(
{
"name": project.name,
"namespace_id": gl_onprem_group_id,
"description": project.description,
}
)
except gitlab.exceptions.GitlabCreateError:
print(
f"project {project.name} already exists, skip project creation ..."
)
if group_info.get("subgroups"):
create_project_from_subgroups_recursive(
gl_com_inst, gl_onprem_inst, group_info["subgroups"], gl_onprem_group_id
)
# le script commence ici, on utilise pipenv shebang pour nous sourcer le fichier .env et éviter d'exposer les tokens
gitlab_com_token = os.getenv("GITLAB_COM_TOKEN")
gitlab_on_prem_token = os.getenv("GITLAB_ON_PREM_TOKEN")
gl_com_inst = gitlab_instance(os.getenv("GITLAB_COM_URL"), gitlab_com_token)
gl_onprem_inst = gitlab_instance(
os.getenv("GITLAB_SELF_HOST_URL"), gitlab_on_prem_token
)
groups_struct = {}
# on liste l'ensemble des groups visible pas mon utilisateur, le get_all=True est important, car il permet de paginer
#(sinon c'est seulement les 20 premiers, on va le retrouver un peu partout)
# la première fonction permet de générer une structure récursive de projets et de sous projets
gl_com_groups = gl_com_inst.groups.list(get_all=True)
groups_struct = build_group_structure(gl_com_groups)
create_groups_and_subgroups_recursive(gl_onprem_inst, groups_struct)
create_project_from_subgroups_recursive(
gl_com_inst, gl_onprem_inst, groups_struct, parent_id=None
)
|
Donc là c’est un peu la grosse folie d’entrée de jeu, on fait de la récursion et une structure un peu compliquée.
Pour mieux comprendre, je vais vous donner un exemple de la structure de données.

Gustave Courbet Le désespéré
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
{
"cyberpunkachien": {
"path": "cyberpunkachien",
"subgroups": {
"Script Python": {
"path": "script-python",
"subgroups": {
"Experiences": {
"path": "experiences",
"subgroups": {
"Ratées": {"path": "rat", "subgroups": {}},
"Réussies": {"path": "reussie", "subgroups": {}},
},
}
},
},
"Secrets": {
"path": "secrets",
"subgroups": {"Très Secret": {"path": "tr-s-secret", "subgroups": {}}},
},
},
}
}
|
si je résume l’arborescence, ça fait ça
1
2
3
|
cyberpunkachien/script-python/experiences/rat
/ /reussie
/secrets/tr-s-secret
|
Donc là mon exemple est déjà compliqué, mais si on à un niveau d’arborescence plus profond le script le gère.
Faut être un genre de psychopathe fini pour faire ce genre de truc, n’est-ce pas ?

À noter aussi que si de nouveaux groupes arrivent pendant que vous faites votre migration, pas de soucis,
le script va gérer les anciens groupes et créer les nouveaux.
Ça enlève pas mal de charges mentales, si j’avais eu ça avant au boulot, j’aurai été content, mais je l’ai refait pour le plaisir de me faire mal !
Bon là on a directement tapé dur avec l’arborescence des groupes et sous-groupes, mais les projets c’est la merde aussi rassures-toi :)
Le problème de mapping de nom revient, mais en plus il faut connaître le nom du sous-groupe dans lequel il est pour pouvoir le récréer au bon endroit dans le nouveau gitlab.
Est-ce qu’on n’est pas content ? Franchement c’est super sympa à faire non :)
Copier les variables de ci/cd dans les groupes et les projets
Bon j’aurais pu parfaitement intégrer cette fonctionnalité dans le premier script, mais il est déjà bien assez infernal comme ça :)
En plus de vouloir découpler les actions. C’est plutôt bien de pouvoir gérer juste les variables ou juste les groupes et nouveaux projets.
L’avantage cette fois si c’est qu’on à plus vraiment besoin de se soucier de l’arborescence compliquée, puisqu’on agit directement sur les projets en vrac !
copy_gitlab_cicd_envs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
#!/usr/bin/env pipenv-shebang
import os
import gitlab
def gitlab_instance(gitlab_url, private_token):
return gitlab.Gitlab(
gitlab_url,
private_token=private_token,
)
def get_groups_vars(gitlab_instance):
print("get groups vars envs")
group_vars = {}
for group in gitlab_instance.groups.list(all=True, membership=True):
for var in group.variables.list():
if not group_vars.get(group.name):
group_vars[group.name] = []
group_vars[group.name].append(
{
"variable_type": var.variable_type,
"key": var.key,
"protected": var.protected,
"masked": var.masked,
"environment_scope": var.environment_scope,
"value": var.value,
}
)
return group_vars
def get_projects_vars(gitlab_instance):
print("get project envs vars.")
env_vars = {}
for project in gitlab_instance.projects.list(all=True, membership=True, lazy=False):
try:
variables = project.variables.list(get_all=True)
if variables:
env_vars[project.name] = []
for var in variables:
env_vars[project.name].append(
{
"variable_type": var.variable_type,
"key": var.key,
"protected": var.protected,
"masked": var.masked,
"environment_scope": var.environment_scope,
"value": var.value,
}
)
except Exception as e:
print(e)
return env_vars
def set_group_var_or_skip_creation(gitlab_instance, group_vars):
for group in gitlab_instance.groups.list(all=True, membership=True):
if group.name in group_vars.keys():
for env in group_vars[group.name]:
try:
print(f"set variables {env} on group {group.name}")
group.variables.create(
{
"key": env["key"],
"value": env["value"],
"protected": env["protected"],
"variable_type": env["variable_type"],
"masked": env["masked"],
"environment_scope": env["environment_scope"],
}
)
except Exception as e:
print(e)
print(f"skip env {env} creation")
def set_projects_vars_or_skip_creation(gitlab_instance, env_vars):
# le lazy False est important ici, pour récupérer les infos complètes qui contiennent les variables
# membership=True permet d'éviter de prendre tous les projets publics de gitlab.com
for project in gitlab_instance.projects.list(all=True, membership=True, lazy=False):
if project.name in env_vars.keys():
for env in env_vars[project.name]:
try:
print(f"set variables {env['key']} on project {project.name}")
project.variables.create(
{
"key": env["key"],
"value": env["value"],
"protected": env["protected"],
"variable_type": env["variable_type"],
"masked": env["masked"],
"environment_scope": env["environment_scope"],
}
)
except Exception as e:
print(e)
print(f"skip env {env} creation")
gitlab_com_token = os.getenv("GITLAB_COM_TOKEN")
gitlab_on_prem_token = os.getenv("GITLAB_ON_PREM_TOKEN")
gl_com_inst = gitlab_instance(os.getenv("GITLAB_COM_URL"), gitlab_com_token)
gl_onprem_inst = gitlab_instance(
os.getenv("GITLAB_SELF_HOST_URL"), gitlab_on_prem_token
)
gl_com_groups_vars = get_groups_vars(gl_com_inst)
set_group_var_or_skip_creation(gl_onprem_inst, gl_com_groups_vars)
gl_com_projects_vars = get_projects_vars(gl_com_inst)
set_projects_vars_or_skip_creation(gl_onprem_inst,gl_com_projects_vars)
|
On remarque que la complexité est bien moindre, par rapport au précédent script.
Mais encore une fois on s’emmerde fort à lister tous les projets et tout les groupes plusieurs fois à cause de ce matching de noms insupportable.
Putain ça y est je suis tilté, j’ai besoin du bol !

l’Apéro est fini, on passe au plat de résistance
Tu penses que mes scripts sont lourds et un peu cons, attends de voir la suite du programme !
C’est l’étape la plus longue et la plus crado du processus, synchroniser l’ensemble du code des projets.
Non parce que là jusqu’à présent c’était marrant :)
sync_repositories.py
Et ce n’est même pas encore le boss final !
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
#!/usr/bin/env pipenv-shebang
import gitlab
import os
import subprocess
from subprocess import check_output
def gitlab_instance(gitlab_url, private_token):
return gitlab.Gitlab(
gitlab_url,
private_token=private_token,
)
def list_gitlab_project(gitlab_instance):
return gitlab_instance.projects.list(all=True, membership=True)
def solve_git_url_on_both_gitlab(projects_list_gitlab_com, projects_list):
git_url = {}
for project in projects_list_gitlab_com:
gitlab_com = project.ssh_url_to_repo
for project_self_host in projects_list:
if project_self_host.name in project.name:
self_host = project_self_host.ssh_url_to_repo
git_url[project.name] = {"origin": gitlab_com, "origin2": self_host}
return git_url
def git_clone_and_add_origin2_to_all_projects(git_url):
os.chdir(repo_path)
try:
for project in git_url:
print(["git", "clone", git_url[project]["origin"]])
folder_name = subprocess.run(
["git", "clone", git_url[project]["origin"]],
capture_output=True,
text=True,
).stderr.split("'")[1]
print(f"cd {os.path.join(repo_path, folder_name)}")
os.chdir(os.path.join(repo_path, folder_name))
print(["git", "remote", "add", "origin2", git_url[project]["origin2"]])
print(
subprocess.run(
["git", "remote", "add", "origin2", git_url[project]["origin2"]],
capture_output=True,
text=True,
)
)
os.chdir(repo_path)
except Exception as e:
print(f"ERROR during cloning project {project}")
def sync_git_folders_code(repo_path):
repos = [
f for f in os.listdir(repo_path) if os.path.isdir(os.path.join(repo_path, f))
]
for repo in repos:
print(f"on repo {repo}")
os.chdir(os.path.join(repo_path, repo))
try:
print("git fetch --all")
subprocess.run(["git", "fetch", "--all"])
print("git push --force origin2 --all")
subprocess.run(["git", "push", "--force", "origin2", "--all"])
print("git push --force origin2 --tags")
subprocess.run(["git", "push", "--force", "origin2", "--tags"])
except Exception as e:
# j'ai arrêté d'essayer, si ça merde d'une façon ou d'une autre, j'ai un gros print qui va faire caca à l'écran
# faudra géré manuellement le repos en question
print("-" * 20)
print(e)
print("-" * 20)
script_path = os.path.dirname(os.path.abspath(__file__))
repo_path = os.path.join(script_path, "repo")
if not os.path.exists(repo_path):
os.makedirs(repo_path)
os.chdir(repo_path)
gitlab_com_token = os.getenv("GITLAB_COM_TOKEN")
gitlab_on_prem_token = os.getenv("GITLAB_ON_PREM_TOKEN")
gl_com_inst = gitlab_instance(os.getenv("GITLAB_COM_URL"), gitlab_com_token)
gl_onprem_inst = gitlab_instance(
os.getenv("GITLAB_SELF_HOST_URL"), gitlab_on_prem_token
)
print("get project list on gitlab.com")
gl_com_projects = list_gitlab_project(gl_com_inst)
print("get project list on self host gitlab")
gl_onprem_projects = list_gitlab_project(gl_onprem_inst)
print("get ssh url on both gitlab")
git_url = solve_git_url_on_both_gitlab(gl_com_projects, gl_onprem_projects)
print("clone and add origin2 to all repository")
git_clone_and_add_origin2_to_all_projects(git_url)
print("run git commands to sync push origin to origin2")
sync_git_folders_code(repo_path)
|
Pour clarifier un peu ce que fait le script de façon macro,
on récupère les url de repos des deux côtés avec ce format.
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"cloudrun_private_access": {
"origin": "[email protected]:cyberpunkachien/cloudrun_private_access.git",
"origin2": "[email protected]:cyberpunkachien/cloudrun_private_access.git",
},
"initiation-devops": {
"origin": "[email protected]:cyberpunkachien/initiation-devops.git",
"origin2": "[email protected]:cyberpunkachien/initiation-devops.git",
}
[...]
}
|
on clone les repos un part un
on ajoute l’origin2 du second gitlab associé
et après on Bourg-la-Reine les repos avec un élégant et subtil
1
2
3
|
git fetch --all
git push --force origin2 --all
git push --force origin2 --tags
|
Ça prend une plombe, c’est crado au possible, mais ça me plait !

Vous prendrez bien un dessert ?
Spécialité du chef, le baba au rhum, pour hurler toutes les insultes qui t’inspirent, tu mettras ça sur le compte du rhum !
Maintenant que tu es bien soulé, il reste un dernier petit détail ou deux.
Si tu as bien testé ton nouveau gitlab, il va falloir inviter tous les gens qui étaient sur l’ancien.
Et par la même occasion, faire le ménage sur les vieux comptes.
Je te raconte ma blague préférée, sur gitlab.com tu vas adorer.
Sur gitlab.com si tu n’es pas admin (que tu payes pas l’entreprise en gros), tu ne peux pas lister les emails des utilisateurs!
Marrant, non ? hahahah, tu te marres là hein ?

Par contre tu peux quand même lister les gens et les permissions dans tous les repos que tu peux lire !
Tu me vois venir ? Bam encore un script éclaté au sol !
Tu penses que c’est amusant gitlab.com de cliquer dans ton interface et d’inviter tous les gens sur chacun des projets et groupes ?

Nom de Dieu de putain de bordel de merde de saloperie de connard d'enculé de ta mère.
Vous voyez, c'est aussi jouissif que de se torcher le cul avec de la soie, j'adore ça."
(Matrix, le Mérovingien)
sync_permissions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
#!/usr/bin/env pipenv-shebang
import gitlab
import os
import sys
import yaml
def gitlab_instance(gitlab_url, private_token):
return gitlab.Gitlab(
gitlab_url,
private_token=private_token,
)
def get_groups_permission(gitlab_instance, permissions):
print("get group permissions")
for group in gitlab_instance.groups.list(all=True, membership=True):
print(".", end="")
sys.stdout.flush()
for member in group.members.list():
permissions[member.name] = {
"email": "you need to set it manually !",
"groups": [],
}
permissions[member.name]["groups"].append({group.name: member.access_level})
return permissions
def get_project_permission(gitlab_instance, permissions):
print("get project permissions")
for project in gitlab_instance.projects.list(all=True, membership=True):
print(".", end="")
sys.stdout.flush()
try:
for member in project.members.list():
permissions[member.name] = {
"email": "you need to set it manually !",
"projects": [],
}
permissions[member.name]["projects"].append(
{project.name: member.access_level}
)
except Exception as e:
print(f"exclude permission for user {e}")
return permissions
def invite_user_on_groups(gitlab_instance, permissions):
for user in permissions:
for group in gitlab_instance.groups.list(all=True, membership=True):
for usr_group in permissions[user].get("groups",[]):
if group.name in usr_group:
print (f"group name : {group.name}")
print(
{"email": permissions[user]["email"],"access_level": usr_group[group.name]}
)
try:
invitation = group.invitations.create(
{
"email": permissions[user]["email"],
"access_level": usr_group[group.name],
}
)
except (gitlab.exceptions.GitlabCreateError,gitlab.exceptions.GitlabInvitationError) as e:
print("error during group invitation for user {user} {e}.")
def invite_user_on_project(gitlab_instance, permissions):
for user in permissions:
for project in gitlab_instance.projects.list(all=True, membership=True):
for usr_project in permissions[user].get("projects",[]):
if project.name in usr_project:
print (f"project name : {project.name}")
print(
{"email": permissions[user]["email"],"access_level": usr_project[project.name]}
)
try:
invitation = project.invitations.create(
{
"email": permissions[user]["email"],
"access_level": usr_project[project.name],
}
)
except (gitlab.exceptions.GitlabCreateError,gitlab.exceptions.GitlabInvitationError) as e:
print("error during group invitation for user {user} {e}.")
def load_data_if_file_exist(yaml_file):
if os.path.exists(yaml_file):
with open(yaml_file, "r") as file:
return yaml.safe_load(file)
return {}
def dump_dict_to_yaml_file(yaml_file, permissions):
with open(yaml_file, "w") as file:
yaml.dump(permissions, file)
def yes_response_or_exit(prompt):
while True:
response = input(prompt + " (yes/no): ").lower()
if response in ["yes", "y"]:
return True
sys.exit(-1)
gitlab_com_token = os.getenv("GITLAB_COM_TOKEN")
gitlab_on_prem_token = os.getenv("GITLAB_ON_PREM_TOKEN")
gl_com_inst = gitlab_instance(os.getenv("GITLAB_COM_URL"), gitlab_com_token)
gl_onprem_inst = gitlab_instance(
os.getenv("GITLAB_SELF_HOST_URL"), gitlab_on_prem_token
)
permissions = {}
script_path = os.path.dirname(os.path.abspath(__file__))
yaml_file = os.path.join(script_path, "permissions.yaml")
permissions = load_data_if_file_exist(yaml_file)
if permissions:
yes_response_or_exit(
"""permission file found, would you like to apply thoses permissions ?
(you should review the script before, and complete emails adresse to make it works)"""
)
invite_user_on_groups(gl_onprem_inst, permissions)
invite_user_on_project(gl_onprem_inst, permissions)
else:
print("permission file not found, get groups and projects users permissions.")
permissions = {}
permissions = get_groups_permission(gl_com_inst, permissions)
permissions = get_project_permission(gl_com_inst, permissions)
dump_dict_to_yaml_file(yaml_file, permissions)
|
donc là le projet, c’est de généré un yaml avec les permissions et les utilisateurs,
et de remplir manuellement le fichier avec les emails des gens (démerde-toi pour les trouver dans un LDAP au autre)
Ça donne par exemple un fichier comme celui-ci.
Si il y a une clef groups c’est des noms de groupes et si c’est une clef projets vous avez compris.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Homer Dalors:
email: [email protected]
groups:
- cyberpunkachien: 50
Jerry Golade:
email: [email protected]
projects:
- facerestoration: 30
Moussah Razeh:
email: [email protected]
groups:
- cyberpunkachien: 50
projects:
- facerestoration: 30
|
Au deuxième lancement on demande d’appliquer ou non le fichier et tous les gens sont invités dans les bons groupes et les bons projets.
Bon sauf si on se trompe dans les emails, mais ça, c’est une autre histoire :)
Café avec du sel et l’addition
Voilà le github avec l’ensemble des scripts, si vous voulez faire joujou.
https://github.com/helldrum/migrate_gitlab_com_to_self_hosted
On en conclut quoi ? La même chose qu’à chaque fois, Gitlab c’est sympa, mais ce n’est pas ton ami.
Probablement qu’on peut faire plus simple, mais je n’ai pas trouvé comment !
Je pense que l’ulcère n’est pas loin, le plus ironique, c’est que ça devait êtres 20 mins in and out adventure , mais ça n’a clairement pas arrangé mon tempérament de gros rageux.
Je ne sais pas, la prochaine fois peut être que je demanderai de payer ;)
