Skip to content

Commit fa8beea

Browse files
committed
registry pull push script
1 parent 22276b3 commit fa8beea

File tree

6 files changed

+284
-8
lines changed

6 files changed

+284
-8
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ __pycache__
77
env
88
.env
99
.idea
10+
.bash_history
11+
.python_history
12+
.cache
1013

1114
# swap
1215
[._]*.s[a-v][a-z]

Dockerfile.compose

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM python:3.10-bookworm
2+
3+
ENV DEBIAN_FRONTEND=noninteractive
4+
ENV MY_USER="app"
5+
ENV MY_GROUP="app"
6+
ARG DOCKER_UID=1000
7+
ARG DOCKER_GID=1000
8+
9+
RUN apt-get update -y \
10+
&& apt-get install -y --no-install-recommends procps curl bash libpq-dev build-essential docker.io
11+
12+
RUN groupadd -g ${DOCKER_GID} -r ${MY_GROUP} \
13+
&& useradd -d /app -u ${DOCKER_UID} -m -s /bin/bash -g ${MY_GROUP} ${MY_USER}
14+
15+
# make docker in docker work
16+
ARG DOCKER_HOST_GID=984
17+
RUN groupmod -g ${DOCKER_HOST_GID} docker
18+
19+
WORKDIR /app
20+
21+
COPY requirements.txt ./
22+
23+
RUN pip install --no-cache-dir -r requirements.txt
24+
25+
CMD ["bash"]

README.md

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
# gitlab-admin
1+
# IMPORTANT!
2+
- Gitlab version 13.12 or higher required.
23

3-
## IMPORTANT!
4-
### Gitlab version 13.12 or higher
5-
6-
## Setup
4+
# Setup with symlinks
75
Env var `GL_ADMIN_PRIVATE_TOKEN` needed to access GitLab as Admin to create and setup projects.
86
Env vars `PG_DB_HOST`, `PG_DB_NAME`, `PG_DB_USER`, `PG_DB_PASS` for access to GitLab PostgreSQL DB are required to set these options (API lacks support):
97
- `skip_outdated_deployment_jobs`
@@ -27,11 +25,22 @@ Install python3 requirements:
2725
pip3 install -r requirements.txt
2826
```
2927

30-
## `./settings.py`
28+
# Setup with docker compose
29+
30+
Make sure your uid is set to 1000, otherwise you will have permission issues with the mounted volumes.
31+
32+
Check docker host docker group id, update it in `Dockerfile.compose` if needed.
33+
34+
Run:
35+
```
36+
docker compose run --rm --build gitlab-admin
37+
```
38+
39+
# `./settings.py`
3140
Setup global GitLab settings.
3241
Run `./settings.py --apply-settings` to apply GitLab settings.
3342

34-
## `./projects.py`
43+
# `./projects.py`
3544
Setup multiple GitLab projects with settings defined by YAML, see [projects.yaml.example](projects.yaml.example) for example.
3645

3746
Run `./projects.py --setup-projects` to create and/or setup projects in GitLab.
@@ -41,8 +50,50 @@ Git is used via local cmd run.
4150

4251
Run `./projects.py --bulk-delete-tags-in-projects` to bulk delete docker registry tags by rules defined in YAML.
4352

44-
## `./issues.py`
53+
# `./issues.py`
4554
Import issues or epics from Jira, rules defined by YAML, see [issues.yaml.example](issues.yaml.example) for example.
4655

4756
Run `./issues.py --import-epics-from-jira` to import epics.
4857
Run `./issues.py --import-issues-from-jira` to import issues.
58+
59+
# `./registry.py`
60+
The script is used to pull the GitLab container registry of specific project locally, then push it to another project.
61+
This is helpful if you need to move or rename the project as GitLab does not support this while there are existing images in the registry.
62+
63+
Set env vars in `.env` file, docker compose will use it:
64+
- `PULL_GITLAB_USER` - GitLab user to pull images from registry
65+
- `PULL_GITLAB_TOKEN` - GitLab user token to pull images from registry and to connect to GitLab API
66+
- `PUSH_GITLAB_USER` - GitLab user to push images to registry
67+
- `PUSH_GITLAB_TOKEN` - GitLab user token to push images to registry and to connect to GitLab API
68+
69+
Enter docker compose container:
70+
```
71+
docker compose run --rm gitlab-admin
72+
```
73+
74+
Pull images, you can omit `--delete-tags-after-pull` first time just to make sure everything works:
75+
```
76+
./registry.py --ids-file ids.txt --pull-gitlab-url https://gitlab.example.org --pull-project-path old/path
77+
```
78+
79+
Pull together with deleting tags after pull:
80+
```
81+
./registry.py --ids-file ids.txt --pull-gitlab-url https://gitlab.example.org --pull-project-path old/path --delete-tags-after-pull
82+
```
83+
84+
Move or rename the project.
85+
86+
Check local images and note old registry location in their repository location column:
87+
```
88+
docker image ls
89+
```
90+
91+
Push images:
92+
```
93+
./registry.py --ids-file ids.txt --push-gitlab-url https://gitlab.example.org --push-project-path new/path --old-registry-location gitlab.example.org:5001/old/path
94+
```
95+
96+
Delete local images:
97+
```
98+
./registry.py --ids-file ids.txt --rm-images
99+
```

compose.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: gitlab-admin
2+
3+
services:
4+
gitlab-admin:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile.compose
8+
user: 1000:1000
9+
# docker in docker
10+
group_add:
11+
- docker
12+
volumes:
13+
- ./:/app
14+
# docker in docker
15+
- /var/run/docker.sock:/var/run/docker.sock
16+
env_file:
17+
- ./.env

registry.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
# This script is used to pull the GitLab container registry of specific project locally, then push it to another project.
5+
# This is helpful if you need to move or rename the project as GitLab does not support this while there are existing images in the registry.
6+
7+
import os
8+
import gitlab
9+
import click
10+
import sys
11+
import docker
12+
13+
@click.command()
14+
@click.option("--pull-gitlab-url", help="The URL of the GitLab instance to pull from.")
15+
@click.option("--pull-project-path", help="The path of the project to pull the registry from.")
16+
@click.option("--delete-tags-after-pull", is_flag=True, default=False, help="Delete the tags in GitLab project after pulling the image.")
17+
@click.option("--push-gitlab-url", help="The URL of the GitLab instance to push to.")
18+
@click.option("--push-project-path", help="The path of the project to push the registry to.")
19+
@click.option("--old-registry-location", help="The location of the old registry, used to detect repository path inside the project registry.")
20+
@click.option("--rm-images", is_flag=True, help="Remove local images.", default=None)
21+
@click.option("--ids-file", help="File containing a list of image IDs pulled or to be pushed, one per line.")
22+
def main(pull_gitlab_url, pull_project_path, push_gitlab_url, push_project_path, ids_file, delete_tags_after_pull, rm_images, old_registry_location):
23+
24+
PULL_GITLAB_USER = os.environ.get("PULL_GITLAB_USER")
25+
if not PULL_GITLAB_USER:
26+
print("ERROR: environment variable PULL_GITLAB_USER is not set.")
27+
sys.exit(1)
28+
29+
PULL_GITLAB_TOKEN = os.environ.get("PULL_GITLAB_TOKEN")
30+
if not PULL_GITLAB_TOKEN:
31+
print("ERROR: environment variable PULL_GITLAB_TOKEN is not set.")
32+
sys.exit(1)
33+
34+
PUSH_GITLAB_USER = os.environ.get("PUSH_GITLAB_USER")
35+
if not PUSH_GITLAB_USER:
36+
print("ERROR: environment variable PUSH_GITLAB_USER is not set.")
37+
sys.exit(1)
38+
39+
PUSH_GITLAB_TOKEN = os.environ.get("PUSH_GITLAB_TOKEN")
40+
if not PUSH_GITLAB_TOKEN:
41+
print("ERROR: environment variable PUSH_GITLAB_TOKEN is not set.")
42+
sys.exit(1)
43+
44+
if pull_gitlab_url is None and push_gitlab_url is None and rm_images is None:
45+
print("ERROR: either --pull-gitlab-url or --push-gitlab-url or --rm-images must be provided.")
46+
sys.exit(1)
47+
48+
if (
49+
(pull_gitlab_url is not None and (push_gitlab_url is not None or rm_images is not None))
50+
or (push_gitlab_url is not None and (pull_gitlab_url is not None or rm_images is not None))
51+
or (rm_images is not None and (pull_gitlab_url is not None or push_gitlab_url is not None))
52+
):
53+
print("ERROR: only one of --pull-gitlab-url or --push-gitlab-url or --rm-images can be provided.")
54+
sys.exit(1)
55+
56+
if pull_gitlab_url and pull_project_path is None:
57+
print("ERROR: --pull-project-path must be provided when --pull-gitlab-url is provided.")
58+
sys.exit(1)
59+
60+
if push_gitlab_url and push_project_path is None:
61+
print("ERROR: --push-project-path must be provided when --push-gitlab-url is provided.")
62+
sys.exit(1)
63+
64+
if push_gitlab_url and old_registry_location is None:
65+
print("ERROR: --old-registry-location must be provided when --push-gitlab-url is provided.")
66+
sys.exit(1)
67+
68+
if ids_file is None:
69+
print("ERROR: --ids-file must be provided.")
70+
sys.exit(1)
71+
72+
if pull_project_path:
73+
74+
gl = gitlab.Gitlab(pull_gitlab_url, private_token=PULL_GITLAB_TOKEN)
75+
gl.auth()
76+
77+
docker_client = docker.from_env()
78+
79+
project = gl.projects.get(pull_project_path)
80+
81+
docker_client.login(
82+
username=PULL_GITLAB_USER,
83+
password=PULL_GITLAB_TOKEN,
84+
registry=project.container_registry_image_prefix.split('/')[0]
85+
)
86+
87+
with open(ids_file, 'w') as file:
88+
89+
for repository in project.repositories.list(all=True, tags_count=True):
90+
91+
print(f"Pulling repository '{repository.id}' from project '{project.path_with_namespace}', path: {repository.path}, tags_count: {repository.tags_count}.")
92+
for tag in repository.tags.list(all=True):
93+
94+
tag_extended = repository.tags.get(tag.name)
95+
print(f" Pulling tag '{tag_extended.name}', location: {tag_extended.location}, created_at: {tag_extended.created_at}, total_size: {tag_extended.total_size}.")
96+
97+
pull_image = docker_client.images.pull(tag_extended.location)
98+
print(f" Pulled image: {pull_image.id}.")
99+
100+
# Remove something:image_id before writing into the file
101+
if ':' in pull_image.id:
102+
pull_image_id = pull_image.id.split(':')[1]
103+
104+
file.write(f"{pull_image_id}\n")
105+
106+
if delete_tags_after_pull:
107+
print(f" Deleting tag '{tag_extended.name}' from project '{project.path_with_namespace}'.")
108+
tag_extended.delete()
109+
110+
docker_client.close()
111+
112+
if push_project_path:
113+
114+
gl = gitlab.Gitlab(push_gitlab_url, private_token=PUSH_GITLAB_TOKEN)
115+
gl.auth()
116+
117+
docker_client = docker.from_env()
118+
119+
project = gl.projects.get(push_project_path)
120+
121+
docker_client.login(
122+
username=PUSH_GITLAB_USER,
123+
password=PUSH_GITLAB_TOKEN,
124+
registry=project.container_registry_image_prefix.split('/')[0]
125+
)
126+
127+
with open(ids_file, 'r') as file:
128+
129+
for line in file:
130+
image_id = line.strip()
131+
132+
image = docker_client.images.get(image_id)
133+
print(f"Pushing image: {image.id}.")
134+
135+
# If the image tag contains two colons, it means it has a port specified.
136+
if ':' in image.tags[0] and image.tags[0].count(':') > 1:
137+
image_tag = image.tags[0].split(':')[2]
138+
image_without_tag = image.tags[0].split(':')[0] + ":" + image.tags[0].split(':')[1]
139+
# If the image tag contains one colon, it means the port is not specified, and the tag is after the first colon.
140+
elif ':' in image.tags[0] and image.tags[0].count(':') == 1:
141+
image_tag = image.tags[0].split(':')[1]
142+
image_without_tag = image.tags[0].split(':')[0]
143+
# In other cases just use skip this image.
144+
else:
145+
print(f" Image {image.id} has no repository specific tag, skipping.")
146+
continue
147+
148+
print(f" Image {image.id} has repository specific tag: {image.tags[0]} and tag: {image_tag}.")
149+
150+
# Remove old_registry_location from the image_without_tag - it will give repository path inside project registry.
151+
repository_path = image_without_tag.replace(old_registry_location, '')
152+
153+
# Add a tag to the image for the new repository in the new project.
154+
new_tag = f"{project.container_registry_image_prefix}{repository_path}:{image_tag}"
155+
print(f" Tagging image {image.id} with new repository specific tag: {new_tag}.")
156+
image.tag(new_tag)
157+
158+
# Push the image to the new project.
159+
print(f" Pushing image {image.id} to new project {project.path_with_namespace} with tag: {new_tag}.")
160+
push_image = docker_client.images.push(new_tag)
161+
162+
docker_client.close()
163+
164+
if rm_images:
165+
166+
docker_client = docker.from_env()
167+
168+
with open(ids_file, 'r') as file:
169+
170+
for line in file:
171+
image_id = line.strip()
172+
print(f"Removing image: {image_id}.")
173+
docker_client.images.remove(image=image_id, force=True)
174+
175+
docker_client.close()
176+
177+
if __name__ == "__main__":
178+
main()

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ requests==2.31.0
77
deepdiff
88
rich
99
gql
10+
docker
11+
click

0 commit comments

Comments
 (0)