diff --git a/bulker/bulkerlib/implementations/s3.go b/bulker/bulkerlib/implementations/s3.go index 3e3e42cc0..e42b8c7d7 100644 --- a/bulker/bulkerlib/implementations/s3.go +++ b/bulker/bulkerlib/implementations/s3.go @@ -34,6 +34,7 @@ type S3Config struct { Bucket string `mapstructure:"bucket,omitempty" json:"bucket,omitempty" yaml:"bucket,omitempty"` Region string `mapstructure:"region,omitempty" json:"region,omitempty" yaml:"region,omitempty"` Endpoint string `mapstructure:"endpoint,omitempty" json:"endpoint,omitempty" yaml:"endpoint,omitempty"` + AddressingStyle string `mapstructure:"addressingStyle,omitempty" json:"addressingStyle,omitempty" yaml:"addressingStyle,omitempty"` UsePresignedURL bool `mapstructure:"usePresignedURL,omitempty" json:"usePresignedURL,omitempty" yaml:"usePresignedURL,omitempty"` RoleARN string `mapstructure:"roleARN" json:"roleARN" yaml:"roleARN"` @@ -136,7 +137,18 @@ func NewS3(s3Config *S3Config) (*S3, error) { o.Region = s3Config.Region if s3Config.Endpoint != "" { o.BaseEndpoint = &s3Config.Endpoint + } + // Preserve historical behavior by default (path-style for custom endpoints), + // but allow explicit override via addressingStyle. + switch s3Config.AddressingStyle { + case "path": o.UsePathStyle = true + case "virtual-hosted": + o.UsePathStyle = false + case "auto", "": + o.UsePathStyle = s3Config.Endpoint != "" + default: + o.UsePathStyle = s3Config.Endpoint != "" } } client := s3.NewFromConfig(awsCfg, o) diff --git a/webapps/console/lib/schema/destinations.tsx b/webapps/console/lib/schema/destinations.tsx index 8a333d5ec..dba93cd2e 100644 --- a/webapps/console/lib/schema/destinations.tsx +++ b/webapps/console/lib/schema/destinations.tsx @@ -68,6 +68,10 @@ const s3Regions = [ "us-gov-west-1", ] as const; +// Allowed S3 URL addressing styles. "auto" preserves the historical behavior: +// path-style for custom endpoints, virtual-hosted-style for AWS. +const s3AddressingStyles = ["auto", "virtual-hosted", "path"] as const; + export const MASKED_SECRET = "__MASKED_BY_JITSU__"; /** @@ -888,7 +892,13 @@ export const coreDestinations: DestinationType[] = [ .describe( "Authentication Method::S3 authentication method: IAM Role based or Access Key" ), - region: z.enum(s3Regions).default(s3Regions[0]).describe("S3 Region::S3 Region"), + region: z + .string() + .min(1) + .default(s3Regions[0]) + .describe( + "S3 Region::S3 Region. Any AWS region (e.g. us-east-1, eu-central-1) or custom region for S3-compatible storage." + ), roleARN: z .string() .optional() @@ -905,6 +915,13 @@ export const coreDestinations: DestinationType[] = [ secretAccessKey: z.string().optional().describe("S3 Secret Access Key::S3 Secret Access Key"), bucket: z.string().describe("S3 Bucket Name::S3 Bucket Name"), endpoint: z.string().optional().describe("Custom endpoint of S3-compatible server"), + addressingStyle: z + .enum(s3AddressingStyles) + .optional() + .default("auto") + .describe( + "S3 Addressing Style::S3 URL addressing style: auto (path-style for custom endpoint, virtual-hosted otherwise), virtual-hosted, or path" + ), }) .merge(blockStorageSettings), credentialsUi: {