Skip to content

Commit 4727c33

Browse files
author
Roy Lin
committed
feat(build): COPY --from=<external image> pulls and copies from that image
Docker resolves COPY --from=<ref> as an external image when <ref> is neither a stage alias nor index, pulling it and copying from its rootfs; a3s-box errored 'stage not found'. The COPY handler (in the async build loop) now falls back to pulling the image and extracting it to a temp rootfs (memoized per build so several copies from one image pull once) when the stage lookup fails. Verified on Linux: COPY --from=alpine /etc/alpine-release copies the file (3.23.4).
1 parent c1eb98b commit 4727c33

1 file changed

Lines changed: 57 additions & 2 deletions

File tree

  • src/runtime/src/oci/build/engine

src/runtime/src/oci/build/engine/mod.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ pub async fn build(config: BuildConfig, store: Arc<ImageStore>) -> Result<BuildR
232232

233233
// Track completed stages: (alias, rootfs_path)
234234
let mut completed_stages: Vec<(Option<String>, PathBuf)> = Vec::new();
235+
// Cache external images already pulled+extracted for `COPY --from=<image>`
236+
// (keyed by image ref) so multiple copies from one image pull once.
237+
let mut external_from_rootfs: HashMap<String, PathBuf> = HashMap::new();
235238

236239
// Create temp directory for build workspace
237240
let build_dir = tempfile::TempDir::new()
@@ -433,14 +436,29 @@ pub async fn build(config: BuildConfig, store: Arc<ImageStore>) -> Result<BuildR
433436
dst
434437
);
435438
}
436-
let from_rootfs = resolve_stage_rootfs(from_ref, &completed_stages)?;
439+
// `--from` is a prior stage (by alias or index) or, like
440+
// Docker, an external image reference to pull and copy
441+
// from.
442+
let from_rootfs: PathBuf =
443+
match resolve_stage_rootfs(from_ref, &completed_stages) {
444+
Ok(stage_rootfs) => stage_rootfs.to_path_buf(),
445+
Err(_) => {
446+
resolve_external_image_rootfs(
447+
from_ref,
448+
&store,
449+
build_dir.path(),
450+
&mut external_from_rootfs,
451+
)
452+
.await?
453+
}
454+
};
437455
// .dockerignore applies to the build context, not to a
438456
// source stage's rootfs.
439457
let layer_info = handle_copy(
440458
src,
441459
dst,
442460
chown.as_deref(),
443-
from_rootfs,
461+
&from_rootfs,
444462
&rootfs_dir,
445463
&layers_dir,
446464
&state.workdir,
@@ -970,6 +988,43 @@ async fn handle_from(
970988
Ok((base_layers, base_diff_ids, config))
971989
}
972990

991+
/// Resolve `COPY --from=<image>` when `<image>` is not a build stage: pull the
992+
/// external image and extract it to a temp rootfs to copy from (Docker behavior).
993+
/// Memoized per build so several copies from one image pull only once.
994+
async fn resolve_external_image_rootfs(
995+
image_ref: &str,
996+
store: &Arc<ImageStore>,
997+
build_dir: &Path,
998+
cache: &mut HashMap<String, PathBuf>,
999+
) -> Result<PathBuf> {
1000+
if let Some(dir) = cache.get(image_ref) {
1001+
return Ok(dir.clone());
1002+
}
1003+
1004+
let dir = build_dir.join(format!("copyfrom_{}", cache.len()));
1005+
std::fs::create_dir_all(&dir).map_err(|e| {
1006+
BoxError::BuildError(format!(
1007+
"Failed to create COPY --from image rootfs {}: {}",
1008+
dir.display(),
1009+
e
1010+
))
1011+
})?;
1012+
1013+
let puller = ImagePuller::new(store.clone(), RegistryAuth::from_env());
1014+
let oci_image = puller.pull(image_ref).await.map_err(|e| {
1015+
BoxError::BuildError(format!(
1016+
"COPY --from={}: not a build stage and could not be pulled as an image: {}",
1017+
image_ref, e
1018+
))
1019+
})?;
1020+
for layer_path in oci_image.layer_paths() {
1021+
extract_layer(layer_path, &dir)?;
1022+
}
1023+
1024+
cache.insert(image_ref.to_string(), dir.clone());
1025+
Ok(dir)
1026+
}
1027+
9731028
fn validate_build_config(config: &BuildConfig) -> Result<()> {
9741029
if config.platforms.len() > 1 {
9751030
return Err(BoxError::BuildError(

0 commit comments

Comments
 (0)