diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index 99a96aa6d..2d408dd9a 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -145,8 +145,10 @@ impl Resolve for State { "get builder", format_serror(&e.context("failed to get builder").into()), )); - return handle_early_return(update, build.id, build.name, false) - .await; + return handle_early_return( + update, build.id, build.name, false, + ) + .await; } }; @@ -196,6 +198,9 @@ impl Resolve for State { // Interpolate variables / secrets into build args let mut global_replacers = HashSet::new(); let mut secret_replacers = HashSet::new(); + let mut secret_replacers_for_log = HashSet::new(); + + // Interpolate into build args for arg in &mut build.config.build_args { // first pass - global variables let (res, more_replacers) = svi::interpolate_variables( @@ -214,10 +219,40 @@ impl Resolve for State { false, ) .context("failed to interpolate core secrets")?; + secret_replacers_for_log.extend( + more_replacers.iter().map(|(_, variable)| variable.clone()), + ); secret_replacers.extend(more_replacers); arg.value = res; } + // Interpolate into secret args + for arg in &mut build.config.secret_args { + // first pass - global variables + let (res, more_replacers) = svi::interpolate_variables( + &arg.value, + &variables, + svi::Interpolator::DoubleBrackets, + false, + ) + .context("failed to interpolate global variables")?; + global_replacers.extend(more_replacers); + // second pass - core secrets + let (res, more_replacers) = svi::interpolate_variables( + &res, + &core_config.secrets, + svi::Interpolator::DoubleBrackets, + false, + ) + .context("failed to interpolate core secrets")?; + secret_replacers_for_log.extend( + more_replacers.into_iter().map(|(_, variable)| variable), + ); + // Secret args don't need to be in replacers sent to periphery. + // The secret args don't end up in the command like build args do. + arg.value = res; + } + // Show which variables were interpolated if !global_replacers.is_empty() { update.push_simple_log( @@ -232,9 +267,9 @@ impl Resolve for State { if !secret_replacers.is_empty() { update.push_simple_log( "interpolate core secrets", - secret_replacers - .iter() - .map(|(_, variable)| format!("replaced: {variable}")) + secret_replacers_for_log + .into_iter() + .map(|variable| format!("replaced: {variable}")) .collect::>() .join("\n"), ); diff --git a/bin/periphery/src/api/build.rs b/bin/periphery/src/api/build.rs index 93d5feec2..05e33d1d5 100644 --- a/bin/periphery/src/api/build.rs +++ b/bin/periphery/src/api/build.rs @@ -1,4 +1,4 @@ -use anyhow::Context; +use anyhow::{anyhow, Context}; use command::run_monitor_command; use formatting::format_serror; use monitor_client::entities::{ @@ -20,10 +20,7 @@ use crate::{ }; impl Resolve for State { - #[instrument( - name = "Build", - skip(self, registry_token, core_replacers, aws_ecr) - )] + #[instrument(name = "Build", skip_all)] async fn resolve( &self, build::Build { @@ -43,6 +40,7 @@ impl Resolve for State { build_path, dockerfile_path, build_args, + secret_args, labels, extra_args, use_buildx, @@ -88,6 +86,8 @@ impl Resolve for State { let image_name = get_image_name(&build, |_| aws_ecr) .context("failed to make image name")?; let build_args = parse_build_args(build_args); + let _secret_args = + parse_secret_args(secret_args, *skip_secret_interp)?; let labels = parse_labels(labels); let extra_args = parse_extra_args(extra_args); let buildx = if *use_buildx { " buildx" } else { "" }; @@ -100,9 +100,9 @@ impl Resolve for State { // Construct command let command = format!( - "cd {} && docker{buildx} build{build_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}", - build_dir.display() - ); + "cd {} && docker{buildx} build{build_args}{_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}", + build_dir.display() + ); if *skip_secret_interp { let build_log = @@ -133,6 +133,8 @@ impl Resolve for State { logs.push(build_log); } + cleanup_secret_env_vars(secret_args); + Ok(logs) } } @@ -152,6 +154,47 @@ fn parse_build_args(build_args: &[EnvironmentVar]) -> String { .join("") } +fn parse_secret_args( + secret_args: &[EnvironmentVar], + skip_secret_interp: bool, +) -> anyhow::Result { + let periphery_config = periphery_config(); + Ok( + secret_args + .iter() + .map(|EnvironmentVar { variable, value }| { + if variable.is_empty() { + return Err(anyhow!("secret variable cannot be empty string")) + } else if variable.contains('=') { + return Err(anyhow!("invalid variable {variable}. variable cannot contain '='")) + } + let value = if skip_secret_interp { + value.to_string() + } else { + svi::interpolate_variables( + value, + &periphery_config.secrets, + svi::Interpolator::DoubleBrackets, + true, + ) + .context( + "failed to interpolate periphery secrets into build secrets", + )?.0 + }; + std::env::set_var(variable, value); + anyhow::Ok(format!(" --secret id={variable}")) + }) + .collect::>>()? + .join(""), + ) +} + +fn cleanup_secret_env_vars(secret_args: &[EnvironmentVar]) { + secret_args.iter().for_each( + |EnvironmentVar { variable, .. }| std::env::remove_var(variable), + ) +} + // impl Resolve for State { diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index 3c36e20d1..c8524aa26 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -143,7 +143,7 @@ pub struct BuildConfig { pub extra_args: Vec, /// Docker build arguments. - /// + /// /// These values are visible in the final image by running `docker inspect`. #[serde( default, @@ -157,9 +157,15 @@ pub struct BuildConfig { pub build_args: Vec, /// Secret arguments. - /// + /// /// These values remain hidden in the final image by using /// docker secret mounts. See ``. + /// + /// To use the values, add commands like this in the Dockerfile: + /// ``` + /// RUN --mount=type=secret,id=SECRET_KEY \ + /// SECRET_VALUE=$(cat /run/secrets/SECRET_KEY) ... + /// ``` #[serde( default, deserialize_with = "super::env_vars_deserializer"