Add upstream endpoints on container virtual registry
What does this MR do and why?
In this MR, we add the following endpoints to manage the virtual registry upstreams.
🍋 API endpoints for VirtualRegistries::Container::Upstream
Route | Notes |
---|---|
GET /groups/:id/-/virtual_registries/containers/upstreams | Get upstreams for the given top level group |
GET /virtual_registries/container/registries/:id/upstreams | Get the upstreams of a given registry |
POST /virtual_registries/container/registries/:id/upstreams | Create an upstream for a given registry |
GET /virtual_registries/container/upstreams/:id | Get the upstream details |
PATCH /virtual_registries/container/upstreams/:id | Update the upstream details |
DELETE /virtual_registries/container/upstreams/:id | Delete an upstream |
We add the above endpoints behind the feature flag container_virtual_registry
and we also add a license check for this feature.
How to set up and validate locally
🍓 Prerequisites:
- Enable the feature flag
Feature.enable(:container_virtual_registries)
-
Have a **personal access token **ready. Here is a guide: https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token
-
Have a group ID ready
Group.first.id
-
Setup your GDK with an enterprise license (Premium). Follow these steps: https://gitlab.com/gitlab-org/customers-gitlab-com/-/blob/main/doc/setup/gitlab.md#adding-a-license-from-staging-customers-portal-to-your-gdk.
-
cURL
in your terminal or you may also use Postman (or similar)
🍓 Testing the endpoints:
1. Listing all virtual registries for the group: GET /groups/:id/-/virtual_registries/container/upstreams
curl --location 'http://gdk.test:3000/api/v4/groups/22/-/virtual_registries/container/upstreams' \
--header 'PRIVATE-TOKEN: glpat-xxxxx'
Result will look like, depending on what is in your database:
[
{
"id": 4,
"name": "DockerHub Test",
"description": "Test DockerHub upstream - failcase",
"group_id": 22,
"url": "https://registry-1.docker.io",
"username": "jagodie33",
"cache_validity_hours": 24,
"created_at": "2025-08-22T09:30:39.474Z",
"updated_at": "2025-08-22T09:30:39.474Z"
}
]
2. Get the upstreams of a given registry: GET /virtual_registries/container/registries/:id/upstreams
curl --location 'http://gdk.test:3000/api/v4/virtual_registries/container/registries/3/upstreams' \
--header 'PRIVATE-TOKEN: glpat-xxxxx'
Result:
[
{
"id": 8,
"name": "upstream1",
"description": null,
"group_id": 22,
"url": "https://sample.com",
"username": "testuser",
"cache_validity_hours": 24,
"created_at": "2025-09-20T10:15:40.258Z",
"updated_at": "2025-09-20T10:15:40.258Z",
"registry_upstream": {
"id": 9,
"position": 1,
"registry_id": 3
}
}
]
3. Creating a new virtual registry: POST /virtual_registries/container/registries/:registry_id/upstreams
Feel free to update the parameters.
curl --location 'http://gdk.test:3000/api/v4/virtual_registries/container/registries/3/upstreams' \
--header 'PRIVATE-TOKEN: glpat-xxxxx' \
--form 'url="https://sample.com"' \
--form 'username="testuser"' \
--form 'password="testpass"' \
--form 'name="upstream1"'
Result will look something like:
{
"id": 8,
"name": "upstream1",
"description": null,
"group_id": 22,
"url": "https://sample.com",
"username": "testuser",
"cache_validity_hours": 24,
"created_at": "2025-09-20T10:15:40.258Z",
"updated_at": "2025-09-20T10:15:40.258Z",
"registry_upstream": {
"id": 9,
"position": 1,
"registry_id": 3
}
}
You can try the GET request again from (1) and you should see this newly created upstream as a part of the list.
GET /virtual_registries/container/upstreams/:id
4. Get the details of a virtual registry upstream: curl --location 'http://gdk.test:3000/api/v4/virtual_registries/container/upstreams/8' \
--header 'PRIVATE-TOKEN: glpat-xxxxx'
The result would look like:
{
"id": 8,
"name": "upstream1",
"description": null,
"group_id": 22,
"url": "https://sample.com",
"username": "testuser",
"cache_validity_hours": 24,
"created_at": "2025-09-20T10:15:40.258Z",
"updated_at": "2025-09-20T10:15:40.258Z",
"registry_upstreams": [
{
"id": 9,
"position": 1,
"registry_id": 3
}
]
}
5. Updating a virtual registry upstream: PATCH /virtual_registries/container/upstreams/:id
curl --location --request PATCH 'http://gdk.test:3000/api/v4/virtual_registries/container/upstreams/8' \
--header 'PRIVATE-TOKEN: glpat-xxxxx' \
--form 'name="upstream1"'
Result would be a 200
if successful.
You can try doing the GET request in (3) and should see the updated values.
6. Deleting a virtual registry: DELETE /virtual_registries/container/upstreams/:id
curl --location --request DELETE 'http://gdk.test:3000/api/v4/virtual_registries/container/upstreams/8' \
--header 'PRIVATE-TOKEN: glpat-xxxxx'
Result would be a 204 No Content
if successful.
You can try again the the request in (1) to list all the virtual registries of the group and the deleted virtual registry would no longer be a part of it.
🍎 Database Query Plans
-
👉 Inserting an upstream
SQL Query
INSERT INTO "virtual_registries_container_upstreams" ("group_id", "created_at", "updated_at", "username", "password", "url", "name")
VALUES (22, '2025-09-20 13:18:49.227018', '2025-09-20 13:18:49.227018', '{"p":"26ArqW6I","h":{"iv":"I85F5wMkNQ5orxTV","at":"F1Mva8QLHleaj0mM0OdUZg==","i":"YTdjNg=="}}', '{"p":"TBtmevy4","h":{"iv":"+DiRCa3YNEJjENEv","at":"IW0JmKgvIPyMILzKyy4qag==","i":"YTdjNg=="}}', 'https://aa.com', 'test')
RETURNING
"id"
Execution Plan
ModifyTable on public.virtual_registries_container_upstreams (cost=0.00..0.01 rows=1 width=194) (actual time=0.673..0.675 rows=1 loops=1)
Buffers: shared hit=78 read=4 dirtied=6 written=3
WAL: records=7 fpi=0 bytes=811
I/O Timings: read=0.119 write=0.060
-> Result (cost=0.00..0.01 rows=1 width=194) (actual time=0.166..0.166 rows=1 loops=1)
Buffers: shared hit=13 read=1 dirtied=1
WAL: records=1 fpi=0 bytes=99
I/O Timings: read=0.051 write=0.000
Trigger RI_ConstraintTrigger_c_1067341121 for constraint fk_rails_c97afd8bbd: time=2.691 calls=1
Settings: effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', work_mem = '100MB'
Details and visualization: https://postgres.ai/console/gitlab/gitlab-production-main/sessions/43631/commands/133259
-
👉 Updating a registry upstream
SQL Query
UPDATE
"virtual_registries_container_upstreams"
SET
"group_id" = 22,
"updated_at" = '2025-09-20 13:33:35.651236',
"name" = 'asdf'
WHERE
"virtual_registries_container_upstreams"."id" = 1
Execution Plan
ModifyTable on public.virtual_registries_container_upstreams (cost=0.14..3.16 rows=0 width=0) (actual time=0.022..0.023 rows=0 loops=1)
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
-> Index Scan using virtual_registries_container_upstreams_pkey on public.virtual_registries_container_upstreams (cost=0.14..3.16 rows=1 width=54) (actual time=0.021..0.021 rows=0 loops=1)
Index Cond: (virtual_registries_container_upstreams.id = 1)
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
Settings: work_mem = '100MB', effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4'
Details and visualization: https://postgres.ai/console/gitlab/gitlab-production-main/sessions/43631/commands/133260
-
👉 Deleting a registry upstream
SQL Query
DELETE FROM "virtual_registries_container_upstreams"
WHERE "virtual_registries_container_upstreams"."id" = 1
Execution Plan
ModifyTable on public.virtual_registries_container_upstreams (cost=0.14..3.16 rows=0 width=0) (actual time=0.017..0.018 rows=0 loops=1)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
-> Index Scan using virtual_registries_container_upstreams_pkey on public.virtual_registries_container_upstreams (cost=0.14..3.16 rows=1 width=6) (actual time=0.016..0.016 rows=0 loops=1)
Index Cond: (virtual_registries_container_upstreams.id = 1)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
Settings: effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', work_mem = '100MB'
Details and visualization: https://postgres.ai/console/gitlab/gitlab-production-main/sessions/43631/commands/133263
-
👉 Fetching upstreams of the group (max 5 upstreams per registry, and max 5 registry per group)
SQL Query
SELECT
"virtual_registries_container_upstreams".*
FROM
"virtual_registries_container_upstreams"
WHERE
"virtual_registries_container_upstreams"."group_id" = 22
Execution Plan
Index Scan using virtual_registries_container_upstreams_on_group_id on public.virtual_registries_container_upstreams (cost=0.14..3.16 rows=1 width=194) (actual time=0.027..0.027 rows=0 loops=1)
Index Cond: (virtual_registries_container_upstreams.group_id = 22)
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
Settings: work_mem = '100MB', effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4'
Details and visualization:: https://postgres.ai/console/gitlab/gitlab-production-main/sessions/43631/commands/133261
-
👉 Fetching upstreams of a virtual registry (max 5 upstreams per registry)
SQL Query
SELECT
"virtual_registries_container_upstreams"."id" AS t0_r0,
"virtual_registries_container_upstreams"."group_id" AS t0_r1,
"virtual_registries_container_upstreams"."created_at" AS t0_r2,
"virtual_registries_container_upstreams"."updated_at" AS t0_r3,
"virtual_registries_container_upstreams"."cache_validity_hours" AS t0_r4,
"virtual_registries_container_upstreams"."username" AS t0_r5,
"virtual_registries_container_upstreams"."password" AS t0_r6,
"virtual_registries_container_upstreams"."url" AS t0_r7,
"virtual_registries_container_upstreams"."name" AS t0_r8,
"virtual_registries_container_upstreams"."description" AS t0_r9,
"registry_upstreams"."id" AS t1_r0,
"registry_upstreams"."group_id" AS t1_r1,
"registry_upstreams"."registry_id" AS t1_r2,
"registry_upstreams"."upstream_id" AS t1_r3,
"registry_upstreams"."created_at" AS t1_r4,
"registry_upstreams"."updated_at" AS t1_r5,
"registry_upstreams"."position" AS t1_r6
FROM
"virtual_registries_container_upstreams"
LEFT OUTER JOIN "virtual_registries_container_registry_upstreams" "registry_upstreams" ON "registry_upstreams"."upstream_id" = "virtual_registries_container_upstreams"."id"
WHERE
"registry_upstreams"."registry_id" = 5
AND "virtual_registries_container_upstreams"."id" = 9
Execution Plan
Nested Loop (cost=0.28..6.33 rows=1 width=244) (actual time=0.018..0.019 rows=0 loops=1)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
-> Index Scan using virtual_registries_container_upstreams_pkey on public.virtual_registries_container_upstreams (cost=0.14..3.16 rows=1 width=194) (actual time=0.017..0.017 rows=0 loops=1)
Index Cond: (virtual_registries_container_upstreams.id = 9)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
-> Index Scan using constraint_vreg_container_reg_upst_on_unique_reg_pos on public.virtual_registries_container_registry_upstreams registry_upstreams (cost=0.14..3.16 rows=1 width=50) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: (registry_upstreams.registry_id = 5)
Filter: (registry_upstreams.upstream_id = 9)
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', work_mem = '100MB', effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5'
Details and visualization:: https://postgres.ai/console/gitlab/gitlab-production-main/sessions/43631/commands/133262
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to #548794 (closed)