@@ -20,7 +20,8 @@ use crate::{
},
docker::{
Docker, DockerClient, DockerComposeConfig, DockerComposeService, DockerComposeServiceBuild,
- DockerComposeVolume, DockerInspect, DockerPs, get_remote_dir_from_config,
+ DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs,
+ get_remote_dir_from_config,
},
features::{DevContainerFeatureJson, FeatureManifest, parse_oci_feature_ref},
get_oci_token,
@@ -1137,18 +1138,30 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
// If the main service uses a different service's network bridge, append to that service's ports instead
if let Some(network_service_name) = network_mode_service {
if let Some(service) = service_declarations.get_mut(network_service_name) {
- service.ports.push(format!("{port}:{port}"));
+ service.ports.push(DockerComposeServicePort {
+ target: port.clone(),
+ published: port.clone(),
+ ..Default::default()
+ });
} else {
service_declarations.insert(
network_service_name.to_string(),
DockerComposeService {
- ports: vec![format!("{port}:{port}")],
+ ports: vec![DockerComposeServicePort {
+ target: port.clone(),
+ published: port.clone(),
+ ..Default::default()
+ }],
..Default::default()
},
);
}
} else {
- main_service.ports.push(format!("{port}:{port}"));
+ main_service.ports.push(DockerComposeServicePort {
+ target: port.clone(),
+ published: port.clone(),
+ ..Default::default()
+ });
}
}
let other_service_ports: Vec<(&str, &str)> = forward_ports
@@ -1171,12 +1184,20 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
.collect();
for (service_name, port) in other_service_ports {
if let Some(service) = service_declarations.get_mut(service_name) {
- service.ports.push(format!("{port}:{port}"));
+ service.ports.push(DockerComposeServicePort {
+ target: port.to_string(),
+ published: port.to_string(),
+ ..Default::default()
+ });
} else {
service_declarations.insert(
service_name.to_string(),
DockerComposeService {
- ports: vec![format!("{port}:{port}")],
+ ports: vec![DockerComposeServicePort {
+ target: port.to_string(),
+ published: port.to_string(),
+ ..Default::default()
+ }],
..Default::default()
},
);
@@ -1186,18 +1207,30 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true
if let Some(port) = &self.dev_container().app_port {
if let Some(network_service_name) = network_mode_service {
if let Some(service) = service_declarations.get_mut(network_service_name) {
- service.ports.push(format!("{port}:{port}"));
+ service.ports.push(DockerComposeServicePort {
+ target: port.clone(),
+ published: port.clone(),
+ ..Default::default()
+ });
} else {
service_declarations.insert(
network_service_name.to_string(),
DockerComposeService {
- ports: vec![format!("{port}:{port}")],
+ ports: vec![DockerComposeServicePort {
+ target: port.clone(),
+ published: port.clone(),
+ ..Default::default()
+ }],
..Default::default()
},
);
}
} else {
- main_service.ports.push(format!("{port}:{port}"));
+ main_service.ports.push(DockerComposeServicePort {
+ target: port.clone(),
+ published: port.clone(),
+ ..Default::default()
+ });
}
}
@@ -3278,6 +3311,8 @@ chmod +x ./install.sh
#[cfg(not(target_os = "windows"))]
#[gpui::test]
async fn test_spawns_devcontainer_with_docker_compose(cx: &mut TestAppContext) {
+ use crate::docker::DockerComposeServicePort;
+
cx.executor().allow_parking();
env_logger::try_init().ok();
let given_devcontainer_contents = r#"
@@ -3540,10 +3575,26 @@ ENV DOCKER_BUILDKIT=1
"db".to_string(),
DockerComposeService {
ports: vec![
- "8083:8083".to_string(),
- "5432:5432".to_string(),
- "1234:1234".to_string(),
- "8084:8084".to_string()
+ DockerComposeServicePort {
+ target: "8083".to_string(),
+ published: "8083".to_string(),
+ ..Default::default()
+ },
+ DockerComposeServicePort {
+ target: "5432".to_string(),
+ published: "5432".to_string(),
+ ..Default::default()
+ },
+ DockerComposeServicePort {
+ target: "1234".to_string(),
+ published: "1234".to_string(),
+ ..Default::default()
+ },
+ DockerComposeServicePort {
+ target: "8084".to_string(),
+ published: "8084".to_string(),
+ ..Default::default()
+ },
],
..Default::default()
},
@@ -86,6 +86,43 @@ pub(crate) struct DockerComposeServiceBuild {
pub(crate) additional_contexts: Option<HashMap<String, String>>,
}
+#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]
+pub(crate) struct DockerComposeServicePort {
+ #[serde(deserialize_with = "deserialize_string_or_int")]
+ pub(crate) target: String,
+ #[serde(deserialize_with = "deserialize_string_or_int")]
+ pub(crate) published: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) mode: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) protocol: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) host_ip: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) app_protocol: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) name: Option<String>,
+}
+
+fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ use serde::Deserialize;
+
+ #[derive(Deserialize)]
+ #[serde(untagged)]
+ enum StringOrInt {
+ String(String),
+ Int(u32),
+ }
+
+ match StringOrInt::deserialize(deserializer)? {
+ StringOrInt::String(s) => Ok(s),
+ StringOrInt::Int(b) => Ok(b.to_string()),
+ }
+}
+
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)]
pub(crate) struct DockerComposeService {
pub(crate) image: Option<String>,
@@ -109,7 +146,7 @@ pub(crate) struct DockerComposeService {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) env_file: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub(crate) ports: Vec<String>,
+ pub(crate) ports: Vec<DockerComposeServicePort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) network_mode: Option<String>,
}
@@ -491,8 +528,8 @@ mod test {
command_json::deserialize_json_output,
devcontainer_json::MountDefinition,
docker::{
- Docker, DockerComposeConfig, DockerComposeService, DockerComposeVolume, DockerInspect,
- DockerPs, get_remote_dir_from_config,
+ Docker, DockerComposeConfig, DockerComposeService, DockerComposeServicePort,
+ DockerComposeVolume, DockerInspect, DockerPs, get_remote_dir_from_config,
},
};
@@ -879,6 +916,22 @@ mod test {
"POSTGRES_PORT": "5432",
"POSTGRES_USER": "postgres"
},
+ "ports": [
+ {
+ "target": "5443",
+ "published": "5442"
+ },
+ {
+ "name": "custom port",
+ "protocol": "udp",
+ "host_ip": "127.0.0.1",
+ "app_protocol": "http",
+ "mode": "host",
+ "target": "8081",
+ "published": "8083"
+
+ }
+ ],
"image": "mcr.microsoft.com/devcontainers/rust:2-1-bookworm",
"network_mode": "service:db",
"volumes": [
@@ -943,6 +996,23 @@ mod test {
target: "/workspaces".to_string(),
}],
network_mode: Some("service:db".to_string()),
+
+ ports: vec![
+ DockerComposeServicePort {
+ target: "5443".to_string(),
+ published: "5442".to_string(),
+ ..Default::default()
+ },
+ DockerComposeServicePort {
+ target: "8081".to_string(),
+ published: "8083".to_string(),
+ mode: Some("host".to_string()),
+ protocol: Some("udp".to_string()),
+ host_ip: Some("127.0.0.1".to_string()),
+ app_protocol: Some("http".to_string()),
+ name: Some("custom port".to_string()),
+ },
+ ],
..Default::default()
},
),