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