devcontainers / spec

Development Containers: Use a container as a full-featured development environment.
https://containers.dev
Creative Commons Attribution 4.0 International
3.46k stars 215 forks source link

Proposal: enable optional bind mounts #132

Open stuartleeks opened 1 year ago

stuartleeks commented 1 year ago

When specifying mounts in devcontainer.json, we can use volume or bind mounts:

 "mounts": [
    // Keep command history
    {"type":"volume", "source": "my-bash-history", "target":"/home/vscode/commandhistory"},
    // Mounts the login details from the host machine to azcli works in the container
    {"type": "bind", "source": "${env:HOME}${env:USERPROFILE}/.azure", "target":"/home/vscode/.azure"},
    // Mount docker socket for docker builds
    {"type": "bind", "source": "/var/run/docker.sock", "target": "/var/run/docker.sock"}
  ],

By default, if the src doesn't exist on the host the dev container start fails. In some scenarios it is desirable to create a mount if it exists, but it is preferable to continue without the mount rather than fail to start.

An example is when you wish to mount the .azure folder from the host into the dev container to share login credentials between host and container as a convenience mechanism. In this scenario, it would be nice to make the mount optional, i.e. to strip the mount out of the configuration if the src doesn't exist on the host. In this example, this would be if the user doesn't have the azure CLI installed on the host. If the mount was optional, the dev container would still run and the user would log in inside the dev container.

With this extra property to items in the mounts array to allow bind mounts to be indicated as optional, the above bind mount for the .azure folder would look like:

 "mounts": [
    // Mounts the login details from the host machine to azcli works in the container
    {"type": "bind", "source": "${env:HOME}${env:USERPROFILE}/.azure", "target":"/home/vscode/.azure", "optional": true},
  ],

This could also be supported by the string form:

 "mounts": [
    // Mounts the login details from the host machine to azcli works in the container
    "type=bind,source=${env:HOME}${env:USERPROFILE}/.azure,target=/home/vscode/.azure,optional=true",
  ],
max06 commented 1 year ago

Instead of having a boolean for one case, it might make sense to allow different solutions if the desired mount target does not exist.

Example:

 "mounts": [
    // Keep command history
    {"type":"volume", "source": "my-bash-history", "target":"/home/vscode/commandhistory"}
  ]

would benefit from an automatically created host folder, instead of ignoring the mount.

My users are notoriously lazy when it comes to reading the repository readmes, they would complain if their shell history is lost with every recreate, just because the host folder doesn't exist.


@stuartleeks: If you use jsonc (not yaml, sorry) as language in your codeblock, it stops complaining about the comments 👍🏼

stuartleeks commented 1 year ago

@max06, that's a good point.

In which case, potentially something like an onMissingSource option instead of optional:

 "mounts": [
    // Mounts the login details from the host machine to azcli works in the container
    {"type": "bind", "source": "${env:HOME}${env:USERPROFILE}/.azure", "target":"/home/vscode/.azure", "onMissingSource": "skip"},
  ],

This would allow for a value of skip which would give the same behaviour as optional:true in the original suggestion. It could also allow create which would create the host folder.

That said, in discussion with @chrmarti, it sounds like there are challenges here as the docker context that is used for building/running containers could be remoted over SSH. In this scenario, I'm not clear how we would test for the existence of host paths.

max06 commented 1 year ago

That said, in discussion with @chrmarti, it sounds like there are challenges here as the docker context that is used for building/running containers could be remoted over SSH. In this scenario, I'm not clear how we would test for the existence of host paths.

PS C:\Users\max06> docker run -it --rm --mount type=bind,source=/home/max06/nada,target=/tmp/test ubuntu
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /home/max06/nada.

This example (executed in windows) uses a docker context with a docker socket over ssh on a linux host. This requires the source of the mount to be located on that docker host. Testing for it is simple: Try to bind mount it. The docker cli returns an error if the source doesn't exist.

chrmarti commented 1 year ago

Trying to bind mount to test if something exists takes time. In the SSH case we also have an ssh connection that we use to run the docker (and other) commands, we can use that to check for files/folders. One edge case with that is when the Docker daemon still runs somewhere else and does not bind mount to the filesystem on the SSH server (similarly with local and WSL cases) - we might have to detect this and fall back to using a temporary container.

Chuxel commented 1 year ago

Yeah, I don't think we'd need to test the bind mount as much as see if the source folder exists on the host which is fast. If it doesn't, we remove the mount. We'd recommend using Remote - SSH rather than DOCKER_HOST at this point. In this case, the test should occur on the remote host rather than locally just like initalizeCommand.

We could provide an option to enable this optional behavior though since there's no guarantee people will want it. I agree actually creating the container to test is going to be really slow.

FWIW - I hacked in a workaround into one of the templates to create the source folder if it doesn't exist in a way that works on both Windows and macOS/Linux. It's a bit hacky though since I fake out "bash" on Windows using a bash.cmd file. This appears to be the best way to enable cross-OS support with initalizeCommand near as I can tell.... but that's a separate issue.

https://github.com/devcontainers/templates/tree/main/src/kubernetes-helm/.devcontainer

max06 commented 1 year ago

There's an "oldschool" way of using devcontainers on a remote machine, before it could be done through the ssh remote extension: By setting a docker context to a ssh target:

PS C:\Users\max06> docker context ls
NAME                TYPE                DESCRIPTION                               DOCKER ENDPOINT                             KUBERNETES ENDPOINT   ORCHESTRATOR
datengrab           moby                                                          ssh://max06@192.168.27.10

Not sure if you have a control channel to the target machine this way.

Chuxel commented 1 year ago

Yep. We've updated guidance here. https://code.visualstudio.com/remote/advancedcontainers/develop-remote-host#_connect-using-the-remote-ssh-extension-recommended

max06 commented 1 year ago

Guess I should update my setup here then 😅

justin-mccann commented 1 year ago

Some of our developers hit this same issue when we enabled docker-from-docker and bind mounted the host ~/.kube directory, since they had never run kubectl in their host environment before. We added a mkdir as part of the initializeCommand to work around it.

evilhamsterman commented 1 year ago

This would be really powerful if we could also use variables in the mounts other than just the devcontainerId. I was thinking about how the various persistence features could be rolled into the features that install apps like the various cloud cli

I was thinking something like

{
  "options": {
    "persistence": {
      "type": "boolean",
      "description": "Should persistence be enable for authentication token",
      "default": true
    },
    "tokenpath": {
      "type": "string",
      "description": "Path to your existing token files",
      "default": ""
    }
  },
  "mounts": [
    {
      "source": "auth-token-volume",
      "target": "/dc/auth-token",
      "type": "volume",
      "enable": "${option:persistence}"
    },
    {
      "source": "${option:tokenpath}",
      "target": "/dc/bound-token",
      "type": "bind"
    },
    {
      "source": "${localEnv:APP_AUTH_PATH}",
      "target": "/dc/env-bound-token",
      "type": "bind"
    }
  ],
  "containerEnv": {
    "APP_AUTH_PATH": "/dc/bound-token"
  }
}
AngellusMortis commented 11 months ago

Having optional mounts would still be amazing, but we are getting around the mount point not existing with an initalize command:

"initializeCommand": {
    "mkdir-posix": "mkdir -p $HOME/.config $HOME/.docker || true",
},
MartinBernstorff commented 9 months ago

Strong support here as well; running into issues where I need a directory to be present, both for Windows and POSIX hosts. Unsure how to handle this atm, optional would allow us to handle it after container init.

digiexchris commented 4 months ago

My use case is embedded development, where the serial tty devices may or may not exist depending on which one is in use. Writing firmware for hardware agnostic platforms like Zephyr RTOS, this happens a lot. I'm constantly switching from the ESP32's serial debug output on ttyACM0 to a different board with a different usb chip coming in as ttyUSB0 or the CMSIS-DAP adapter that needs a particular /dev/bus/usb device mounted. Currently, I have to unplug and replug the right device, determine where it'll be mounted, edit the config, and restart the container. it'd be preferred if I could list all of my device mounts, and have it just mount the ones that exist when the container restarts.