Skip to content

Commit 674ab6b

Browse files
authored
Merge pull request #139 from eScienceLab/develop
Profiles Container with 5-Safes Profile
2 parents abe30e8 + 7697b5e commit 674ab6b

File tree

6 files changed

+167
-39
lines changed

6 files changed

+167
-39
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Create and publish a Docker image (with profiles)
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: ${{ github.repository }}-fivesafes-profile
11+
12+
jobs:
13+
build-and-push-image:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
packages: write
18+
attestations: write
19+
id-token: write
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: Log in to the Container registry
25+
uses: docker/login-action@v3
26+
with:
27+
registry: ${{ env.REGISTRY }}
28+
username: ${{ github.actor }}
29+
password: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Extract metadata (tags, labels) for Docker
32+
id: meta
33+
uses: docker/metadata-action@v5
34+
with:
35+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
36+
37+
- name: Build and push Docker image
38+
id: push
39+
uses: docker/build-push-action@v6
40+
with:
41+
context: .
42+
file: ./Dockerfile.fivesafes-profile
43+
push: true
44+
tags: ${{ steps.meta.outputs.tags }}
45+
labels: ${{ steps.meta.outputs.labels }}
46+
47+
- name: Generate artifact attestation
48+
uses: actions/attest-build-provenance@v2
49+
with:
50+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
51+
subject-digest: ${{ steps.push.outputs.digest }}
52+
push-to-registry: true

Dockerfile.fivesafes-profile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
FROM python:3.11-slim
2+
3+
ARG FIVE_SAFES_PROFILE_VERSION=five-safes-0.7.4-beta
4+
ARG PROFILES_ARCHIVE_URL=https://github.com/eScienceLab/rocrate-validator/archive/refs/tags/${FIVE_SAFES_PROFILE_VERSION}.tar.gz
5+
ARG PY_VER=3.11
6+
7+
# Install required system packages, including git
8+
RUN apt-get update && apt-get install -y git wget && rm -rf /var/lib/apt/lists/*
9+
10+
WORKDIR /app
11+
12+
COPY requirements.txt .
13+
RUN pip install --upgrade pip
14+
RUN pip install --no-cache-dir -r requirements.txt
15+
16+
COPY cratey.py LICENSE /app/
17+
COPY app /app/app
18+
RUN <<EOF_WRF
19+
wget -O /tmp/rocrate-validator-profiles.tar.gz "$PROFILES_ARCHIVE_URL"
20+
tar -xzf /tmp/rocrate-validator-profiles.tar.gz \
21+
-C /usr/local/lib/python${PY_VER}/site-packages/rocrate_validator/profiles/ \
22+
--strip-components=3 \
23+
"rocrate-validator-${FIVE_SAFES_PROFILE_VERSION}/rocrate_validator/profiles/five-safes-crate"
24+
rm /tmp/rocrate-validator-profiles.tar.gz
25+
EOF_WRF
26+
27+
RUN useradd -ms /bin/bash flaskuser
28+
RUN chown -R flaskuser:flaskuser /app
29+
30+
ENV FIVE_SAFES_PROFILE_VERSION=${FIVE_SAFES_PROFILE_VERSION}
31+
32+
USER flaskuser
33+
34+
EXPOSE 5000
35+
36+
CMD ["flask", "run", "--host=0.0.0.0"]
37+
38+
LABEL org.opencontainers.image.source="https://github.com/eScienceLab/Cratey-Validator"
39+
LABEL org.cratey.five-safes-profile-version="${FIVE_SAFES_PROFILE_VERSION}"

app/tasks/validation_tasks.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
get_validation_status_from_minio,
2121
get_minio_client,
2222
find_rocrate_object_on_minio,
23-
find_validation_object_on_minio
23+
find_validation_object_on_minio,
2424
)
2525
from app.utils.webhook_utils import send_webhook_notification
2626

@@ -29,8 +29,12 @@
2929

3030
@celery.task
3131
def process_validation_task_by_id(
32-
minio_config: dict, crate_id: str, root_path: str, profile_name: str | None,
33-
webhook_url: str | None, profiles_path: str | None
32+
minio_config: dict,
33+
crate_id: str,
34+
root_path: str,
35+
profile_name: str | None,
36+
webhook_url: str | None,
37+
profiles_path: str | None,
3438
) -> None:
3539
"""
3640
Background task to process the RO-Crate validation by ID.
@@ -52,12 +56,16 @@ def process_validation_task_by_id(
5256

5357
try:
5458
# Fetch the RO-Crate from MinIO using the provided ID:
55-
file_path = fetch_ro_crate_from_minio(minio_client, minio_config["bucket"], crate_id, root_path)
59+
file_path = fetch_ro_crate_from_minio(
60+
minio_client, minio_config["bucket"], crate_id, root_path
61+
)
5662

5763
logging.info(f"Processing validation task for {file_path}")
5864

5965
# Perform validation:
60-
validation_result = perform_ro_crate_validation(file_path, profile_name, profiles_path=profiles_path)
66+
validation_result = perform_ro_crate_validation(
67+
file_path, profile_name, profiles_path=profiles_path
68+
)
6169

6270
if isinstance(validation_result, str):
6371
logging.error(f"Validation failed: {validation_result}")
@@ -70,7 +78,13 @@ def process_validation_task_by_id(
7078
logging.info(f"RO Crate {crate_id} is invalid.")
7179

7280
# Update the validation status in MinIO:
73-
update_validation_status_in_minio(minio_client, minio_config["bucket"], crate_id, root_path, validation_result.to_json())
81+
update_validation_status_in_minio(
82+
minio_client,
83+
minio_config["bucket"],
84+
crate_id,
85+
root_path,
86+
validation_result.to_json(),
87+
)
7488

7589
# TODO: Prepare the data to send to the webhook, and send the webhook notification.
7690

@@ -98,7 +112,10 @@ def process_validation_task_by_id(
98112

99113
@celery.task
100114
def process_validation_task_by_metadata(
101-
crate_json: str, profile_name: str | None, webhook_url: str | None, profiles_path: Optional[str] = None
115+
crate_json: str,
116+
profile_name: str | None,
117+
webhook_url: str | None,
118+
profiles_path: Optional[str] = None,
102119
) -> ValidationResult | str:
103120
"""
104121
Background task to process the RO-Crate validation for a given json metadata string.
@@ -116,10 +133,9 @@ def process_validation_task_by_metadata(
116133
logging.info("Processing validation task for provided metadata string")
117134

118135
# Perform validation:
119-
validation_result = perform_metadata_validation(crate_json,
120-
profile_name,
121-
profiles_path
122-
)
136+
validation_result = perform_metadata_validation(
137+
crate_json, profile_name, profiles_path=profiles_path
138+
)
123139

124140
if isinstance(validation_result, str):
125141
logging.error(f"Validation failed: {validation_result}")
@@ -150,7 +166,10 @@ def process_validation_task_by_metadata(
150166

151167

152168
def perform_ro_crate_validation(
153-
file_path: str, profile_name: str | None, skip_checks_list: Optional[list] = None, profiles_path: Optional[str] = None
169+
file_path: str,
170+
profile_name: str | None,
171+
skip_checks_list: Optional[list] = None,
172+
profiles_path: Optional[str] = None,
154173
) -> ValidationResult | str:
155174
"""
156175
Validates an RO-Crate using the provided file path and profile name.
@@ -177,7 +196,7 @@ def perform_ro_crate_validation(
177196
rocrate_uri=full_file_path,
178197
**({"profile_identifier": profile_name} if profile_name else {}),
179198
**({"skip_checks": skip_checks_list} if skip_checks_list else {}),
180-
**({"profiles_path": profiles_path} if profiles_path else {})
199+
**({"profiles_path": profiles_path} if profiles_path else {}),
181200
)
182201

183202
return services.validate(settings)
@@ -188,7 +207,10 @@ def perform_ro_crate_validation(
188207

189208

190209
def perform_metadata_validation(
191-
crate_json: str, profile_name: str | None, skip_checks_list: Optional[list] = None, profiles_path: Optional[str] = None
210+
crate_json: str,
211+
profile_name: str | None,
212+
skip_checks_list: Optional[list] = None,
213+
profiles_path: Optional[str] = None,
192214
) -> ValidationResult | str:
193215
"""
194216
Validates only RO-Crate metadata provided as a json string.
@@ -210,7 +232,7 @@ def perform_metadata_validation(
210232
**({"metadata_dict": json.loads(crate_json)}),
211233
**({"profile_identifier": profile_name} if profile_name else {}),
212234
**({"skip_checks": skip_checks_list} if skip_checks_list else {}),
213-
**({"profiles_path": profiles_path} if profiles_path else {})
235+
**({"profiles_path": profiles_path} if profiles_path else {}),
214236
)
215237

216238
return services.validate(settings)
@@ -221,10 +243,10 @@ def perform_metadata_validation(
221243

222244

223245
def check_ro_crate_exists(
224-
minio_client: object,
225-
bucket_name: str,
226-
crate_id: str,
227-
root_path: str,
246+
minio_client: object,
247+
bucket_name: str,
248+
crate_id: str,
249+
root_path: str,
228250
) -> bool:
229251
"""
230252
Checks for the existence of an RO-Crate using the provided Crate ID.
@@ -245,10 +267,10 @@ def check_ro_crate_exists(
245267

246268

247269
def check_validation_exists(
248-
minio_client: object,
249-
bucket_name: str,
250-
crate_id: str,
251-
root_path: str,
270+
minio_client: object,
271+
bucket_name: str,
272+
crate_id: str,
273+
root_path: str,
252274
) -> bool:
253275
"""
254276
Checks for the existence of a validation result using the provided Crate ID.
@@ -269,10 +291,10 @@ def check_validation_exists(
269291

270292

271293
def return_ro_crate_validation(
272-
minio_client: object,
273-
bucket_name: str,
274-
crate_id: str,
275-
root_path: str,
294+
minio_client: object,
295+
bucket_name: str,
296+
crate_id: str,
297+
root_path: str,
276298
) -> dict | str:
277299
"""
278300
Retrieves the validation result for an RO-Crate using the provided Crate ID.
@@ -284,4 +306,6 @@ def return_ro_crate_validation(
284306

285307
logging.info(f"Fetching validation result for RO-Crate {crate_id}")
286308

287-
return get_validation_status_from_minio(minio_client, bucket_name, crate_id, root_path)
309+
return get_validation_status_from_minio(
310+
minio_client, bucket_name, crate_id, root_path
311+
)

requirements.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ minio==7.2.20
33
requests==2.32.5
44
Flask==3.1.3
55
Werkzeug==3.1.6
6-
redis==7.2.0
7-
python-dotenv==1.2.1
6+
redis==7.3.0
7+
python-dotenv==1.2.2
88
apiflask==3.0.2
99
roc-validator==0.8.1

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,14 @@ pyshacl==0.30.1
138138
# via roc-validator
139139
python-dateutil==2.9.0.post0
140140
# via celery
141-
python-dotenv==1.2.1
141+
python-dotenv==1.2.2
142142
# via -r requirements.in
143143
rdflib[html]==7.1.4
144144
# via
145145
# owlrl
146146
# pyshacl
147147
# roc-validator
148-
redis==7.2.0
148+
redis==7.3.0
149149
# via -r requirements.in
150150
requests==2.32.5
151151
# via

tests/test_validation_tasks.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,18 @@ def test_process_validation_failure(
229229
# Test function: process_validation_task_by_metadata
230230

231231
@pytest.mark.parametrize(
232-
"crate_json, profile_name, webhook_url, validation_json, validation_value",
232+
"crate_json, profile_name, webhook_url, profiles_path, validation_json, validation_value",
233233
[
234234
(
235235
'{"@context": "https://w3id.org/ro/crate/1.1/context", "@graph": []}',
236236
"test-profile", "https://example.com/webhook",
237+
"/app/profiles",
237238
'{"status": "valid"}', False
238239
),
239240
(
240241
'{"@context": "https://w3id.org/ro/crate/1.1/context", "@graph": []}',
241242
"test-profile", "https://example.com/webhook",
243+
None,
242244
'{"status": "invalid"}', True
243245
)
244246
],
@@ -248,32 +250,38 @@ def test_process_validation_failure(
248250
@mock.patch("app.tasks.validation_tasks.perform_metadata_validation")
249251
def test_metadata_validation(
250252
mock_validate, mock_webhook,
251-
crate_json: str, profile_name: str, webhook_url: str,
253+
crate_json: str, profile_name: str, webhook_url: str, profiles_path: str | None,
252254
validation_json: str, validation_value: bool,
253255
):
254256
mock_result = mock.Mock()
255257
mock_result.has_issues.return_value = validation_value
256258
mock_result.to_json.return_value = validation_json
257259
mock_validate.return_value = mock_result
258260

259-
result = process_validation_task_by_metadata(crate_json, profile_name, webhook_url)
261+
result = process_validation_task_by_metadata(
262+
crate_json, profile_name, webhook_url, profiles_path
263+
)
260264

261265
assert result == validation_json
262-
mock_validate.assert_called_once()
266+
mock_validate.assert_called_once_with(
267+
crate_json, profile_name, profiles_path=profiles_path
268+
)
263269
mock_webhook.assert_called_once_with(webhook_url, validation_json)
264270

265271

266272
@pytest.mark.parametrize(
267-
"crate_json, profile_name, webhook_url, validation_message",
273+
"crate_json, profile_name, webhook_url, profiles_path, validation_message",
268274
[
269275
(
270276
'{"@context": "https://w3id.org/ro/crate/1.1/context", "@graph": []}',
271277
"test-profile", "https://example.com/webhook",
278+
"/app/profiles",
272279
"Validation error"
273280
),
274281
(
275282
'{"@context": "https://w3id.org/ro/crate/1.1/context", "@graph": []}',
276283
"test-profile", None,
284+
None,
277285
"Validation error"
278286
)
279287
],
@@ -283,16 +291,21 @@ def test_metadata_validation(
283291
@mock.patch("app.tasks.validation_tasks.perform_metadata_validation")
284292
def test_validation_fails_and_sends_error_notification_to_webhook(
285293
mock_validate, mock_webhook,
286-
crate_json: str, profile_name: str, webhook_url: str,
294+
crate_json: str, profile_name: str, webhook_url: str, profiles_path: str | None,
287295
validation_message: str
288296
):
289297

290298
mock_validate.return_value = validation_message
291299

292-
result = process_validation_task_by_metadata(crate_json, profile_name, webhook_url)
300+
result = process_validation_task_by_metadata(
301+
crate_json, profile_name, webhook_url, profiles_path
302+
)
293303

294304
assert isinstance(result, str)
295305
assert validation_message in result
306+
mock_validate.assert_called_once_with(
307+
crate_json, profile_name, profiles_path=profiles_path
308+
)
296309

297310
if webhook_url is not None:
298311
# Error webhook should be sent

0 commit comments

Comments
 (0)