Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e343196
WIP: Enable bucket versioning
nss10 Jul 7, 2026
efdeabb
[UNTESTED]Add initial s3files setup + separate out s3 files into a ne…
nss10 Jul 21, 2026
41926ab
Update EKS security group filter to use EKS group name
nss10 Jul 21, 2026
6e9abd5
Update docstring
nss10 Jul 21, 2026
6e368cd
Add a TODO comment regarding security group creation, maybe can reduc…
nss10 Jul 21, 2026
f4f2b75
Fix minor bugs and add more TODOs.
nss10 Jul 21, 2026
0248c3d
Add file system status implementation and waiter
nss10 Jul 22, 2026
e172079
Update import path
nss10 Jul 22, 2026
73ad87c
Move boto3 clients to a different file to avoid circular imports
nss10 Jul 22, 2026
1ac0aa4
Update exception type
nss10 Jul 22, 2026
f363791
Fix get_s3_files_system and add/update TODO comments
nss10 Jul 22, 2026
15182e5
Create dynamic IAM role for S3Files
nss10 Jul 23, 2026
fbc7545
Add STORAGE_TYPE config variable, add documentation for s3files
nss10 Jul 23, 2026
80cd425
Make S3Files a configurable setup
nss10 Jul 23, 2026
0eaf83e
Merge branch 'master' into feat/s3Files
nss10 Jul 23, 2026
2cf8c10
Make mount target provisioning a bgTask + Update docs
nss10 Jul 23, 2026
fc32d15
Update comments
nss10 Jul 23, 2026
bb84de3
Add missing function call.
nss10 Jul 23, 2026
df89174
Add more comments
nss10 Jul 24, 2026
00f569f
Re-organize aws_utils into aws.bucket, aws.aws_utils, aws.clients. (T…
nss10 Jul 24, 2026
3f56313
Fix patching in unit tests. All current tests pass
nss10 Jul 24, 2026
28614f8
Add more TODOs
nss10 Jul 24, 2026
aeb3071
Add unit tests for S3Files
nss10 Jul 27, 2026
24ae147
Merge branch 'master' of github.com:uc-cdis/gen3-workflow into feat/s…
paulineribeyre Jul 28, 2026
9ed86df
update deps
paulineribeyre Jul 28, 2026
cc2f471
Delete versions
paulineribeyre Jul 29, 2026
186ce56
fix s3files tests
paulineribeyre Jul 29, 2026
a9d5e80
fix other tests
paulineribeyre Jul 29, 2026
ed12503
pull
paulineribeyre Jul 30, 2026
3bd72a6
EKS_SECURITY_GROUP_NAMES update
paulineribeyre Jul 30, 2026
dd7f7be
update tests
paulineribeyre Jul 30, 2026
d2ee9c5
address review comments
paulineribeyre Jul 30, 2026
dbea8ac
update deps
paulineribeyre Jul 30, 2026
3fc64d7
Merge branch 'master' of github.com:uc-cdis/gen3-workflow into delete…
paulineribeyre Jul 30, 2026
261d6a3
fix merge mistakes
paulineribeyre Jul 30, 2026
e357b4b
split test_storage.py and test_misc.py
paulineribeyre Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 20 additions & 53 deletions gen3workflow/aws/bucket.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import random
from cachelib import SimpleCache
from typing import Tuple, Union

import asyncio
Expand Down Expand Up @@ -453,78 +454,44 @@ async def create_user_bucket(user_id: str) -> Tuple[str, str, str]:
await asyncio.sleep(delay)


def get_all_bucket_objects(user_bucket_name: str) -> list:
"""
Get all objects from the specified S3 bucket.
"""
response = clients.s3_client.list_objects_v2(Bucket=user_bucket_name)
object_list = response.get("Contents", [])

# list_objects_v2 can utmost return 1000 objects in a single response
# if there are more objects, the response will have a key "IsTruncated" set to True
# and a key "NextContinuationToken" which can be used to get the next set of objects

# TODO:
# Currently, all objects are loaded into memory, which can be problematic for large buckets.
# To optimize, convert this function into a generator that accepts a `batch_size` parameter
# (capped at 1,000) and yields objects in batches.
# This is fine for now because this code is only called during integration tests, with a small
# number of files in the bucket.
while response.get("IsTruncated"):
response = clients.s3_client.list_objects_v2(
Bucket=user_bucket_name,
ContinuationToken=response.get("NextContinuationToken"),
)
object_list += response.get("Contents", [])

return object_list


# TODO: Handle `delete_all_bucket_objects` when bucket versioning is enabled. Needed for CI, when S3Files is enabled.
def delete_all_bucket_objects(user_id: str, user_bucket_name: str) -> None:
def _delete_all_bucket_objects(user_id: str, user_bucket_name: str) -> None:
"""
Deletes all objects from the specified S3 bucket.

Args:
user_id (str): The user's unique Gen3 ID.
user_bucket_name (str): The name of the S3 bucket.
"""
object_list = get_all_bucket_objects(user_bucket_name)

if not object_list:
return

logger.debug(
f"Deleting all contents from '{user_bucket_name}' for user '{user_id}' before deleting the bucket"
)
keys = [{"Key": obj.get("Key")} for obj in object_list]

# According to the docs, up to 1000 objects can be deleted in a single request:
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.delete_objects

# TODO: When `get_all_bucket_objects` is converted to a generator,
# we can remove this batching logic and retrieve objects in batches of 1,000 for deletion.
limit = 1000
for offset in range(0, len(keys), limit):
response = clients.s3_client.delete_objects(
Bucket=user_bucket_name,
Delete={"Objects": keys[offset : offset + limit]},
)
bucket = clients.s3_resource.Bucket(user_bucket_name)

# Cancel incomplete multipart uploads to avoid storage charges for orphaned parts
for upload in bucket.multipart_uploads.all():
upload.abort()

for response in bucket.object_versions.delete():
# boto returns one response for each underlying batch
if response.get("Errors"):
logger.error(
f"Failed to delete objects from bucket '{user_bucket_name}' for user '{user_id}': {response}"
raise Exception(
f"Unable to delete bucket object versions: {response['Errors']}"
)

for response in bucket.objects.delete():
if response.get("Errors"):
raise Exception(
f"Unable to delete bucket object versions: {response['Errors']}"
)
raise Exception(response)


def cleanup_user_bucket(user_id: str, delete_bucket: bool = False) -> Union[str, None]:
def cleanup_user_bucket(user_id: str, delete_bucket: bool) -> Union[str, None]:
"""
Empty a user's S3 bucket and optionally delete the bucket.

Args:
user_id: User identifier used to derive the bucket name.
delete_bucket: If True, delete the bucket after removing all objects.
Defaults to False (only objects are removed).

Returns:
Bucket name if it exists and cleanup was performed, otherwise None
Expand All @@ -545,7 +512,7 @@ def cleanup_user_bucket(user_id: str, delete_bucket: bool = False) -> Union[str,
)
return None
try:
delete_all_bucket_objects(user_id, user_bucket_name)
_delete_all_bucket_objects(user_id, user_bucket_name)
if delete_bucket:
logger.info(
f"Initializing delete for bucket '{user_bucket_name}' for user '{user_id}'"
Expand Down
10 changes: 8 additions & 2 deletions gen3workflow/aws/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from gen3workflow.config import config


def get_boto3_client(service_name: str, **kwargs):
def get_boto3_client(service_name: str, type: str = "client", **kwargs):
"""
Create a boto3 client for the specified AWS service,
using credentials from the config if provided,
Expand All @@ -16,11 +16,17 @@ def get_boto3_client(service_name: str, **kwargs):
kwargs["aws_secret_access_key"] = config[
"S3_ENDPOINTS_AWS_SECRET_ACCESS_KEY"
]
return boto3.client(service_name, **kwargs)
if type == "client":
return boto3.client(service_name, **kwargs)
else:
return boto3.resource(service_name, **kwargs)


iam_client = get_boto3_client("iam")
s3_client = get_boto3_client("s3", region_name=config["USER_BUCKETS_REGION"])
s3_resource = get_boto3_client(
"s3", type="resource", region_name=config["USER_BUCKETS_REGION"]
)
kms_client = get_boto3_client("kms", region_name=config["USER_BUCKETS_REGION"])
sts_client = get_boto3_client("sts")
eks_client = get_boto3_client("eks", region_name=config["EKS_CLUSTER_REGION"])
Expand Down
6 changes: 1 addition & 5 deletions gen3workflow/aws/s3_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,8 @@ def _get_eks_security_groups() -> List[tuple[str, str]]:
Return a list of tuples consisting of (sg_id, sg_name) for the EKS security group that Karpenter attaches
to an EKS worker node in this cluster.
"""
eks_sg_names = config.get("EKS_SECURITY_GROUP_NAMES")
if not eks_sg_names:
eks_sg_names = [f"{config["EKS_CLUSTER_NAME"]}_EKS_workers_sg"]

security_groups = clients.ec2_client.describe_security_groups(
Filters=[{"Name": "group-name", "Values": eks_sg_names}]
Filters=[{"Name": "group-name", "Values": config["EKS_SECURITY_GROUP_NAMES"]}]
)["SecurityGroups"]
return [(group["GroupId"], group["GroupName"]) for group in security_groups]

Expand Down
3 changes: 1 addition & 2 deletions gen3workflow/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,5 @@ WORKER_PODS_NAMESPACE: default-namespace

# Names of the EKS security groups that Karpenter attaches to an EKS worker node in this cluster
# Usually going to be in the lines of f"{config["EKS_CLUSTER_NAME"]}_EKS_workers_sg"
# Steps to fetch the values are mentioned in the (docs/s3Files.md#determining-the-eks-security-group-names-for-s3-files-setup)

# Steps to fetch the values: https://github.com/uc-cdis/gen3-workflow/blob/master/docs/s3Files.md#determining-the-eks-security-group-names-for-s3-files-setup
EKS_SECURITY_GROUP_NAMES: []
5 changes: 5 additions & 0 deletions gen3workflow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ def validate_top_level_configs(self) -> None:
self["S3_ENDPOINTS_AWS_SECRET_ACCESS_KEY"]
), "Both 'S3_ENDPOINTS_AWS_ACCESS_KEY_ID' and 'S3_ENDPOINTS_AWS_SECRET_ACCESS_KEY' must be configured, or both must be left empty"

if self["ENABLE_S3_FILES"]:
assert (
len(self["EKS_SECURITY_GROUP_NAMES"]) > 0
), "EKS_SECURITY_GROUP_NAMES must be configured when ENABLE_S3_FILES is True"


config = Gen3WorkflowConfig(DEFAULT_CFG_PATH)
try:
Expand Down
7 changes: 2 additions & 5 deletions gen3workflow/routes/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
from gen3workflow import logger
from gen3workflow.auth import Auth
from gen3workflow.aws import s3_files
from gen3workflow.aws.bucket import (
cleanup_user_bucket,
create_user_bucket,
)
from gen3workflow.aws.bucket import cleanup_user_bucket, create_user_bucket
from gen3workflow.config import config

router = APIRouter(prefix="/storage")
Expand Down Expand Up @@ -133,7 +130,7 @@ async def empty_user_bucket(request: Request, auth=Depends(Auth)) -> None:
"delete", [f"/services/workflow/gen3-workflow/storage/{user_id}"]
)
logger.info(f"User '{user_id}' emptying their storage bucket")
deleted_bucket_name = cleanup_user_bucket(user_id)
deleted_bucket_name = cleanup_user_bucket(user_id, delete_bucket=False)

if not deleted_bucket_name:
raise HTTPException(
Expand Down
Loading
Loading