Skip to content

Commit a755b6c

Browse files
authored
feat: Delete granule endpoint (#485)
* breaking: using latest uds-lib + update docker * feat: use latest uds-lib * feat: add delete single granule endpoint * fix: deleting individually from real index. not from alias * feat: add test case * fix: add optional delete files flag * fix: wrong authorization action * fix: temp disable cumulus delete * fix: disable s3 deletion to see if cumulus delete it * fix: update test
1 parent d9ded15 commit a755b6c

File tree

6 files changed

+209
-3
lines changed

6 files changed

+209
-3
lines changed

cumulus_lambda_functions/cumulus_wrapper/query_granules.py

+44
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def __init__(self, cumulus_base: str, cumulus_token: str):
2121
super().__init__(cumulus_base, cumulus_token)
2222
self._conditions.append('status=completed')
2323
self._item_transformer = ItemTransformer()
24+
self.__collection_id = None
2425

2526
def with_filter(self, filter_key, filter_values: list):
2627
if len(filter_values) < 1:
@@ -34,6 +35,7 @@ def with_filter(self, filter_key, filter_values: list):
3435

3536
def with_collection_id(self, collection_id: str):
3637
self._conditions.append(f'{self.__collection_id_key}={collection_id}')
38+
self.__collection_id = collection_id
3739
return self
3840

3941
def with_bbox(self):
@@ -130,6 +132,48 @@ def query_direct_to_private_api(self, private_api_prefix: str, transform=True):
130132
return {'server_error': f'error while invoking:{str(e)}'}
131133
return {'results': stac_list}
132134

135+
def delete_entry(self, private_api_prefix: str, granule_id: str):
136+
payload = {
137+
'httpMethod': 'DELETE',
138+
'resource': '/{proxy+}',
139+
'path': f'/{self.__granules_key}/{self.__collection_id}/{granule_id}',
140+
'queryStringParameters': {**{k[0]: k[1] for k in [k1.split('=') for k1 in self._conditions]}},
141+
# 'queryStringParameters': {'limit': '30'},
142+
'headers': {
143+
'Content-Type': 'application/json',
144+
},
145+
# 'body': json.dumps({"action": "removeFromCmr"})
146+
}
147+
LOGGER.debug(f'payload: {payload}')
148+
try:
149+
query_result = self._invoke_api(payload, private_api_prefix)
150+
"""
151+
{'statusCode': 200, 'body': '{"meta":{"name":"cumulus-api","stack":"am-uds-dev-cumulus","table":"granule","limit":3,"page":1,"count":0},"results":[]}', 'headers': {'x-powered-by': 'Express', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubDomains', 'content-type': 'application/json; charset=utf-8', 'content-length': '120', 'etag': 'W/"78-YdHqDNIH4LuOJMR39jGNA/23yOQ"', 'date': 'Tue, 07 Jun 2022 22:30:44 GMT', 'connection': 'close'}, 'isBase64Encoded': False}
152+
"""
153+
if query_result['statusCode'] >= 500:
154+
LOGGER.error(f'server error status code: {query_result.statusCode}. details: {query_result}')
155+
return {'server_error': query_result}
156+
if query_result['statusCode'] >= 400:
157+
LOGGER.error(f'client error status code: {query_result.statusCode}. details: {query_result}')
158+
return {'client_error': query_result}
159+
query_result = json.loads(query_result['body'])
160+
LOGGER.info(f'json query_result: {query_result}')
161+
"""
162+
{
163+
"detail": "Record deleted"
164+
}
165+
"""
166+
if 'detail' not in query_result:
167+
LOGGER.error(f'missing key: detail. invalid response json: {query_result}')
168+
return {'server_error': f'missing key: detail. invalid response json: {query_result}'}
169+
if query_result['detail'] != 'Record deleted':
170+
LOGGER.error(f'Wrong Message: {query_result}')
171+
return {'server_error': f'Wrong Message: {query_result}'}
172+
except Exception as e:
173+
LOGGER.exception('error while invoking')
174+
return {'server_error': f'error while invoking:{str(e)}'}
175+
return {}
176+
133177
def query(self, transform=True):
134178
conditions_str = '&'.join(self._conditions)
135179
LOGGER.info(f'cumulus_base: {self.cumulus_base}')

cumulus_lambda_functions/lib/uds_db/granules_db_index.py

+17
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,23 @@ def get_entry(self, tenant: str, tenant_venue: str, doc_id: str, ):
201201
raise ValueError(f"no such granule: {doc_id}")
202202
return result
203203

204+
def delete_entry(self, tenant: str, tenant_venue: str, doc_id: str, ):
205+
read_alias_name = f'{DBConstants.granules_read_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
206+
result = self.__es.query({
207+
'size': 9999,
208+
'query': {'term': {'_id': doc_id}}
209+
}, read_alias_name)
210+
if result is None:
211+
raise ValueError(f"no such granule: {doc_id}")
212+
for each_granule in result['hits']['hits']:
213+
delete_result = self.__es.delete_by_query({
214+
'query': {'term': {'_id': each_granule['_id']}}
215+
}, each_granule['_index'])
216+
LOGGER.debug(f'delete_result: {delete_result}')
217+
if delete_result is None:
218+
raise ValueError(f"error deleting {each_granule}")
219+
return result
220+
204221
def update_entry(self, tenant: str, tenant_venue: str, json_body: dict, doc_id: str, ):
205222
write_alias_name = f'{DBConstants.granules_write_alias_prefix}_{tenant}_{tenant_venue}'.lower().strip()
206223
json_body['event_time'] = TimeUtils.get_current_unix_milli()

cumulus_lambda_functions/uds_api/granules_api.py

+50
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
import os
33
from typing import Union
44

5+
from mdps_ds_lib.lib.aws.aws_s3 import AwsS3
6+
from pystac import Item
7+
8+
from cumulus_lambda_functions.cumulus_wrapper.query_granules import GranulesQuery
9+
510
from cumulus_lambda_functions.daac_archiver.daac_archiver_logic import DaacArchiverLogic
611
from cumulus_lambda_functions.uds_api.dapa.daac_archive_crud import DaacArchiveCrud, DaacDeleteModel, DaacAddModel, \
712
DaacUpdateModel
@@ -239,6 +244,51 @@ async def get_single_granule_dapa(request: Request, collection_id: str, granule_
239244
raise HTTPException(status_code=500, detail=str(e))
240245
return granules_result
241246

247+
@router.delete("/{collection_id}/items/{granule_id}")
248+
@router.delete("/{collection_id}/items/{granule_id}/")
249+
async def delete_single_granule_dapa(request: Request, collection_id: str, granule_id: str):
250+
authorizer: UDSAuthorizorAbstract = UDSAuthorizerFactory() \
251+
.get_instance(UDSAuthorizerFactory.cognito,
252+
es_url=os.getenv('ES_URL'),
253+
es_port=int(os.getenv('ES_PORT', '443'))
254+
)
255+
auth_info = FastApiUtils.get_authorization_info(request)
256+
collection_identifier = UdsCollections.decode_identifier(collection_id)
257+
if not authorizer.is_authorized_for_collection(DBConstants.delete, collection_id,
258+
auth_info['ldap_groups'],
259+
collection_identifier.tenant,
260+
collection_identifier.venue):
261+
LOGGER.debug(f'user: {auth_info["username"]} is not authorized for {collection_id}')
262+
raise HTTPException(status_code=403, detail=json.dumps({
263+
'message': 'not authorized to execute this action'
264+
}))
265+
try:
266+
LOGGER.debug(f'deleting granule: {granule_id}')
267+
cumulus_lambda_prefix = os.getenv('CUMULUS_LAMBDA_PREFIX')
268+
cumulus = GranulesQuery('https://na/dev', 'NA')
269+
cumulus.with_collection_id(collection_id)
270+
cumulus_delete_result = cumulus.delete_entry(cumulus_lambda_prefix, granule_id) # TODO not sure it is correct granule ID
271+
LOGGER.debug(f'cumulus_delete_result: {cumulus_delete_result}')
272+
es_delete_result = GranulesDbIndex().delete_entry(collection_identifier.tenant,
273+
collection_identifier.venue,
274+
granule_id
275+
)
276+
LOGGER.debug(f'es_delete_result: {es_delete_result}')
277+
# es_delete_result = [Item.from_dict(k['_source']) for k in es_delete_result['hits']['hits']]
278+
# if delete_files is False:
279+
# LOGGER.debug(f'Not deleting files as it is set to false in the request')
280+
# return {}
281+
# s3 = AwsS3()
282+
# for each_granule in es_delete_result:
283+
# s3_urls = [v.href for k, v in each_granule.assets.items()]
284+
# LOGGER.debug(f'deleting S3 for {each_granule.id} - s3_urls: {s3_urls}')
285+
# delete_result = s3.delete_multiple(s3_urls=s3_urls)
286+
# LOGGER.debug(f'delete_result for {each_granule.id} - delete_result: {delete_result}')
287+
except Exception as e:
288+
LOGGER.exception('failed during get_granules_dapa')
289+
raise HTTPException(status_code=500, detail=str(e))
290+
return {}
291+
242292

243293
@router.put("/{collection_id}/archive/{granule_id}")
244294
@router.put("/{collection_id}/archive/{granule_id}/")

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jsonschema==4.23.0
1515
jsonschema-specifications==2023.12.1
1616
lark==0.12.0
1717
mangum==0.18.0
18-
mdps-ds-lib==1.1.1
18+
mdps-ds-lib==1.1.1.dev000200
1919
pydantic==2.9.2
2020
pydantic_core==2.23.4
2121
pygeofilter==0.2.4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import base64
2+
import json
3+
import os
4+
from unittest import TestCase
5+
6+
import requests
7+
from dotenv import load_dotenv
8+
from mdps_ds_lib.lib.aws.aws_s3 import AwsS3
9+
from mdps_ds_lib.lib.cognito_login.cognito_login import CognitoLogin
10+
11+
12+
class TestGranulesDeletion(TestCase):
13+
def setUp(self) -> None:
14+
super().setUp()
15+
load_dotenv()
16+
self._url_prefix = f'{os.environ.get("UNITY_URL")}/{os.environ.get("UNITY_STAGE", "sbx-uds-dapa")}'
17+
self.cognito_login = CognitoLogin() \
18+
.with_client_id(os.environ.get('CLIENT_ID', '')) \
19+
.with_cognito_url(os.environ.get('COGNITO_URL', '')) \
20+
.with_verify_ssl(False) \
21+
.start(base64.standard_b64decode(os.environ.get('USERNAME')).decode(),
22+
base64.standard_b64decode(os.environ.get('PASSWORD')).decode())
23+
self.bearer_token = self.cognito_login.token
24+
self.stage = os.environ.get("UNITY_URL").split('/')[-1]
25+
self.uds_url = f'{os.environ.get("UNITY_URL")}/{os.environ.get("UNITY_STAGE", "sbx-uds-dapa")}/'
26+
self.custom_metadata_body = {
27+
'tag': {'type': 'keyword'},
28+
'c_data1': {'type': 'long'},
29+
'c_data2': {'type': 'boolean'},
30+
'c_data3': {'type': 'keyword'},
31+
}
32+
33+
self.tenant = 'UDS_LOCAL_TEST_3' # 'uds_local_test' # 'uds_sandbox'
34+
self.tenant_venue = 'DEV' # 'DEV1' # 'dev'
35+
self.collection_name = 'CCC-04' # 'uds_collection' # 'sbx_collection'
36+
self.collection_version = '08'.replace('.', '') # '2402011200'
37+
return
38+
39+
def test_01_setup_permissions(self):
40+
collection_url = f'{self._url_prefix}/admin/auth'
41+
admin_add_body = {
42+
"actions": ["READ", "CREATE", "DELETE"],
43+
"resources": [f"URN:NASA:UNITY:{self.tenant}:{self.tenant_venue}:.*"],
44+
"tenant": self.tenant,
45+
"venue": self.tenant_venue,
46+
"group_name": "Unity_Viewer"
47+
}
48+
s = requests.session()
49+
s.trust_env = False
50+
response = s.put(url=collection_url, headers={
51+
'Authorization': f'Bearer {self.cognito_login.token}',
52+
'Content-Type': 'application/json',
53+
}, verify=False, data=json.dumps(admin_add_body))
54+
self.assertEqual(response.status_code, 200, f'wrong status code: {response.text}')
55+
response_json = response.content.decode()
56+
print(response_json)
57+
return
58+
59+
def test_delete_all(self):
60+
collection_id = f'URN:NASA:UNITY:{self.tenant}:{self.tenant_venue}:{self.collection_name}___001'
61+
post_url = f'{self.uds_url}collections/{collection_id}/items/' # MCP Dev
62+
headers = {
63+
'Authorization': f'Bearer {self.bearer_token}',
64+
}
65+
print(post_url)
66+
query_result = requests.get(url=post_url,
67+
headers=headers,
68+
)
69+
self.assertEqual(query_result.status_code, 200, f'wrong status code. {query_result.text}')
70+
response_json = json.loads(query_result.text)
71+
print(json.dumps(response_json, indent=4))
72+
self.assertTrue(len(response_json['features']) > 0, f'empty collection :(')
73+
deleting_granule_id = response_json['features'][0]['id']
74+
75+
asset_urls = [v['href'] for k, v in response_json['features'][0]['assets'].items()]
76+
print(asset_urls)
77+
post_url = f'{self.uds_url}collections/{collection_id}/items/{deleting_granule_id}/' # MCP Dev
78+
print(post_url)
79+
query_result = requests.delete(url=post_url,
80+
headers=headers,
81+
)
82+
self.assertEqual(query_result.status_code, 200, f'wrong status code. {query_result.text}')
83+
response_json = json.loads(query_result.text)
84+
print(json.dumps(response_json, indent=4))
85+
86+
post_url = f'{self.uds_url}collections/{collection_id}/items/' # MCP Dev
87+
query_result = requests.get(url=post_url, headers=headers,)
88+
self.assertEqual(query_result.status_code, 200, f'wrong status code. {query_result.text}')
89+
response_json = json.loads(query_result.text)
90+
print(json.dumps(response_json, indent=4))
91+
92+
s3 = AwsS3()
93+
for each_url in asset_urls:
94+
self.assertFalse(s3.set_s3_url(each_url).exists(s3.target_bucket, s3.target_key), f'file still exists: {each_url}')
95+
return

tests/integration_tests/test_stage_out_ingestion.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def setUp(self) -> None:
4242

4343
self.tenant = 'UDS_LOCAL_TEST_3' # 'uds_local_test' # 'uds_sandbox'
4444
self.tenant_venue = 'DEV' # 'DEV1' # 'dev'
45-
self.collection_name = 'AAA' # 'uds_collection' # 'sbx_collection'
45+
self.collection_name = 'CCC' # 'uds_collection' # 'sbx_collection'
4646
self.collection_version = '24.03.20.14.40'.replace('.', '') # '2402011200'
4747
return
4848

@@ -232,7 +232,7 @@ def test_03_upload_complete_catalog_role_as_key(self):
232232
"type": "Point",
233233
"coordinates": [0.0, 0.0]
234234
},
235-
bbox=[0.0, 0.1, 0.1, 0.0],
235+
bbox=[0.0, 0.0, 0.1, 0.1],
236236
datetime=TimeUtils().parse_from_unix(0, True).get_datetime_obj(),
237237
properties={
238238
"start_datetime": "2016-01-31T18:00:00.009057Z",

0 commit comments

Comments
 (0)