Skip to content

Commit 6b125bb

Browse files
authored
[ENG-847] add project_deleter (#985)
1 parent b5e3e34 commit 6b125bb

File tree

14 files changed

+502
-172
lines changed

14 files changed

+502
-172
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ test_unit:
5151
test_integration: docker_build
5252
docker compose -f tests/docker/docker-compose.yaml pull -q; \
5353
docker compose --project-directory=`pwd` -f tests/docker/docker-compose.yaml up -d; \
54+
bash tests/integration/project_deleter_fixture.sh; \
5455
poetry run pytest -vv tests/integration; \
5556
exit_code=$$?; \
5657
docker compose -f tests/docker/docker-compose.yaml kill; \

charts/platform-registry/templates/deployment.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ spec:
6565
- name: NP_REGISTRY_AUTH_TOKEN
6666
{{- if .Values.platform.token }}
6767
{{ toYaml .Values.platform.token | indent 10 }}
68+
{{- end }}
69+
- name: NP_REGISTRY_EVENTS_URL
70+
value: {{ .Values.platform.eventsUrl }}
71+
- name: NP_REGISTRY_EVENTS_TOKEN
72+
{{- if .Values.platform.token }}
73+
{{- toYaml .Values.platform.token | nindent 10 }}
6874
{{- end }}
6975
{{- if .Values.sentry }}
7076
- name: SENTRY_DSN

charts/platform-registry/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ replicas: 2
99
platform:
1010
clusterName: ""
1111
authUrl: ""
12+
eventsUrl: http://platform-events:8080/apis/events
1213
token: {}
1314

1415
upstreamRegistry:

platform_registry_api/api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
)
3939
from pydantic import BaseModel, ConfigDict, ValidationError
4040

41+
from platform_registry_api.project_deleter import ProjectDeleter
42+
4143
from .config import (
4244
Config,
4345
EnvironConfigFactory,
@@ -260,8 +262,14 @@ async def on_request_redirect(
260262

261263
app[V2_APP][UPSTREAM_CLIENT] = upstream_client
262264

265+
deleter = await exit_stack.enter_async_context(
266+
ProjectDeleter(upstream_client, config.events)
267+
)
268+
263269
yield
264270

271+
await deleter.aclose()
272+
265273
app.cleanup_ctx.append(_init_app)
266274

267275
root_handler = RootHandler()

platform_registry_api/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from enum import Enum
66
from typing import Any
77

8+
from apolo_events_client import EventsClientConfig
89
from yarl import URL
910

1011

@@ -67,6 +68,7 @@ class Config:
6768
upstream_registry: UpstreamRegistryConfig
6869
auth: AuthConfig
6970
cluster_name: str
71+
events: EventsClientConfig | None = None
7072

7173

7274
class EnvironConfigFactory:
@@ -143,15 +145,24 @@ def create_auth(self) -> AuthConfig:
143145
token = self._environ["NP_REGISTRY_AUTH_TOKEN"]
144146
return AuthConfig(server_endpoint_url=url, service_token=token)
145147

148+
def create_events(self) -> EventsClientConfig | None:
149+
if "NP_REGISTRY_EVENTS_URL" in self._environ:
150+
url = URL(self._environ["NP_REGISTRY_EVENTS_URL"])
151+
token = self._environ["NP_REGISTRY_EVENTS_TOKEN"]
152+
return EventsClientConfig(url=url, token=token, name="platform-registry")
153+
return None
154+
146155
def create(self) -> Config:
147156
server_config = self.create_server()
148157
upstream_registry_config = self.create_upstream_registry()
149158
auth_config = self.create_auth()
150159
cluster_name = self._environ["NP_CLUSTER_NAME"]
160+
events = self.create_events()
151161
assert cluster_name
152162
return Config(
153163
server=server_config,
154164
upstream_registry=upstream_registry_config,
155165
auth=auth_config,
156166
cluster_name=cluster_name,
167+
events=events,
157168
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import logging
2+
from typing import Self
3+
4+
from apolo_events_client import (
5+
EventsClientConfig,
6+
EventType,
7+
RecvEvent,
8+
StreamType,
9+
from_config,
10+
)
11+
12+
from .upstream_client import UpstreamV2ApiClient
13+
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class ProjectDeleter:
19+
ADMIN_STREAM = StreamType("platform-admin")
20+
PROJECT_REMOVE = EventType("project-remove")
21+
22+
def __init__(
23+
self, upstream_client: UpstreamV2ApiClient, config: EventsClientConfig | None
24+
) -> None:
25+
self._upstream_client = upstream_client
26+
self._client = from_config(config)
27+
28+
async def __aenter__(self) -> Self:
29+
logger.info("Subscribe for %r", self.ADMIN_STREAM)
30+
await self._client.subscribe_group(self.ADMIN_STREAM, self._on_admin_event)
31+
logger.info("Subscribed")
32+
return self
33+
34+
async def __aexit__(self, exc_typ: object, exc_val: object, exc_tb: object) -> None:
35+
await self.aclose()
36+
37+
async def aclose(self) -> None:
38+
await self._client.aclose()
39+
40+
async def _on_admin_event(self, ev: RecvEvent) -> None:
41+
if ev.event_type == self.PROJECT_REMOVE:
42+
assert ev.org
43+
assert ev.project
44+
await self._upstream_client.delete_project_images(
45+
org=ev.org, project=ev.project
46+
)
47+
await self._client.ack({self.ADMIN_STREAM: [ev.tag]})

poetry.lock

Lines changed: 21 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ neuro-logging = "^25.6.1"
3232
yarl = "^1.18.3"
3333
pydantic = "^2.11.7"
3434
pyjwt = "^2.10.1"
35+
apolo-events-client = "^25.7.7"
3536

3637
[tool.poetry.group.dev]
3738
optional = true

tests/docker/docker-compose.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ services:
1313
volumes:
1414
- ./tests/docker/registry/config.yml:/etc/docker/registry/config.yml
1515
- ./tests/docker/docker_auth/server.pem:/etc/docker/registry/server.pem
16+
- registry_data:/var/lib/registry
17+
environment:
18+
- REGISTRY_STORAGE_DELETE_ENABLED=true
1619
links:
1720
- auth
1821

@@ -43,3 +46,6 @@ services:
4346
- auth
4447
- upstream
4548
- auth_server
49+
50+
volumes:
51+
registry_data:

tests/e2e/tests.sh

Lines changed: 1 addition & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -4,176 +4,7 @@ set -e
44
set -x
55
export SHELLOPTS
66

7-
CLUSTER_NAME=test-cluster
8-
9-
function fix_base64() {
10-
if command -v gbase64 >/dev/null 2>&1 ; then
11-
gbase64 "$@"
12-
else
13-
base64 "$@"
14-
fi
15-
}
16-
17-
ORG=test-org
18-
PROJECT=test-project
19-
20-
function generate_user_token() {
21-
local name=$1
22-
local auth_container=$(docker ps --filter name=auth_server -q)
23-
docker exec $auth_container platform-auth-make-token $name
24-
}
25-
26-
function create_regular_user() {
27-
local name=$1
28-
local data="{\"name\": \"$name\"}"
29-
curl --fail --data "$data" -H "Authorization: Bearer $ADMIN_TOKEN" \
30-
http://localhost:5003/api/v1/users
31-
# Grant permissions to the user images
32-
local url="http://localhost:5003/api/v1/users/$name/permissions"
33-
local data="[{\"uri\":\"image://$CLUSTER_NAME/$ORG\",\"action\":\"manage\"}]"
34-
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -d "$data" $url --fail
35-
}
36-
37-
function share_resource_on_read() {
38-
local resource=$1
39-
local who_token=$2
40-
local whom=$3
41-
local url="http://localhost:5003/api/v1/users/$whom/permissions"
42-
local data="[{\"uri\":$resource,\"action\":\"read\"}]"
43-
curl -s -X POST -H "Authorization: Bearer $who_token" -d "$data" $url --fail
44-
}
45-
46-
function wait_for_registry() {
47-
local cmd="curl http://127.0.0.1:5000/v2/ &> /dev/null"
48-
# this for loop waits until the registry api is available
49-
for _ in {1..150}; do # timeout for 5 minutes
50-
if eval "$cmd"; then
51-
break
52-
fi
53-
sleep 2
54-
done
55-
}
56-
57-
58-
function docker_login() {
59-
local name=$1
60-
local token=$2
61-
docker login -u $name -p $token 127.0.0.1:5000
62-
}
63-
64-
function test_push_catalog_pull() {
65-
echo -e "\n"
66-
67-
local name=$(uuidgen | awk '{print tolower($0)}')
68-
local token=$(generate_user_token $name)
69-
create_regular_user $name
70-
docker_login $name $token
71-
local repo_path="$ORG/$PROJECT"
72-
73-
echo "step 1: pull non existent"
74-
local output=$(docker pull 127.0.0.1:5000/$repo_path/unknown:latest 2>&1)
75-
[[ $output == *"manifest for 127.0.0.1:5000/$repo_path/unknown:latest not found"* ]]
76-
77-
echo "step 2: remove images and check catalog"
78-
docker rmi ubuntu:latest 127.0.0.1:5000/$repo_path/ubuntu:latest || :
79-
docker rmi alpine:latest 127.0.0.1:5000/$repo_path/alpine:latest || :
80-
test_catalog $name $token ""
81-
82-
echo "step 3: push ubuntu, check catalog"
83-
docker_tag_push $name $token "ubuntu"
84-
local expected="\"$repo_path/ubuntu\""
85-
test_catalog $name $token "$expected"
86-
test_repo_tags_list $name $token "$repo_path/ubuntu"
87-
88-
echo "step 4: push alpine, check catalog"
89-
docker_tag_push $name $token "alpine"
90-
local expected="\"$repo_path/alpine\", \"$repo_path/ubuntu\""
91-
test_catalog $name $token "$expected"
92-
93-
echo "step 5: remove ubuntu, check pull"
94-
docker rmi ubuntu:latest
95-
docker pull 127.0.0.1:5000/$repo_path/ubuntu:latest
96-
97-
echo "step 6: remove alpine, check pull"
98-
docker rmi alpine:latest
99-
docker pull 127.0.0.1:5000/$repo_path/alpine:latest
100-
}
101-
102-
103-
function docker_tag_push() {
104-
local name=$1
105-
local token=$2
106-
local image=$3
107-
docker pull $image:latest
108-
docker tag $image:latest 127.0.0.1:5000/$ORG/$PROJECT/$image:latest
109-
docker push 127.0.0.1:5000/$ORG/$PROJECT/$image:latest
110-
}
111-
112-
function test_catalog() {
113-
local name=$1
114-
local token=$2
115-
local expected="$3"
116-
local url="http://127.0.0.1:5000/v2/_catalog?org=$ORG&project=$PROJECT"
117-
local auth_basic_token=$(echo -n $name:$token | fix_base64 -w 0)
118-
local output=$(curl -sH "Authorization: Basic $auth_basic_token" $url)
119-
echo $output | grep -w "{\"repositories\": \[$expected\]}"
120-
}
121-
122-
function test_digest() {
123-
local name=$1
124-
local token=$2
125-
local image=$3
126-
local tag=$4
127-
local url="http://127.0.0.1:5000/v2/$image/manifests/$tag"
128-
local auth_basic_token=$(echo -n $name:$token | fix_base64 -w 0)
129-
local output=$(curl --verbose -sH "Authorization: Basic $auth_basic_token" $url 2>&1)
130-
echo $output | grep -w "Docker-Content-Digest:"
131-
}
132-
133-
function test_repo_tags_list() {
134-
local name=$1
135-
local token=$2
136-
local repo="$3"
137-
local url="http://127.0.0.1:5000/v2/$repo/tags/list"
138-
local auth_basic_token=$(echo -n $name:$token | fix_base64 -w 0)
139-
local output=$(curl -sH "Authorization: Basic $auth_basic_token" $url)
140-
echo $output | grep "\"name\": \"$repo\""
141-
echo $output | grep "\"tags\": \["
142-
}
143-
144-
function get_registry_token_for_catalog() {
145-
# the way to get auth token for accessing _catalog without using platform-registry-api:
146-
local username=$1
147-
local password=$2
148-
local registry_url=$3
149-
local service=$4
150-
local auth_url="$registry_url?service=$service&scope=registry:catalog:*"
151-
local auth_basic_token=$(echo -n $username:$password | fix_base64 -w 0)
152-
curl -sH "Authorization: Basic $auth_basic_token" "$auth_url" | jq -r .token
153-
# NOTE (A Yushkovskiy, 25.12.2018) Read materials:
154-
# - on docker registry auth protocol:
155-
# https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md
156-
# - on docker listing catalog REST API:
157-
# https://docs.docker.com/registry/spec/api/#listing-repositories
158-
# - examples of ACL rules for docker registry image:
159-
# https://github.com/cesanta/docker_auth/blob/master/examples/reference.yml
160-
}
161-
162-
function debug_docker_catalog_local() {
163-
local user=testuser
164-
local password=testpassword
165-
local registry_token=`get_registry_token_for_catalog "$user" "$password" "http://localhost:5001/auth" "upstream"`
166-
curl -sH "Authorization: Bearer $registry_token" "http://localhost:5002/v2/_catalog" | jq
167-
}
168-
169-
function debug_docker_catalog_gcr() {
170-
local user=$1
171-
local password=$2
172-
local registry_token=`get_registry_token_for_catalog "$user" "$password" "https://gcr.io/v2/token" "gcr.io"`
173-
curl -sH "Authorization: Bearer $registry_token" "https://gcr.io/v2/_catalog" | jq
174-
}
175-
176-
ADMIN_TOKEN=$(generate_user_token admin)
7+
source tests/test_utils.sh
1778

1789
wait_for_registry
17910

0 commit comments

Comments
 (0)