Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 75 additions & 16 deletions src/commands/runtime/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,9 +1152,11 @@ impl RuntimeBuildCommand {
// modes, so it would otherwise vanish exactly where builds usually
// run. TUI queues it above the task region; JSON mode writes it to
// stderr without corrupting the NDJSON stream.
let project_dir = std::path::Path::new(&self.config_path)
.parent()
.unwrap_or(std::path::Path::new("."));
// The staging files are handed to the container through `/opt/src`,
// which is the resolved `src_dir`, so they have to be written under
// that root and not under the config file's directory. The two differ
// whenever `src_dir` is set.
let project_dir = config.project_root(&self.config_path);
Comment thread
nicksinas marked this conversation as resolved.
if let Err(e) = self
.generate_tuf_staging(
config,
Expand All @@ -1165,7 +1167,7 @@ impl RuntimeBuildCommand {
repo_release,
container_helper,
runs_on_context,
project_dir,
&project_dir,
)
.await
{
Expand Down Expand Up @@ -1388,9 +1390,10 @@ echo -n '}}'
)?;

// Phase 3: Write signed files into the build volume via a container run.
// We write them to a temp dir under the project directory (accessible inside
// the container as /opt/src/.tuf-staging-tmp/), then copy into the volume.
let tmp_dir = project_dir.join(".tuf-staging-tmp");
// They go to a temp dir under the project root, then a container copy
// moves them into the volume.
let (staging, staging_dir) = tuf_staging_dir(project_dir)?;
let tmp_dir = staging.path();
let tmp_delegations = tmp_dir.join("delegations");
std::fs::create_dir_all(&tmp_delegations)
.context("Failed to create TUF staging temp directory")?;
Expand All @@ -1409,8 +1412,8 @@ echo -n '}}'
r#"set -euo pipefail
DEST="$AVOCADO_PREFIX/runtimes/{runtime_name}/var-staging/lib/avocado/tuf-staging"
mkdir -p "$DEST/delegations"
cp /opt/src/.tuf-staging-tmp/targets.json "$DEST/targets.json"
cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
cp "/opt/src/{staging_dir}/targets.json" "$DEST/targets.json"
cp "/opt/src/{staging_dir}/delegations/runtime-{runtime_uuid}.json" \
"$DEST/delegations/runtime-{runtime_uuid}.json"
"#
);
Expand All @@ -1430,10 +1433,18 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
env_vars: self.runtime_env_vars(),
..Default::default()
};
run_container_command(container_helper, copy_run_config, runs_on_context).await?;
let copied =
run_container_command(container_helper, copy_run_config, runs_on_context).await?;

// Clean up host temp files.
let _ = std::fs::remove_dir_all(&tmp_dir);
drop(staging);

if !copied {
return Err(anyhow::anyhow!(
"failed to copy the delegation staging files into the build volume for runtime '{}'",
self.runtime_name
));
}

print_info(
&format!(
Expand Down Expand Up @@ -1507,7 +1518,8 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
)?;

// Phase 3: Write files into the build volume.
let tmp_dir = project_dir.join(".tuf-staging-tmp");
let (staging, staging_dir) = tuf_staging_dir(project_dir)?;
let tmp_dir = staging.path();
let tmp_delegations = tmp_dir.join("delegations");
std::fs::create_dir_all(&tmp_delegations)
.context("Failed to create TUF staging temp directory")?;
Expand All @@ -1526,8 +1538,8 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
r#"set -euo pipefail
DEST="$AVOCADO_PREFIX/runtimes/{runtime_name}/var-staging/lib/avocado/tuf-staging"
mkdir -p "$DEST/delegations"
cp /opt/src/.tuf-staging-tmp/targets.json "$DEST/targets.json"
cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
cp "/opt/src/{staging_dir}/targets.json" "$DEST/targets.json"
cp "/opt/src/{staging_dir}/delegations/runtime-{runtime_uuid}.json" \
"$DEST/delegations/runtime-{runtime_uuid}.json"
"#
);
Expand All @@ -1547,9 +1559,17 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
env_vars: self.runtime_env_vars(),
..Default::default()
};
run_container_command(container_helper, copy_run_config, runs_on_context).await?;
let copied =
run_container_command(container_helper, copy_run_config, runs_on_context).await?;

let _ = std::fs::remove_dir_all(&tmp_dir);
drop(staging);

if !copied {
return Err(anyhow::anyhow!(
"failed to copy the content delegation staging files into the build volume for runtime '{}'",
self.runtime_name
));
}

print_info(
&format!(
Expand Down Expand Up @@ -3158,6 +3178,24 @@ fn collect_runtime_compile_entries(merged_runtime: &serde_yaml::Value) -> Vec<Ru
}

/// Helper function to run a container command, using shared context if available
/// A fresh staging directory under the project root for one copy into the
/// volume. The container sees it as `/opt/src/<name>/`. Unique per call, so
/// two builds sharing a `src_dir` cannot overwrite or delete each other's
/// files; removed when the handle drops.
fn tuf_staging_dir(project_dir: &std::path::Path) -> Result<(tempfile::TempDir, String)> {
let staging = tempfile::Builder::new()
.prefix(".tuf-staging-")
.tempdir_in(project_dir)
.context("Failed to create TUF staging temp directory")?;
let name = staging
.path()
.file_name()
.and_then(|n| n.to_str())
.context("TUF staging temp directory has no name")?
.to_string();
Ok((staging, name))
}

async fn run_container_command(
container_helper: &SdkContainer,
config: RunConfig,
Expand Down Expand Up @@ -3317,6 +3355,27 @@ fi

#[cfg(test)]
mod tests {
/// Each staging call gets its own directory under the project root, so
/// two builds sharing a `src_dir` never write or delete each other's
/// files. The name is what the copy script appends to `/opt/src`.
#[test]
fn tuf_staging_dir_is_unique_per_call_and_removed_on_drop() {
let root = tempfile::TempDir::new().unwrap();
let (a, a_name) = super::tuf_staging_dir(root.path()).unwrap();
let (b, b_name) = super::tuf_staging_dir(root.path()).unwrap();
assert_ne!(a_name, b_name);
assert!(a_name.starts_with(".tuf-staging-"));
assert_eq!(a.path(), root.path().join(&a_name));
assert!(a.path().is_dir() && b.path().is_dir());
let a_path = a.path().to_path_buf();
drop(a);
assert!(
!a_path.exists(),
"dropping the handle removes the directory"
);
assert!(b.path().is_dir(), "the other call's directory is untouched");
}

#[test]
fn fit_assembly_script_is_gated_and_handles_signing_both_ways() {
let s = super::generate_fit_assembly_script();
Expand Down
31 changes: 27 additions & 4 deletions src/utils/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2382,14 +2382,15 @@ impl Config {
return Ok(RemoteExtensionMerge::default());
}

// Get src_dir for loading volume state
// `src_dir` is the base for `path:` sources. The volume state is keyed
// on the working directory, not on `src_dir`.
let config_path_str = config_path.to_string_lossy();
let src_dir = temp_config
.get_resolved_src_dir::<&str>(config_path_str.as_ref())
.unwrap_or_else(|| config_file_dir(config_path).to_path_buf());

// Try to load volume state for container-based config reading
let volume_state = crate::utils::volume::VolumeState::load_from_dir(&src_dir)
let volume_state = crate::utils::volume::VolumeState::load_from_current_dir()
.ok()
.flatten();

Expand Down Expand Up @@ -5505,8 +5506,9 @@ impl Config {
.to_path_buf()
});

// Try to load volume state and get the mountpoint
if let Ok(Some(volume_state)) = crate::utils::volume::VolumeState::load_from_dir(&src_dir) {
// Try to load volume state and get the mountpoint. The state file is
// keyed on the working directory, not on `src_dir`.
if let Ok(Some(volume_state)) = crate::utils::volume::VolumeState::load_from_current_dir() {
// Use synchronous Docker inspect to get the mountpoint
if let Ok(mountpoint) =
crate::utils::ext_source_reader::volume_mountpoint(&volume_state)
Expand Down Expand Up @@ -6895,6 +6897,27 @@ mod tests {
);
}

/// With `src_dir` set, the project root is `src_dir` and not the config
/// file's directory. The runtime build writes its TUF staging files under
/// the root and the container reads them back through `/opt/src`, which
/// is `src_dir`; a root taken from the config path put the two out of step.
#[test]
fn project_root_follows_src_dir_above_the_config_dir() {
let dir = std::env::temp_dir().join(format!(
"avocado-project-root-src-dir-{}",
std::process::id()
));
let config_dir = dir.join("project").join("avocado");
std::fs::create_dir_all(&config_dir).unwrap();
let config_path = config_dir.join("avocado.yaml");

let config = super::Config::load_from_yaml_str("src_dir: ..\nextensions: {}\n").unwrap();
let root = config.project_root(&config_path);
assert_eq!(root, dir.join("project").canonicalize().unwrap());

let _ = std::fs::remove_dir_all(&dir);
}

/// Recording the feed set must be a no-op when there is nothing to record.
///
/// Two ways to get this wrong, and the first shipped: comparing the record's
Expand Down
39 changes: 39 additions & 0 deletions src/utils/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ impl VolumeState {
Ok(Some(state))
}

/// Load the volume state the container layer keys on.
///
/// `get_or_create_volume` writes `.avocado-state` in the process working
/// directory (`SdkContainer::cwd`), not in `src_dir`. Readers that looked in
/// `src_dir` found nothing whenever the two differed, so every reader of
/// the state file goes through this one key.
pub fn load_from_current_dir() -> Result<Option<Self>> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self::load_from_dir(&cwd)
}

/// Save volume state to .avocado-state file in the given directory
pub fn save_to_dir(&self, dir_path: &Path) -> Result<()> {
let state_file = dir_path.join(".avocado-state");
Expand Down Expand Up @@ -629,4 +640,32 @@ mod tests {
avo_abandonment_reason("avo-different-uuid", Some(&state.source_path)).unwrap();
assert!(reason.contains("not this volume"));
}

/// The state file lives in the working directory, which is where
/// `get_or_create_volume` writes it. A copy in `src_dir` is not consulted,
/// so a `src_dir: ..` project run from its config directory finds the
/// volume the container layer created there.
#[test]
#[serial_test::serial]
fn load_from_current_dir_reads_the_working_directory_only() {
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().canonicalize().unwrap();
let config_dir = src_dir.join("avocado");
fs::create_dir_all(&config_dir).unwrap();
VolumeState::new(src_dir.clone(), "docker".to_string())
.save_to_dir(&src_dir)
.unwrap();

// Restore the cwd before asserting, so a failure here does not leave
// later tests running from a deleted directory.
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&config_dir).unwrap();
let from_config_dir = VolumeState::load_from_current_dir();
std::env::set_current_dir(&src_dir).unwrap();
let from_src_dir = VolumeState::load_from_current_dir();
std::env::set_current_dir(prev).unwrap();

assert!(from_config_dir.unwrap().is_none());
assert!(from_src_dir.unwrap().is_some());
}
}
Loading