devcontainer_json.rs

   1use std::{collections::HashMap, fmt::Display, path::Path, sync::Arc};
   2
   3use crate::{command_json::CommandRunner, devcontainer_api::DevContainerError};
   4use serde::{Deserialize, Deserializer, Serialize};
   5use serde_json_lenient::Value;
   6use util::command::Command;
   7
   8#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
   9#[serde(untagged)]
  10pub(crate) enum ForwardPort {
  11    Number(u16),
  12    String(String),
  13}
  14
  15#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
  16#[serde(rename_all = "camelCase")]
  17pub(crate) enum PortAttributeProtocol {
  18    Https,
  19    Http,
  20}
  21
  22#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
  23#[serde(rename_all = "camelCase")]
  24pub(crate) enum OnAutoForward {
  25    Notify,
  26    OpenBrowser,
  27    OpenBrowserOnce,
  28    OpenPreview,
  29    Silent,
  30    Ignore,
  31}
  32
  33#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
  34#[serde(rename_all = "camelCase")]
  35pub(crate) struct PortAttributes {
  36    label: String,
  37    on_auto_forward: OnAutoForward,
  38    elevate_if_needed: bool,
  39    require_local_port: bool,
  40    protocol: PortAttributeProtocol,
  41}
  42
  43#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
  44#[serde(rename_all = "camelCase")]
  45pub(crate) enum UserEnvProbe {
  46    None,
  47    InteractiveShell,
  48    LoginShell,
  49    LoginInteractiveShell,
  50}
  51
  52#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
  53#[serde(rename_all = "camelCase")]
  54pub(crate) enum ShutdownAction {
  55    None,
  56    StopContainer,
  57    StopCompose,
  58}
  59
  60#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
  61#[serde(rename_all = "camelCase")]
  62pub(crate) struct MountDefinition {
  63    pub(crate) source: String,
  64    pub(crate) target: String,
  65    #[serde(rename = "type")]
  66    pub(crate) mount_type: Option<String>,
  67}
  68
  69impl Display for MountDefinition {
  70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  71        write!(
  72            f,
  73            "type={},source={},target={},consistency=cached",
  74            self.mount_type.clone().unwrap_or_else(|| {
  75                if self.source.starts_with('/') {
  76                    "bind".to_string()
  77                } else {
  78                    "volume".to_string()
  79                }
  80            }),
  81            self.source,
  82            self.target
  83        )
  84    }
  85}
  86
  87/// Represents the value associated with a feature ID in the `features` map of devcontainer.json.
  88///
  89/// Per the spec, the value can be:
  90/// - A boolean (`true` to enable with defaults)
  91/// - A string (shorthand for `{"version": "<value>"}`)
  92/// - An object mapping option names to string or boolean values
  93///
  94/// See: https://containers.dev/implementors/features/#devcontainerjson-properties
  95#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
  96#[serde(untagged)]
  97pub(crate) enum FeatureOptions {
  98    Bool(bool),
  99    String(String),
 100    Options(HashMap<String, FeatureOptionValue>),
 101}
 102
 103#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
 104#[serde(untagged)]
 105pub(crate) enum FeatureOptionValue {
 106    Bool(bool),
 107    String(String),
 108}
 109impl std::fmt::Display for FeatureOptionValue {
 110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 111        match self {
 112            FeatureOptionValue::Bool(b) => write!(f, "{}", b),
 113            FeatureOptionValue::String(s) => write!(f, "{}", s),
 114        }
 115    }
 116}
 117
 118#[derive(Clone, Debug, Serialize, Eq, PartialEq, Default)]
 119pub(crate) struct ZedCustomizationsWrapper {
 120    pub(crate) zed: ZedCustomization,
 121}
 122
 123#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)]
 124pub(crate) struct ZedCustomization {
 125    #[serde(default)]
 126    pub(crate) extensions: Vec<String>,
 127}
 128
 129#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
 130#[serde(rename_all = "camelCase")]
 131pub(crate) struct ContainerBuild {
 132    pub(crate) dockerfile: String,
 133    context: Option<String>,
 134    pub(crate) args: Option<HashMap<String, String>>,
 135    options: Option<Vec<String>>,
 136    target: Option<String>,
 137    #[serde(default, deserialize_with = "deserialize_string_or_array")]
 138    cache_from: Option<Vec<String>>,
 139}
 140
 141#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
 142struct LifecycleScriptInternal {
 143    command: Option<String>,
 144    args: Vec<String>,
 145}
 146
 147impl LifecycleScriptInternal {
 148    fn from_args(args: Vec<String>) -> Self {
 149        let command = args.get(0).map(|a| a.to_string());
 150        let remaining = args.iter().skip(1).map(|a| a.to_string()).collect();
 151        Self {
 152            command,
 153            args: remaining,
 154        }
 155    }
 156}
 157
 158#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
 159pub struct LifecycleScript {
 160    scripts: HashMap<String, LifecycleScriptInternal>,
 161}
 162
 163#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
 164#[serde(rename_all = "camelCase")]
 165pub(crate) struct HostRequirements {
 166    cpus: Option<u16>,
 167    memory: Option<String>,
 168    storage: Option<String>,
 169}
 170
 171#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
 172#[serde(rename_all = "camelCase")]
 173pub(crate) enum LifecycleCommand {
 174    InitializeCommand,
 175    OnCreateCommand,
 176    UpdateContentCommand,
 177    PostCreateCommand,
 178    PostStartCommand,
 179}
 180
 181#[derive(Debug, PartialEq, Eq)]
 182pub(crate) enum DevContainerBuildType {
 183    Image,
 184    Dockerfile,
 185    DockerCompose,
 186    None,
 187}
 188#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)]
 189#[serde(rename_all = "camelCase")]
 190pub(crate) struct DevContainer {
 191    pub(crate) image: Option<String>,
 192    pub(crate) name: Option<String>,
 193    pub(crate) remote_user: Option<String>,
 194    pub(crate) forward_ports: Option<Vec<ForwardPort>>,
 195    pub(crate) ports_attributes: Option<HashMap<String, PortAttributes>>,
 196    pub(crate) other_ports_attributes: Option<PortAttributes>,
 197    pub(crate) container_env: Option<HashMap<String, String>>,
 198    pub(crate) remote_env: Option<HashMap<String, String>>,
 199    pub(crate) container_user: Option<String>,
 200    #[serde(rename = "updateRemoteUserUID")]
 201    pub(crate) update_remote_user_uid: Option<bool>,
 202    user_env_probe: Option<UserEnvProbe>,
 203    override_command: Option<bool>,
 204    shutdown_action: Option<ShutdownAction>,
 205    init: Option<bool>,
 206    pub(crate) privileged: Option<bool>,
 207    cap_add: Option<Vec<String>>,
 208    security_opt: Option<Vec<String>>,
 209    #[serde(default, deserialize_with = "deserialize_mount_definitions")]
 210    pub(crate) mounts: Option<Vec<MountDefinition>>,
 211    pub(crate) features: Option<HashMap<String, FeatureOptions>>,
 212    pub(crate) override_feature_install_order: Option<Vec<String>>,
 213    pub(crate) customizations: Option<ZedCustomizationsWrapper>,
 214    pub(crate) build: Option<ContainerBuild>,
 215    #[serde(default, deserialize_with = "deserialize_string_or_int")]
 216    pub(crate) app_port: Option<String>,
 217    #[serde(default, deserialize_with = "deserialize_mount_definition")]
 218    pub(crate) workspace_mount: Option<MountDefinition>,
 219    pub(crate) workspace_folder: Option<String>,
 220    run_args: Option<Vec<String>>,
 221    #[serde(default, deserialize_with = "deserialize_string_or_array")]
 222    pub(crate) docker_compose_file: Option<Vec<String>>,
 223    pub(crate) service: Option<String>,
 224    run_services: Option<Vec<String>>,
 225    pub(crate) initialize_command: Option<LifecycleScript>,
 226    pub(crate) on_create_command: Option<LifecycleScript>,
 227    pub(crate) update_content_command: Option<LifecycleScript>,
 228    pub(crate) post_create_command: Option<LifecycleScript>,
 229    pub(crate) post_start_command: Option<LifecycleScript>,
 230    pub(crate) post_attach_command: Option<LifecycleScript>,
 231    wait_for: Option<LifecycleCommand>,
 232    host_requirements: Option<HostRequirements>,
 233}
 234
 235pub(crate) fn deserialize_devcontainer_json(json: &str) -> Result<DevContainer, DevContainerError> {
 236    match serde_json_lenient::from_str(json) {
 237        Ok(devcontainer) => Ok(devcontainer),
 238        Err(e) => {
 239            log::error!("Unable to deserialize devcontainer from json: {e}");
 240            Err(DevContainerError::DevContainerParseFailed)
 241        }
 242    }
 243}
 244
 245impl DevContainer {
 246    pub(crate) fn build_type(&self) -> DevContainerBuildType {
 247        if self.image.is_some() {
 248            return DevContainerBuildType::Image;
 249        } else if self.docker_compose_file.is_some() {
 250            return DevContainerBuildType::DockerCompose;
 251        } else if self.build.is_some() {
 252            return DevContainerBuildType::Dockerfile;
 253        }
 254        return DevContainerBuildType::None;
 255    }
 256
 257    pub(crate) fn has_features(&self) -> bool {
 258        self.features
 259            .as_ref()
 260            .map(|features| !features.is_empty())
 261            .unwrap_or(false)
 262    }
 263}
 264
 265// Custom deserializer that parses the entire customizations object as a
 266// serde_json_lenient::Value first, then extracts the "zed" portion.
 267// This avoids a bug in serde_json_lenient's `ignore_value` codepath which
 268// does not handle trailing commas in skipped values.
 269impl<'de> Deserialize<'de> for ZedCustomizationsWrapper {
 270    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 271    where
 272        D: Deserializer<'de>,
 273    {
 274        let value = Value::deserialize(deserializer)?;
 275        let zed = value
 276            .get("zed")
 277            .map(|zed_value| serde_json_lenient::from_value::<ZedCustomization>(zed_value.clone()))
 278            .transpose()
 279            .map_err(serde::de::Error::custom)?
 280            .unwrap_or_default();
 281        Ok(ZedCustomizationsWrapper { zed })
 282    }
 283}
 284
 285impl LifecycleScript {
 286    fn from_map(args: HashMap<String, Vec<String>>) -> Self {
 287        Self {
 288            scripts: args
 289                .into_iter()
 290                .map(|(k, v)| (k, LifecycleScriptInternal::from_args(v)))
 291                .collect(),
 292        }
 293    }
 294    fn from_str(args: &str) -> Self {
 295        let script: Vec<String> = args.split(" ").map(|a| a.to_string()).collect();
 296
 297        Self::from_args(script)
 298    }
 299    fn from_args(args: Vec<String>) -> Self {
 300        Self::from_map(HashMap::from([("default".to_string(), args)]))
 301    }
 302    pub fn script_commands(&self) -> HashMap<String, Command> {
 303        self.scripts
 304            .iter()
 305            .filter_map(|(k, v)| {
 306                if let Some(inner_command) = &v.command {
 307                    let mut command = Command::new(inner_command);
 308                    command.args(&v.args);
 309                    Some((k.clone(), command))
 310                } else {
 311                    log::warn!(
 312                        "Lifecycle script command {k}, value {:?} has no program to run. Skipping",
 313                        v
 314                    );
 315                    None
 316                }
 317            })
 318            .collect()
 319    }
 320
 321    pub async fn run(
 322        &self,
 323        command_runnder: &Arc<dyn CommandRunner>,
 324        working_directory: &Path,
 325    ) -> Result<(), DevContainerError> {
 326        for (command_name, mut command) in self.script_commands() {
 327            log::debug!("Running script {command_name}");
 328
 329            command.current_dir(working_directory);
 330
 331            let output = command_runnder
 332                .run_command(&mut command)
 333                .await
 334                .map_err(|e| {
 335                    log::error!("Error running command {command_name}: {e}");
 336                    DevContainerError::CommandFailed(command_name.clone())
 337                })?;
 338            if !output.status.success() {
 339                let std_err = String::from_utf8_lossy(&output.stderr);
 340                log::error!(
 341                    "Command {command_name} produced a non-successful output. StdErr: {std_err}"
 342                );
 343            }
 344            let std_out = String::from_utf8_lossy(&output.stdout);
 345            log::debug!("Command {command_name} output:\n {std_out}");
 346        }
 347        Ok(())
 348    }
 349}
 350
 351impl<'de> Deserialize<'de> for LifecycleScript {
 352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 353    where
 354        D: Deserializer<'de>,
 355    {
 356        use serde::de::{self, Visitor};
 357        use std::fmt;
 358
 359        struct LifecycleScriptVisitor;
 360
 361        impl<'de> Visitor<'de> for LifecycleScriptVisitor {
 362            type Value = LifecycleScript;
 363
 364            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
 365                formatter.write_str("a string, an array of strings, or a map of arrays")
 366            }
 367
 368            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
 369            where
 370                E: de::Error,
 371            {
 372                Ok(LifecycleScript::from_str(value))
 373            }
 374
 375            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
 376            where
 377                A: de::SeqAccess<'de>,
 378            {
 379                let mut array = Vec::new();
 380                while let Some(elem) = seq.next_element()? {
 381                    array.push(elem);
 382                }
 383                Ok(LifecycleScript::from_args(array))
 384            }
 385
 386            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
 387            where
 388                A: de::MapAccess<'de>,
 389            {
 390                let mut result = HashMap::new();
 391                while let Some(key) = map.next_key::<String>()? {
 392                    let value: Value = map.next_value()?;
 393                    let script_args = match value {
 394                        Value::String(s) => {
 395                            s.split(" ").map(|s| s.to_string()).collect::<Vec<String>>()
 396                        }
 397                        Value::Array(arr) => {
 398                            let strings: Vec<String> = arr
 399                                .into_iter()
 400                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
 401                                .collect();
 402                            strings
 403                        }
 404                        _ => continue,
 405                    };
 406                    result.insert(key, script_args);
 407                }
 408                Ok(LifecycleScript::from_map(result))
 409            }
 410        }
 411
 412        deserializer.deserialize_any(LifecycleScriptVisitor)
 413    }
 414}
 415
 416fn deserialize_mount_definition<'de, D>(
 417    deserializer: D,
 418) -> Result<Option<MountDefinition>, D::Error>
 419where
 420    D: serde::Deserializer<'de>,
 421{
 422    use serde::Deserialize;
 423    use serde::de::Error;
 424
 425    #[derive(Deserialize)]
 426    #[serde(untagged)]
 427    enum MountItem {
 428        Object(MountDefinition),
 429        String(String),
 430    }
 431
 432    let item = MountItem::deserialize(deserializer)?;
 433
 434    let mount = match item {
 435        MountItem::Object(mount) => mount,
 436        MountItem::String(s) => {
 437            let mut source = None;
 438            let mut target = None;
 439            let mut mount_type = None;
 440
 441            for part in s.split(',') {
 442                let part = part.trim();
 443                if let Some((key, value)) = part.split_once('=') {
 444                    match key.trim() {
 445                        "source" => source = Some(value.trim().to_string()),
 446                        "target" => target = Some(value.trim().to_string()),
 447                        "type" => mount_type = Some(value.trim().to_string()),
 448                        _ => {} // Ignore unknown keys
 449                    }
 450                }
 451            }
 452
 453            let source = source
 454                .ok_or_else(|| D::Error::custom(format!("mount string missing 'source': {}", s)))?;
 455            let target = target
 456                .ok_or_else(|| D::Error::custom(format!("mount string missing 'target': {}", s)))?;
 457
 458            MountDefinition {
 459                source,
 460                target,
 461                mount_type,
 462            }
 463        }
 464    };
 465
 466    Ok(Some(mount))
 467}
 468
 469fn deserialize_mount_definitions<'de, D>(
 470    deserializer: D,
 471) -> Result<Option<Vec<MountDefinition>>, D::Error>
 472where
 473    D: serde::Deserializer<'de>,
 474{
 475    use serde::Deserialize;
 476    use serde::de::Error;
 477
 478    #[derive(Deserialize)]
 479    #[serde(untagged)]
 480    enum MountItem {
 481        Object(MountDefinition),
 482        String(String),
 483    }
 484
 485    let items = Vec::<MountItem>::deserialize(deserializer)?;
 486    let mut mounts = Vec::new();
 487
 488    for item in items {
 489        match item {
 490            MountItem::Object(mount) => mounts.push(mount),
 491            MountItem::String(s) => {
 492                let mut source = None;
 493                let mut target = None;
 494                let mut mount_type = None;
 495
 496                for part in s.split(',') {
 497                    let part = part.trim();
 498                    if let Some((key, value)) = part.split_once('=') {
 499                        match key.trim() {
 500                            "source" => source = Some(value.trim().to_string()),
 501                            "target" => target = Some(value.trim().to_string()),
 502                            "type" => mount_type = Some(value.trim().to_string()),
 503                            _ => {} // Ignore unknown keys
 504                        }
 505                    }
 506                }
 507
 508                let source = source.ok_or_else(|| {
 509                    D::Error::custom(format!("mount string missing 'source': {}", s))
 510                })?;
 511                let target = target.ok_or_else(|| {
 512                    D::Error::custom(format!("mount string missing 'target': {}", s))
 513                })?;
 514
 515                mounts.push(MountDefinition {
 516                    source,
 517                    target,
 518                    mount_type,
 519                });
 520            }
 521        }
 522    }
 523
 524    Ok(Some(mounts))
 525}
 526
 527fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
 528where
 529    D: serde::Deserializer<'de>,
 530{
 531    use serde::Deserialize;
 532
 533    #[derive(Deserialize)]
 534    #[serde(untagged)]
 535    enum StringOrInt {
 536        String(String),
 537        Int(u32),
 538    }
 539
 540    match StringOrInt::deserialize(deserializer)? {
 541        StringOrInt::String(s) => Ok(Some(s)),
 542        StringOrInt::Int(b) => Ok(Some(b.to_string())),
 543    }
 544}
 545
 546fn deserialize_string_or_array<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
 547where
 548    D: serde::Deserializer<'de>,
 549{
 550    use serde::Deserialize;
 551
 552    #[derive(Deserialize)]
 553    #[serde(untagged)]
 554    enum StringOrArray {
 555        String(String),
 556        Array(Vec<String>),
 557    }
 558
 559    match StringOrArray::deserialize(deserializer)? {
 560        StringOrArray::String(s) => Ok(Some(vec![s])),
 561        StringOrArray::Array(b) => Ok(Some(b)),
 562    }
 563}
 564
 565#[cfg(test)]
 566mod test {
 567    use std::collections::HashMap;
 568
 569    use crate::{
 570        devcontainer_api::DevContainerError,
 571        devcontainer_json::{
 572            ContainerBuild, DevContainer, DevContainerBuildType, FeatureOptions, ForwardPort,
 573            HostRequirements, LifecycleCommand, LifecycleScript, MountDefinition, OnAutoForward,
 574            PortAttributeProtocol, PortAttributes, ShutdownAction, UserEnvProbe, ZedCustomization,
 575            ZedCustomizationsWrapper, deserialize_devcontainer_json,
 576        },
 577    };
 578
 579    #[test]
 580    fn should_deserialize_customizations_with_unknown_keys() {
 581        let json_with_other_customizations = r#"
 582            {
 583                "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
 584                "customizations": {
 585                  "vscode": {
 586                    "extensions": [
 587                      "dbaeumer.vscode-eslint",
 588                      "GitHub.vscode-pull-request-github",
 589                    ],
 590                  },
 591                  "zed": {
 592                    "extensions": ["vue", "ruby"],
 593                  },
 594                  "codespaces": {
 595                    "repositories": {
 596                      "devcontainers/features": {
 597                        "permissions": {
 598                          "contents": "write",
 599                          "workflows": "write",
 600                        },
 601                      },
 602                    },
 603                  },
 604                },
 605            }
 606        "#;
 607
 608        let result = deserialize_devcontainer_json(json_with_other_customizations);
 609
 610        assert!(
 611            result.is_ok(),
 612            "Should ignore unknown customization keys, but got: {:?}",
 613            result.err()
 614        );
 615        let devcontainer = result.expect("ok");
 616        assert_eq!(
 617            devcontainer.customizations,
 618            Some(ZedCustomizationsWrapper {
 619                zed: ZedCustomization {
 620                    extensions: vec!["vue".to_string(), "ruby".to_string()]
 621                }
 622            })
 623        );
 624    }
 625
 626    #[test]
 627    fn should_deserialize_customizations_without_zed_key() {
 628        let json_without_zed = r#"
 629            {
 630                "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
 631                "customizations": {
 632                    "vscode": {
 633                        "extensions": ["dbaeumer.vscode-eslint"]
 634                    }
 635                }
 636            }
 637        "#;
 638
 639        let result = deserialize_devcontainer_json(json_without_zed);
 640
 641        assert!(
 642            result.is_ok(),
 643            "Should handle missing zed key in customizations, but got: {:?}",
 644            result.err()
 645        );
 646        let devcontainer = result.expect("ok");
 647        assert_eq!(
 648            devcontainer.customizations,
 649            Some(ZedCustomizationsWrapper {
 650                zed: ZedCustomization { extensions: vec![] }
 651            })
 652        );
 653    }
 654
 655    #[test]
 656    fn should_deserialize_simple_devcontainer_json() {
 657        let given_bad_json = "{ \"image\": 123 }";
 658
 659        let result = deserialize_devcontainer_json(given_bad_json);
 660
 661        assert!(result.is_err());
 662        assert_eq!(
 663            result.expect_err("err"),
 664            DevContainerError::DevContainerParseFailed
 665        );
 666
 667        let given_image_container_json = r#"
 668            // These are some external comments. serde_lenient should handle them
 669            {
 670                // These are some internal comments
 671                "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
 672                "name": "myDevContainer",
 673                "remoteUser": "root",
 674                "forwardPorts": [
 675                    "db:5432",
 676                    3000
 677                ],
 678                "portsAttributes": {
 679                    "3000": {
 680                        "label": "This Port",
 681                        "onAutoForward": "notify",
 682                        "elevateIfNeeded": false,
 683                        "requireLocalPort": true,
 684                        "protocol": "https"
 685                    },
 686                    "db:5432": {
 687                        "label": "This Port too",
 688                        "onAutoForward": "silent",
 689                        "elevateIfNeeded": true,
 690                        "requireLocalPort": false,
 691                        "protocol": "http"
 692                    }
 693                },
 694                "otherPortsAttributes": {
 695                    "label": "Other Ports",
 696                    "onAutoForward": "openBrowser",
 697                    "elevateIfNeeded": true,
 698                    "requireLocalPort": true,
 699                    "protocol": "https"
 700                },
 701                "updateRemoteUserUID": true,
 702                "remoteEnv": {
 703                    "MYVAR1": "myvarvalue",
 704                    "MYVAR2": "myvarothervalue"
 705                },
 706                "initializeCommand": ["echo", "initialize_command"],
 707                "onCreateCommand": "echo on_create_command",
 708                "updateContentCommand": {
 709                    "first": "echo update_content_command",
 710                    "second": ["echo", "update_content_command"]
 711                },
 712                "postCreateCommand": ["echo", "post_create_command"],
 713                "postStartCommand": "echo post_start_command",
 714                "postAttachCommand": {
 715                    "something": "echo post_attach_command",
 716                    "something1": "echo something else",
 717                },
 718                "waitFor": "postStartCommand",
 719                "userEnvProbe": "loginShell",
 720                "features": {
 721              		"ghcr.io/devcontainers/features/aws-cli:1": {},
 722              		"ghcr.io/devcontainers/features/anaconda:1": {}
 723               	},
 724                "overrideFeatureInstallOrder": [
 725                    "ghcr.io/devcontainers/features/anaconda:1",
 726                    "ghcr.io/devcontainers/features/aws-cli:1"
 727                ],
 728                "hostRequirements": {
 729                    "cpus": 2,
 730                    "memory": "8gb",
 731                    "storage": "32gb",
 732                    // Note that we're not parsing this currently
 733                    "gpu": true,
 734                },
 735                "appPort": 8081,
 736                "containerEnv": {
 737                    "MYVAR3": "myvar3",
 738                    "MYVAR4": "myvar4"
 739                },
 740                "containerUser": "myUser",
 741                "mounts": [
 742                    {
 743                        "source": "/localfolder/app",
 744                        "target": "/workspaces/app",
 745                        "type": "volume"
 746                    }
 747                ],
 748                "runArgs": [
 749                    "-c",
 750                    "some_command"
 751                ],
 752                "shutdownAction": "stopContainer",
 753                "overrideCommand": true,
 754                "workspaceFolder": "/workspaces",
 755                "workspaceMount": "source=/app,target=/workspaces/app,type=bind,consistency=cached",
 756                "customizations": {
 757                    "vscode": {
 758                        // Just confirm that this can be included and ignored
 759                    },
 760                    "zed": {
 761                        "extensions": [
 762                            "html"
 763                        ]
 764                    }
 765                }
 766            }
 767            "#;
 768
 769        let result = deserialize_devcontainer_json(given_image_container_json);
 770
 771        assert!(result.is_ok());
 772        let devcontainer = result.expect("ok");
 773        assert_eq!(
 774            devcontainer,
 775            DevContainer {
 776                image: Some(String::from("mcr.microsoft.com/devcontainers/base:ubuntu")),
 777                name: Some(String::from("myDevContainer")),
 778                remote_user: Some(String::from("root")),
 779                forward_ports: Some(vec![
 780                    ForwardPort::String("db:5432".to_string()),
 781                    ForwardPort::Number(3000),
 782                ]),
 783                ports_attributes: Some(HashMap::from([
 784                    (
 785                        "3000".to_string(),
 786                        PortAttributes {
 787                            label: "This Port".to_string(),
 788                            on_auto_forward: OnAutoForward::Notify,
 789                            elevate_if_needed: false,
 790                            require_local_port: true,
 791                            protocol: PortAttributeProtocol::Https
 792                        }
 793                    ),
 794                    (
 795                        "db:5432".to_string(),
 796                        PortAttributes {
 797                            label: "This Port too".to_string(),
 798                            on_auto_forward: OnAutoForward::Silent,
 799                            elevate_if_needed: true,
 800                            require_local_port: false,
 801                            protocol: PortAttributeProtocol::Http
 802                        }
 803                    )
 804                ])),
 805                other_ports_attributes: Some(PortAttributes {
 806                    label: "Other Ports".to_string(),
 807                    on_auto_forward: OnAutoForward::OpenBrowser,
 808                    elevate_if_needed: true,
 809                    require_local_port: true,
 810                    protocol: PortAttributeProtocol::Https
 811                }),
 812                update_remote_user_uid: Some(true),
 813                remote_env: Some(HashMap::from([
 814                    ("MYVAR1".to_string(), "myvarvalue".to_string()),
 815                    ("MYVAR2".to_string(), "myvarothervalue".to_string())
 816                ])),
 817                initialize_command: Some(LifecycleScript::from_args(vec![
 818                    "echo".to_string(),
 819                    "initialize_command".to_string()
 820                ])),
 821                on_create_command: Some(LifecycleScript::from_str("echo on_create_command")),
 822                update_content_command: Some(LifecycleScript::from_map(HashMap::from([
 823                    (
 824                        "first".to_string(),
 825                        vec!["echo".to_string(), "update_content_command".to_string()]
 826                    ),
 827                    (
 828                        "second".to_string(),
 829                        vec!["echo".to_string(), "update_content_command".to_string()]
 830                    )
 831                ]))),
 832                post_create_command: Some(LifecycleScript::from_str("echo post_create_command")),
 833                post_start_command: Some(LifecycleScript::from_args(vec![
 834                    "echo".to_string(),
 835                    "post_start_command".to_string()
 836                ])),
 837                post_attach_command: Some(LifecycleScript::from_map(HashMap::from([
 838                    (
 839                        "something".to_string(),
 840                        vec!["echo".to_string(), "post_attach_command".to_string()]
 841                    ),
 842                    (
 843                        "something1".to_string(),
 844                        vec![
 845                            "echo".to_string(),
 846                            "something".to_string(),
 847                            "else".to_string()
 848                        ]
 849                    )
 850                ]))),
 851                wait_for: Some(LifecycleCommand::PostStartCommand),
 852                user_env_probe: Some(UserEnvProbe::LoginShell),
 853                features: Some(HashMap::from([
 854                    (
 855                        "ghcr.io/devcontainers/features/aws-cli:1".to_string(),
 856                        FeatureOptions::Options(HashMap::new())
 857                    ),
 858                    (
 859                        "ghcr.io/devcontainers/features/anaconda:1".to_string(),
 860                        FeatureOptions::Options(HashMap::new())
 861                    )
 862                ])),
 863                override_feature_install_order: Some(vec![
 864                    "ghcr.io/devcontainers/features/anaconda:1".to_string(),
 865                    "ghcr.io/devcontainers/features/aws-cli:1".to_string()
 866                ]),
 867                host_requirements: Some(HostRequirements {
 868                    cpus: Some(2),
 869                    memory: Some("8gb".to_string()),
 870                    storage: Some("32gb".to_string()),
 871                }),
 872                app_port: Some("8081".to_string()),
 873                container_env: Some(HashMap::from([
 874                    ("MYVAR3".to_string(), "myvar3".to_string()),
 875                    ("MYVAR4".to_string(), "myvar4".to_string())
 876                ])),
 877                container_user: Some("myUser".to_string()),
 878                mounts: Some(vec![MountDefinition {
 879                    source: "/localfolder/app".to_string(),
 880                    target: "/workspaces/app".to_string(),
 881                    mount_type: Some("volume".to_string()),
 882                }]),
 883                run_args: Some(vec!["-c".to_string(), "some_command".to_string()]),
 884                shutdown_action: Some(ShutdownAction::StopContainer),
 885                override_command: Some(true),
 886                workspace_folder: Some("/workspaces".to_string()),
 887                workspace_mount: Some(MountDefinition {
 888                    source: "/app".to_string(),
 889                    target: "/workspaces/app".to_string(),
 890                    mount_type: Some("bind".to_string())
 891                }),
 892                customizations: Some(ZedCustomizationsWrapper {
 893                    zed: ZedCustomization {
 894                        extensions: vec!["html".to_string()]
 895                    }
 896                }),
 897                ..Default::default()
 898            }
 899        );
 900
 901        assert_eq!(devcontainer.build_type(), DevContainerBuildType::Image);
 902    }
 903
 904    #[test]
 905    fn should_deserialize_docker_compose_devcontainer_json() {
 906        let given_docker_compose_json = r#"
 907            // These are some external comments. serde_lenient should handle them
 908            {
 909                // These are some internal comments
 910                "name": "myDevContainer",
 911                "remoteUser": "root",
 912                "forwardPorts": [
 913                    "db:5432",
 914                    3000
 915                ],
 916                "portsAttributes": {
 917                    "3000": {
 918                        "label": "This Port",
 919                        "onAutoForward": "notify",
 920                        "elevateIfNeeded": false,
 921                        "requireLocalPort": true,
 922                        "protocol": "https"
 923                    },
 924                    "db:5432": {
 925                        "label": "This Port too",
 926                        "onAutoForward": "silent",
 927                        "elevateIfNeeded": true,
 928                        "requireLocalPort": false,
 929                        "protocol": "http"
 930                    }
 931                },
 932                "otherPortsAttributes": {
 933                    "label": "Other Ports",
 934                    "onAutoForward": "openBrowser",
 935                    "elevateIfNeeded": true,
 936                    "requireLocalPort": true,
 937                    "protocol": "https"
 938                },
 939                "updateRemoteUserUID": true,
 940                "remoteEnv": {
 941                    "MYVAR1": "myvarvalue",
 942                    "MYVAR2": "myvarothervalue"
 943                },
 944                "initializeCommand": ["echo", "initialize_command"],
 945                "onCreateCommand": "echo on_create_command",
 946                "updateContentCommand": {
 947                    "first": "echo update_content_command",
 948                    "second": ["echo", "update_content_command"]
 949                },
 950                "postCreateCommand": ["echo", "post_create_command"],
 951                "postStartCommand": "echo post_start_command",
 952                "postAttachCommand": {
 953                    "something": "echo post_attach_command",
 954                    "something1": "echo something else",
 955                },
 956                "waitFor": "postStartCommand",
 957                "userEnvProbe": "loginShell",
 958                "features": {
 959              		"ghcr.io/devcontainers/features/aws-cli:1": {},
 960              		"ghcr.io/devcontainers/features/anaconda:1": {}
 961               	},
 962                "overrideFeatureInstallOrder": [
 963                    "ghcr.io/devcontainers/features/anaconda:1",
 964                    "ghcr.io/devcontainers/features/aws-cli:1"
 965                ],
 966                "hostRequirements": {
 967                    "cpus": 2,
 968                    "memory": "8gb",
 969                    "storage": "32gb",
 970                    // Note that we're not parsing this currently
 971                    "gpu": true,
 972                },
 973                "dockerComposeFile": "docker-compose.yml",
 974                "service": "myService",
 975                "runServices": [
 976                    "myService",
 977                    "mySupportingService"
 978                ],
 979                "workspaceFolder": "/workspaces/thing",
 980                "shutdownAction": "stopCompose",
 981                "overrideCommand": true
 982            }
 983            "#;
 984        let result = deserialize_devcontainer_json(given_docker_compose_json);
 985
 986        assert!(result.is_ok());
 987        let devcontainer = result.expect("ok");
 988        assert_eq!(
 989            devcontainer,
 990            DevContainer {
 991                name: Some(String::from("myDevContainer")),
 992                remote_user: Some(String::from("root")),
 993                forward_ports: Some(vec![
 994                    ForwardPort::String("db:5432".to_string()),
 995                    ForwardPort::Number(3000),
 996                ]),
 997                ports_attributes: Some(HashMap::from([
 998                    (
 999                        "3000".to_string(),
1000                        PortAttributes {
1001                            label: "This Port".to_string(),
1002                            on_auto_forward: OnAutoForward::Notify,
1003                            elevate_if_needed: false,
1004                            require_local_port: true,
1005                            protocol: PortAttributeProtocol::Https
1006                        }
1007                    ),
1008                    (
1009                        "db:5432".to_string(),
1010                        PortAttributes {
1011                            label: "This Port too".to_string(),
1012                            on_auto_forward: OnAutoForward::Silent,
1013                            elevate_if_needed: true,
1014                            require_local_port: false,
1015                            protocol: PortAttributeProtocol::Http
1016                        }
1017                    )
1018                ])),
1019                other_ports_attributes: Some(PortAttributes {
1020                    label: "Other Ports".to_string(),
1021                    on_auto_forward: OnAutoForward::OpenBrowser,
1022                    elevate_if_needed: true,
1023                    require_local_port: true,
1024                    protocol: PortAttributeProtocol::Https
1025                }),
1026                update_remote_user_uid: Some(true),
1027                remote_env: Some(HashMap::from([
1028                    ("MYVAR1".to_string(), "myvarvalue".to_string()),
1029                    ("MYVAR2".to_string(), "myvarothervalue".to_string())
1030                ])),
1031                initialize_command: Some(LifecycleScript::from_args(vec![
1032                    "echo".to_string(),
1033                    "initialize_command".to_string()
1034                ])),
1035                on_create_command: Some(LifecycleScript::from_str("echo on_create_command")),
1036                update_content_command: Some(LifecycleScript::from_map(HashMap::from([
1037                    (
1038                        "first".to_string(),
1039                        vec!["echo".to_string(), "update_content_command".to_string()]
1040                    ),
1041                    (
1042                        "second".to_string(),
1043                        vec!["echo".to_string(), "update_content_command".to_string()]
1044                    )
1045                ]))),
1046                post_create_command: Some(LifecycleScript::from_str("echo post_create_command")),
1047                post_start_command: Some(LifecycleScript::from_args(vec![
1048                    "echo".to_string(),
1049                    "post_start_command".to_string()
1050                ])),
1051                post_attach_command: Some(LifecycleScript::from_map(HashMap::from([
1052                    (
1053                        "something".to_string(),
1054                        vec!["echo".to_string(), "post_attach_command".to_string()]
1055                    ),
1056                    (
1057                        "something1".to_string(),
1058                        vec![
1059                            "echo".to_string(),
1060                            "something".to_string(),
1061                            "else".to_string()
1062                        ]
1063                    )
1064                ]))),
1065                wait_for: Some(LifecycleCommand::PostStartCommand),
1066                user_env_probe: Some(UserEnvProbe::LoginShell),
1067                features: Some(HashMap::from([
1068                    (
1069                        "ghcr.io/devcontainers/features/aws-cli:1".to_string(),
1070                        FeatureOptions::Options(HashMap::new())
1071                    ),
1072                    (
1073                        "ghcr.io/devcontainers/features/anaconda:1".to_string(),
1074                        FeatureOptions::Options(HashMap::new())
1075                    )
1076                ])),
1077                override_feature_install_order: Some(vec![
1078                    "ghcr.io/devcontainers/features/anaconda:1".to_string(),
1079                    "ghcr.io/devcontainers/features/aws-cli:1".to_string()
1080                ]),
1081                host_requirements: Some(HostRequirements {
1082                    cpus: Some(2),
1083                    memory: Some("8gb".to_string()),
1084                    storage: Some("32gb".to_string()),
1085                }),
1086                docker_compose_file: Some(vec!["docker-compose.yml".to_string()]),
1087                service: Some("myService".to_string()),
1088                run_services: Some(vec![
1089                    "myService".to_string(),
1090                    "mySupportingService".to_string(),
1091                ]),
1092                workspace_folder: Some("/workspaces/thing".to_string()),
1093                shutdown_action: Some(ShutdownAction::StopCompose),
1094                override_command: Some(true),
1095                ..Default::default()
1096            }
1097        );
1098
1099        assert_eq!(
1100            devcontainer.build_type(),
1101            DevContainerBuildType::DockerCompose
1102        );
1103    }
1104
1105    #[test]
1106    fn should_deserialize_dockerfile_devcontainer_json() {
1107        let given_dockerfile_container_json = r#"
1108            // These are some external comments. serde_lenient should handle them
1109            {
1110                // These are some internal comments
1111                "name": "myDevContainer",
1112                "remoteUser": "root",
1113                "forwardPorts": [
1114                    "db:5432",
1115                    3000
1116                ],
1117                "portsAttributes": {
1118                    "3000": {
1119                        "label": "This Port",
1120                        "onAutoForward": "notify",
1121                        "elevateIfNeeded": false,
1122                        "requireLocalPort": true,
1123                        "protocol": "https"
1124                    },
1125                    "db:5432": {
1126                        "label": "This Port too",
1127                        "onAutoForward": "silent",
1128                        "elevateIfNeeded": true,
1129                        "requireLocalPort": false,
1130                        "protocol": "http"
1131                    }
1132                },
1133                "otherPortsAttributes": {
1134                    "label": "Other Ports",
1135                    "onAutoForward": "openBrowser",
1136                    "elevateIfNeeded": true,
1137                    "requireLocalPort": true,
1138                    "protocol": "https"
1139                },
1140                "updateRemoteUserUID": true,
1141                "remoteEnv": {
1142                    "MYVAR1": "myvarvalue",
1143                    "MYVAR2": "myvarothervalue"
1144                },
1145                "initializeCommand": ["echo", "initialize_command"],
1146                "onCreateCommand": "echo on_create_command",
1147                "updateContentCommand": {
1148                    "first": "echo update_content_command",
1149                    "second": ["echo", "update_content_command"]
1150                },
1151                "postCreateCommand": ["echo", "post_create_command"],
1152                "postStartCommand": "echo post_start_command",
1153                "postAttachCommand": {
1154                    "something": "echo post_attach_command",
1155                    "something1": "echo something else",
1156                },
1157                "waitFor": "postStartCommand",
1158                "userEnvProbe": "loginShell",
1159                "features": {
1160              		"ghcr.io/devcontainers/features/aws-cli:1": {},
1161              		"ghcr.io/devcontainers/features/anaconda:1": {}
1162               	},
1163                "overrideFeatureInstallOrder": [
1164                    "ghcr.io/devcontainers/features/anaconda:1",
1165                    "ghcr.io/devcontainers/features/aws-cli:1"
1166                ],
1167                "hostRequirements": {
1168                    "cpus": 2,
1169                    "memory": "8gb",
1170                    "storage": "32gb",
1171                    // Note that we're not parsing this currently
1172                    "gpu": true,
1173                },
1174                "appPort": 8081,
1175                "containerEnv": {
1176                    "MYVAR3": "myvar3",
1177                    "MYVAR4": "myvar4"
1178                },
1179                "containerUser": "myUser",
1180                "mounts": [
1181                    {
1182                        "source": "/localfolder/app",
1183                        "target": "/workspaces/app",
1184                        "type": "volume"
1185                    },
1186                    "source=dev-containers-cli-bashhistory,target=/home/node/commandhistory",
1187                ],
1188                "runArgs": [
1189                    "-c",
1190                    "some_command"
1191                ],
1192                "shutdownAction": "stopContainer",
1193                "overrideCommand": true,
1194                "workspaceFolder": "/workspaces",
1195                "workspaceMount": "source=/folder,target=/workspace,type=bind,consistency=cached",
1196                "build": {
1197                   	"dockerfile": "DockerFile",
1198                   	"context": "..",
1199                   	"args": {
1200                   	    "MYARG": "MYVALUE"
1201                   	},
1202                   	"options": [
1203                   	    "--some-option",
1204                   	    "--mount"
1205                   	],
1206                   	"target": "development",
1207                   	"cacheFrom": "some_image"
1208                }
1209            }
1210            "#;
1211
1212        let result = deserialize_devcontainer_json(given_dockerfile_container_json);
1213
1214        assert!(result.is_ok());
1215        let devcontainer = result.expect("ok");
1216        assert_eq!(
1217            devcontainer,
1218            DevContainer {
1219                name: Some(String::from("myDevContainer")),
1220                remote_user: Some(String::from("root")),
1221                forward_ports: Some(vec![
1222                    ForwardPort::String("db:5432".to_string()),
1223                    ForwardPort::Number(3000),
1224                ]),
1225                ports_attributes: Some(HashMap::from([
1226                    (
1227                        "3000".to_string(),
1228                        PortAttributes {
1229                            label: "This Port".to_string(),
1230                            on_auto_forward: OnAutoForward::Notify,
1231                            elevate_if_needed: false,
1232                            require_local_port: true,
1233                            protocol: PortAttributeProtocol::Https
1234                        }
1235                    ),
1236                    (
1237                        "db:5432".to_string(),
1238                        PortAttributes {
1239                            label: "This Port too".to_string(),
1240                            on_auto_forward: OnAutoForward::Silent,
1241                            elevate_if_needed: true,
1242                            require_local_port: false,
1243                            protocol: PortAttributeProtocol::Http
1244                        }
1245                    )
1246                ])),
1247                other_ports_attributes: Some(PortAttributes {
1248                    label: "Other Ports".to_string(),
1249                    on_auto_forward: OnAutoForward::OpenBrowser,
1250                    elevate_if_needed: true,
1251                    require_local_port: true,
1252                    protocol: PortAttributeProtocol::Https
1253                }),
1254                update_remote_user_uid: Some(true),
1255                remote_env: Some(HashMap::from([
1256                    ("MYVAR1".to_string(), "myvarvalue".to_string()),
1257                    ("MYVAR2".to_string(), "myvarothervalue".to_string())
1258                ])),
1259                initialize_command: Some(LifecycleScript::from_args(vec![
1260                    "echo".to_string(),
1261                    "initialize_command".to_string()
1262                ])),
1263                on_create_command: Some(LifecycleScript::from_str("echo on_create_command")),
1264                update_content_command: Some(LifecycleScript::from_map(HashMap::from([
1265                    (
1266                        "first".to_string(),
1267                        vec!["echo".to_string(), "update_content_command".to_string()]
1268                    ),
1269                    (
1270                        "second".to_string(),
1271                        vec!["echo".to_string(), "update_content_command".to_string()]
1272                    )
1273                ]))),
1274                post_create_command: Some(LifecycleScript::from_str("echo post_create_command")),
1275                post_start_command: Some(LifecycleScript::from_args(vec![
1276                    "echo".to_string(),
1277                    "post_start_command".to_string()
1278                ])),
1279                post_attach_command: Some(LifecycleScript::from_map(HashMap::from([
1280                    (
1281                        "something".to_string(),
1282                        vec!["echo".to_string(), "post_attach_command".to_string()]
1283                    ),
1284                    (
1285                        "something1".to_string(),
1286                        vec![
1287                            "echo".to_string(),
1288                            "something".to_string(),
1289                            "else".to_string()
1290                        ]
1291                    )
1292                ]))),
1293                wait_for: Some(LifecycleCommand::PostStartCommand),
1294                user_env_probe: Some(UserEnvProbe::LoginShell),
1295                features: Some(HashMap::from([
1296                    (
1297                        "ghcr.io/devcontainers/features/aws-cli:1".to_string(),
1298                        FeatureOptions::Options(HashMap::new())
1299                    ),
1300                    (
1301                        "ghcr.io/devcontainers/features/anaconda:1".to_string(),
1302                        FeatureOptions::Options(HashMap::new())
1303                    )
1304                ])),
1305                override_feature_install_order: Some(vec![
1306                    "ghcr.io/devcontainers/features/anaconda:1".to_string(),
1307                    "ghcr.io/devcontainers/features/aws-cli:1".to_string()
1308                ]),
1309                host_requirements: Some(HostRequirements {
1310                    cpus: Some(2),
1311                    memory: Some("8gb".to_string()),
1312                    storage: Some("32gb".to_string()),
1313                }),
1314                app_port: Some("8081".to_string()),
1315                container_env: Some(HashMap::from([
1316                    ("MYVAR3".to_string(), "myvar3".to_string()),
1317                    ("MYVAR4".to_string(), "myvar4".to_string())
1318                ])),
1319                container_user: Some("myUser".to_string()),
1320                mounts: Some(vec![
1321                    MountDefinition {
1322                        source: "/localfolder/app".to_string(),
1323                        target: "/workspaces/app".to_string(),
1324                        mount_type: Some("volume".to_string()),
1325                    },
1326                    MountDefinition {
1327                        source: "dev-containers-cli-bashhistory".to_string(),
1328                        target: "/home/node/commandhistory".to_string(),
1329                        mount_type: None,
1330                    }
1331                ]),
1332                run_args: Some(vec!["-c".to_string(), "some_command".to_string()]),
1333                shutdown_action: Some(ShutdownAction::StopContainer),
1334                override_command: Some(true),
1335                workspace_folder: Some("/workspaces".to_string()),
1336                workspace_mount: Some(MountDefinition {
1337                    source: "/folder".to_string(),
1338                    target: "/workspace".to_string(),
1339                    mount_type: Some("bind".to_string())
1340                }),
1341                build: Some(ContainerBuild {
1342                    dockerfile: "DockerFile".to_string(),
1343                    context: Some("..".to_string()),
1344                    args: Some(HashMap::from([(
1345                        "MYARG".to_string(),
1346                        "MYVALUE".to_string()
1347                    )])),
1348                    options: Some(vec!["--some-option".to_string(), "--mount".to_string()]),
1349                    target: Some("development".to_string()),
1350                    cache_from: Some(vec!["some_image".to_string()]),
1351                }),
1352                ..Default::default()
1353            }
1354        );
1355
1356        assert_eq!(devcontainer.build_type(), DevContainerBuildType::Dockerfile);
1357    }
1358}