7095 lines · 309.3 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}; use crate::{    DevContainerConfig, DevContainerContext,    command_json::{CommandRunner, DefaultCommandRunner},    devcontainer_api::{DevContainerError, DevContainerUp},    devcontainer_json::{        DevContainer, DevContainerBuildType, FeatureOptions, ForwardPort, MountDefinition,        deserialize_devcontainer_json,    },    docker::{        Docker, DockerClient, DockerComposeConfig, DockerComposeService, DockerComposeServiceBuild,        DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs,        get_remote_dir_from_config,    },    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<String, DevContainerError> {        let mut replaced_content = content            .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('\\', "/"),            );        for (k, v) in &self.local_environment {            let find = format!("${{localEnv:{k}}}");            replaced_content = replaced_content.replace(&find, &v.replace('\\', "/"));        }         Ok(replaced_content)    }     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(&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(remote_env) = self.dev_container().remote_env.clone() {            let mut raw = serde_json_lenient::to_string(&remote_env).map_err(|e| {                log::error!(                    "Unexpected error serializing dev container remote_env: {e} - {:?}",                    remote_env                );                DevContainerError::DevContainerParseFailed            })?;            for (k, v) in container_env {                raw = raw.replace(&format!("${{containerEnv:{k}}}"), v);            }            let reserialized: HashMap<String, String> = serde_json_lenient::from_str(&raw)                .map_err(|e| {                    log::error!(                        "Unexpected error reserializing dev container remote env: {e} - {:?}",                        &raw                    );                    DevContainerError::DevContainerParseFailed                })?;            for (k, v) in reserialized {                merged_remote_env.insert(k, v);            }        }        Ok(merged_remote_env)    }     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| b.dockerfile)                    .map(|dockerfile| self.config_directory.join(dockerfile))            }            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);         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 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;            }             let feature_id = extract_feature_id(feature_ref);            let consecutive_id = format!("{}_{}", feature_id, index);            let feature_dir = build_info.features_content_dir.join(&consecutive_id);             self.fs.create_dir(&feature_dir).await.map_err(|e| {                log::error!(                    "Failed to create feature directory for {}: {e}",                    feature_ref                );                DevContainerError::FilesystemError            })?;             let oci_ref = parse_oci_feature_ref(feature_ref).ok_or_else(|| {                log::error!(                    "Feature '{}' is not a supported OCI feature reference",                    feature_ref                );                DevContainerError::DevContainerParseFailed            })?;            let TokenResponse { token } =                get_oci_token(&oci_ref.registry, &oci_ref.path, &self.http_client)                    .await                    .map_err(|e| {                        log::error!("Failed to get OCI token for feature '{}': {e}", feature_ref);                        DevContainerError::ResourceFetchFailed                    })?;            let manifest = get_oci_manifest(                &oci_ref.registry,                &oci_ref.path,                &token,                &self.http_client,                &oci_ref.version,                None,            )            .await            .map_err(|e| {                log::error!(                    "Failed to fetch OCI manifest for feature '{}': {e}",                    feature_ref                );                DevContainerError::ResourceFetchFailed            })?;            let digest = &manifest                .layers                .first()                .ok_or_else(|| {                    log::error!(                        "OCI manifest for feature '{}' contains no layers",                        feature_ref                    );                    DevContainerError::ResourceFetchFailed                })?                .digest;            download_oci_tarball(                &token,                &oci_ref.registry,                &oci_ref.path,                digest,                "application/vnd.devcontainers.layer.v1+tar",                &feature_dir,                &self.http_client,                &self.fs,                None,            )            .await?;             let feature_json_path = &feature_dir.join("devcontainer-feature.json");            if !self.fs.is_file(feature_json_path).await {                let message = format!(                    "No devcontainer-feature.json found in {:?}, no defaults to apply",                    feature_json_path                );                log::error!("{}", &message);                return Err(DevContainerError::ResourceFetchFailed);            }             let contents = self.fs.load(&feature_json_path).await.map_err(|e| {                log::error!("error reading devcontainer-feature.json: {:?}", e);                DevContainerError::FilesystemError            })?;             let contents_parsed = self.parse_nonremote_vars_for_content(&contents)?;             let feature_json: DevContainerFeatureJson =                serde_json_lenient::from_str(&contents_parsed).map_err(|e| {                    log::error!("Failed to parse devcontainer-feature.json: {e}");                    DevContainerError::ResourceFetchFailed                })?;             let feature_manifest = FeatureManifest::new(consecutive_id, feature_dir, feature_json);             log::debug!("Downloaded OCI feature content for '{}'", feature_ref);             let env_content = feature_manifest                .write_feature_env(&self.fs, options)                .await?;             let wrapper_content = generate_install_wrapper(feature_ref, feature_id, &env_content)?;             self.fs                .write(                    &feature_manifest                        .file_path()                        .join("devcontainer-features-install.sh"),                    &wrapper_content.as_bytes(),                )                .await

Lines 1–500 of 7095.