feat(docker): add container info

This commit is contained in:
lurchfresser
2026-06-26 09:02:01 +02:00
committed by Jan De Dobbeleer
parent 4f19175e0c
commit 4a6a376110
4 changed files with 244 additions and 13 deletions
+93 -2
View File
@@ -2,7 +2,9 @@ package segments
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"slices"
@@ -12,8 +14,12 @@ import (
const (
// FetchContext is the property used to fetch the current docker context
FetchContext options.Option = "fetch_context"
// DefaultDockerContext is the default docker context name
defaultDockerContext = "default"
// DockerCommand is the property used to specify the docker command to use
DockerCommand options.Option = "docker_command"
// Filter is the property used to specify a filter to apply to docker ps results in environment mode, see https://docs.docker.com/reference/cli/docker/container/ls/#filter
Filter options.Option = "filter"
)
type DockerConfig struct {
@@ -22,8 +28,19 @@ type DockerConfig struct {
type Docker struct {
Base
command *cmd
Context string
Containers []Container
}
Context string
type Container struct {
ID string
Image string
Command string
Created string
Status string
Ports string
Names string
}
func (d *Docker) Template() string {
@@ -59,6 +76,7 @@ func (d *Docker) Enabled() bool {
extensions = d.options.StringArray(LanguageExtensions, extensions)
displayMode := d.options.String(DisplayMode, DisplayModeContext)
switch displayMode {
case DisplayModeContext:
return d.fetchContext()
@@ -73,6 +91,40 @@ func (d *Docker) Enabled() bool {
}
return true
case DisplayModeEnvironment:
// always fetch context first
_ = d.fetchContext()
if d.Context == "" {
d.Context = defaultStr
}
dockerCommand := d.options.String(DockerCommand, "docker")
if !d.env.HasCommand(dockerCommand) {
return false
}
filter := d.options.String(Filter, "")
// Use Go template formatting with tab separation
format := `{{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}`
args := []string{"ps", "--format", format}
if len(filter) > 0 {
args = append(args, "--filter", filter)
}
d.command = &cmd{
executable: dockerCommand,
args: args,
}
containers, err := d.fetchContainers()
if err != nil {
return false
}
d.Containers = containers
return len(d.Containers) > 0
}
return false
@@ -111,3 +163,42 @@ func (d *Docker) fetchContext() bool {
return false
}
func (d *Docker) fetchContainers() ([]Container, error) {
if d.command == nil {
return nil, nil
}
output, err := d.env.RunCommand(d.command.executable, d.command.args...)
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
return nil, nil
}
containers := make([]Container, 0, len(lines))
for i, line := range lines {
line = strings.TrimRight(line, "\r")
fields := strings.Split(line, "\t")
if len(fields) != 7 {
return nil, fmt.Errorf("invalid docker ps output on line %d: expected 7 fields, got %d", i+1, len(fields))
}
containers = append(containers, Container{
ID: fields[0],
Image: fields[1],
Command: fields[2],
Created: fields[3],
Status: fields[4],
Ports: fields[5],
Names: fields[6],
})
}
return containers, nil
}
+108
View File
@@ -90,3 +90,111 @@ func TestDockerFiles(t *testing.T) {
assert.Equal(t, tc.ExpectedEnabled, docker.Enabled(), tc.Case)
}
}
func TestDockerEnvironment(t *testing.T) {
cases := []struct {
Case string
ExpectedContext string
Filter string
DockerCommand string
CommandOutput string
ExpectedEnabled bool
HasCommand bool
}{
{
Case: "Docker command not found",
ExpectedEnabled: false,
HasCommand: false,
},
{
Case: "No running containers",
ExpectedEnabled: false,
HasCommand: true,
CommandOutput: "",
},
{
Case: "One running container",
ExpectedEnabled: true,
HasCommand: true,
CommandOutput: "c1\timage1\tcmd1\tcreated1\tstatus1\tports1\tnames1",
ExpectedContext: "1 running (c1)",
},
{
Case: "Multiple running containers",
ExpectedEnabled: true,
HasCommand: true,
CommandOutput: "c1\timage1\tcmd1\tcreated1\tstatus1\tports1\tnames1\nc2\timage2\tcmd2\tcreated2\tstatus2\tports2\tnames2",
ExpectedContext: "2 running (c1)",
},
{
Case: "Multiple running containers with windows line endings",
ExpectedEnabled: true,
HasCommand: true,
CommandOutput: "c1\timage1\tcmd1\tcreated1\tstatus1\tports1\tnames1\r\nc2\timage2\tcmd2\tcreated2\tstatus2\tports2\tnames2\r\n",
ExpectedContext: "2 running (c1)",
},
{
Case: "Output with empty line",
ExpectedEnabled: false,
HasCommand: true,
CommandOutput: "c1\timage1\tcmd1\tcreated1\tstatus1\tports1\tnames1\n\nc2\timage2\tcmd2\tcreated2\tstatus2\tports2\tnames2",
},
{
Case: "Filter applied",
ExpectedEnabled: true,
HasCommand: true,
Filter: "name=service1",
CommandOutput: "c1\timage1\tcmd1\tcreated1\tstatus1\tports1\tnames1",
ExpectedContext: "1 running (c1)",
},
{
Case: "Custom docker command",
ExpectedEnabled: true,
HasCommand: true,
DockerCommand: "podman",
CommandOutput: "c1\timage1\tcmd1\tcreated1\tstatus1\tports1\tnames1",
ExpectedContext: "1 running (c1)",
},
}
for _, tc := range cases {
t.Run(tc.Case, func(t *testing.T) {
env := new(mock.Environment)
env.On("Getenv", mock_.Anything).Return("")
env.On("Home").Return("")
env.On("FileContent", mock_.Anything).Return("")
dockerCommand := "docker"
if len(tc.DockerCommand) > 0 {
dockerCommand = tc.DockerCommand
}
env.On("HasCommand", dockerCommand).Return(tc.HasCommand)
format := `{{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}`
args := []string{"ps", "--format", format}
if len(tc.Filter) > 0 {
args = append(args, "--filter", tc.Filter)
}
env.On("RunCommand", dockerCommand, args).Return(tc.CommandOutput, nil)
props := options.Map{
DisplayMode: DisplayModeEnvironment,
Filter: tc.Filter,
}
if len(tc.DockerCommand) > 0 {
props[DockerCommand] = tc.DockerCommand
}
docker := &Docker{}
docker.Init(props, env)
assert.Equal(t, tc.ExpectedEnabled, docker.Enabled())
if tc.ExpectedEnabled {
template := `{{ len .Containers }} running ({{ (index .Containers 0).ID }})`
assert.Equal(t, tc.ExpectedContext, renderTemplate(env, template, docker))
}
})
}
}
+14 -1
View File
@@ -1615,7 +1615,8 @@
"$ref": "#/definitions/display_mode",
"enum": [
"files",
"context"
"context",
"environment"
]
},
"extensions": {
@@ -1632,6 +1633,18 @@
"title": "Fetch Context",
"description": "Fetch the Docker context",
"default": true
},
"docker_command": {
"type": "string",
"title": "Docker Command",
"description": "Command used to call Docker",
"default": "docker"
},
"filter": {
"type": "string",
"title": "Filter",
"description": "Filter passed to docker ps. See https://docs.docker.com/reference/cli/docker/container/ls/#filter",
"default": ""
}
},
"unevaluatedProperties": false
+29 -10
View File
@@ -6,7 +6,7 @@ sidebar_label: Docker
## What
Display the current [Docker][docker] context. Will not be active when using the default context.
Display the current [Docker][docker] context, or a list of running containers when `display_mode` is set to `environment`.
## Sample Configuration
@@ -20,16 +20,23 @@ import Config from "@site/src/components/Config.js";
foreground: "#000000",
background: "#0B59E7",
template: " \uf308 {{ .Context }} ",
options: {
display_mode: "environment",
docker_command: "docker",
filter: "name=oh-my-posh-db-1"
}
}}
/>
## Options
| Name | Type | Default | Description |
| --------------- | :--------: | :------------------------------------------------------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `display_mode` | `string` | `context` | <ul><li>`files`: the segment is only displayed when a file `extensions` listed is present</li><li>`context`: displays the segment when a Docker context active</li></ul> |
| `fetch_context` | `boolean` | `true` | also fetch the current active Docker context when in the `files` display mode |
| `extensions` | `[]string` | `compose.yml, compose.yaml, docker-compose.yml, docker-compose.yaml, Dockerfile` | allows to override the default list of file extensions to validate |
| Name | Type | Default | Description |
| ---------------- | ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `display_mode` | `string` | `context` | `files`, `context`, or `environment` |
| `fetch_context` | `boolean` | `true` | fetch context in `files` mode |
| `docker_command` | `string` | `docker` | command used in `environment` mode |
| `filter` | `string` | | passed to `docker ps`, see [the filter documentation](https://docs.docker.com/engine/reference/commandline/ps/#filtering) |
| `extensions` | `[]string` | compose files and Dockerfile | overrides the file checks |
## Template ([info][templates])
@@ -43,10 +50,22 @@ import Config from "@site/src/components/Config.js";
### Properties
| Name | Type | Description |
| ---------- | -------- | -------------------------- |
| `.Context` | `string` | the current active context |
| Name | Type | Description |
| ------------- | ------------- | ------------------------------------------------------------- |
| `.Context` | `string` | current active context |
| `.Containers` | `[]Container` | running containers from `docker ps` (`environment` mode only) |
#### Container
| Name | Type | Description |
| ---------- | -------- | ----------------- |
| `.ID` | `string` | container ID |
| `.Image` | `string` | container image |
| `.Command` | `string` | container command |
| `.Created` | `string` | created time |
| `.Status` | `string` | container status |
| `.Ports` | `string` | published ports |
| `.Names` | `string` | container name |
[docker]: https://www.docker.com/
[go-text-template]: https://golang.org/pkg/text/template/
[templates]: /docs/configuration/templates