January 24, 2023

How to get awesome Docker profiles in Windows Terminal?

Windows Terminal showing docker containers and images? Of course it is possible! We can easily start a new shell directly inside of a container, or attach to an already running one by simply selecting a profile from a drop-down.

Solution

We are using Windows Terminal JSON Fragment Extension, which essentially is a JSON file we generate and gets dynamically merged with the main settings file. Actually in our case, it is just a file containing profiles describing which command to run to get a working shell.

Next we need access to a docker.sock, in order to use Docker API for discovering running containers and known images. Afterwards we can generate mentioned fragment JSON, which is understood by Windows Terminal. Moreover, for eye candy we will also configure a Docker icon next to our profiles so that we can quickly find them 😉

Finally, all our scripts are going to be containerized and be run periodically every few seconds to keep profile list up to date. Easy right? Let’s get to work!

Lets get it started!

Before jumping into to many details, lets see it in action. Because everything is already containerized and published to Docker Hub we can quickly extended our profile list just by executing docker run. More information about the configuration options can be found on the Docker Hub.

At this point, you might need to adjust docker.sock location (I was running Ubuntu 22.04 on WSL). Additionally you might wish to modify known images list so it suits your preferences.

$fragmentPath = "Users/$($env:USERNAME)/AppData/Local/Microsoft/Windows Terminal/Fragments/DevOpsifyMe.ContainersTerminal"
$windowsPath = "c:/$fragmentPath"
$linuxPath = "/mnt/c/$fragmentPath"
New-Item -Type Directory $windowsPath -Force | Out-Null

docker run `
    -d --restart=always `
    --name containersterminal `
    -e FRAGMENT_WIN_PATH=$windowsPath `
    -e KNOWN_IMAGES='debian;ubuntu;golang' `
    -v /run/docker.sock:/var/run/docker.sock `
    -v "$($linuxPath):/output" `
    devopsifyme/containersterminal:latest

Once executed, open ‘%localappdata%\Microsoft\Windows Terminal\Fragments\DevOpsifyMe.ContainersTerminal’ and check if docker.json and docker.bmp files are there. If you cannot find them, you probably should look at the container logs to see what is going on.

From this point on it is straightforward, just restart Windows Terminal and select one of your new profiles 🙂 Next we are going to look at some of the aspects of what we just did.

Starting a shell inside Docker

First we need to learn how to attach to a running container from a CLI, because this is the exact command we are going to put into our Windows Terminal profile. Moreover similar command can be used to start a new container as well.

# attach to an already running container
docker exec -it <CONTAINER_NAME> sh

# run and attach to a new container
docker run -it --rm <IMAGE_TAG> sh

Now there are few parts of that command that are important to us.

  • -t arguments tells that we want pseudo-TTY (teletypewriter) allocated… unix history, long story short, it allocates a terminal for us to use,
  • -i argument tells that we want an interactive session, which opens STDIN, which essentially allows us to type characters. Without it, we would have only STDOUT by default,
  • –rm argument tells Docker to remove a container as soon as it exits,
  • sh – here we tell to start a sh process inside of a running container. We are putting our bets high that most of the containers should have it available. After connecting we can easily elevate to bash or pwsh if it is available in an image we are using.

Important to notice that even if we kill docker process, which detaches TTY session, container will still continue to run. Therefore it is important to type ‘exit’ to close a session and make container exit (does not apply if we attach to an already running container).

Windows Terminal JSON Fragment

Time to look at the file we want to generate.

{
  "profiles": [
    {
      "name": "Container containersterminal - devopsifyme/containersterminal:latest",
      "commandline": "docker exec -it bc8965179a62 sh",
      "closeOnExit": "graceful",
      "icon": "c:/Users/USER_NAME/AppData/Local/Microsoft/Windows Terminal/Fragments/DevOpsifyMe.ContainersTerminal/docker.bmp",
      "hidden": true
    }
  ]
}

As you can see, it is pretty straightforward and we need to set only a handful of properties:

  • name – self-describing, must be unique, will be used as title
  • commandline – self-describing, executes docker -it commands
  • icon – full path to an icon visible in UI next to the profile
  • hidden – hides profile from the dropdown, but can be easily enabled in settings

Last but not least, notice that the files have to be placed in %LOCALAPPDATA%\Microsoft\Windows Terminal\Fragments\<TOOL_NAME> folder. From there Windows Terminal will try to load all JSON files it can find.

Query Docker API from PowerShell

Now that we know what kind of file we need to generate, lets find the source data. Luckily we don’t need to learn any new commands, as we can use good old Docker CLI for this purpose.

docker container ls --format "{{json . }}"

In the above snippet you can notice that the only modification is adding --format "{{json . }}" which of course causes command to return JSON. That in turn allows us to pipe output to ConvertFrom-Json cmdlet, so that we get a nice object to extract data from 🙂

Last thing left is to simply loop over returned containers and generate JSON file. Not going into details it can be done with the following pseudo-code:

  • First get list of running containers
  • Then transform to an expected by Windows Terminal structure
  • Lastly export to a JSON file
$containers = docker container ls --format "{{json . }}" | ConvertFrom-Json

$profiles = $containers | % {
  @{
    "name": "Container $($_.Name) - $($_.Image)",
    "commandline": "docker exec -it $($_.ID) sh"
  }
}

$contents = @{
  profiles = @($profiles)
}

$contents | ConvertTo-Json -Depth 99 > docker.json

Take a look at devopsifyme-containersterminal (github.com) for full code.

Worth to know…

There are few things you should be aware of!

First one is that when closing terminal tab for newly started containers, as expected we also close “docker cli” process. What might not be expected is that the container itself still continues to run. Therefore in the current implementation it is important to manually type ‘exit’ which also closes our TTY session. That in turn also terminates the container if it was the main session. As a failsafe our container also runs a cleanup job that terminates containers older than 4 hours – not ideal, but let me know if you have better ideas!

Second one is that even if we type ‘exit’, the container still remains, just in exited state. Because of this, over time we might accumulate quite a lot of abandoned containers. Therefore best is to run docker prune from time to time, or run our containers with ‘--rm‘ argument which tells docker to automatically remove container after exiting.

Third one is the shell we run – as we get the highest chance of success with sh, this is the one we start by default. From there it is easy to type bash or pwsh if one is available in our image.

Summary

To sum it all up, it is a pretty easy solution that allows us to have disposable shells easily accessible from our favorite terminal 😉 It can also come handy if we want to get a shell inside of a running container for troubleshooting purposes quickly. At this point the published container is tailored for running on a WSL deployed docker, but if there is an interest it can easily be adjusted for any remote docker or even multiple docker contexts.

References

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.