Skip to content

Commit c279c31

Browse files
committed
fix(storage): support AWS_S3_ADDRESSING_STYLE env var for S3 virtual/path addressing
1 parent 4225bc5 commit c279c31

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

apps/api/plane/settings/storage.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
# Third party imports
1010
import boto3
11+
from botocore.config import Config
1112
from botocore.exceptions import ClientError
1213
from urllib.parse import quote
1314

@@ -36,6 +37,21 @@ def __init__(self, request=None):
3637
# Use the SIGNED_URL_EXPIRATION environment variable for the expiration time (default: 3600 seconds)
3738
self.signed_url_expiration = int(os.environ.get("SIGNED_URL_EXPIRATION", "3600"))
3839

40+
# S3 addressing style: 'auto', 'virtual', or 'path' (default: 'auto')
41+
# virtual = virtual-hosted style (e.g., https://bucket.s3.amazonaws.com)
42+
# path = path style (e.g., https://s3.amazonaws.com/bucket)
43+
# auto = let botocore decide based on bucket name
44+
addressing_style = os.environ.get("AWS_S3_ADDRESSING_STYLE", "auto").lower()
45+
46+
# Create boto3 Config with addressing style if explicitly set
47+
if addressing_style in ("virtual", "path"):
48+
boto_config = Config(
49+
signature_version="s3v4",
50+
s3={"addressing_style": addressing_style},
51+
)
52+
else:
53+
boto_config = Config(signature_version="s3v4")
54+
3955
if os.environ.get("USE_MINIO") == "1":
4056
# Determine protocol based on environment variable
4157
if os.environ.get("MINIO_ENDPOINT_SSL") == "1":
@@ -49,7 +65,7 @@ def __init__(self, request=None):
4965
aws_secret_access_key=self.aws_secret_access_key,
5066
region_name=self.aws_region,
5167
endpoint_url=(f"{endpoint_protocol}://{request.get_host()}" if request else self.aws_s3_endpoint_url),
52-
config=boto3.session.Config(signature_version="s3v4"),
68+
config=boto_config,
5369
)
5470
else:
5571
# Create an S3 client
@@ -59,7 +75,7 @@ def __init__(self, request=None):
5975
aws_secret_access_key=self.aws_secret_access_key,
6076
region_name=self.aws_region,
6177
endpoint_url=self.aws_s3_endpoint_url,
62-
config=boto3.session.Config(signature_version="s3v4"),
78+
config=boto_config,
6379
)
6480

6581
def generate_presigned_post(self, object_name, file_type, file_size, expiration=None):

apps/api/plane/tests/unit/settings/test_storage.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,97 @@ def test_explicit_expiration_overrides_default(self, mock_boto3):
204204
mock_s3_client.generate_presigned_url.assert_called_once()
205205
call_kwargs = mock_s3_client.generate_presigned_url.call_args[1]
206206
assert call_kwargs["ExpiresIn"] == 120
207+
208+
209+
@pytest.mark.unit
210+
class TestS3StorageAddressingStyle:
211+
"""Test the S3 addressing style configuration via AWS_S3_ADDRESSING_STYLE"""
212+
213+
@patch.dict(
214+
os.environ,
215+
{
216+
"AWS_ACCESS_KEY_ID": "test-key",
217+
"AWS_SECRET_ACCESS_KEY": "test-secret",
218+
"AWS_S3_BUCKET_NAME": "test-bucket",
219+
"AWS_REGION": "us-east-1",
220+
"AWS_S3_ENDPOINT_URL": "https://s3.amazonaws.com",
221+
"AWS_S3_ADDRESSING_STYLE": "virtual",
222+
},
223+
clear=True,
224+
)
225+
@patch("plane.settings.storage.boto3")
226+
def test_virtual_addressing_style(self, mock_boto3):
227+
"""Test that virtual addressing style is configured via botocore Config"""
228+
mock_boto3.client.return_value = Mock()
229+
230+
storage = S3Storage()
231+
232+
call_kwargs = mock_boto3.client.call_args[1]
233+
assert call_kwargs["config"].s3["addressing_style"] == "virtual"
234+
235+
@patch.dict(
236+
os.environ,
237+
{
238+
"AWS_ACCESS_KEY_ID": "test-key",
239+
"AWS_SECRET_ACCESS_KEY": "test-secret",
240+
"AWS_S3_BUCKET_NAME": "test-bucket",
241+
"AWS_REGION": "us-east-1",
242+
"AWS_S3_ENDPOINT_URL": "https://s3.amazonaws.com",
243+
"AWS_S3_ADDRESSING_STYLE": "path",
244+
},
245+
clear=True,
246+
)
247+
@patch("plane.settings.storage.boto3")
248+
def test_path_addressing_style(self, mock_boto3):
249+
"""Test that path addressing style is configured via botocore Config"""
250+
mock_boto3.client.return_value = Mock()
251+
252+
storage = S3Storage()
253+
254+
call_kwargs = mock_boto3.client.call_args[1]
255+
assert call_kwargs["config"].s3["addressing_style"] == "path"
256+
257+
@patch.dict(
258+
os.environ,
259+
{
260+
"AWS_ACCESS_KEY_ID": "test-key",
261+
"AWS_SECRET_ACCESS_KEY": "test-secret",
262+
"AWS_S3_BUCKET_NAME": "test-bucket",
263+
"AWS_REGION": "us-east-1",
264+
"AWS_S3_ENDPOINT_URL": "https://s3.amazonaws.com",
265+
},
266+
clear=True,
267+
)
268+
@patch("plane.settings.storage.boto3")
269+
def test_auto_addressing_style_by_default(self, mock_boto3):
270+
"""Test that auto addressing style is used by default (no s3 config in botocore Config)"""
271+
mock_boto3.client.return_value = Mock()
272+
273+
storage = S3Storage()
274+
275+
call_kwargs = mock_boto3.client.call_args[1]
276+
# When addressing_style is 'auto' or not set, botocore Config should not have s3 dict
277+
assert "s3" not in call_kwargs["config"]._user_provided_options
278+
279+
@patch.dict(
280+
os.environ,
281+
{
282+
"AWS_ACCESS_KEY_ID": "test-key",
283+
"AWS_SECRET_ACCESS_KEY": "test-secret",
284+
"AWS_S3_BUCKET_NAME": "test-bucket",
285+
"AWS_REGION": "us-east-1",
286+
"AWS_S3_ENDPOINT_URL": "https://nyc3.digitaloceanspaces.com",
287+
"AWS_S3_ADDRESSING_STYLE": "virtual",
288+
},
289+
clear=True,
290+
)
291+
@patch("plane.settings.storage.boto3")
292+
def test_virtual_style_with_digitalocean_spaces(self, mock_boto3):
293+
"""Test virtual addressing style works with DigitalOcean Spaces"""
294+
mock_boto3.client.return_value = Mock()
295+
296+
storage = S3Storage()
297+
298+
call_kwargs = mock_boto3.client.call_args[1]
299+
assert call_kwargs["config"].s3["addressing_style"] == "virtual"
300+
assert call_kwargs["endpoint_url"] == "https://nyc3.digitaloceanspaces.com"

0 commit comments

Comments
 (0)