github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/formatter/container.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package formatter 5 6 import ( 7 "fmt" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/distribution/reference" 14 "github.com/docker/docker/api/types" 15 "github.com/docker/docker/pkg/stringid" 16 "github.com/docker/go-units" 17 ) 18 19 const ( 20 defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" 21 22 namesHeader = "NAMES" 23 commandHeader = "COMMAND" 24 runningForHeader = "CREATED" 25 mountsHeader = "MOUNTS" 26 localVolumes = "LOCAL VOLUMES" 27 networksHeader = "NETWORKS" 28 ) 29 30 // NewContainerFormat returns a Format for rendering using a Context 31 func NewContainerFormat(source string, quiet bool, size bool) Format { 32 switch source { 33 case TableFormatKey, "": // table formatting is the default if none is set. 34 if quiet { 35 return DefaultQuietFormat 36 } 37 format := defaultContainerTableFormat 38 if size { 39 format += `\t{{.Size}}` 40 } 41 return Format(format) 42 case RawFormatKey: 43 if quiet { 44 return `container_id: {{.ID}}` 45 } 46 format := `container_id: {{.ID}} 47 image: {{.Image}} 48 command: {{.Command}} 49 created_at: {{.CreatedAt}} 50 state: {{- pad .State 1 0}} 51 status: {{- pad .Status 1 0}} 52 names: {{.Names}} 53 labels: {{- pad .Labels 1 0}} 54 ports: {{- pad .Ports 1 0}} 55 ` 56 if size { 57 format += `size: {{.Size}}\n` 58 } 59 return Format(format) 60 default: // custom format 61 if quiet { 62 return DefaultQuietFormat 63 } 64 return Format(source) 65 } 66 } 67 68 // ContainerWrite renders the context for a list of containers 69 func ContainerWrite(ctx Context, containers []types.Container) error { 70 render := func(format func(subContext SubContext) error) error { 71 for _, container := range containers { 72 err := format(&ContainerContext{trunc: ctx.Trunc, c: container}) 73 if err != nil { 74 return err 75 } 76 } 77 return nil 78 } 79 return ctx.Write(NewContainerContext(), render) 80 } 81 82 // ContainerContext is a struct used for rendering a list of containers in a Go template. 83 type ContainerContext struct { 84 HeaderContext 85 trunc bool 86 c types.Container 87 88 // FieldsUsed is used in the pre-processing step to detect which fields are 89 // used in the template. It's currently only used to detect use of the .Size 90 // field which (if used) automatically sets the '--size' option when making 91 // the API call. 92 FieldsUsed map[string]any 93 } 94 95 // NewContainerContext creates a new context for rendering containers 96 func NewContainerContext() *ContainerContext { 97 containerCtx := ContainerContext{} 98 containerCtx.Header = SubHeaderContext{ 99 "ID": ContainerIDHeader, 100 "Names": namesHeader, 101 "Image": ImageHeader, 102 "Command": commandHeader, 103 "CreatedAt": CreatedAtHeader, 104 "RunningFor": runningForHeader, 105 "Ports": PortsHeader, 106 "State": StateHeader, 107 "Status": StatusHeader, 108 "Size": SizeHeader, 109 "Labels": LabelsHeader, 110 "Mounts": mountsHeader, 111 "LocalVolumes": localVolumes, 112 "Networks": networksHeader, 113 } 114 return &containerCtx 115 } 116 117 // MarshalJSON makes ContainerContext implement json.Marshaler 118 func (c *ContainerContext) MarshalJSON() ([]byte, error) { 119 return MarshalJSON(c) 120 } 121 122 // ID returns the container's ID as a string. Depending on the `--no-trunc` 123 // option being set, the full or truncated ID is returned. 124 func (c *ContainerContext) ID() string { 125 if c.trunc { 126 return stringid.TruncateID(c.c.ID) 127 } 128 return c.c.ID 129 } 130 131 // Names returns a comma-separated string of the container's names, with their 132 // slash (/) prefix stripped. Additional names for the container (related to the 133 // legacy `--link` feature) are omitted. 134 func (c *ContainerContext) Names() string { 135 names := StripNamePrefix(c.c.Names) 136 if c.trunc { 137 for _, name := range names { 138 if len(strings.Split(name, "/")) == 1 { 139 names = []string{name} 140 break 141 } 142 } 143 } 144 return strings.Join(names, ",") 145 } 146 147 // StripNamePrefix removes prefix from string, typically container names as returned by `ContainersList` API 148 func StripNamePrefix(ss []string) []string { 149 sss := make([]string, len(ss)) 150 for i, s := range ss { 151 sss[i] = s[1:] 152 } 153 return sss 154 } 155 156 // Image returns the container's image reference. If the trunc option is set, 157 // the image's registry digest can be included. 158 func (c *ContainerContext) Image() string { 159 if c.c.Image == "" { 160 return "<no image>" 161 } 162 if c.trunc { 163 if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { 164 return trunc 165 } 166 // truncate digest if no-trunc option was not selected 167 ref, err := reference.ParseNormalizedNamed(c.c.Image) 168 if err == nil { 169 if nt, ok := ref.(reference.NamedTagged); ok { 170 // case for when a tag is provided 171 if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { 172 return reference.FamiliarString(namedTagged) 173 } 174 } else { 175 // case for when a tag is not provided 176 named := reference.TrimNamed(ref) 177 return reference.FamiliarString(named) 178 } 179 } 180 } 181 182 return c.c.Image 183 } 184 185 // Command returns's the container's command. If the trunc option is set, the 186 // returned command is truncated (ellipsized). 187 func (c *ContainerContext) Command() string { 188 command := c.c.Command 189 if c.trunc { 190 command = Ellipsis(command, 20) 191 } 192 return strconv.Quote(command) 193 } 194 195 // CreatedAt returns the "Created" date/time of the container as a unix timestamp. 196 func (c *ContainerContext) CreatedAt() string { 197 return time.Unix(c.c.Created, 0).String() 198 } 199 200 // RunningFor returns a human-readable representation of the duration for which 201 // the container has been running. 202 // 203 // Note that this duration is calculated on the client, and as such is influenced 204 // by clock skew between the client and the daemon. 205 func (c *ContainerContext) RunningFor() string { 206 createdAt := time.Unix(c.c.Created, 0) 207 return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" 208 } 209 210 // Ports returns a comma-separated string representing open ports of the container 211 // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp" 212 // it's used by command 'docker ps' 213 // Both published and exposed ports are included. 214 func (c *ContainerContext) Ports() string { 215 return DisplayablePorts(c.c.Ports) 216 } 217 218 // State returns the container's current state (e.g. "running" or "paused") 219 func (c *ContainerContext) State() string { 220 return c.c.State 221 } 222 223 // Status returns the container's status in a human readable form (for example, 224 // "Up 24 hours" or "Exited (0) 8 days ago") 225 func (c *ContainerContext) Status() string { 226 return c.c.Status 227 } 228 229 // Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)") 230 func (c *ContainerContext) Size() string { 231 if c.FieldsUsed == nil { 232 c.FieldsUsed = map[string]any{} 233 } 234 c.FieldsUsed["Size"] = struct{}{} 235 srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) 236 sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) 237 238 sf := srw 239 if c.c.SizeRootFs > 0 { 240 sf = fmt.Sprintf("%s (virtual %s)", srw, sv) 241 } 242 return sf 243 } 244 245 // Labels returns a comma-separated string of labels present on the container. 246 func (c *ContainerContext) Labels() string { 247 if c.c.Labels == nil { 248 return "" 249 } 250 251 joinLabels := make([]string, 0, len(c.c.Labels)) 252 for k, v := range c.c.Labels { 253 joinLabels = append(joinLabels, k+"="+v) 254 } 255 return strings.Join(joinLabels, ",") 256 } 257 258 // Label returns the value of the label with the given name or an empty string 259 // if the given label does not exist. 260 func (c *ContainerContext) Label(name string) string { 261 if c.c.Labels == nil { 262 return "" 263 } 264 return c.c.Labels[name] 265 } 266 267 // Mounts returns a comma-separated string of mount names present on the container. 268 // If the trunc option is set, names can be truncated (ellipsized). 269 func (c *ContainerContext) Mounts() string { 270 var name string 271 mounts := make([]string, 0, len(c.c.Mounts)) 272 for _, m := range c.c.Mounts { 273 if m.Name == "" { 274 name = m.Source 275 } else { 276 name = m.Name 277 } 278 if c.trunc { 279 name = Ellipsis(name, 15) 280 } 281 mounts = append(mounts, name) 282 } 283 return strings.Join(mounts, ",") 284 } 285 286 // LocalVolumes returns the number of volumes using the "local" volume driver. 287 func (c *ContainerContext) LocalVolumes() string { 288 count := 0 289 for _, m := range c.c.Mounts { 290 if m.Driver == "local" { 291 count++ 292 } 293 } 294 295 return strconv.Itoa(count) 296 } 297 298 // Networks returns a comma-separated string of networks that the container is 299 // attached to. 300 func (c *ContainerContext) Networks() string { 301 if c.c.NetworkSettings == nil { 302 return "" 303 } 304 305 networks := make([]string, 0, len(c.c.NetworkSettings.Networks)) 306 for k := range c.c.NetworkSettings.Networks { 307 networks = append(networks, k) 308 } 309 310 return strings.Join(networks, ",") 311 } 312 313 // DisplayablePorts returns formatted string representing open ports of container 314 // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp" 315 // it's used by command 'docker ps' 316 func DisplayablePorts(ports []types.Port) string { 317 type portGroup struct { 318 first uint16 319 last uint16 320 } 321 groupMap := make(map[string]*portGroup) 322 var result []string //nolint:prealloc 323 var hostMappings []string 324 var groupMapKeys []string 325 sort.Slice(ports, func(i, j int) bool { 326 return comparePorts(ports[i], ports[j]) 327 }) 328 329 for _, port := range ports { 330 current := port.PrivatePort 331 portKey := port.Type 332 if port.IP != "" { 333 if port.PublicPort != current { 334 hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type)) 335 continue 336 } 337 portKey = port.IP + "/" + port.Type 338 } 339 group := groupMap[portKey] 340 341 if group == nil { 342 groupMap[portKey] = &portGroup{first: current, last: current} 343 // record order that groupMap keys are created 344 groupMapKeys = append(groupMapKeys, portKey) 345 continue 346 } 347 if current == (group.last + 1) { 348 group.last = current 349 continue 350 } 351 352 result = append(result, formGroup(portKey, group.first, group.last)) 353 groupMap[portKey] = &portGroup{first: current, last: current} 354 } 355 for _, portKey := range groupMapKeys { 356 g := groupMap[portKey] 357 result = append(result, formGroup(portKey, g.first, g.last)) 358 } 359 result = append(result, hostMappings...) 360 return strings.Join(result, ", ") 361 } 362 363 func formGroup(key string, start, last uint16) string { 364 parts := strings.Split(key, "/") 365 groupType := parts[0] 366 var ip string 367 if len(parts) > 1 { 368 ip = parts[0] 369 groupType = parts[1] 370 } 371 group := strconv.Itoa(int(start)) 372 if start != last { 373 group = fmt.Sprintf("%s-%d", group, last) 374 } 375 if ip != "" { 376 group = fmt.Sprintf("%s:%s->%s", ip, group, group) 377 } 378 return group + "/" + groupType 379 } 380 381 func comparePorts(i, j types.Port) bool { 382 if i.PrivatePort != j.PrivatePort { 383 return i.PrivatePort < j.PrivatePort 384 } 385 386 if i.IP != j.IP { 387 return i.IP < j.IP 388 } 389 390 if i.PublicPort != j.PublicPort { 391 return i.PublicPort < j.PublicPort 392 } 393 394 return i.Type < j.Type 395 }