Skip to content
Merged
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
5 changes: 1 addition & 4 deletions cmd/devcontainer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ pub fn resolve_command_help<'a>(
let mut current = command_help(command)?;
let mut consumed_args = 0;

loop {
let Some(next_arg) = args.get(consumed_args) else {
break;
};
while let Some(next_arg) = args.get(consumed_args) {
let Some(child) = child_command(&current.path, next_arg) else {
break;
};
Expand Down
21 changes: 14 additions & 7 deletions cmd/devcontainer/src/commands/common/config_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,20 @@ pub(crate) fn load_resolved_config(args: &[String]) -> Result<(PathBuf, PathBuf,
})
.or_else(|| {
Some(
crate::runtime::context::derived_workspace_mount(&workspace_folder, args)
.map(|derived| derived.remote_workspace_folder)
.unwrap_or_else(|| {
crate::runtime::context::default_remote_workspace_folder(Some(
&workspace_folder,
))
}),
if crate::runtime::compose::uses_compose_config(&parsed)
&& parsed.get("workspaceFolder").is_none()
&& parsed.get("workspaceMount").is_none()
{
"/".to_string()
} else {
crate::runtime::context::derived_workspace_mount(&workspace_folder, args)
.map(|derived| derived.remote_workspace_folder)
.unwrap_or_else(|| {
crate::runtime::context::default_remote_workspace_folder(Some(
&workspace_folder,
))
})
},
)
});
let substituted = config::substitute_local_context(
Expand Down
17 changes: 13 additions & 4 deletions cmd/devcontainer/src/runtime/compose/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,19 @@ pub(super) fn compose_args_owned(
}

pub(super) fn reject_unsupported_build_options(args: &[String]) -> Result<(), String> {
for flag in ["--cache-from", "--cache-to", "--platform", "--label"] {
if compose_build_option_is_present(args, flag) {
return Err(format!("{flag} not supported for compose builds."));
}
if compose_build_option_is_present(args, "--cache-to") {
return Err("--cache-to not supported for compose builds.".to_string());
}
if compose_build_option_is_present(args, "--platform")
|| compose_build_option_is_present(args, "--push")
{
return Err("--platform or --push not supported.".to_string());
}
if compose_build_option_is_present(args, "--output") {
return Err("--output not supported.".to_string());
}
if compose_build_option_is_present(args, "--label") {
return Err("--label not supported for compose builds.".to_string());
}
Ok(())
}
Expand Down
53 changes: 33 additions & 20 deletions cmd/devcontainer/src/runtime/compose/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ pub(crate) fn load_compose_spec(resolved: &ResolvedConfig) -> Result<Option<Comp
.config_file
.parent()
.unwrap_or(resolved.workspace_folder.as_path());
let files = service::compose_files(&resolved.configuration, config_root)?;
let files = service::compose_files(
&resolved.configuration,
config_root,
&resolved.workspace_folder,
)?;
let service = resolved
.configuration
.get("service")
Expand Down Expand Up @@ -73,15 +77,20 @@ pub(crate) fn build_service(resolved: &ResolvedConfig, args: &[String]) -> Resul
)?;

if spec.has_build {
let build_override_file = override_file::compose_build_override_file(&spec, args)?;
let mut build_args = vec!["--pull".to_string()];
if common::has_flag(args, "--no-cache") || common::has_flag(args, "--build-no-cache") {
build_args.push("--no-cache".to_string());
}
build_args.push(spec.service.clone());
let result = engine::run_compose(
args,
args::compose_args_owned(&spec, "build", None, build_args),
)?;
args::compose_args_owned(&spec, "build", build_override_file.as_ref(), build_args),
);
if let Some(build_override_file) = build_override_file {
let _ = std::fs::remove_file(build_override_file);
}
let result = result?;
if result.status_code != 0 {
return Err(engine::stderr_or_stdout(&result));
}
Expand Down Expand Up @@ -115,17 +124,6 @@ pub(crate) fn build_service(resolved: &ResolvedConfig, args: &[String]) -> Resul
return Ok(built_image);
}

if common::has_flag(args, "--push") {
if let Some(image) = &spec.image {
let push_result = engine::run_engine(args, vec!["push".to_string(), image.clone()])?;
if push_result.status_code != 0 {
return Err(engine::stderr_or_stdout(&push_result));
}
} else {
return Err("Compose build push requires the service to declare an image".to_string());
}
}

Ok(spec
.image
.clone()
Expand All @@ -137,6 +135,7 @@ pub(crate) fn up_service(
args: &[String],
remote_workspace_folder: &str,
image_name: &str,
no_recreate: bool,
) -> Result<(), String> {
let spec = load_compose_spec(resolved)?
.ok_or_else(|| "Compose configuration was expected but not found".to_string())?;
Expand All @@ -150,14 +149,28 @@ pub(crate) fn up_service(
None
},
)?;
let mut up_args = vec!["-d".to_string()];
if no_recreate {
up_args.push("--no-recreate".to_string());
}
if let Some(run_services) = resolved
.configuration
.get("runServices")
.and_then(Value::as_array)
.filter(|services| !services.is_empty())
{
let mut has_primary_service = false;
for service in run_services.iter().filter_map(Value::as_str) {
has_primary_service |= service == spec.service;
up_args.push(service.to_string());
}
if !has_primary_service {
up_args.push(spec.service.clone());
}
}
let result = engine::run_compose(
args,
args::compose_args_with_override(
&spec,
"up",
&["-d", &spec.service],
override_file.as_ref(),
),
args::compose_args_owned(&spec, "up", override_file.as_ref(), up_args),
)?;
if let Some(override_file) = override_file {
let _ = std::fs::remove_file(override_file);
Expand Down
179 changes: 158 additions & 21 deletions cmd/devcontainer/src/runtime/compose/override_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,51 @@ use super::super::container;
use super::super::context::ResolvedConfig;
use super::super::metadata::serialized_container_metadata;
use super::super::paths::unique_temp_path;
use override_mounts::{compose_additional_volumes, compose_environment, compose_workspace_volume};
use override_yaml::{escape_compose_label, escape_compose_scalar, render_compose_volume_entry};
use super::service::{self, ServiceDefinition};
use super::ComposeSpec;
use override_mounts::{
compose_additional_volumes, compose_environment, compose_named_volumes,
compose_workspace_volume,
};
use override_yaml::{
escape_compose_label, escape_compose_scalar, render_compose_string_sequence,
render_compose_volume_entry, render_named_volume_entry,
};

pub(super) fn compose_build_override_file(
spec: &ComposeSpec,
args: &[String],
) -> Result<Option<PathBuf>, String> {
let cache_from = common::parse_option_values(args, "--cache-from");
if cache_from.is_empty() {
return Ok(None);
}

let mut content = service::read_version_prefix(&spec.files).unwrap_or_default();
content.push_str(&format!(
"services:\n '{}':\n build:\n cache_from:\n",
spec.service
));
for value in cache_from {
content.push_str(&format!(" - '{}'\n", escape_compose_scalar(&value)));
}

let override_file = unique_temp_path("devcontainer-compose-build-override", Some("yml"));
std::fs::write(&override_file, content).map_err(|error| error.to_string())?;
Ok(Some(override_file))
}

pub(super) fn compose_metadata_override_file(
resolved: &ResolvedConfig,
args: &[String],
remote_workspace_folder: &str,
image_name: Option<&str>,
) -> Result<Option<PathBuf>, String> {
let service_name = resolved
.configuration
.get("service")
.and_then(Value::as_str)
.ok_or_else(|| "Compose configuration must define service".to_string())?;
let metadata = serialized_container_metadata(
&resolved.configuration,
remote_workspace_folder,
Expand All @@ -45,14 +81,12 @@ pub(super) fn compose_metadata_override_file(
return Ok(None);
}

let mut content = String::from("services:\n");
let (version_prefix, service_definition) = compose_override_context(resolved, service_name);
let mut content = version_prefix;
content.push_str("services:\n");
content.push_str(&format!(
" '{}':\n labels:{}\n",
resolved
.configuration
.get("service")
.and_then(Value::as_str)
.ok_or_else(|| "Compose configuration must define service".to_string())?,
service_name,
labels
.iter()
.map(|label| format!("\n - '{}'", escape_compose_label(label)))
Expand All @@ -64,15 +98,23 @@ pub(super) fn compose_metadata_override_file(
escape_compose_scalar(image_name)
));
}
content.push_str(&format!(
" entrypoint: {}\n",
compose_wrapper_entrypoint(resolved, service_definition.as_ref())?
));
if let Some(command) = compose_wrapper_command(resolved, service_definition.as_ref())? {
content.push_str(&format!(" command: {command}\n"));
}
let mut volumes = Vec::new();
if let Some(volume) = compose_workspace_volume(resolved, args, remote_workspace_folder) {
volumes.push(volume);
}
volumes.extend(compose_additional_volumes(resolved, args));
volumes.extend(compose_additional_volumes(resolved, args)?);
let named_volumes = compose_named_volumes(&volumes);
if !volumes.is_empty() {
content.push_str("\n volumes:\n");
for volume in volumes {
content.push_str(&render_compose_volume_entry(&volume));
for volume in &volumes {
content.push_str(&render_compose_volume_entry(volume));
}
}
if let Some(environment) = compose_environment(&resolved.configuration) {
Expand Down Expand Up @@ -131,27 +173,122 @@ pub(super) fn compose_metadata_override_file(
content.push_str(&format!(" - '{}'\n", escape_compose_scalar(option)));
}
}
if let Some(entrypoint) = resolved
.configuration
.get("entrypoint")
.and_then(Value::as_str)
{
content.push_str(&format!(
" entrypoint: '{}'\n",
escape_compose_scalar(entrypoint)
));
}
if container::should_add_gpu_capability(&resolved.configuration, args)? {
content.push_str(
" deploy:\n resources:\n reservations:\n devices:\n - capabilities: [gpu]\n",
);
}
if !named_volumes.is_empty() {
content.push_str("\nvolumes:\n");
for named_volume in &named_volumes {
content.push_str(&render_named_volume_entry(named_volume));
}
}

let override_file = unique_override_file_path();
std::fs::write(&override_file, content).map_err(|error| error.to_string())?;
Ok(Some(override_file))
}

fn compose_override_context(
resolved: &ResolvedConfig,
service_name: &str,
) -> (String, Option<ServiceDefinition>) {
let config_root = resolved
.config_file
.parent()
.unwrap_or(resolved.workspace_folder.as_path());
let Ok(compose_files) = service::compose_files(
&resolved.configuration,
config_root,
&resolved.workspace_folder,
) else {
return (String::new(), None);
};
let version_prefix = service::read_version_prefix(&compose_files).unwrap_or_default();
let service_definition = service::inspect_service_definition(&compose_files, service_name).ok();
(version_prefix, service_definition)
}

fn compose_wrapper_entrypoint(
resolved: &ResolvedConfig,
service_definition: Option<&ServiceDefinition>,
) -> Result<String, String> {
let override_command = resolved
.configuration
.get("overrideCommand")
.and_then(Value::as_bool)
.unwrap_or(false);
let compose_entrypoint =
service_definition.and_then(|definition| definition.entrypoint.clone());
let user_entrypoint = if override_command {
Vec::new()
} else {
compose_entrypoint.unwrap_or_default()
};
let custom_entrypoints = merged_entrypoints(resolved).join("\n\n");
let script = format!(
"echo Container started\ntrap \"exit 0\" 15\n{custom_entrypoints}\nexec \"$$@\"\nwhile sleep 1 & wait $$!; do :; done"
);
let mut entrypoint = vec![
"/bin/sh".to_string(),
"-c".to_string(),
script,
"-".to_string(),
];
entrypoint.extend(user_entrypoint);
render_compose_string_sequence(&entrypoint)
}

fn compose_wrapper_command(
resolved: &ResolvedConfig,
service_definition: Option<&ServiceDefinition>,
) -> Result<Option<String>, String> {
let override_command = resolved
.configuration
.get("overrideCommand")
.and_then(Value::as_bool)
.unwrap_or(false);
let compose_entrypoint =
service_definition.and_then(|definition| definition.entrypoint.clone());
let compose_command = service_definition.and_then(|definition| definition.command.clone());
let user_command = if override_command {
Some(Vec::new())
} else if let Some(command) = compose_command.clone() {
Some(command)
} else if compose_entrypoint.is_some() {
Some(Vec::new())
} else {
None
};
if user_command == compose_command {
return Ok(None);
}
user_command
.map(|command| render_compose_string_sequence(&command))
.transpose()
}

fn merged_entrypoints(resolved: &ResolvedConfig) -> Vec<String> {
if let Some(values) = resolved
.configuration
.get("entrypoints")
.and_then(Value::as_array)
{
return values
.iter()
.filter_map(Value::as_str)
.map(ToString::to_string)
.collect();
}
resolved
.configuration
.get("entrypoint")
.and_then(Value::as_str)
.map(|value| vec![value.to_string()])
.unwrap_or_default()
}

fn unique_override_file_path() -> PathBuf {
unique_temp_path("devcontainer-compose-override", Some("yml"))
}
Loading
Loading