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