1+ // Storage configuration and resolution: which provider/bucket/connection each
2+ // storage role (templates, build cache) uses.
3+ //
4+ // Every role resolves through a single pipeline:
5+ //
6+ // storage URL (authoritative, e.g. TEMPLATE_STORAGE_URL)
7+ // │ else: legacy envs → legacyStorageURL()
8+ // ▼
9+ // ParseStorageURL() → StorageSpec → GetStorageProvider()
10+ //
11+ // The legacy environment style (STORAGE_PROVIDER + *_BUCKET_NAME /
12+ // LOCAL_*_STORAGE_BASE_PATH + S3_USE_PATH_STYLE) is kept working by converting
13+ // it into the equivalent storage URL; everything legacy-specific is contained
14+ // in legacyStorageURL and the StorageConfig env-name fields, and is deleted
15+ // wholesale once the legacy envs are retired.
16+
117package storage
218
319import (
20+ "context"
421 "fmt"
522 "net/url"
623 "os"
724 "strconv"
825 "strings"
926
1027 "github.com/e2b-dev/infra/packages/shared/pkg/env"
28+ "github.com/e2b-dev/infra/packages/shared/pkg/limit"
1129 "github.com/e2b-dev/infra/packages/shared/pkg/utils"
1230)
1331
32+ type Provider string
33+
34+ const (
35+ GCPStorageProvider Provider = "GCPBucket"
36+ AWSStorageProvider Provider = "AWSBucket"
37+ LocalStorageProvider Provider = "Local"
38+
39+ DefaultStorageProvider Provider = GCPStorageProvider
40+
41+ storageProviderEnv = "STORAGE_PROVIDER"
42+ )
43+
1444// StorageSpec is a fully resolved storage destination: which provider to use,
1545// which bucket (or local path) to address, and any per-connection options.
1646//
@@ -29,14 +59,106 @@ type StorageSpec struct {
2959 // includes the AWS_ENDPOINT_URL environment variable.
3060 Endpoint string
3161 // UsePathStyle forces S3 path-style addressing (https://host/bucket/key).
32- // Required by most S3-compatible backends. False falls back to the
33- // S3_USE_PATH_STYLE environment variable.
62+ // Required by most S3-compatible backends.
3463 UsePathStyle bool
3564 // Region overrides the S3 region. Empty means the AWS SDK default
3665 // resolution (AWS_REGION et al.).
3766 Region string
3867}
3968
69+ // StorageConfig describes one storage role (templates, build cache): which
70+ // environment variable carries the role's storage URL, and which legacy
71+ // environment variables configure it when no URL is set. All environment
72+ // variables are read lazily at resolve time, so runtime overrides (os.Setenv,
73+ // t.Setenv in tests) are respected.
74+ type StorageConfig struct {
75+ // name identifies the role in error messages.
76+ name string
77+ // storageURLEnv holds the role's storage URL (see ParseStorageURL),
78+ // e.g. "gs://bucket" or "s3://bucket?endpoint=…&s3ForcePathStyle=true".
79+ // When set it is authoritative; otherwise the legacy envs below apply.
80+ storageURLEnv string
81+ // bucketEnv (cloud) and basePathEnv/basePathDefault (local) are the
82+ // legacy env vars consumed by legacyStorageURL.
83+ bucketEnv string
84+ basePathEnv string
85+ basePathDefault string
86+
87+ limiter * limit.Limiter
88+ uploadBaseURL string
89+ hmacKey []byte
90+ }
91+
92+ var TemplateStorageConfig = StorageConfig {
93+ name : "template" ,
94+ storageURLEnv : "TEMPLATE_STORAGE_URL" ,
95+ bucketEnv : "TEMPLATE_BUCKET_NAME" ,
96+ basePathEnv : "LOCAL_TEMPLATE_STORAGE_BASE_PATH" ,
97+ basePathDefault : "/tmp/templates" ,
98+ }
99+
100+ var BuildCacheStorageConfig = StorageConfig {
101+ name : "build cache" ,
102+ storageURLEnv : "BUILD_CACHE_STORAGE_URL" ,
103+ bucketEnv : "BUILD_CACHE_BUCKET_NAME" ,
104+ basePathEnv : "LOCAL_BUILD_CACHE_STORAGE_BASE_PATH" ,
105+ basePathDefault : "/tmp/build-cache" ,
106+ }
107+
108+ // WithLimiter returns a copy of the config with the given limiter set.
109+ func (c StorageConfig ) WithLimiter (limiter * limit.Limiter ) StorageConfig {
110+ c .limiter = limiter
111+
112+ return c
113+ }
114+
115+ // WithLocalUpload returns a copy of the config with the given local upload
116+ // parameters set. These are only used with the local filesystem provider to
117+ // let it generate signed URLs for file uploads.
118+ func (c StorageConfig ) WithLocalUpload (uploadBaseURL string , hmacKey []byte ) StorageConfig {
119+ c .uploadBaseURL = uploadBaseURL
120+ c .hmacKey = hmacKey
121+
122+ return c
123+ }
124+
125+ // ResolveSpec resolves the storage destination for this config. A defined
126+ // storage URL is authoritative; otherwise the legacy environment variables
127+ // (STORAGE_PROVIDER + the role's bucket/base-path envs) are converted into a
128+ // storage URL, so both styles share one parsing and validation path.
129+ func (c StorageConfig ) ResolveSpec () (StorageSpec , error ) {
130+ raw := strings .TrimSpace (os .Getenv (c .storageURLEnv ))
131+ if raw == "" {
132+ legacy , err := legacyStorageURL (c )
133+ if err != nil {
134+ return StorageSpec {}, err
135+ }
136+ raw = legacy
137+ }
138+
139+ return ParseStorageURL (raw )
140+ }
141+
142+ // GetStorageProvider resolves the config and constructs the storage provider
143+ // for it.
144+ func GetStorageProvider (ctx context.Context , cfg StorageConfig ) (StorageProvider , error ) {
145+ spec , err := cfg .ResolveSpec ()
146+ if err != nil {
147+ return nil , err
148+ }
149+
150+ switch spec .Provider {
151+ case LocalStorageProvider :
152+ return newFileSystemStorage (spec .BasePath , cfg ), nil
153+ case AWSStorageProvider :
154+ return newAWSStorage (ctx , spec , cfg .limiter )
155+ case GCPStorageProvider :
156+ return NewGCP (ctx , spec .Bucket , cfg .limiter )
157+ }
158+
159+ return nil , fmt .Errorf ("unknown storage provider: %s" , spec .Provider )
160+ }
161+
40162// ParseStorageURL parses a storage URL into a StorageSpec. The syntax follows
41163// the gocloud.dev blob URL dialect so the mapping bucket → client/connection is
42164// declared in one self-describing string:
@@ -52,10 +174,6 @@ type StorageSpec struct {
52174// fail fast instead of being silently ignored. Credentials are intentionally
53175// not accepted in URLs; they come from the provider's usual environment
54176// (ADC / Workload Identity for gs://, AWS_ACCESS_KEY_ID etc. for s3://).
55- //
56- // Legacy environment-variable configuration (STORAGE_PROVIDER + bucket/path
57- // envs) is converted into this URL form by StorageConfig.ResolveSpec, so both
58- // configuration styles share this single parsing and validation path.
59177func ParseStorageURL (raw string ) (StorageSpec , error ) {
60178 u , err := url .Parse (strings .TrimSpace (raw ))
61179 if err != nil {
@@ -155,6 +273,20 @@ func parseFileURL(u *url.URL) (StorageSpec, error) {
155273 }, nil
156274}
157275
276+ func validateBucketURL (u * url.URL ) error {
277+ if u .Host == "" {
278+ return fmt .Errorf ("storage URL %q: missing bucket name" , u )
279+ }
280+ if u .Path != "" && u .Path != "/" {
281+ return fmt .Errorf ("storage URL %q: key prefixes are not supported (bucket only)" , u )
282+ }
283+ if u .User != nil {
284+ return fmt .Errorf ("storage URL %q: credentials in URLs are not supported" , u )
285+ }
286+
287+ return nil
288+ }
289+
158290// legacyStorageURL converts the legacy environment-variable configuration
159291// (STORAGE_PROVIDER, defaulting to GCPBucket, + the role's bucket/base-path
160292// envs + S3_USE_PATH_STYLE) into a storage URL, so both configuration styles
@@ -198,17 +330,3 @@ func legacyStorageURL(cfg StorageConfig) (string, error) {
198330func (c StorageConfig ) legacyBucket () string {
199331 return utils .RequiredEnv (c .bucketEnv , fmt .Sprintf ("Bucket for storing %s files" , c .name ))
200332}
201-
202- func validateBucketURL (u * url.URL ) error {
203- if u .Host == "" {
204- return fmt .Errorf ("storage URL %q: missing bucket name" , u )
205- }
206- if u .Path != "" && u .Path != "/" {
207- return fmt .Errorf ("storage URL %q: key prefixes are not supported (bucket only)" , u )
208- }
209- if u .User != nil {
210- return fmt .Errorf ("storage URL %q: credentials in URLs are not supported" , u )
211- }
212-
213- return nil
214- }
0 commit comments