Skip to content

Commit dfe2dbb

Browse files
committed
Allow users to delete transform requests
This is accomplished by adding a new boolean to the transform request table, archived. Setting archived to True removes the transform for reports and requests. When a transform is archived, the transform results table records are deleted and the contents of the bucket that held the transformed files are deleted, along with that bucket.
1 parent a49902d commit dfe2dbb

File tree

18 files changed

+266
-9
lines changed

18 files changed

+266
-9
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
Mark dataset rows as stale
3+
4+
Revision ID: v1_6_0
5+
Revises: v1_5_5
6+
Create Date: 2024-11-14 18:19:00.000000
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'v1_6_0'
14+
down_revision = 'v1_5_5'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.add_column('requests', sa.Column('archived',
21+
sa.Boolean(),
22+
nullable=False,
23+
server_default='false'))
24+
25+
def downgrade():
26+
op.drop_column('requests', 'archived')

servicex_app/servicex_app/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ class TransformRequest(db.Model):
168168
request_id = db.Column(db.String(48), unique=True, nullable=False, index=True)
169169
title = db.Column(db.String(128), nullable=True)
170170
submit_time = db.Column(db.DateTime, nullable=False)
171+
archived = db.Column(db.Boolean, nullable=False, default=False)
171172
finish_time = db.Column(db.DateTime, nullable=True)
172173
did = db.Column(db.String(512), unique=False, nullable=False)
173174
did_id = db.Column(db.Integer, unique=False, nullable=False)
@@ -321,6 +322,10 @@ def result_count(self) -> int:
321322
def results(self) -> List['TransformationResult']:
322323
return TransformationResult.query.filter_by(request_id=self.request_id).all()
323324

325+
def truncate_results(self):
326+
TransformationResult.query.filter_by(request_id=self.request_id).delete()
327+
db.session.commit()
328+
324329
@property
325330
def all_files(self) -> List['DatasetFile']:
326331
return DatasetFile.query.filter_by(dataset_id=self.did_id).all()

servicex_app/servicex_app/object_store_manager.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,15 @@ def create_bucket(self, bucket_name):
3939

4040
def list_buckets(self):
4141
return self.minio_client.list_buckets()
42+
43+
def delete_bucket_and_contents(self, bucket_name):
44+
# List all objects in the bucket
45+
objects = self.minio_client.list_objects(bucket_name, recursive=True)
46+
47+
# Remove each object
48+
for obj in objects:
49+
self.minio_client.remove_object(bucket_name, obj.object_name)
50+
51+
# Remove the bucket itself
52+
self.minio_client.remove_bucket(bucket_name)
53+
print(f"Bucket '{bucket_name}' deleted successfully.")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (c) 2024, IRIS-HEP
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# * Redistributions of source code must retain the above copyright notice, this
8+
# list of conditions and the following disclaimer.
9+
#
10+
# * Redistributions in binary form must reproduce the above copyright notice,
11+
# this list of conditions and the following disclaimer in the documentation
12+
# and/or other materials provided with the distribution.
13+
#
14+
# * Neither the name of the copyright holder nor the names of its
15+
# contributors may be used to endorse or promote products derived from
16+
# this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
from servicex_app import ObjectStoreManager
29+
from servicex_app.decorators import auth_required
30+
from servicex_app.models import TransformRequest
31+
from servicex_app.resources.servicex_resource import ServiceXResource
32+
from flask import current_app
33+
34+
35+
class ArchiveTransform(ServiceXResource):
36+
@classmethod
37+
def make_api(cls, object_store_manager: ObjectStoreManager):
38+
cls.object_store = object_store_manager
39+
40+
@auth_required
41+
def delete(self, request_id: str):
42+
transform_req = TransformRequest.lookup(request_id)
43+
if not transform_req:
44+
msg = f'Transformation request not found with id: {request_id}'
45+
current_app.logger.warning(msg, extra={'requestId': request_id})
46+
return {'message': msg}, 404
47+
48+
if not transform_req.status.is_complete:
49+
msg = f"Transform request with id {request_id} is still in progress."
50+
current_app.logger.warning(msg, extra={'requestId': request_id})
51+
return {"message": msg}, 400
52+
53+
user = self.get_requesting_user()
54+
if user and (not user.admin and user.id != transform_req.submitted_by):
55+
return {"message": "You are not authorized to delete this request"}, 403
56+
57+
transform_req.archived = True
58+
transform_req.save_to_db()
59+
transform_req.truncate_results()
60+
if self.object_store:
61+
self.object_store.delete_bucket_and_contents(transform_req.request_id)
62+
return {
63+
"message": f"Transform request with id {request_id} has been archived."
64+
}, 200

servicex_app/servicex_app/resources/transformation/get_all.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ def get(self):
5050
transforms = TransformRequest.query.filter_by(submitted_by=query_id)
5151
else:
5252
current_app.logger.debug("Querying for all transform requests")
53-
transforms = TransformRequest.query.all()
53+
transforms = TransformRequest.query.filter_by(archived=False)
5454
return TransformRequest.return_json(transforms)

servicex_app/servicex_app/resources/transformation/get_one.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ def get(self, request_id):
4444
msg = f'Transformation request not found with id: {request_id}'
4545
current_app.logger.error(msg, extra={'requestId': request_id})
4646
return {'message': msg}, 404
47+
48+
if transform.archived:
49+
msg = f'Transformation request with id: {request_id} is archived'
50+
current_app.logger.error(msg, extra={'requestId': request_id})
51+
return {'message': msg}, 404
52+
4753
transform_json = transform.to_json()
4854
if current_app.config['OBJECT_STORE_ENABLED'] and \
4955
transform_json['result-destination'] == TransformRequest.OBJECT_STORE_DEST:

servicex_app/servicex_app/resources/transformation/status.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def get(self, request_id):
4747
current_app.logger.error(msg, extra={'requestId': request_id})
4848
return {'message': msg}, 404
4949

50+
if transform.archived:
51+
msg = f'Transformation request with id: {request_id} is archived'
52+
current_app.logger.error(msg, extra={'requestId': request_id})
53+
return {'message': msg}, 404
54+
5055
status_request = status_request_parser.parse_args()
5156

5257
# Format timestamps with military timezone, given that they are in UTC.

servicex_app/servicex_app/resources/transformation/submit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def post(self):
176176
request_rec = TransformRequest(
177177
request_id=str(request_id),
178178
title=args.get("title"),
179+
archived=False,
179180
did=dataset_manager.name,
180181
did_id=dataset_manager.id,
181182
submit_time=datetime.now(tz=timezone.utc),

servicex_app/servicex_app/routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from servicex_app.resources.datasets.delete_dataset import DeleteDataset
3131
from servicex_app.resources.datasets.get_all import AllDatasets
3232
from servicex_app.resources.datasets.get_one import OneDataset
33+
from servicex_app.resources.transformation.archive import ArchiveTransform
3334

3435

3536
def add_routes(api, transformer_manager, rabbit_mq_adaptor,
@@ -137,6 +138,10 @@ def add_routes(api, transformer_manager, rabbit_mq_adaptor,
137138
api.add_resource(AllTransformationRequests, prefix)
138139
prefix += "/<string:request_id>"
139140
api.add_resource(TransformationRequest, prefix)
141+
142+
ArchiveTransform.make_api(object_store)
143+
api.add_resource(ArchiveTransform, prefix)
144+
140145
api.add_resource(TransformationStatus, prefix + "/status")
141146

142147
DeploymentStatus.make_api(transformer_manager)

servicex_app/servicex_app/web/dashboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
def dashboard(template_name: str, user_specific=False):
3333
args = parser.parse_args()
3434
sort, order = args["sort"], args["order"]
35-
query = TransformRequest.query
35+
query = TransformRequest.query.filter_by(archived=False)
3636

3737
if user_specific:
3838
query = query.filter_by(submitted_by=session["user_id"])

0 commit comments

Comments
 (0)