8725 lines · 370.9 KB · Rust
RawDownload
use std::{    collections::HashMap,    fmt::Debug,    hash::{DefaultHasher, Hash, Hasher},    path::{Path, PathBuf},    sync::Arc,}; use regex::Regex; use fs::Fs;use http_client::HttpClient;use util::{ResultExt, command::Command, normalize_path}; use crate::{    DevContainerConfig, DevContainerContext,    command_json::{CommandRunner, DefaultCommandRunner},    devcontainer_api::{DevContainerError, DevContainerUp},    devcontainer_json::{        ContainerBuild, DevContainer, DevContainerBuildType, FeatureOptions, ForwardPort,        MountDefinition, deserialize_devcontainer_json, deserialize_devcontainer_json_from_value,        deserialize_devcontainer_json_to_value,    },    docker::{        Docker, DockerClient, DockerComposeConfig, DockerComposeService, DockerComposeServiceBuild,        DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs,    },    features::{DevContainerFeatureJson, FeatureManifest, parse_oci_feature_ref},    get_oci_token,    oci::{TokenResponse, download_oci_tarball, get_oci_manifest},    safe_id_lower,}; enum ConfigStatus {    Deserialized(DevContainer),    VariableParsed(DevContainer),} #[derive(Debug, Clone, Eq, PartialEq, Default)]pub(crate) struct DockerComposeResources {    files: Vec<PathBuf>,    config: DockerComposeConfig,} struct DevContainerManifest {    http_client: Arc<dyn HttpClient>,    fs: Arc<dyn Fs>,    docker_client: Arc<dyn DockerClient>,    command_runner: Arc<dyn CommandRunner>,    raw_config: String,    config: ConfigStatus,    local_environment: HashMap<String, String>,    local_project_directory: PathBuf,    config_directory: PathBuf,    file_name: String,    root_image: Option<DockerInspect>,    features_build_info: Option<FeaturesBuildInfo>,    features: Vec<FeatureManifest>,}const DEFAULT_REMOTE_PROJECT_DIR: &str = "/workspaces";impl DevContainerManifest {    async fn new(        context: &DevContainerContext,        environment: HashMap<String, String>,        docker_client: Arc<dyn DockerClient>,        command_runner: Arc<dyn CommandRunner>,        local_config: DevContainerConfig,        local_project_path: &Path,    ) -> Result<Self, DevContainerError> {        let config_path = local_project_path.join(local_config.config_path.clone());        log::debug!("parsing devcontainer json found in {:?}", &config_path);        let devcontainer_contents = context.fs.load(&config_path).await.map_err(|e| {            log::error!("Unable to read devcontainer contents: {e}");            DevContainerError::DevContainerParseFailed        })?;         let devcontainer = deserialize_devcontainer_json(&devcontainer_contents)?;         let devcontainer_directory = config_path.parent().ok_or_else(|| {            log::error!("Dev container file should be in a directory");            DevContainerError::NotInValidProject        })?;        let file_name = config_path            .file_name()            .and_then(|f| f.to_str())            .ok_or_else(|| {                log::error!("Dev container file has no file name, or is invalid unicode");                DevContainerError::DevContainerParseFailed            })?;         Ok(Self {            fs: context.fs.clone(),            http_client: context.http_client.clone(),            docker_client,            command_runner,            raw_config: devcontainer_contents,            config: ConfigStatus::Deserialized(devcontainer),            local_project_directory: local_project_path.to_path_buf(),            local_environment: environment,            config_directory: devcontainer_directory.to_path_buf(),            file_name: file_name.to_string(),            root_image: None,            features_build_info: None,            features: Vec::new(),        })    }     fn devcontainer_id(&self) -> String {        let mut labels = self.identifying_labels();        labels.sort_by_key(|(key, _)| *key);         let mut hasher = DefaultHasher::new();        for (key, value) in &labels {            key.hash(&mut hasher);            value.hash(&mut hasher);        }         format!("{:016x}", hasher.finish())    }     fn identifying_labels(&self) -> Vec<(&str, String)> {        let labels = vec![            (                "devcontainer.local_folder",                (self.local_project_directory.display()).to_string(),            ),            (                "devcontainer.config_file",                (self.config_file().display()).to_string(),            ),        ];        labels    }     fn parse_nonremote_vars_for_content(        &self,        content: &str,    ) -> Result<serde_json_lenient::Value, DevContainerError> {        let mut value = deserialize_devcontainer_json_to_value(content)?;        let mut to_visit = vec![&mut value];         while let Some(value) = to_visit.pop() {            use serde_json_lenient::Value;             match value {                Value::String(string) => {                    *string = string                        .replace("${devcontainerId}", &self.devcontainer_id())                        .replace(                            "${containerWorkspaceFolderBasename}",                            &self.remote_workspace_base_name().unwrap_or_default(),                        )                        .replace(                            "${localWorkspaceFolderBasename}",                            &self.local_workspace_base_name()?,                        )                        .replace(                            "${containerWorkspaceFolder}",                            &self                                .remote_workspace_folder()                                .map(|path| path.display().to_string())                                .unwrap_or_default()                                .replace('\\', "/"),                        )                        .replace(                            "${localWorkspaceFolder}",                            &self.local_workspace_folder().replace('\\', "/"),                        );                    *string = Self::replace_environment_variables(                        string,                        "localEnv",                        &self.local_environment,                    );                }                 Value::Array(array) => to_visit.extend(array.iter_mut()),                Value::Object(object) => to_visit.extend(object.values_mut()),                 Value::Null | Value::Bool(_) | Value::Number(_) => {}            }        }         Ok(value)    }     fn parse_nonremote_vars(&mut self) -> Result<(), DevContainerError> {        let replaced_content = self.parse_nonremote_vars_for_content(&self.raw_config)?;        let parsed_config = deserialize_devcontainer_json_from_value(replaced_content)?;         self.config = ConfigStatus::VariableParsed(parsed_config);         Ok(())    }     fn runtime_remote_env(        &self,        container_env: &HashMap<String, String>,    ) -> Result<HashMap<String, String>, DevContainerError> {        let mut merged_remote_env = container_env.clone();        // HOME is user-specific, and we will often not run as the image user        merged_remote_env.remove("HOME");        if let Some(mut remote_env) = self.dev_container().remote_env.clone() {            remote_env.values_mut().for_each(|value| {                *value = Self::replace_environment_variables(value, "containerEnv", &container_env)            });            for (k, v) in remote_env {                merged_remote_env.insert(k, v);            }        }        Ok(merged_remote_env)    }     fn replace_environment_variables(        mut orig: &str,        environment_source: &str,        environment: &HashMap<String, String>,    ) -> String {        let mut replaced = String::with_capacity(orig.len());        let prefix = format!("${{{environment_source}:");        while let Some(start) = orig.find(&prefix) {            let var_name_start = start + prefix.len();            let Some(end) = orig[var_name_start..].find('}') else {                // No closing `}` => malformed variable reference => paste as is.                break;            };            let end = var_name_start + end;             let (var_name_end, default_start) =                if let Some(var_name_end) = orig[var_name_start..end].find(':') {                    let var_name_end = var_name_start + var_name_end;                    (var_name_end, var_name_end + 1)                } else {                    (end, end)                };             let var_name = &orig[var_name_start..var_name_end];            if var_name.is_empty() {                // Empty variable name => paste as is.                replaced.push_str(&orig[..end + 1]);                orig = &orig[end + 1..];                continue;            }            let default = &orig[default_start..end];             replaced.push_str(&orig[..start]);            replaced.push_str(                environment                    .get(var_name)                    .map(|value| value.as_str())                    .unwrap_or(default),            );            orig = &orig[end + 1..];        }        replaced.push_str(orig);        replaced    }     fn config_file(&self) -> PathBuf {        self.config_directory.join(&self.file_name)    }     fn dev_container(&self) -> &DevContainer {        match &self.config {            ConfigStatus::Deserialized(dev_container) => dev_container,            ConfigStatus::VariableParsed(dev_container) => dev_container,        }    }     async fn dockerfile_location(&self) -> Option<PathBuf> {        let dev_container = self.dev_container();        match dev_container.build_type() {            DevContainerBuildType::Image(_) => None,            DevContainerBuildType::Dockerfile(build) => {                Some(self.config_directory.join(&build.dockerfile))            }            DevContainerBuildType::DockerCompose => {                let Ok(docker_compose_manifest) = self.docker_compose_manifest().await else {                    return None;                };                let Ok((_, main_service)) = find_primary_service(&docker_compose_manifest, self)                else {                    return None;                };                main_service.build.and_then(|b| {                    let compose_file = docker_compose_manifest.files.first()?;                    resolve_compose_dockerfile(                        compose_file,                        b.context.as_deref(),                        b.dockerfile.as_deref()?,                    )                })            }            DevContainerBuildType::None => None,        }    }     fn generate_features_image_tag(&self, dockerfile_build_path: String) -> String {        let mut hasher = DefaultHasher::new();        let prefix = match &self.dev_container().name {            Some(name) => &safe_id_lower(name),            None => "zed-dc",        };        let prefix = prefix.get(..6).unwrap_or(prefix);        let prefix = prefix.trim_matches(|c: char| !c.is_alphanumeric());         dockerfile_build_path.hash(&mut hasher);         let hash = hasher.finish();        format!("{}-{:x}-features", prefix, hash)    }     /// Gets the base image from the devcontainer with the following precedence:    /// - The devcontainer image if an image is specified    /// - The image sourced in the Dockerfile if a Dockerfile is specified    /// - The image sourced in the docker-compose main service, if one is specified    /// - The image sourced in the docker-compose main service dockerfile, if one is specified    /// If no such image is available, return an error    async fn get_base_image_from_config(&self) -> Result<String, DevContainerError> {        match self.dev_container().build_type() {            DevContainerBuildType::Image(image) => {                return Ok(image);            }            DevContainerBuildType::Dockerfile(build) => {                let dockerfile_contents = self.expanded_dockerfile_content().await?;                return image_from_dockerfile(dockerfile_contents, &build.target).ok_or_else(                    || {                        log::error!("Unable to find base image in Dockerfile");                        DevContainerError::DevContainerParseFailed                    },                );            }            DevContainerBuildType::DockerCompose => {                let docker_compose_manifest = self.docker_compose_manifest().await?;                let (_, main_service) = find_primary_service(&docker_compose_manifest, &self)?;                 if let Some(_) = main_service                    .build                    .as_ref()                    .and_then(|b| b.dockerfile.as_ref())                {                    let dockerfile_contents = self.expanded_dockerfile_content().await?;                    return image_from_dockerfile(                        dockerfile_contents,                        &main_service.build.as_ref().and_then(|b| b.target.clone()),                    )                    .ok_or_else(|| {                        log::error!("Unable to find base image in Dockerfile");                        DevContainerError::DevContainerParseFailed                    });                }                if let Some(image) = &main_service.image {                    return Ok(image.to_string());                }                 log::error!("No valid base image found in docker-compose configuration");                return Err(DevContainerError::DevContainerParseFailed);            }            DevContainerBuildType::None => {                log::error!("Not a valid devcontainer config for build");                return Err(DevContainerError::NotInValidProject);            }        }    }     async fn copy_local_feature(        &self,        feature_ref: &str,        destination: &Path,    ) -> Result<(), DevContainerError> {        let source_path = normalize_path(&self.config_directory.join(feature_ref));         if !self.fs.is_dir(&source_path).await {            log::error!(                "Local feature directory '{}' not found at {:?}",                feature_ref,                source_path            );            return Err(DevContainerError::ResourceFetchFailed);        }         let items = fs::read_dir_items(&*self.fs, &source_path)            .await            .map_err(|e| {                log::error!(                    "Failed to read local feature directory {:?}: {e}",                    source_path                );                DevContainerError::FilesystemError            })?;         for (item_path, is_dir) in &items {            let relative = item_path.strip_prefix(&source_path).map_err(|e| {                log::error!("Failed to compute relative path for {:?}: {e}", item_path);                DevContainerError::FilesystemError            })?;            let dest_path = destination.join(relative);             if *is_dir {                self.fs.create_dir(&dest_path).await.map_err(|e| {                    log::error!("Failed to create directory {:?}: {e}", dest_path);                    DevContainerError::FilesystemError                })?;            } else {                let content = self.fs.load_bytes(item_path).await.map_err(|e| {                    log::error!("Failed to read file {:?}: {e}", item_path);                    DevContainerError::FilesystemError                })?;                self.fs.write(&dest_path, &content).await.map_err(|e| {                    log::error!("Failed to write file {:?}: {e}", dest_path);                    DevContainerError::FilesystemError                })?;            }        }         Ok(())    }     async fn download_feature_and_dockerfile_resources(&mut self) -> Result<(), DevContainerError> {        let dev_container = match &self.config {            ConfigStatus::Deserialized(_) => {                log::error!(                    "Dev container has not yet been parsed for variable expansion. Cannot yet download resources"                );                return Err(DevContainerError::DevContainerParseFailed);            }            ConfigStatus::VariableParsed(dev_container) => dev_container,        };        let root_image_tag = self.get_base_image_from_config().await?;        let root_image = self.docker_client.inspect(&root_image_tag).await?;         let temp_base = std::env::temp_dir().join("devcontainer-zed");        let timestamp = std::time::SystemTime::now()            .duration_since(std::time::UNIX_EPOCH)            .map(|d| d.as_millis())            .unwrap_or(0);         let features_content_dir = temp_base.join(format!("container-features-{}", timestamp));        let empty_context_dir = temp_base.join("empty-folder");         self.fs            .create_dir(&features_content_dir)            .await            .map_err(|e| {                log::error!("Failed to create features content dir: {e}");                DevContainerError::FilesystemError            })?;         self.fs.create_dir(&empty_context_dir).await.map_err(|e| {            log::error!("Failed to create empty context dir: {e}");            DevContainerError::FilesystemError        })?;         let dockerfile_path = features_content_dir.join("Dockerfile.extended");        let image_tag =            self.generate_features_image_tag(dockerfile_path.clone().display().to_string());         let build_info = FeaturesBuildInfo {            dockerfile_path,            features_content_dir,            empty_context_dir,            build_image: dev_container.image.clone(),            image_tag,        };         let features = match &dev_container.features {            Some(features) => features,            None => &HashMap::new(),        };         let container_user = get_container_user_from_config(&root_image, self)?;        let remote_user = get_remote_user_from_config(&root_image, self)?;         let builtin_env_content = format!(            "_CONTAINER_USER={}\n_REMOTE_USER={}\n",            container_user, remote_user        );         let builtin_env_path = build_info            .features_content_dir            .join("devcontainer-features.builtin.env");         self.fs            .write(&builtin_env_path, &builtin_env_content.as_bytes())            .await            .map_err(|e| {                log::error!("Failed to write builtin env file: {e}");                DevContainerError::FilesystemError            })?;         let ordered_features =            resolve_feature_order(features, &dev_container.override_feature_install_order);         for (index, (feature_ref, options)) in ordered_features.iter().enumerate() {            if matches!(options, FeatureOptions::Bool(false)) {                log::debug!(                    "Feature '{}' is disabled (set to false), skipping",                    feature_ref                );                continue;            }

Lines 1–500 of 8725.