master
go 373 lines 10.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package dockerfunc
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/docker/docker/api/types"
14 typesContainer "github.com/docker/docker/api/types/container"
15 "github.com/docker/go-units"
16 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
17 )
18
19 const (
20 containersMethodID = "container-ls"
21 containersMethodHelp = "List Docker containers (equivalent to docker ps -a)."
22 )
23
24 const (
25 colContainerID = iota
26 colImage
27 colCommand
28 colCreated
29 colStatus
30 colState
31 colPorts
32 colNames
33 colContainerIDFull
34 colCreatedUnix
35 )
36
37 const (
38 containersColID = "container_id"
39 containersColImage = "image"
40 containersColCommand = "command"
41 containersColCreated = "created"
42 containersColStatus = "status"
43 containersColState = "state"
44 containersColPorts = "ports"
45 containersColNames = "names"
46 containersColIDFull = "container_id_full"
47 containersColCreatedUnix = "created_unix"
48 )
49
50 func containersMethodConfig() funcapi.MethodConfig {
51 return funcapi.MethodConfig{
52 ID: containersMethodID,
53 Name: "Containers",
54 UpdateEvery: 10,
55 Help: containersMethodHelp,
56 }
57 }
58
59 type funcContainers struct {
60 router *router
61 }
62
63 func newFuncContainers(r *router) *funcContainers {
64 return &funcContainers{router: r}
65 }
66
67 // Compile-time interface check.
68 var _ funcapi.MethodHandler = (*funcContainers)(nil)
69
70 func (f *funcContainers) MethodParams(_ context.Context, method string) ([]funcapi.ParamConfig, error) {
71 if method != containersMethodID {
72 return nil, fmt.Errorf("unknown method: %s", method)
73 }
74 return nil, nil
75 }
76
77 func (f *funcContainers) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
78 if method != containersMethodID {
79 return funcapi.NotFoundResponse(method)
80 }
81
82 client, err := f.router.deps.DockerClient()
83 if err != nil {
84 return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
85 }
86
87 containers, err := client.ContainerList(ctx, typesContainer.ListOptions{All: true})
88 if err != nil {
89 if errors.Is(err, context.DeadlineExceeded) {
90 return funcapi.ErrorResponse(504, "query timed out")
91 }
92 return funcapi.ErrorResponse(500, "failed to list containers: %v", err)
93 }
94
95 sortContainers(containers)
96
97 now := time.Now()
98 rows := make([][]any, 0, len(containers))
99 for _, cntr := range containers {
100 rows = append(rows, buildContainerRow(cntr, now))
101 }
102
103 return &funcapi.FunctionResponse{
104 Status: 200,
105 Help: containersMethodHelp,
106 Columns: buildContainerColumns(),
107 Data: rows,
108 DefaultSortColumn: containersColCreatedUnix,
109 }
110 }
111
112 func (f *funcContainers) Cleanup(ctx context.Context) {}
113
114 func sortContainers(containers []typesContainer.Summary) {
115 sort.SliceStable(containers, func(i, j int) bool {
116 if containers[i].Created != containers[j].Created {
117 return containers[i].Created > containers[j].Created
118 }
119 return containers[i].ID < containers[j].ID
120 })
121 }
122
123 func buildContainerRow(cntr typesContainer.Summary, now time.Time) []any {
124 row := make([]any, colCreatedUnix+1)
125 row[colContainerID] = shortContainerID(cntr.ID)
126 row[colImage] = cntr.Image
127 row[colCommand] = strings.TrimSpace(cntr.Command)
128 row[colCreated] = formatCreated(cntr.Created, now)
129 row[colStatus] = formatStatus(cntr)
130 row[colState] = strings.TrimSpace(cntr.State)
131 row[colPorts] = formatPorts(cntr.Ports)
132 row[colNames] = formatContainerNames(cntr.Names)
133 row[colContainerIDFull] = cntr.ID
134 row[colCreatedUnix] = cntr.Created
135 return row
136 }
137
138 func buildContainerColumns() map[string]any {
139 return map[string]any{
140 containersColID: funcapi.Column{
141 Index: colContainerID,
142 Name: "CONTAINER ID",
143 Type: funcapi.FieldTypeString,
144 Visualization: funcapi.FieldVisualValue,
145 Sort: funcapi.FieldSortAscending,
146 Sortable: true,
147 Sticky: true,
148 Summary: funcapi.FieldSummaryCount,
149 Filter: funcapi.FieldFilterMultiselect,
150 Visible: true,
151 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
152 }.BuildColumn(),
153 containersColImage: funcapi.Column{
154 Index: colImage,
155 Name: "IMAGE",
156 Type: funcapi.FieldTypeString,
157 Visualization: funcapi.FieldVisualValue,
158 Sort: funcapi.FieldSortAscending,
159 Sortable: true,
160 Summary: funcapi.FieldSummaryCount,
161 Filter: funcapi.FieldFilterMultiselect,
162 Visible: true,
163 Wrap: true,
164 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
165 }.BuildColumn(),
166 containersColCommand: funcapi.Column{
167 Index: colCommand,
168 Name: "COMMAND",
169 Type: funcapi.FieldTypeString,
170 Visualization: funcapi.FieldVisualValue,
171 Sort: funcapi.FieldSortAscending,
172 Sortable: false,
173 Summary: funcapi.FieldSummaryCount,
174 Filter: funcapi.FieldFilterNone,
175 Visible: true,
176 Wrap: true,
177 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
178 }.BuildColumn(),
179 containersColCreated: funcapi.Column{
180 Index: colCreated,
181 Name: "CREATED",
182 Type: funcapi.FieldTypeString,
183 Visualization: funcapi.FieldVisualValue,
184 Sort: funcapi.FieldSortDescending,
185 Sortable: false,
186 Summary: funcapi.FieldSummaryCount,
187 Filter: funcapi.FieldFilterMultiselect,
188 Visible: true,
189 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
190 }.BuildColumn(),
191 containersColStatus: funcapi.Column{
192 Index: colStatus,
193 Name: "STATUS",
194 Type: funcapi.FieldTypeString,
195 Visualization: funcapi.FieldVisualValue,
196 Sort: funcapi.FieldSortAscending,
197 Sortable: false,
198 Summary: funcapi.FieldSummaryCount,
199 Filter: funcapi.FieldFilterNone,
200 Visible: true,
201 Wrap: true,
202 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
203 }.BuildColumn(),
204 containersColState: funcapi.Column{
205 Index: colState,
206 Name: "State (Raw)",
207 Type: funcapi.FieldTypeString,
208 Visualization: funcapi.FieldVisualValue,
209 Sort: funcapi.FieldSortAscending,
210 Sortable: true,
211 Summary: funcapi.FieldSummaryCount,
212 Filter: funcapi.FieldFilterMultiselect,
213 Visible: false,
214 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
215 }.BuildColumn(),
216 containersColPorts: funcapi.Column{
217 Index: colPorts,
218 Name: "PORTS",
219 Type: funcapi.FieldTypeString,
220 Visualization: funcapi.FieldVisualValue,
221 Sort: funcapi.FieldSortAscending,
222 Sortable: false,
223 Summary: funcapi.FieldSummaryCount,
224 Filter: funcapi.FieldFilterNone,
225 Visible: true,
226 Wrap: true,
227 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
228 }.BuildColumn(),
229 containersColNames: funcapi.Column{
230 Index: colNames,
231 Name: "NAMES",
232 Type: funcapi.FieldTypeString,
233 Visualization: funcapi.FieldVisualValue,
234 Sort: funcapi.FieldSortAscending,
235 Sortable: true,
236 Summary: funcapi.FieldSummaryCount,
237 Filter: funcapi.FieldFilterMultiselect,
238 Visible: true,
239 Sticky: true,
240 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
241 }.BuildColumn(),
242 containersColIDFull: funcapi.Column{
243 Index: colContainerIDFull,
244 Name: "Container ID (Full)",
245 Type: funcapi.FieldTypeString,
246 Visualization: funcapi.FieldVisualValue,
247 Sort: funcapi.FieldSortAscending,
248 Sortable: false,
249 Summary: funcapi.FieldSummaryCount,
250 Filter: funcapi.FieldFilterMultiselect,
251 Visible: false,
252 UniqueKey: true,
253 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
254 }.BuildColumn(),
255 containersColCreatedUnix: funcapi.Column{
256 Index: colCreatedUnix,
257 Name: "Created (Unix)",
258 Type: funcapi.FieldTypeInteger,
259 Visualization: funcapi.FieldVisualValue,
260 Sort: funcapi.FieldSortDescending,
261 Sortable: true,
262 Summary: funcapi.FieldSummaryMax,
263 Filter: funcapi.FieldFilterNone,
264 Visible: false,
265 ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNumber},
266 }.BuildColumn(),
267 }
268 }
269
270 func shortContainerID(id string) string {
271 if len(id) <= 12 {
272 return id
273 }
274 return id[:12]
275 }
276
277 func formatContainerNames(names []string) string {
278 if len(names) == 0 {
279 return ""
280 }
281
282 clean := make([]string, 0, len(names))
283 for _, name := range names {
284 v := strings.TrimPrefix(strings.TrimSpace(name), "/")
285 if v != "" {
286 clean = append(clean, v)
287 }
288 }
289 return strings.Join(clean, ", ")
290 }
291
292 func formatCreated(created int64, now time.Time) string {
293 if created <= 0 {
294 return ""
295 }
296 createdAt := time.Unix(created, 0)
297 if now.Before(createdAt) {
298 return "0 seconds ago"
299 }
300 return units.HumanDuration(now.Sub(createdAt)) + " ago"
301 }
302
303 func formatStatus(cntr typesContainer.Summary) string {
304 if cntr.Status != "" {
305 return cntr.Status
306 }
307
308 switch cntr.State {
309 case "running":
310 return "Up"
311 case "paused":
312 return "Paused"
313 case "restarting":
314 return "Restarting"
315 case "removing":
316 return "Removing"
317 case "exited":
318 return "Exited"
319 case "dead":
320 return "Dead"
321 case "created":
322 return "Created"
323 default:
324 return cntr.State
325 }
326 }
327
328 func formatPorts(ports []typesContainer.Port) string {
329 if len(ports) == 0 {
330 return ""
331 }
332
333 clone := append([]types.Port(nil), ports...)
334 sort.Slice(clone, func(i, j int) bool {
335 if clone[i].PrivatePort != clone[j].PrivatePort {
336 return clone[i].PrivatePort < clone[j].PrivatePort
337 }
338 if clone[i].PublicPort != clone[j].PublicPort {
339 return clone[i].PublicPort < clone[j].PublicPort
340 }
341 if clone[i].IP != clone[j].IP {
342 return clone[i].IP < clone[j].IP
343 }
344 return clone[i].Type < clone[j].Type
345 })
346
347 out := make([]string, 0, len(clone))
348 for _, p := range clone {
349 v := formatPort(p)
350 if v != "" {
351 out = append(out, v)
352 }
353 }
354 return strings.Join(out, ", ")
355 }
356
357 func formatPort(p typesContainer.Port) string {
358 proto := p.Type
359 if proto == "" {
360 proto = "tcp"
361 }
362
363 switch {
364 case p.PublicPort > 0 && p.IP != "":
365 return fmt.Sprintf("%s:%d->%d/%s", p.IP, p.PublicPort, p.PrivatePort, proto)
366 case p.PublicPort > 0:
367 return fmt.Sprintf("%d->%d/%s", p.PublicPort, p.PrivatePort, proto)
368 case p.PrivatePort > 0:
369 return fmt.Sprintf("%d/%s", p.PrivatePort, proto)
370 default:
371 return ""
372 }
373 }