Skip to content

Commit af07a53

Browse files
authored
Add s3 credential support (#1606)
1 parent 2e1de90 commit af07a53

File tree

17 files changed

+246
-134
lines changed

17 files changed

+246
-134
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/rtconfgen/convert.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,10 @@ func (c *legacyConverter) Convert() (*config.Runtime, error) {
384384
switch prov := cluster.Provider.(type) {
385385
case *runtimev1.BucketCluster_S3_:
386386
p.S3 = &config.S3BucketProvider{
387-
Region: prov.S3.GetRegion(),
388-
Endpoint: prov.S3.Endpoint,
387+
Region: prov.S3.GetRegion(),
388+
Endpoint: prov.S3.Endpoint,
389+
AccessKeyID: prov.S3.AccessKeyId,
390+
SecretAccessKey: ptrOrNil(c.secretString(prov.S3.SecretAccessKey)),
389391
}
390392
case *runtimev1.BucketCluster_Gcs:
391393
p.GCS = &config.GCSBucketProvider{
@@ -640,6 +642,14 @@ func ptr[T comparable](val T) *T {
640642
return &val
641643
}
642644

645+
func ptrOrNil[T comparable](val T) *T {
646+
var zero T
647+
if val == zero {
648+
return nil
649+
}
650+
return &val
651+
}
652+
643653
func randomMapValue[K comparable, V any](m map[K]V) (V, bool) {
644654
for _, v := range m {
645655
return v, true

proto/encore/runtime/v1/infra.pb.go

Lines changed: 121 additions & 92 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/encore/runtime/v1/infra.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ message BucketCluster {
323323

324324
// Endpoint override, if any. Must be specified if using a non-standard AWS region.
325325
optional string endpoint = 2;
326+
327+
// Set these to use explicit credentials for this bucket,
328+
// as opposed to resolving using AWS's default credential chain.
329+
optional string access_key_id = 3;
330+
optional SecretData secret_access_key = 4;
326331
}
327332

328333
message GCS {

runtimes/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ md5 = "0.7.0"
9393
aws-sdk-s3 = "1.58.0"
9494
aws-smithy-types = { version = "1.2.8", features = ["byte-stream-poll-next", "rt-tokio"] }
9595
percent-encoding = "2.3.1"
96+
aws-credential-types = "1.2.1"
9697

9798
[build-dependencies]
9899
prost-build = "0.12.3"

runtimes/core/src/infracfg.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub struct GCS {
4646
pub struct S3 {
4747
pub region: String,
4848
pub endpoint: Option<String>,
49+
pub access_key_id: Option<String>,
50+
pub secret_access_key: Option<EnvString>,
4951
pub buckets: HashMap<String, Bucket>,
5052
}
5153

@@ -461,6 +463,11 @@ pub fn map_infra_to_runtime(infra: InfraConfig) -> RuntimeConfig {
461463
pbruntime::bucket_cluster::S3 {
462464
region: s3.region,
463465
endpoint: s3.endpoint,
466+
access_key_id: s3.access_key_id,
467+
secret_access_key: s3
468+
.secret_access_key
469+
.as_ref()
470+
.map(map_env_string_to_secret_data),
464471
},
465472
)),
466473
buckets: s3

runtimes/core/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ impl Runtime {
331331
.context("failed to resolve gateway push subscriptions")?;
332332

333333
let pubsub = pubsub::Manager::new(tracer.clone(), resources.pubsub_clusters, &md)?;
334-
let objects = objects::Manager::new(tracer.clone(), resources.bucket_clusters, &md);
334+
let objects =
335+
objects::Manager::new(&secrets, tracer.clone(), resources.bucket_clusters, &md);
335336
let sqldb = sqldb::ManagerConfig {
336337
clusters: resources.sql_clusters,
337338
creds: &creds,

runtimes/core/src/objects/gcs/bucket.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ impl objects::ObjectImpl for Object {
363363
return Err(PublicUrlError::PrivateBucket);
364364
};
365365

366-
let url = objects::public_url(base_url, self.bkt.key_prefix.as_deref(), &self.key);
366+
let url = objects::public_url(base_url, &self.key);
367367
Ok(url)
368368
}
369369
}

runtimes/core/src/objects/manager.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::encore::parser::meta::v1 as meta;
55
use crate::encore::runtime::v1 as pb;
66
use crate::names::EncoreName;
77
use crate::objects::{gcs, noop, s3, BucketImpl, ClusterImpl};
8+
use crate::secrets;
89
use crate::trace::Tracer;
910

1011
use super::Bucket;
@@ -17,8 +18,13 @@ pub struct Manager {
1718
}
1819

1920
impl Manager {
20-
pub fn new(tracer: Tracer, clusters: Vec<pb::BucketCluster>, md: &meta::Data) -> Self {
21-
let bucket_cfg = make_cfg_maps(clusters, md);
21+
pub fn new(
22+
secrets: &secrets::Manager,
23+
tracer: Tracer,
24+
clusters: Vec<pb::BucketCluster>,
25+
md: &meta::Data,
26+
) -> Self {
27+
let bucket_cfg = make_cfg_maps(secrets, clusters, md);
2228

2329
Self {
2430
tracer,
@@ -54,13 +60,20 @@ impl Manager {
5460
}
5561

5662
fn make_cfg_maps(
63+
secrets: &secrets::Manager,
5764
clusters: Vec<pb::BucketCluster>,
5865
_md: &meta::Data,
5966
) -> HashMap<EncoreName, (Arc<dyn ClusterImpl>, pb::Bucket)> {
6067
let mut bucket_map = HashMap::new();
6168

6269
for cluster_cfg in clusters {
63-
let cluster = new_cluster(&cluster_cfg);
70+
let cluster = match cluster_cfg.provider {
71+
Some(provider) => new_cluster(secrets, provider),
72+
None => {
73+
log::error!("missing bucket cluster provider: {}", cluster_cfg.rid);
74+
Arc::new(noop::Cluster)
75+
}
76+
};
6477

6578
for bucket_cfg in cluster_cfg.buckets {
6679
bucket_map.insert(
@@ -73,14 +86,18 @@ fn make_cfg_maps(
7386
bucket_map
7487
}
7588

76-
fn new_cluster(cluster: &pb::BucketCluster) -> Arc<dyn ClusterImpl> {
77-
let Some(provider) = &cluster.provider else {
78-
log::error!("missing bucket cluster provider: {}", cluster.rid);
79-
return Arc::new(noop::Cluster);
80-
};
81-
89+
fn new_cluster(
90+
secrets: &secrets::Manager,
91+
provider: pb::bucket_cluster::Provider,
92+
) -> Arc<dyn ClusterImpl> {
8293
match provider {
83-
pb::bucket_cluster::Provider::S3(s3cfg) => Arc::new(s3::Cluster::new(s3cfg)),
94+
pb::bucket_cluster::Provider::S3(s3cfg) => {
95+
let secret_access_key = s3cfg
96+
.secret_access_key
97+
.as_ref()
98+
.map(|k| secrets.load(k.clone()));
99+
Arc::new(s3::Cluster::new(s3cfg, secret_access_key))
100+
}
84101
pb::bucket_cluster::Provider::Gcs(gcscfg) => Arc::new(gcs::Cluster::new(gcscfg.clone())),
85102
}
86103
}

runtimes/core/src/objects/mod.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,16 +493,13 @@ fn escape_path(s: &str) -> Cow<'_, str> {
493493
percent_encoding::percent_encode(s.as_bytes(), PATH).into()
494494
}
495495

496-
/// Computes the public url given a base url, optional key prefix, and object name.
497-
fn public_url(base_url: String, key_prefix: Option<&str>, name: &str) -> String {
496+
/// Computes the public url given a base url and object name.
497+
fn public_url(base_url: String, name: &str) -> String {
498498
let mut url = base_url;
499499

500500
if !url.ends_with('/') {
501501
url.push('/');
502502
}
503-
if let Some(key_prefix) = key_prefix {
504-
url.push_str(&escape_path(key_prefix));
505-
}
506503
url.push_str(&escape_path(name));
507504
url
508505
}

runtimes/core/src/objects/s3/bucket.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ impl objects::ObjectImpl for Object {
375375
return Err(PublicUrlError::PrivateBucket);
376376
};
377377

378-
let url = objects::public_url(base_url, self.bkt.key_prefix.as_deref(), &self.name);
378+
let url = objects::public_url(base_url, &self.name);
379379
Ok(url)
380380
}
381381
}

runtimes/core/src/objects/s3/mod.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::sync::Arc;
33
use crate::encore::runtime::v1 as pb;
44
use crate::objects;
55
use crate::objects::s3::bucket::Bucket;
6+
use crate::secrets::Secret;
67
use aws_sdk_s3 as s3;
78

89
mod bucket;
@@ -13,8 +14,8 @@ pub struct Cluster {
1314
}
1415

1516
impl Cluster {
16-
pub fn new(cfg: &pb::bucket_cluster::S3) -> Self {
17-
let client = Arc::new(LazyS3Client::new(cfg.clone()));
17+
pub fn new(cfg: pb::bucket_cluster::S3, secret_access_key: Option<Secret>) -> Self {
18+
let client = Arc::new(LazyS3Client::new(cfg, secret_access_key));
1819
Self { client }
1920
}
2021
}
@@ -27,6 +28,7 @@ impl objects::ClusterImpl for Cluster {
2728

2829
struct LazyS3Client {
2930
cfg: pb::bucket_cluster::S3,
31+
secret_access_key: Option<Secret>,
3032
cell: tokio::sync::OnceCell<Arc<s3::Client>>,
3133
}
3234

@@ -37,9 +39,10 @@ impl std::fmt::Debug for LazyS3Client {
3739
}
3840

3941
impl LazyS3Client {
40-
fn new(cfg: pb::bucket_cluster::S3) -> Self {
42+
fn new(cfg: pb::bucket_cluster::S3, secret_access_key: Option<Secret>) -> Self {
4143
Self {
4244
cfg,
45+
secret_access_key,
4346
cell: tokio::sync::OnceCell::new(),
4447
}
4548
}
@@ -54,6 +57,26 @@ impl LazyS3Client {
5457
builder = builder.endpoint_url(endpoint.clone());
5558
}
5659

60+
if let (Some(access_key_id), Some(secret_access_key)) = (
61+
self.cfg.access_key_id.as_ref(),
62+
self.secret_access_key.as_ref(),
63+
) {
64+
use aws_credential_types::Credentials;
65+
let secret_access_key = secret_access_key
66+
.get()
67+
.expect("unable to resolve s3 secret access key");
68+
let secret_access_key = std::str::from_utf8(secret_access_key)
69+
.expect("unable to parse s3 secret access key as utf-8");
70+
71+
builder = builder.credentials_provider(Credentials::new(
72+
access_key_id,
73+
secret_access_key,
74+
None,
75+
None,
76+
"encore-runtime",
77+
));
78+
}
79+
5780
let cfg = builder.load().await;
5881
Arc::new(s3::Client::new(&cfg))
5982
})

runtimes/go/appruntime/exported/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ type S3BucketProvider struct {
406406
// The endpoint to use. If nil, the default endpoint for the region is used.
407407
// Must be set for non-AWS endpoints.
408408
Endpoint *string `json:"endpoint"`
409+
410+
// The access key to use. If either is nil, the default credentials are used.
411+
AccessKeyID *string `json:"access_key_id"`
412+
SecretAccessKey *string `json:"secret_access_key"`
409413
}
410414

411415
type GCSBucketProvider struct {

runtimes/go/appruntime/exported/config/infra/config.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,20 @@ func (p *ObjectStorage) UnmarshalJSON(data []byte) error {
132132
}
133133

134134
type S3 struct {
135-
Region string `json:"region"`
136-
Endpoint string `json:"endpoint,omitempty"`
137-
Buckets map[string]*Bucket `json:"buckets,omitempty"`
135+
Region string `json:"region"`
136+
Endpoint string `json:"endpoint,omitempty"`
137+
138+
AccessKeyID string `json:"access_key_id,omitempty"`
139+
SecretAccessKey EnvString `json:"secret_access_key,omitempty"`
140+
141+
Buckets map[string]*Bucket `json:"buckets,omitempty"`
138142
}
139143

140144
func (a *S3) Validate(v *validator) {
141145
v.ValidateField("region", NotZero(a.Region))
146+
if a.AccessKeyID != "" {
147+
v.ValidatePtrEnvRef("secret_access_key", &a.SecretAccessKey, "S3 Secret Access Key", NotZero[string])
148+
}
142149
ValidateChildMap(v, "buckets", a.Buckets)
143150
}
144151

runtimes/go/appruntime/exported/config/parse.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,10 @@ func parseInfraConfigEnv(infraCfgPath string) *Runtime {
376376
case "s3":
377377
cfg.BucketProviders[i] = &BucketProvider{
378378
S3: &S3BucketProvider{
379-
Region: storage.S3.Region,
380-
Endpoint: nilOr(storage.S3.Endpoint),
379+
Region: storage.S3.Region,
380+
Endpoint: nilOr(storage.S3.Endpoint),
381+
AccessKeyID: nilOr(storage.S3.AccessKeyID),
382+
SecretAccessKey: nilOr(storage.S3.SecretAccessKey.Value()),
381383
},
382384
}
383385
}

runtimes/go/storage/objects/bucket.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,6 @@ func (b *Bucket) PublicURL(object string, options ...PublicURLOption) *url.URL {
143143
if !strings.HasSuffix(u.Path, "/") {
144144
u.Path += "/"
145145
}
146-
147-
if b.runtimeCfg.KeyPrefix != "" {
148-
u.Path += escape(b.runtimeCfg.KeyPrefix, encodePath)
149-
}
150-
151146
u.Path += escape(object, encodePath)
152147

153148
return &u

runtimes/go/storage/objects/internal/providers/s3/bucket.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"cloud.google.com/go/storage"
1111
"github.com/aws/aws-sdk-go-v2/aws"
1212
awsConfig "github.com/aws/aws-sdk-go-v2/config"
13+
awsCreds "github.com/aws/aws-sdk-go-v2/credentials"
1314
"github.com/aws/aws-sdk-go-v2/service/s3"
1415
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
1516
"github.com/aws/smithy-go"
@@ -23,8 +24,8 @@ type Manager struct {
2324
runtime *config.Runtime
2425
clients map[*config.BucketProvider]*s3.Client
2526

26-
cfgOnce sync.Once
27-
awsCfg aws.Config
27+
cfgOnce sync.Once
28+
awsDefaultConfig aws.Config
2829
}
2930

3031
func NewManager(ctx context.Context, runtime *config.Runtime) *Manager {
@@ -155,7 +156,20 @@ func (mgr *Manager) clientForProvider(prov *config.BucketProvider) *s3.Client {
155156
return client
156157
}
157158

158-
cfg := mgr.getConfig()
159+
// If we have a custom access key and secret, use them instead of the default config.
160+
var cfg aws.Config
161+
if prov.S3.AccessKeyID != nil && prov.S3.SecretAccessKey != nil {
162+
var err error
163+
cfg, err = awsConfig.LoadDefaultConfig(context.Background(),
164+
awsConfig.WithCredentialsProvider(awsCreds.NewStaticCredentialsProvider(*prov.S3.AccessKeyID, *prov.S3.SecretAccessKey, "")),
165+
)
166+
if err != nil {
167+
panic(fmt.Sprintf("unable to load AWS config: %v", err))
168+
}
169+
} else {
170+
cfg = mgr.defaultConfig()
171+
}
172+
159173
client := s3.New(s3.Options{
160174
Region: prov.S3.Region,
161175
BaseEndpoint: prov.S3.Endpoint,
@@ -166,17 +180,16 @@ func (mgr *Manager) clientForProvider(prov *config.BucketProvider) *s3.Client {
166180
return client
167181
}
168182

169-
// getConfig loads the required AWS config to connect to AWS
170-
func (mgr *Manager) getConfig() aws.Config {
183+
// defaultConfig loads the required AWS config to connect to AWS
184+
func (mgr *Manager) defaultConfig() aws.Config {
171185
mgr.cfgOnce.Do(func() {
172186
cfg, err := awsConfig.LoadDefaultConfig(context.Background())
173187
if err != nil {
174188
panic(fmt.Sprintf("unable to load AWS config: %v", err))
175189
}
176-
mgr.awsCfg = cfg
177-
190+
mgr.awsDefaultConfig = cfg
178191
})
179-
return mgr.awsCfg
192+
return mgr.awsDefaultConfig
180193
}
181194

182195
func mapErr(err error) error {

0 commit comments

Comments
 (0)