Skip to content

Commit f771d1b

Browse files
committed
feat(add docker swarm tasks object)
1 parent 9dd5fff commit f771d1b

14 files changed

Lines changed: 514 additions & 29 deletions

File tree

internal/dao/common/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ func Inspect(cli *client.Client, ctx context.Context, resourceType, id string) (
154154
data, _, err = cli.SecretInspectWithRaw(ctx, id)
155155
case "config":
156156
data, _, err = cli.ConfigInspectWithRaw(ctx, id)
157+
case "task":
158+
data, _, err = cli.TaskInspectWithRaw(ctx, id)
157159
default:
158160
return "", fmt.Errorf("unknown resource type: %s", resourceType)
159161
}

internal/dao/docker.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/jr-k/d4s/internal/dao/docker/volume"
3232
"github.com/jr-k/d4s/internal/dao/swarm/node"
3333
"github.com/jr-k/d4s/internal/dao/swarm/service"
34+
"github.com/jr-k/d4s/internal/dao/swarm/task"
3435
)
3536

3637
// Re-export types for backward compatibility / convenience
@@ -45,6 +46,7 @@ type Node = node.Node
4546
type Secret = secret.Secret
4647
type Config = dconfig.Config
4748
type Stack = stack.Stack
49+
type Task = task.Task
4850
type ComposeProject = compose.ComposeProject
4951

5052
// Cached container info for instant scoped queries (drill-down)
@@ -75,6 +77,7 @@ type DockerClient struct {
7577
Secret *secret.Manager
7678
Config *dconfig.Manager
7779
Stack *stack.Manager
80+
Task *task.Manager
7881
Compose *compose.Manager
7982

8083
// Resource cache for fast scoped queries and stale-while-revalidate
@@ -117,6 +120,7 @@ func NewDockerClient(contextName string, apiTimeout time.Duration, defaultContex
117120
Secret: secret.NewManager(cli, ctx),
118121
Config: dconfig.NewManager(cli, ctx),
119122
Stack: stack.NewManager(cli, ctx),
123+
Task: task.NewManager(cli, ctx),
120124
Compose: compose.NewManager(cli, ctx),
121125
containerInfoMap: make(map[string]containerInfoCache),
122126
refreshing: make(map[string]bool),
@@ -587,6 +591,18 @@ func (d *DockerClient) StackPS(name string) (string, error) {
587591
return d.Stack.PS(name)
588592
}
589593

594+
func (d *DockerClient) ListTasks() ([]common.Resource, error) {
595+
return d.Task.List()
596+
}
597+
598+
func (d *DockerClient) ListTasksForServiceResource(serviceID string) ([]common.Resource, error) {
599+
return d.Task.ListForService(serviceID)
600+
}
601+
602+
func (d *DockerClient) ListTasksForNodeResource(nodeID string) ([]common.Resource, error) {
603+
return d.Task.ListForNode(nodeID)
604+
}
605+
590606
func (d *DockerClient) StopComposeProject(projectName string) error {
591607
return d.Compose.Stop(projectName)
592608
}

internal/dao/swarm/task/task.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package task
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
dt "github.com/docker/docker/api/types"
9+
"github.com/docker/docker/api/types/filters"
10+
"github.com/docker/docker/api/types/swarm"
11+
"github.com/docker/docker/client"
12+
"github.com/gdamore/tcell/v2"
13+
"github.com/jr-k/d4s/internal/dao/common"
14+
"github.com/jr-k/d4s/internal/ui/styles"
15+
)
16+
17+
type Manager struct {
18+
cli *client.Client
19+
ctx context.Context
20+
}
21+
22+
func NewManager(cli *client.Client, ctx context.Context) *Manager {
23+
return &Manager{cli: cli, ctx: ctx}
24+
}
25+
26+
type Task struct {
27+
ID string
28+
Name string
29+
Image string
30+
Node string
31+
DesiredState string
32+
CurrentState string
33+
Error string
34+
ContainerID string
35+
ServiceID string
36+
}
37+
38+
func (t Task) GetID() string { return t.ID }
39+
func (t Task) GetCells() []string {
40+
id := t.ID
41+
if len(id) > 12 {
42+
id = id[:12]
43+
}
44+
containerID := t.ContainerID
45+
if len(containerID) > 12 {
46+
containerID = containerID[:12]
47+
}
48+
return []string{id, t.Name, t.Image, t.Node, t.DesiredState, t.CurrentState, t.Error, containerID}
49+
}
50+
51+
func (t Task) GetStatusColor() (tcell.Color, tcell.Color) {
52+
switch strings.ToLower(t.CurrentState) {
53+
case "running":
54+
return styles.ColorIdle, styles.ColorBlack
55+
case "complete":
56+
return styles.ColorStatusGray, styles.ColorBlack
57+
case "failed", "rejected", "orphaned":
58+
return styles.ColorStatusRed, styles.ColorBlack
59+
case "preparing", "starting", "assigned", "accepted", "ready":
60+
return styles.ColorStatusBlue, styles.ColorBlack
61+
case "pending", "new":
62+
return styles.ColorStatusOrange, styles.ColorBlack
63+
case "shutdown", "remove":
64+
return styles.ColorStatusGray, styles.ColorBlack
65+
}
66+
return styles.ColorFg, styles.ColorBlack
67+
}
68+
69+
func ucfirst(s string) string {
70+
if len(s) == 0 {
71+
return s
72+
}
73+
return strings.ToUpper(s[:1]) + s[1:]
74+
}
75+
76+
func (t Task) GetColumnValue(column string) string {
77+
switch strings.ToLower(column) {
78+
case "id":
79+
return t.ID
80+
case "name":
81+
return t.Name
82+
case "image":
83+
return t.Image
84+
case "node":
85+
return t.Node
86+
case "desired state":
87+
return t.DesiredState
88+
case "current state":
89+
return t.CurrentState
90+
case "error":
91+
return t.Error
92+
case "container":
93+
return t.ContainerID
94+
}
95+
return ""
96+
}
97+
98+
func (t Task) GetDefaultColumn() string {
99+
return "Name"
100+
}
101+
102+
func (t Task) GetDefaultSortColumn() string {
103+
return "Name"
104+
}
105+
106+
func (m *Manager) List() ([]common.Resource, error) {
107+
tasks, err := m.cli.TaskList(m.ctx, dt.TaskListOptions{})
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
nodes := m.resolveNodes()
113+
services := m.resolveServices()
114+
115+
return m.toResources(tasks, nodes, services), nil
116+
}
117+
118+
func (m *Manager) ListForService(serviceID string) ([]common.Resource, error) {
119+
filter := filters.NewArgs()
120+
filter.Add("service", serviceID)
121+
122+
tasks, err := m.cli.TaskList(m.ctx, dt.TaskListOptions{Filters: filter})
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
nodes := m.resolveNodes()
128+
services := m.resolveServices()
129+
130+
return m.toResources(tasks, nodes, services), nil
131+
}
132+
133+
func (m *Manager) ListForNode(nodeID string) ([]common.Resource, error) {
134+
filter := filters.NewArgs()
135+
filter.Add("node", nodeID)
136+
137+
tasks, err := m.cli.TaskList(m.ctx, dt.TaskListOptions{Filters: filter})
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
nodes := m.resolveNodes()
143+
services := m.resolveServices()
144+
145+
return m.toResources(tasks, nodes, services), nil
146+
}
147+
148+
func (m *Manager) toResources(tasks []swarm.Task, nodes map[string]string, services map[string]string) []common.Resource {
149+
var res []common.Resource
150+
for _, t := range tasks {
151+
imageName := ""
152+
if t.Spec.ContainerSpec != nil {
153+
imageName = t.Spec.ContainerSpec.Image
154+
if idx := strings.LastIndex(imageName, "@"); idx != -1 {
155+
imageName = imageName[:idx]
156+
}
157+
}
158+
159+
nodeName := t.NodeID
160+
if name, ok := nodes[t.NodeID]; ok {
161+
nodeName = name
162+
}
163+
164+
serviceName := t.ServiceID
165+
if name, ok := services[t.ServiceID]; ok {
166+
serviceName = name
167+
}
168+
169+
name := fmt.Sprintf("%s.%d", serviceName, t.Slot)
170+
171+
containerID := ""
172+
if t.Status.ContainerStatus != nil {
173+
containerID = t.Status.ContainerStatus.ContainerID
174+
}
175+
176+
currentState := ucfirst(string(t.Status.State))
177+
178+
errMsg := t.Status.Err
179+
if len(errMsg) > 50 {
180+
errMsg = errMsg[:50] + "…"
181+
}
182+
183+
res = append(res, Task{
184+
ID: t.ID,
185+
Name: name,
186+
Image: imageName,
187+
Node: nodeName,
188+
DesiredState: ucfirst(string(t.DesiredState)),
189+
CurrentState: currentState,
190+
Error: errMsg,
191+
ContainerID: containerID,
192+
ServiceID: t.ServiceID,
193+
})
194+
}
195+
return res
196+
}
197+
198+
func (m *Manager) resolveNodes() map[string]string {
199+
nodes := make(map[string]string)
200+
list, err := m.cli.NodeList(m.ctx, dt.NodeListOptions{})
201+
if err != nil {
202+
return nodes
203+
}
204+
for _, n := range list {
205+
nodes[n.ID] = n.Description.Hostname
206+
}
207+
return nodes
208+
}
209+
210+
func (m *Manager) resolveServices() map[string]string {
211+
services := make(map[string]string)
212+
list, err := m.cli.ServiceList(m.ctx, dt.ServiceListOptions{})
213+
if err != nil {
214+
return services
215+
}
216+
for _, s := range list {
217+
services[s.ID] = s.Spec.Name
218+
}
219+
return services
220+
}

internal/ui/app.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/jr-k/d4s/internal/ui/views/configs"
2929
"github.com/jr-k/d4s/internal/ui/views/secrets"
3030
"github.com/jr-k/d4s/internal/ui/views/stacks"
31+
"github.com/jr-k/d4s/internal/ui/views/tasks"
3132
"github.com/jr-k/d4s/internal/ui/views/services"
3233
"github.com/jr-k/d4s/internal/ui/views/volumes"
3334
"github.com/jr-k/d4s/internal/updater"
@@ -342,6 +343,17 @@ func (a *App) initUI() {
342343
}
343344
a.Views[styles.TitleSecrets] = vSecrets
344345

346+
// Tasks
347+
vTasks := view.NewResourceView(a, styles.TitleTasks)
348+
vTasks.ShortcutsFunc = tasks.GetShortcuts
349+
vTasks.FetchFunc = tasks.Fetch
350+
vTasks.InspectFunc = tasks.Inspect
351+
vTasks.Headers = tasks.Headers
352+
vTasks.InputHandler = func(event *tcell.EventKey) *tcell.EventKey {
353+
return tasks.InputHandler(vTasks, event)
354+
}
355+
a.Views[styles.TitleTasks] = vTasks
356+
345357
// Stacks
346358
vStacks := view.NewResourceView(a, styles.TitleStacks)
347359
vStacks.ShortcutsFunc = stacks.GetShortcuts
@@ -573,6 +585,8 @@ func (a *App) resolveDefaultView() string {
573585
return styles.TitleConfigs
574586
case "stacks", "stack":
575587
return styles.TitleStacks
588+
case "tasks", "task":
589+
return styles.TitleTasks
576590
default:
577591
return styles.TitleContainers
578592
}

internal/ui/app_navigation.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ func (a *App) ExecuteCmd(cmd string) {
4545
switchToRoot(styles.TitleConfigs)
4646
case "k", "st", "stack", "stacks":
4747
switchToRoot(styles.TitleStacks)
48+
case "t", "task", "tasks":
49+
switchToRoot(styles.TitleTasks)
4850
case "h", "help", "?":
4951
a.Pages.AddPage("help", a.Help, true, true)
5052
default:

internal/ui/components/command/command.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ var availableCommands = []string{
116116
"compose",
117117
"configs",
118118
"stacks",
119+
"tasks",
119120
"help",
120121
"aliases",
121122
"q",
@@ -128,6 +129,7 @@ var availableCommands = []string{
128129
"p",
129130
"a",
130131
"k",
132+
"t",
131133
}
132134

133135
// findBestSuggestion finds the best matching command for autocompletion

internal/ui/dialogs/help.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ func NewHelpView(app common.AppController) tview.Primitive {
3131
{fmt.Sprintf("[%s]:p[-] Compose", k), ""},
3232
{"", ""},
3333
{fmt.Sprintf("[%s::b]SWARM", a), ""},
34-
{fmt.Sprintf("[%s]:s[-] Services", k), fmt.Sprintf("[%s]:no[-] Nodes", k)},
35-
{fmt.Sprintf("[%s]:x[-] Secrets", k), fmt.Sprintf("[%s]:f[-] Configs", k)},
36-
{fmt.Sprintf("[%s]:k[-] Stacks", k), ""},
34+
{fmt.Sprintf("[%s]:no[-] Nodes", k), fmt.Sprintf("[%s]:t[-] Tasks", k)},
35+
{fmt.Sprintf("[%s]:k[-] Stacks", k), fmt.Sprintf("[%s]:f[-] Configs", k)},
36+
{fmt.Sprintf("[%s]:s[-] Services", k), fmt.Sprintf("[%s]:x[-] Secrets", k)},
3737
{"", ""},
3838
{fmt.Sprintf("[%s::b]NAVIGATION", a), ""},
3939
{fmt.Sprintf("[%s]←/→[-], [%s]j/k[-] Navigate", k, k), fmt.Sprintf("[%s]enter[-] Drill Down", k)},

internal/ui/styles/styles.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const (
137137
TitleSecrets = "Secrets"
138138
TitleConfigs = "Configs"
139139
TitleStacks = "Stacks"
140+
TitleTasks = "Tasks"
140141
)
141142

142143
// invertColor inverts a tcell.Color by flipping its lightness while preserving hue and saturation.

0 commit comments

Comments
 (0)