diff --git a/Cargo.lock b/Cargo.lock index d3bbaa3..ecf6f22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,6 +662,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "buildomat-aws-common" +version = "0.1.0" +dependencies = [ + "aws-config", + "aws-credential-types", + "thiserror 2.0.18", +] + [[package]] name = "buildomat-bunyan" version = "0.0.0" @@ -754,6 +763,7 @@ dependencies = [ "aws-sdk-ec2", "aws-types", "base64 0.22.1", + "buildomat-aws-common", "buildomat-client", "buildomat-common", "buildomat-types", @@ -1017,6 +1027,7 @@ dependencies = [ "aws-credential-types", "aws-sdk-s3", "aws-types", + "buildomat-aws-common", "buildomat-common", "buildomat-database", "buildomat-download", diff --git a/Cargo.toml b/Cargo.toml index f44a2b6..7c34fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "agent", + "aws-common", "bin", "bunyan", "client", diff --git a/aws-common/Cargo.toml b/aws-common/Cargo.toml new file mode 100644 index 0000000..042f75f --- /dev/null +++ b/aws-common/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "buildomat-aws-common" +version = "0.1.0" +edition = "2024" + +[dependencies] +aws-config = { workspace = true } +aws-credential-types = { workspace = true } +thiserror = { workspace = true } diff --git a/aws-common/src/lib.rs b/aws-common/src/lib.rs new file mode 100644 index 0000000..3706a14 --- /dev/null +++ b/aws-common/src/lib.rs @@ -0,0 +1,68 @@ +use aws_config::default_provider::credentials::DefaultCredentialsChain; +use aws_config::{BehaviorVersion, ConfigLoader, Region, SdkConfig}; +use aws_credential_types::Credentials; +use aws_credential_types::provider::SharedCredentialsProvider; + +pub struct AwsConfig { + pub access_key_id: Option, + pub secret_access_key: Option, + pub profile: Option, + pub region: String, +} + +impl AwsConfig { + pub async fn into_sdk_config(self) -> Result { + let creds: SharedCredentialsProvider = + match (self.access_key_id, self.secret_access_key, self.profile) { + /* + * When an hardcoded AWS access key is present, provide it statically. + */ + (Some(aki), Some(sak), None) => SharedCredentialsProvider::new( + Credentials::new(aki, sak, None, None, "buildomat"), + ), + /* + * When a profile is selected, tell the default credential chain + * to use it, dynamically fetching the access key. This could be + * used authenticate with AWS SSO on a developer machine. + */ + (None, None, Some(profile)) => SharedCredentialsProvider::new( + DefaultCredentialsChain::builder() + .profile_name(&profile) + .build() + .await, + ), + /* + * When nothing is selected, use the default credential chain to + * dynamically fetch the access key. This could be used to + * authenticate an AWS instance using their metadata service. + */ + (None, None, None) => SharedCredentialsProvider::new( + DefaultCredentialsChain::builder().build().await, + ), + /* + * Provide good error messages for invalid configurations. + */ + (Some(_), None, _) | (None, Some(_), _) => { + return Err(AwsConfigError::IncompleteAccessKey); + } + (Some(_), Some(_), Some(_)) => { + return Err(AwsConfigError::MultipleCredentials); + } + }; + + Ok(ConfigLoader::default() + .region(Region::new(self.region)) + .credentials_provider(creds) + .behavior_version(BehaviorVersion::v2026_01_12()) + .load() + .await) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AwsConfigError { + #[error("both access_key_id and secret_access_key are required")] + IncompleteAccessKey, + #[error("cannot use both an aws profile and hardcoded credentials")] + MultipleCredentials, +} diff --git a/factory/aws/Cargo.toml b/factory/aws/Cargo.toml index 2d880b3..4794ff7 100644 --- a/factory/aws/Cargo.toml +++ b/factory/aws/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" license = "MPL-2.0" [dependencies] +buildomat-aws-common = { path = "../../aws-common" } buildomat-client = { path = "../../client" } buildomat-common = { path = "../../common" } buildomat-types = { path = "../../types" } diff --git a/factory/aws/src/aws.rs b/factory/aws/src/aws.rs index ab8011a..687d073 100644 --- a/factory/aws/src/aws.rs +++ b/factory/aws/src/aws.rs @@ -8,15 +8,13 @@ use std::time::{Duration, UNIX_EPOCH}; use std::{collections::HashMap, time::SystemTime}; use anyhow::{anyhow, bail, Result}; -use aws_config::Region; -use aws_config::{meta::region::RegionProviderChain, BehaviorVersion}; -use aws_sdk_ec2::config::Credentials; use aws_sdk_ec2::types::{ BlockDeviceMapping, EbsBlockDevice, Filter, InstanceNetworkInterfaceSpecification, InstanceType, ResourceType, Tag, TagSpecification, }; use base64::Engine; +use buildomat_aws_common::AwsConfig; use buildomat_client::types::*; use slog::{debug, error, info, o, warn, Logger}; @@ -589,25 +587,14 @@ async fn aws_worker_one( pub(crate) async fn aws_worker(c: Arc) -> Result<()> { let log = c.log.new(o!("component" => "worker")); - let region = RegionProviderChain::first_try(Region::new( - c.config.aws.region.clone(), - )) - .region() - .await - .ok_or_else(|| anyhow!("could not select region"))?; - let creds = Credentials::new( - &c.config.aws.access_key_id, - &c.config.aws.secret_access_key, - None, - None, - "config-file", - ); - - let cfg = aws_config::defaults(BehaviorVersion::v2026_01_12()) - .region(region) - .credentials_provider(creds) - .load() - .await; + let cfg = AwsConfig { + access_key_id: c.config.aws.access_key_id.clone(), + secret_access_key: c.config.aws.secret_access_key.clone(), + profile: c.config.aws.profile.clone(), + region: c.config.aws.region.clone(), + } + .into_sdk_config() + .await?; let ec2 = aws_sdk_ec2::Client::new(&cfg); diff --git a/factory/aws/src/config.rs b/factory/aws/src/config.rs index ba97e8b..29c3f2d 100644 --- a/factory/aws/src/config.rs +++ b/factory/aws/src/config.rs @@ -43,8 +43,9 @@ pub(crate) struct ConfigFileAwsTarget { #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] pub(crate) struct ConfigFileAws { - pub access_key_id: String, - pub secret_access_key: String, + pub access_key_id: Option, + pub secret_access_key: Option, + pub profile: Option, pub region: String, pub vpc: String, pub subnet: String, diff --git a/server/Cargo.toml b/server/Cargo.toml index bf34ab6..6525190 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" license = "MPL-2.0" [dependencies] +buildomat-aws-common = { path = "../aws-common" } buildomat-common = { path = "../common" } buildomat-database = { path = "../database" } buildomat-download = { path = "../download" } diff --git a/server/src/config.rs b/server/src/config.rs index 5115eda..dee1482 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -102,29 +102,14 @@ pub struct ConfigFileAdmin { #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct ConfigFileStorage { - pub access_key_id: String, - pub secret_access_key: String, + pub access_key_id: Option, + pub secret_access_key: Option, + pub profile: Option, pub bucket: String, pub prefix: String, pub region: String, } -impl ConfigFileStorage { - pub fn creds(&self) -> aws_credential_types::Credentials { - aws_credential_types::Credentials::new( - &self.access_key_id, - &self.secret_access_key, - None, - None, - "buildomat", - ) - } - - pub fn region(&self) -> aws_types::region::Region { - aws_types::region::Region::new(self.region.to_string()) - } -} - pub fn load>(path: P) -> Result { read_toml(path.as_ref()) } diff --git a/server/src/main.rs b/server/src/main.rs index b7358da..7d9e5b4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -39,6 +39,7 @@ mod files; mod jobs; mod workers; +use buildomat_aws_common::AwsConfig; use db::{ AuthUser, Job, JobEvent, JobFile, JobFileId, JobId, JobOutput, JobOutputAndFile, Worker, WorkerEvent, @@ -1053,12 +1054,15 @@ async fn main() -> Result<()> { dbfile.push("data.sqlite3"); let db = db::Database::new(log.clone(), dbfile, config.sqlite.cache_kb)?; - let awscfg = aws_config::ConfigLoader::default() - .region(config.storage.region()) - .credentials_provider(config.storage.creds()) - .behavior_version(aws_config::BehaviorVersion::v2026_01_12()) - .load() - .await; + let awscfg = AwsConfig { + access_key_id: config.storage.access_key_id.clone(), + secret_access_key: config.storage.secret_access_key.clone(), + profile: config.storage.profile.clone(), + region: config.storage.region.clone(), + } + .into_sdk_config() + .await?; + let s3 = aws_sdk_s3::Client::new(&awscfg); let files = files::Files::new(log.new(o!("component" => "files")));