ls: format opt

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2023-11-12 02:22:17 +01:00
committed by CrazyMax
parent cb37886658
commit 20c947990c
44 changed files with 14579 additions and 58 deletions

View File

@@ -0,0 +1,183 @@
package formatter
import (
"fmt"
"sort"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
const (
defaultBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}\t{{.Description}}"
cacheIDHeader = "CACHE ID"
cacheTypeHeader = "CACHE TYPE"
parentHeader = "PARENT"
lastUsedSinceHeader = "LAST USED"
usageCountHeader = "USAGE"
inUseHeader = "IN USE"
sharedHeader = "SHARED"
)
// NewBuildCacheFormat returns a Format for rendering using a Context
func NewBuildCacheFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return DefaultQuietFormat
}
return Format(defaultBuildCacheTableFormat)
case RawFormatKey:
if quiet {
return `build_cache_id: {{.ID}}`
}
format := `build_cache_id: {{.ID}}
parent_id: {{.Parent}}
build_cache_type: {{.CacheType}}
description: {{.Description}}
created_at: {{.CreatedAt}}
created_since: {{.CreatedSince}}
last_used_at: {{.LastUsedAt}}
last_used_since: {{.LastUsedSince}}
usage_count: {{.UsageCount}}
in_use: {{.InUse}}
shared: {{.Shared}}
`
return Format(format)
}
return Format(source)
}
func buildCacheSort(buildCache []*types.BuildCache) {
sort.Slice(buildCache, func(i, j int) bool {
lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt
switch {
case lui == nil && luj == nil:
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
case lui == nil:
return true
case luj == nil:
return false
case lui.Equal(*luj):
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
default:
return lui.Before(*luj)
}
})
}
// BuildCacheWrite renders the context for a list of containers
func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
render := func(format func(subContext SubContext) error) error {
buildCacheSort(buildCaches)
for _, bc := range buildCaches {
err := format(&buildCacheContext{trunc: ctx.Trunc, v: bc})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(newBuildCacheContext(), render)
}
type buildCacheContext struct {
HeaderContext
trunc bool
v *types.BuildCache
}
func newBuildCacheContext() *buildCacheContext {
buildCacheCtx := buildCacheContext{}
buildCacheCtx.Header = SubHeaderContext{
"ID": cacheIDHeader,
"Parent": parentHeader,
"CacheType": cacheTypeHeader,
"Size": SizeHeader,
"CreatedSince": CreatedSinceHeader,
"LastUsedSince": lastUsedSinceHeader,
"UsageCount": usageCountHeader,
"InUse": inUseHeader,
"Shared": sharedHeader,
"Description": DescriptionHeader,
}
return &buildCacheCtx
}
func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *buildCacheContext) ID() string {
id := c.v.ID
if c.trunc {
id = stringid.TruncateID(c.v.ID)
}
if c.v.InUse {
return id + "*"
}
return id
}
func (c *buildCacheContext) Parent() string {
var parent string
if len(c.v.Parents) > 0 {
parent = strings.Join(c.v.Parents, ", ")
} else {
parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility
}
if c.trunc {
return stringid.TruncateID(parent)
}
return parent
}
func (c *buildCacheContext) CacheType() string {
return c.v.Type
}
func (c *buildCacheContext) Description() string {
return c.v.Description
}
func (c *buildCacheContext) Size() string {
return units.HumanSizeWithPrecision(float64(c.v.Size), 3)
}
func (c *buildCacheContext) CreatedAt() string {
return c.v.CreatedAt.String()
}
func (c *buildCacheContext) CreatedSince() string {
return units.HumanDuration(time.Now().UTC().Sub(c.v.CreatedAt)) + " ago"
}
func (c *buildCacheContext) LastUsedAt() string {
if c.v.LastUsedAt == nil {
return ""
}
return c.v.LastUsedAt.String()
}
func (c *buildCacheContext) LastUsedSince() string {
if c.v.LastUsedAt == nil {
return ""
}
return units.HumanDuration(time.Now().UTC().Sub(*c.v.LastUsedAt)) + " ago"
}
func (c *buildCacheContext) UsageCount() string {
return fmt.Sprintf("%d", c.v.UsageCount)
}
func (c *buildCacheContext) InUse() string {
return fmt.Sprintf("%t", c.v.InUse)
}
func (c *buildCacheContext) Shared() string {
return fmt.Sprintf("%t", c.v.Shared)
}

View File

@@ -0,0 +1,392 @@
package formatter
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
const (
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
namesHeader = "NAMES"
commandHeader = "COMMAND"
runningForHeader = "CREATED"
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
)
// NewContainerFormat returns a Format for rendering using a Context
func NewContainerFormat(source string, quiet bool, size bool) Format {
switch source {
case TableFormatKey, "": // table formatting is the default if none is set.
if quiet {
return DefaultQuietFormat
}
format := defaultContainerTableFormat
if size {
format += `\t{{.Size}}`
}
return Format(format)
case RawFormatKey:
if quiet {
return `container_id: {{.ID}}`
}
format := `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
state: {{- pad .State 1 0}}
status: {{- pad .Status 1 0}}
names: {{.Names}}
labels: {{- pad .Labels 1 0}}
ports: {{- pad .Ports 1 0}}
`
if size {
format += `size: {{.Size}}\n`
}
return Format(format)
default: // custom format
if quiet {
return DefaultQuietFormat
}
return Format(source)
}
}
// ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx Context, containers []types.Container) error {
render := func(format func(subContext SubContext) error) error {
for _, container := range containers {
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(NewContainerContext(), render)
}
// ContainerContext is a struct used for rendering a list of containers in a Go template.
type ContainerContext struct {
HeaderContext
trunc bool
c types.Container
// FieldsUsed is used in the pre-processing step to detect which fields are
// used in the template. It's currently only used to detect use of the .Size
// field which (if used) automatically sets the '--size' option when making
// the API call.
FieldsUsed map[string]interface{}
}
// NewContainerContext creates a new context for rendering containers
func NewContainerContext() *ContainerContext {
containerCtx := ContainerContext{}
containerCtx.Header = SubHeaderContext{
"ID": ContainerIDHeader,
"Names": namesHeader,
"Image": ImageHeader,
"Command": commandHeader,
"CreatedAt": CreatedAtHeader,
"RunningFor": runningForHeader,
"Ports": PortsHeader,
"State": StateHeader,
"Status": StatusHeader,
"Size": SizeHeader,
"Labels": LabelsHeader,
"Mounts": mountsHeader,
"LocalVolumes": localVolumes,
"Networks": networksHeader,
}
return &containerCtx
}
// MarshalJSON makes ContainerContext implement json.Marshaler
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
// ID returns the container's ID as a string. Depending on the `--no-trunc`
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
}
return c.c.ID
}
// Names returns a comma-separated string of the container's names, with their
// slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted.
func (c *ContainerContext) Names() string {
names := StripNamePrefix(c.c.Names)
if c.trunc {
for _, name := range names {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
}
}
}
return strings.Join(names, ",")
}
// StripNamePrefix removes prefix from string, typically container names as returned by `ContainersList` API
func StripNamePrefix(ss []string) []string {
sss := make([]string, len(ss))
for i, s := range ss {
sss[i] = s[1:]
}
return sss
}
// Image returns the container's image reference. If the trunc option is set,
// the image's registry digest can be included.
func (c *ContainerContext) Image() string {
if c.c.Image == "" {
return "<no image>"
}
if c.trunc {
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
return trunc
}
// truncate digest if no-trunc option was not selected
ref, err := reference.ParseNormalizedNamed(c.c.Image)
if err == nil {
if nt, ok := ref.(reference.NamedTagged); ok {
// case for when a tag is provided
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
return reference.FamiliarString(namedTagged)
}
} else {
// case for when a tag is not provided
named := reference.TrimNamed(ref)
return reference.FamiliarString(named)
}
}
}
return c.c.Image
}
// Command returns's the container's command. If the trunc option is set, the
// returned command is truncated (ellipsized).
func (c *ContainerContext) Command() string {
command := c.c.Command
if c.trunc {
command = Ellipsis(command, 20)
}
return strconv.Quote(command)
}
// CreatedAt returns the "Created" date/time of the container as a unix timestamp.
func (c *ContainerContext) CreatedAt() string {
return time.Unix(c.c.Created, 0).String()
}
// RunningFor returns a human-readable representation of the duration for which
// the container has been running.
//
// Note that this duration is calculated on the client, and as such is influenced
// by clock skew between the client and the daemon.
func (c *ContainerContext) RunningFor() string {
createdAt := time.Unix(c.c.Created, 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
// Ports returns a comma-separated string representing open ports of the container
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
// it's used by command 'docker ps'
// Both published and exposed ports are included.
func (c *ContainerContext) Ports() string {
return DisplayablePorts(c.c.Ports)
}
// State returns the container's current state (e.g. "running" or "paused")
func (c *ContainerContext) State() string {
return c.c.State
}
// Status returns the container's status in a human readable form (for example,
// "Up 24 hours" or "Exited (0) 8 days ago")
func (c *ContainerContext) Status() string {
return c.c.Status
}
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
func (c *ContainerContext) Size() string {
if c.FieldsUsed == nil {
c.FieldsUsed = map[string]interface{}{}
}
c.FieldsUsed["Size"] = struct{}{}
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
sf := srw
if c.c.SizeRootFs > 0 {
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
}
return sf
}
// Labels returns a comma-separated string of labels present on the container.
func (c *ContainerContext) Labels() string {
if c.c.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, k+"="+v)
}
return strings.Join(joinLabels, ",")
}
// Label returns the value of the label with the given name or an empty string
// if the given label does not exist.
func (c *ContainerContext) Label(name string) string {
if c.c.Labels == nil {
return ""
}
return c.c.Labels[name]
}
// Mounts returns a comma-separated string of mount names present on the container.
// If the trunc option is set, names can be truncated (ellipsized).
func (c *ContainerContext) Mounts() string {
var name string
var mounts []string
for _, m := range c.c.Mounts {
if m.Name == "" {
name = m.Source
} else {
name = m.Name
}
if c.trunc {
name = Ellipsis(name, 15)
}
mounts = append(mounts, name)
}
return strings.Join(mounts, ",")
}
// LocalVolumes returns the number of volumes using the "local" volume driver.
func (c *ContainerContext) LocalVolumes() string {
count := 0
for _, m := range c.c.Mounts {
if m.Driver == "local" {
count++
}
}
return fmt.Sprintf("%d", count)
}
// Networks returns a comma-separated string of networks that the container is
// attached to.
func (c *ContainerContext) Networks() string {
if c.c.NetworkSettings == nil {
return ""
}
networks := []string{}
for k := range c.c.NetworkSettings.Networks {
networks = append(networks, k)
}
return strings.Join(networks, ",")
}
// DisplayablePorts returns formatted string representing open ports of container
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
// it's used by command 'docker ps'
func DisplayablePorts(ports []types.Port) string {
type portGroup struct {
first uint16
last uint16
}
groupMap := make(map[string]*portGroup)
var result []string
var hostMappings []string
var groupMapKeys []string
sort.Slice(ports, func(i, j int) bool {
return comparePorts(ports[i], ports[j])
})
for _, port := range ports {
current := port.PrivatePort
portKey := port.Type
if port.IP != "" {
if port.PublicPort != current {
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
continue
}
portKey = fmt.Sprintf("%s/%s", port.IP, port.Type)
}
group := groupMap[portKey]
if group == nil {
groupMap[portKey] = &portGroup{first: current, last: current}
// record order that groupMap keys are created
groupMapKeys = append(groupMapKeys, portKey)
continue
}
if current == (group.last + 1) {
group.last = current
continue
}
result = append(result, formGroup(portKey, group.first, group.last))
groupMap[portKey] = &portGroup{first: current, last: current}
}
for _, portKey := range groupMapKeys {
g := groupMap[portKey]
result = append(result, formGroup(portKey, g.first, g.last))
}
result = append(result, hostMappings...)
return strings.Join(result, ", ")
}
func formGroup(key string, start, last uint16) string {
parts := strings.Split(key, "/")
groupType := parts[0]
var ip string
if len(parts) > 1 {
ip = parts[0]
groupType = parts[1]
}
group := strconv.Itoa(int(start))
if start != last {
group = fmt.Sprintf("%s-%d", group, last)
}
if ip != "" {
group = fmt.Sprintf("%s:%s->%s", ip, group, group)
}
return fmt.Sprintf("%s/%s", group, groupType)
}
func comparePorts(i, j types.Port) bool {
if i.PrivatePort != j.PrivatePort {
return i.PrivatePort < j.PrivatePort
}
if i.IP != j.IP {
return i.IP < j.IP
}
if i.PublicPort != j.PublicPort {
return i.PublicPort < j.PublicPort
}
return i.Type < j.Type
}

View File

@@ -0,0 +1,86 @@
package formatter
const (
// ClientContextTableFormat is the default client context format.
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
dockerEndpointHeader = "DOCKER ENDPOINT"
quietContextFormat = "{{.Name}}"
maxErrLength = 45
)
// NewClientContextFormat returns a Format for rendering using a Context
func NewClientContextFormat(source string, quiet bool) Format {
if quiet {
return quietContextFormat
}
if source == TableFormatKey {
return ClientContextTableFormat
}
return Format(source)
}
// ClientContext is a context for display
type ClientContext struct {
Name string
Description string
DockerEndpoint string
Current bool
Error string
}
// ClientContextWrite writes formatted contexts using the Context
func ClientContextWrite(ctx Context, contexts []*ClientContext) error {
render := func(format func(subContext SubContext) error) error {
for _, context := range contexts {
if err := format(&clientContextContext{c: context}); err != nil {
return err
}
}
return nil
}
return ctx.Write(newClientContextContext(), render)
}
type clientContextContext struct {
HeaderContext
c *ClientContext
}
func newClientContextContext() *clientContextContext {
ctx := clientContextContext{}
ctx.Header = SubHeaderContext{
"Name": NameHeader,
"Description": DescriptionHeader,
"DockerEndpoint": dockerEndpointHeader,
"Error": ErrorHeader,
}
return &ctx
}
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *clientContextContext) Current() bool {
return c.c.Current
}
func (c *clientContextContext) Name() string {
return c.c.Name
}
func (c *clientContextContext) Description() string {
return c.c.Description
}
func (c *clientContextContext) DockerEndpoint() string {
return c.c.DockerEndpoint
}
// Error returns the truncated error (if any) that occurred when loading the context.
func (c *clientContextContext) Error() string {
// TODO(thaJeztah) add "--no-trunc" option to context ls and set default to 30 cols to match "docker service ps"
return Ellipsis(c.c.Error, maxErrLength)
}

View File

@@ -0,0 +1,48 @@
package formatter
import "strings"
// Common header constants
const (
CreatedSinceHeader = "CREATED"
CreatedAtHeader = "CREATED AT"
SizeHeader = "SIZE"
LabelsHeader = "LABELS"
NameHeader = "NAME"
DescriptionHeader = "DESCRIPTION"
DriverHeader = "DRIVER"
ScopeHeader = "SCOPE"
StateHeader = "STATE"
StatusHeader = "STATUS"
PortsHeader = "PORTS"
ImageHeader = "IMAGE"
ErrorHeader = "ERROR"
ContainerIDHeader = "CONTAINER ID"
)
// SubContext defines what Context implementation should provide
type SubContext interface {
FullHeader() interface{}
}
// SubHeaderContext is a map destined to formatter header (table format)
type SubHeaderContext map[string]string
// Label returns the header label for the specified string
func (c SubHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
// HeaderContext provides the subContext interface for managing headers
type HeaderContext struct {
Header interface{}
}
// FullHeader returns the header as an interface
func (c *HeaderContext) FullHeader() interface{} {
return c.Header
}

View File

@@ -0,0 +1,477 @@
package formatter
import (
"bytes"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/volume"
units "github.com/docker/go-units"
)
const (
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}"
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
typeHeader = "TYPE"
totalHeader = "TOTAL"
activeHeader = "ACTIVE"
reclaimableHeader = "RECLAIMABLE"
containersHeader = "CONTAINERS"
sharedSizeHeader = "SHARED SIZE"
uniqueSizeHeader = "UNIQUE SIZE"
)
// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
type DiskUsageContext struct {
Context
Verbose bool
LayersSize int64
Images []*image.Summary
Containers []*types.Container
Volumes []*volume.Volume
BuildCache []*types.BuildCache
BuilderSize int64
}
func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
ctx.buffer = bytes.NewBufferString("")
ctx.header = ""
ctx.Format = Format(format)
ctx.preFormat()
return ctx.parseFormat()
}
// NewDiskUsageFormat returns a format for rendering an DiskUsageContext
func NewDiskUsageFormat(source string, verbose bool) Format {
switch {
case verbose && source == RawFormatKey:
format := `{{range .Images}}type: Image
` + NewImageFormat(source, false, true) + `
{{end -}}
{{range .Containers}}type: Container
` + NewContainerFormat(source, false, true) + `
{{end -}}
{{range .Volumes}}type: Volume
` + NewVolumeFormat(source, false) + `
{{end -}}
{{range .BuildCache}}type: Build Cache
` + NewBuildCacheFormat(source, false) + `
{{end -}}`
return format
case !verbose && source == TableFormatKey:
return Format(defaultDiskUsageTableFormat)
case !verbose && source == RawFormatKey:
format := `type: {{.Type}}
total: {{.TotalCount}}
active: {{.Active}}
size: {{.Size}}
reclaimable: {{.Reclaimable}}
`
return Format(format)
default:
return Format(source)
}
}
func (ctx *DiskUsageContext) Write() (err error) {
if ctx.Verbose {
return ctx.verboseWrite()
}
ctx.buffer = bytes.NewBufferString("")
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
totalSize: ctx.LayersSize,
images: ctx.Images,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
containers: ctx.Containers,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
volumes: ctx.Volumes,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageBuilderContext{
builderSize: ctx.BuilderSize,
buildCache: ctx.BuildCache,
})
if err != nil {
return err
}
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
diskUsageContainersCtx.Header = SubHeaderContext{
"Type": typeHeader,
"TotalCount": totalHeader,
"Active": activeHeader,
"Size": SizeHeader,
"Reclaimable": reclaimableHeader,
}
ctx.postFormat(tmpl, &diskUsageContainersCtx)
return err
}
type diskUsageContext struct {
Images []*imageContext
Containers []*ContainerContext
Volumes []*volumeContext
BuildCache []*buildCacheContext
}
func (ctx *DiskUsageContext) verboseWrite() error {
duc := &diskUsageContext{
Images: make([]*imageContext, 0, len(ctx.Images)),
Containers: make([]*ContainerContext, 0, len(ctx.Containers)),
Volumes: make([]*volumeContext, 0, len(ctx.Volumes)),
BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
}
trunc := ctx.Format.IsTable()
// First images
for _, i := range ctx.Images {
repo := "<none>"
tag := "<none>"
if len(i.RepoTags) > 0 && !isDangling(*i) {
// Only show the first tag
ref, err := reference.ParseNormalizedNamed(i.RepoTags[0])
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
repo = reference.FamiliarName(ref)
tag = nt.Tag()
}
}
duc.Images = append(duc.Images, &imageContext{
repo: repo,
tag: tag,
trunc: trunc,
i: *i,
})
}
// Now containers
for _, c := range ctx.Containers {
// Don't display the virtual size
c.SizeRootFs = 0
duc.Containers = append(duc.Containers, &ContainerContext{trunc: trunc, c: *c})
}
// And volumes
for _, v := range ctx.Volumes {
duc.Volumes = append(duc.Volumes, &volumeContext{v: *v})
}
// And build cache
buildCacheSort(ctx.BuildCache)
for _, v := range ctx.BuildCache {
duc.BuildCache = append(duc.BuildCache, &buildCacheContext{v: v, trunc: trunc})
}
if ctx.Format == TableFormatKey {
return ctx.verboseWriteTable(duc)
}
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
return err
}
return tmpl.Execute(ctx.Output, duc)
}
func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error {
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
if err != nil {
return err
}
ctx.Output.Write([]byte("Images space usage:\n\n"))
for _, img := range duc.Images {
if err := ctx.contextFormat(tmpl, img); err != nil {
return err
}
}
ctx.postFormat(tmpl, newImageContext())
tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
if err != nil {
return err
}
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
for _, c := range duc.Containers {
if err := ctx.contextFormat(tmpl, c); err != nil {
return err
}
}
ctx.postFormat(tmpl, NewContainerContext())
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
if err != nil {
return err
}
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
for _, v := range duc.Volumes {
if err := ctx.contextFormat(tmpl, v); err != nil {
return err
}
}
ctx.postFormat(tmpl, newVolumeContext())
tmpl, err = ctx.startSubsection(defaultDiskUsageBuildCacheTableFormat)
if err != nil {
return err
}
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
for _, v := range duc.BuildCache {
if err := ctx.contextFormat(tmpl, v); err != nil {
return err
}
}
ctx.postFormat(tmpl, newBuildCacheContext())
return nil
}
type diskUsageImagesContext struct {
HeaderContext
totalSize int64
images []*image.Summary
}
func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *diskUsageImagesContext) Type() string {
return "Images"
}
func (c *diskUsageImagesContext) TotalCount() string {
return strconv.Itoa(len(c.images))
}
func (c *diskUsageImagesContext) Active() string {
used := 0
for _, i := range c.images {
if i.Containers > 0 {
used++
}
}
return strconv.Itoa(used)
}
func (c *diskUsageImagesContext) Size() string {
return units.HumanSize(float64(c.totalSize))
}
func (c *diskUsageImagesContext) Reclaimable() string {
var used int64
for _, i := range c.images {
if i.Containers != 0 {
if i.Size == -1 || i.SharedSize == -1 {
continue
}
used += i.Size - i.SharedSize
}
}
reclaimable := c.totalSize - used
if c.totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
}
return units.HumanSize(float64(reclaimable))
}
type diskUsageContainersContext struct {
HeaderContext
containers []*types.Container
}
func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *diskUsageContainersContext) Type() string {
return "Containers"
}
func (c *diskUsageContainersContext) TotalCount() string {
return strconv.Itoa(len(c.containers))
}
func (c *diskUsageContainersContext) isActive(container types.Container) bool {
return strings.Contains(container.State, "running") ||
strings.Contains(container.State, "paused") ||
strings.Contains(container.State, "restarting")
}
func (c *diskUsageContainersContext) Active() string {
used := 0
for _, container := range c.containers {
if c.isActive(*container) {
used++
}
}
return strconv.Itoa(used)
}
func (c *diskUsageContainersContext) Size() string {
var size int64
for _, container := range c.containers {
size += container.SizeRw
}
return units.HumanSize(float64(size))
}
func (c *diskUsageContainersContext) Reclaimable() string {
var reclaimable int64
var totalSize int64
for _, container := range c.containers {
if !c.isActive(*container) {
reclaimable += container.SizeRw
}
totalSize += container.SizeRw
}
if totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
}
return units.HumanSize(float64(reclaimable))
}
type diskUsageVolumesContext struct {
HeaderContext
volumes []*volume.Volume
}
func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *diskUsageVolumesContext) Type() string {
return "Local Volumes"
}
func (c *diskUsageVolumesContext) TotalCount() string {
return strconv.Itoa(len(c.volumes))
}
func (c *diskUsageVolumesContext) Active() string {
used := 0
for _, v := range c.volumes {
if v.UsageData.RefCount > 0 {
used++
}
}
return strconv.Itoa(used)
}
func (c *diskUsageVolumesContext) Size() string {
var size int64
for _, v := range c.volumes {
if v.UsageData.Size != -1 {
size += v.UsageData.Size
}
}
return units.HumanSize(float64(size))
}
func (c *diskUsageVolumesContext) Reclaimable() string {
var reclaimable int64
var totalSize int64
for _, v := range c.volumes {
if v.UsageData.Size != -1 {
if v.UsageData.RefCount == 0 {
reclaimable += v.UsageData.Size
}
totalSize += v.UsageData.Size
}
}
if totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
}
return units.HumanSize(float64(reclaimable))
}
type diskUsageBuilderContext struct {
HeaderContext
builderSize int64
buildCache []*types.BuildCache
}
func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *diskUsageBuilderContext) Type() string {
return "Build Cache"
}
func (c *diskUsageBuilderContext) TotalCount() string {
return strconv.Itoa(len(c.buildCache))
}
func (c *diskUsageBuilderContext) Active() string {
numActive := 0
for _, bc := range c.buildCache {
if bc.InUse {
numActive++
}
}
return strconv.Itoa(numActive)
}
func (c *diskUsageBuilderContext) Size() string {
return units.HumanSize(float64(c.builderSize))
}
func (c *diskUsageBuilderContext) Reclaimable() string {
var inUseBytes int64
for _, bc := range c.buildCache {
if bc.InUse && !bc.Shared {
inUseBytes += bc.Size
}
}
return units.HumanSize(float64(c.builderSize - inUseBytes))
}

View File

@@ -0,0 +1,61 @@
package formatter
import (
"unicode/utf8"
"golang.org/x/text/width"
)
// charWidth returns the number of horizontal positions a character occupies,
// and is used to account for wide characters when displaying strings.
//
// In a broad sense, wide characters include East Asian Wide, East Asian Full-width,
// (when not in East Asian context) see http://unicode.org/reports/tr11/.
func charWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
default:
return 1
}
}
// Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…).
// For maxDisplayWidth of 1 and lower, no ellipsis is appended.
// For maxDisplayWidth of 1, first char of string will return even if its width > 1.
func Ellipsis(s string, maxDisplayWidth int) string {
if maxDisplayWidth <= 0 {
return ""
}
rs := []rune(s)
if maxDisplayWidth == 1 {
return string(rs[0])
}
byteLen := len(s)
if byteLen == utf8.RuneCountInString(s) {
if byteLen <= maxDisplayWidth {
return s
}
return string(rs[:maxDisplayWidth-1]) + "…"
}
var (
display []int
displayWidth int
)
for _, r := range rs {
cw := charWidth(r)
displayWidth += cw
display = append(display, displayWidth)
}
if displayWidth <= maxDisplayWidth {
return s
}
for i := range display {
if display[i] <= maxDisplayWidth-1 && display[i+1] > maxDisplayWidth-1 {
return string(rs[:i+1]) + "…"
}
}
return s
}

View File

@@ -0,0 +1,128 @@
package formatter
import (
"bytes"
"io"
"strings"
"text/template"
"github.com/docker/cli/cli/command/formatter/tabwriter"
"github.com/docker/cli/templates"
"github.com/pkg/errors"
)
// Format keys used to specify certain kinds of output formats
const (
TableFormatKey = "table"
RawFormatKey = "raw"
PrettyFormatKey = "pretty"
JSONFormatKey = "json"
DefaultQuietFormat = "{{.ID}}"
JSONFormat = "{{json .}}"
)
// Format is the format string rendered using the Context
type Format string
// IsTable returns true if the format is a table-type format
func (f Format) IsTable() bool {
return strings.HasPrefix(string(f), TableFormatKey)
}
// IsJSON returns true if the format is the json format
func (f Format) IsJSON() bool {
return string(f) == JSONFormatKey
}
// Contains returns true if the format contains the substring
func (f Format) Contains(sub string) bool {
return strings.Contains(string(f), sub)
}
// Context contains information required by the formatter to print the output as desired.
type Context struct {
// Output is the output stream to which the formatted string is written.
Output io.Writer
// Format is used to choose raw, table or custom format for the output.
Format Format
// Trunc when set to true will truncate the output of certain fields such as Container ID.
Trunc bool
// internal element
finalFormat string
header interface{}
buffer *bytes.Buffer
}
func (c *Context) preFormat() {
c.finalFormat = string(c.Format)
// TODO: handle this in the Format type
switch {
case c.Format.IsTable():
c.finalFormat = c.finalFormat[len(TableFormatKey):]
case c.Format.IsJSON():
c.finalFormat = JSONFormat
}
c.finalFormat = strings.Trim(c.finalFormat, " ")
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
c.finalFormat = r.Replace(c.finalFormat)
}
func (c *Context) parseFormat() (*template.Template, error) {
tmpl, err := templates.Parse(c.finalFormat)
if err != nil {
return tmpl, errors.Wrap(err, "template parsing error")
}
return tmpl, err
}
func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {
if c.Format.IsTable() {
t := tabwriter.NewWriter(c.Output, 10, 1, 3, ' ', 0)
buffer := bytes.NewBufferString("")
tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader())
buffer.WriteTo(t)
t.Write([]byte("\n"))
c.buffer.WriteTo(t)
t.Flush()
} else {
c.buffer.WriteTo(c.Output)
}
}
func (c *Context) contextFormat(tmpl *template.Template, subContext SubContext) error {
if err := tmpl.Execute(c.buffer, subContext); err != nil {
return errors.Wrap(err, "template parsing error")
}
if c.Format.IsTable() && c.header != nil {
c.header = subContext.FullHeader()
}
c.buffer.WriteString("\n")
return nil
}
// SubFormat is a function type accepted by Write()
type SubFormat func(func(SubContext) error) error
// Write the template to the buffer using this Context
func (c *Context) Write(sub SubContext, f SubFormat) error {
c.buffer = bytes.NewBufferString("")
c.preFormat()
tmpl, err := c.parseFormat()
if err != nil {
return err
}
subFormat := func(subContext SubContext) error {
return c.contextFormat(tmpl, subContext)
}
if err := f(subFormat); err != nil {
return err
}
c.postFormat(tmpl, sub)
return nil
}

View File

@@ -0,0 +1,283 @@
package formatter
import (
"fmt"
"time"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
const (
defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t{{.Size}}"
defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t{{.Size}}"
imageIDHeader = "IMAGE ID"
repositoryHeader = "REPOSITORY"
tagHeader = "TAG"
digestHeader = "DIGEST"
)
// ImageContext contains image specific information required by the formatter, encapsulate a Context struct.
type ImageContext struct {
Context
Digest bool
}
func isDangling(image image.Summary) bool {
if len(image.RepoTags) == 0 && len(image.RepoDigests) == 0 {
return true
}
return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
}
// NewImageFormat returns a format for rendering an ImageContext
func NewImageFormat(source string, quiet bool, digest bool) Format {
switch source {
case TableFormatKey:
switch {
case quiet:
return DefaultQuietFormat
case digest:
return defaultImageTableFormatWithDigest
default:
return defaultImageTableFormat
}
case RawFormatKey:
switch {
case quiet:
return `image_id: {{.ID}}`
case digest:
return `repository: {{ .Repository }}
tag: {{.Tag}}
digest: {{.Digest}}
image_id: {{.ID}}
created_at: {{.CreatedAt}}
virtual_size: {{.Size}}
`
default:
return `repository: {{ .Repository }}
tag: {{.Tag}}
image_id: {{.ID}}
created_at: {{.CreatedAt}}
virtual_size: {{.Size}}
`
}
}
format := Format(source)
if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
format += "\t{{.Digest}}"
}
return format
}
// ImageWrite writes the formatter images using the ImageContext
func ImageWrite(ctx ImageContext, images []image.Summary) error {
render := func(format func(subContext SubContext) error) error {
return imageFormat(ctx, images, format)
}
return ctx.Write(newImageContext(), render)
}
// needDigest determines whether the image digest should be ignored or not when writing image context
func needDigest(ctx ImageContext) bool {
return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
}
func imageFormat(ctx ImageContext, images []image.Summary, format func(subContext SubContext) error) error {
for _, image := range images {
formatted := []*imageContext{}
if isDangling(image) {
formatted = append(formatted, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: "<none>",
tag: "<none>",
digest: "<none>",
})
} else {
formatted = imageFormatTaggedAndDigest(ctx, image)
}
for _, imageCtx := range formatted {
if err := format(imageCtx); err != nil {
return err
}
}
}
return nil
}
func imageFormatTaggedAndDigest(ctx ImageContext, image image.Summary) []*imageContext {
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
images := []*imageContext{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
addImage := func(repo, tag, digest string) {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: digest,
})
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !needDigest(ctx) {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
addImage(repo, tag, "<none>")
continue
}
// Display the digests for each tag
for _, dgst := range digests {
addImage(repo, tag, dgst)
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
addImage(repo, "<none>", dgst)
}
} else {
addImage(repo, "<none>", "")
}
}
return images
}
type imageContext struct {
HeaderContext
trunc bool
i image.Summary
repo string
tag string
digest string
}
func newImageContext() *imageContext {
imageCtx := imageContext{}
imageCtx.Header = SubHeaderContext{
"ID": imageIDHeader,
"Repository": repositoryHeader,
"Tag": tagHeader,
"Digest": digestHeader,
"CreatedSince": CreatedSinceHeader,
"CreatedAt": CreatedAtHeader,
"Size": SizeHeader,
"Containers": containersHeader,
"VirtualSize": SizeHeader, // Deprecated: VirtualSize is deprecated, and equivalent to Size.
"SharedSize": sharedSizeHeader,
"UniqueSize": uniqueSizeHeader,
}
return &imageCtx
}
func (c *imageContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *imageContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.i.ID)
}
return c.i.ID
}
func (c *imageContext) Repository() string {
return c.repo
}
func (c *imageContext) Tag() string {
return c.tag
}
func (c *imageContext) Digest() string {
return c.digest
}
func (c *imageContext) CreatedSince() string {
createdAt := time.Unix(c.i.Created, 0)
if createdAt.IsZero() {
return ""
}
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *imageContext) CreatedAt() string {
return time.Unix(c.i.Created, 0).String()
}
func (c *imageContext) Size() string {
return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
}
func (c *imageContext) Containers() string {
if c.i.Containers == -1 {
return "N/A"
}
return fmt.Sprintf("%d", c.i.Containers)
}
// VirtualSize shows the virtual size of the image and all of its parent
// images. Starting with docker 1.10, images are self-contained, and
// the VirtualSize is identical to Size.
//
// Deprecated: VirtualSize is deprecated, and equivalent to [imageContext.Size].
func (c *imageContext) VirtualSize() string {
return units.HumanSize(float64(c.i.Size))
}
func (c *imageContext) SharedSize() string {
if c.i.SharedSize == -1 {
return "N/A"
}
return units.HumanSize(float64(c.i.SharedSize))
}
func (c *imageContext) UniqueSize() string {
if c.i.Size == -1 || c.i.SharedSize == -1 {
return "N/A"
}
return units.HumanSize(float64(c.i.Size - c.i.SharedSize))
}

View File

@@ -0,0 +1,68 @@
package formatter
import (
"encoding/json"
"reflect"
"unicode"
"github.com/pkg/errors"
)
// MarshalJSON marshals x into json
// It differs a bit from encoding/json MarshalJSON function for formatter
func MarshalJSON(x interface{}) ([]byte, error) {
m, err := marshalMap(x)
if err != nil {
return nil, err
}
return json.Marshal(m)
}
// marshalMap marshals x to map[string]interface{}
func marshalMap(x interface{}) (map[string]interface{}, error) {
val := reflect.ValueOf(x)
if val.Kind() != reflect.Ptr {
return nil, errors.Errorf("expected a pointer to a struct, got %v", val.Kind())
}
if val.IsNil() {
return nil, errors.Errorf("expected a pointer to a struct, got nil pointer")
}
valElem := val.Elem()
if valElem.Kind() != reflect.Struct {
return nil, errors.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind())
}
typ := val.Type()
m := make(map[string]interface{})
for i := 0; i < val.NumMethod(); i++ {
k, v, err := marshalForMethod(typ.Method(i), val.Method(i))
if err != nil {
return nil, err
}
if k != "" {
m[k] = v
}
}
return m, nil
}
var unmarshallableNames = map[string]struct{}{"FullHeader": {}}
// marshalForMethod returns the map key and the map value for marshalling the method.
// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()")
func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) {
if val.Kind() != reflect.Func {
return "", nil, errors.Errorf("expected func, got %v", val.Kind())
}
name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut()
_, blackListed := unmarshallableNames[name]
// FIXME: In text/template, (numOut == 2) is marshallable,
// if the type of the second param is error.
marshallable := unicode.IsUpper(rune(name[0])) && !blackListed &&
numIn == 0 && numOut == 1
if !marshallable {
return "", nil, nil
}
result := val.Call(make([]reflect.Value, numIn))
intf := result[0].Interface()
return name, intf, nil
}

View File

@@ -0,0 +1,604 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package tabwriter implements a write filter (tabwriter.Writer) that
// translates tabbed columns in input into properly aligned text.
//
// The package is using the Elastic Tabstops algorithm described at
// http://nickgravgaard.com/elastictabstops/index.html.
//
// The text/tabwriter package is frozen and is not accepting new features.
// based on https://github.com/golang/go/blob/master/src/text/tabwriter/tabwriter.go Last modified 690ac40 on 31 Jan
//nolint:gocyclo,nakedret,revive,stylecheck,unused // ignore linting errors, so that we can stick close to upstream
package tabwriter
import (
"io"
"github.com/mattn/go-runewidth"
)
// ----------------------------------------------------------------------------
// Filter implementation
// A cell represents a segment of text terminated by tabs or line breaks.
// The text itself is stored in a separate buffer; cell only describes the
// segment's size in bytes, its width in runes, and whether it's an htab
// ('\t') terminated cell.
type cell struct {
size int // cell size in bytes
width int // cell width in runes
htab bool // true if the cell is terminated by an htab ('\t')
}
// A Writer is a filter that inserts padding around tab-delimited
// columns in its input to align them in the output.
//
// The Writer treats incoming bytes as UTF-8-encoded text consisting
// of cells terminated by horizontal ('\t') or vertical ('\v') tabs,
// and newline ('\n') or formfeed ('\f') characters; both newline and
// formfeed act as line breaks.
//
// Tab-terminated cells in contiguous lines constitute a column. The
// Writer inserts padding as needed to make all cells in a column have
// the same width, effectively aligning the columns. It assumes that
// all characters have the same width, except for tabs for which a
// tabwidth must be specified. Column cells must be tab-terminated, not
// tab-separated: non-tab terminated trailing text at the end of a line
// forms a cell but that cell is not part of an aligned column.
// For instance, in this example (where | stands for a horizontal tab):
//
// aaaa|bbb|d
// aa |b |dd
// a |
// aa |cccc|eee
//
// the b and c are in distinct columns (the b column is not contiguous
// all the way). The d and e are not in a column at all (there's no
// terminating tab, nor would the column be contiguous).
//
// The Writer assumes that all Unicode code points have the same width;
// this may not be true in some fonts or if the string contains combining
// characters.
//
// If DiscardEmptyColumns is set, empty columns that are terminated
// entirely by vertical (or "soft") tabs are discarded. Columns
// terminated by horizontal (or "hard") tabs are not affected by
// this flag.
//
// If a Writer is configured to filter HTML, HTML tags and entities
// are passed through. The widths of tags and entities are
// assumed to be zero (tags) and one (entities) for formatting purposes.
//
// A segment of text may be escaped by bracketing it with Escape
// characters. The tabwriter passes escaped text segments through
// unchanged. In particular, it does not interpret any tabs or line
// breaks within the segment. If the StripEscape flag is set, the
// Escape characters are stripped from the output; otherwise they
// are passed through as well. For the purpose of formatting, the
// width of the escaped text is always computed excluding the Escape
// characters.
//
// The formfeed character acts like a newline but it also terminates
// all columns in the current line (effectively calling Flush). Tab-
// terminated cells in the next line start new columns. Unless found
// inside an HTML tag or inside an escaped text segment, formfeed
// characters appear as newlines in the output.
//
// The Writer must buffer input internally, because proper spacing
// of one line may depend on the cells in future lines. Clients must
// call Flush when done calling Write.
type Writer struct {
// configuration
output io.Writer
minwidth int
tabwidth int
padding int
padbytes [8]byte
flags uint
// current state
buf []byte // collected text excluding tabs or line breaks
pos int // buffer position up to which cell.width of incomplete cell has been computed
cell cell // current incomplete cell; cell.width is up to buf[pos] excluding ignored sections
endChar byte // terminating char of escaped sequence (Escape for escapes, '>', ';' for HTML tags/entities, or 0)
lines [][]cell // list of lines; each line is a list of cells
widths []int // list of column widths in runes - re-used during formatting
}
// addLine adds a new line.
// flushed is a hint indicating whether the underlying writer was just flushed.
// If so, the previous line is not likely to be a good indicator of the new line's cells.
func (b *Writer) addLine(flushed bool) {
// Grow slice instead of appending,
// as that gives us an opportunity
// to re-use an existing []cell.
if n := len(b.lines) + 1; n <= cap(b.lines) {
b.lines = b.lines[:n]
b.lines[n-1] = b.lines[n-1][:0]
} else {
b.lines = append(b.lines, nil)
}
if !flushed {
// The previous line is probably a good indicator
// of how many cells the current line will have.
// If the current line's capacity is smaller than that,
// abandon it and make a new one.
if n := len(b.lines); n >= 2 {
if prev := len(b.lines[n-2]); prev > cap(b.lines[n-1]) {
b.lines[n-1] = make([]cell, 0, prev)
}
}
}
}
// Reset the current state.
func (b *Writer) reset() {
b.buf = b.buf[:0]
b.pos = 0
b.cell = cell{}
b.endChar = 0
b.lines = b.lines[0:0]
b.widths = b.widths[0:0]
b.addLine(true)
}
// Internal representation (current state):
//
// - all text written is appended to buf; tabs and line breaks are stripped away
// - at any given time there is a (possibly empty) incomplete cell at the end
// (the cell starts after a tab or line break)
// - cell.size is the number of bytes belonging to the cell so far
// - cell.width is text width in runes of that cell from the start of the cell to
// position pos; html tags and entities are excluded from this width if html
// filtering is enabled
// - the sizes and widths of processed text are kept in the lines list
// which contains a list of cells for each line
// - the widths list is a temporary list with current widths used during
// formatting; it is kept in Writer because it's re-used
//
// |<---------- size ---------->|
// | |
// |<- width ->|<- ignored ->| |
// | | | |
// [---processed---tab------------<tag>...</tag>...]
// ^ ^ ^
// | | |
// buf start of incomplete cell pos
// Formatting can be controlled with these flags.
const (
// Ignore html tags and treat entities (starting with '&'
// and ending in ';') as single characters (width = 1).
FilterHTML uint = 1 << iota
// Strip Escape characters bracketing escaped text segments
// instead of passing them through unchanged with the text.
StripEscape
// Force right-alignment of cell content.
// Default is left-alignment.
AlignRight
// Handle empty columns as if they were not present in
// the input in the first place.
DiscardEmptyColumns
// Always use tabs for indentation columns (i.e., padding of
// leading empty cells on the left) independent of padchar.
TabIndent
// Print a vertical bar ('|') between columns (after formatting).
// Discarded columns appear as zero-width columns ("||").
Debug
)
// A Writer must be initialized with a call to Init. The first parameter (output)
// specifies the filter output. The remaining parameters control the formatting:
//
// minwidth minimal cell width including any padding
// tabwidth width of tab characters (equivalent number of spaces)
// padding padding added to a cell before computing its width
// padchar ASCII char used for padding
// if padchar == '\t', the Writer will assume that the
// width of a '\t' in the formatted output is tabwidth,
// and cells are left-aligned independent of align_left
// (for correct-looking results, tabwidth must correspond
// to the tab width in the viewer displaying the result)
// flags formatting control
func (b *Writer) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer {
if minwidth < 0 || tabwidth < 0 || padding < 0 {
panic("negative minwidth, tabwidth, or padding")
}
b.output = output
b.minwidth = minwidth
b.tabwidth = tabwidth
b.padding = padding
for i := range b.padbytes {
b.padbytes[i] = padchar
}
if padchar == '\t' {
// tab padding enforces left-alignment
flags &^= AlignRight
}
b.flags = flags
b.reset()
return b
}
// debugging support (keep code around)
func (b *Writer) dump() {
pos := 0
for i, line := range b.lines {
print("(", i, ") ")
for _, c := range line {
print("[", string(b.buf[pos:pos+c.size]), "]")
pos += c.size
}
print("\n")
}
print("\n")
}
// local error wrapper so we can distinguish errors we want to return
// as errors from genuine panics (which we don't want to return as errors)
type osError struct {
err error
}
func (b *Writer) write0(buf []byte) {
n, err := b.output.Write(buf)
if n != len(buf) && err == nil {
err = io.ErrShortWrite
}
if err != nil {
panic(osError{err})
}
}
func (b *Writer) writeN(src []byte, n int) {
for n > len(src) {
b.write0(src)
n -= len(src)
}
b.write0(src[0:n])
}
var (
newline = []byte{'\n'}
tabs = []byte("\t\t\t\t\t\t\t\t")
)
func (b *Writer) writePadding(textw, cellw int, useTabs bool) {
if b.padbytes[0] == '\t' || useTabs {
// padding is done with tabs
if b.tabwidth == 0 {
return // tabs have no width - can't do any padding
}
// make cellw the smallest multiple of b.tabwidth
cellw = (cellw + b.tabwidth - 1) / b.tabwidth * b.tabwidth
n := cellw - textw // amount of padding
if n < 0 {
panic("internal error")
}
b.writeN(tabs, (n+b.tabwidth-1)/b.tabwidth)
return
}
// padding is done with non-tab characters
b.writeN(b.padbytes[0:], cellw-textw)
}
var vbar = []byte{'|'}
func (b *Writer) writeLines(pos0 int, line0, line1 int) (pos int) {
pos = pos0
for i := line0; i < line1; i++ {
line := b.lines[i]
// if TabIndent is set, use tabs to pad leading empty cells
useTabs := b.flags&TabIndent != 0
for j, c := range line {
if j > 0 && b.flags&Debug != 0 {
// indicate column break
b.write0(vbar)
}
if c.size == 0 {
// empty cell
if j < len(b.widths) {
b.writePadding(c.width, b.widths[j], useTabs)
}
} else {
// non-empty cell
useTabs = false
if b.flags&AlignRight == 0 { // align left
b.write0(b.buf[pos : pos+c.size])
pos += c.size
if j < len(b.widths) {
b.writePadding(c.width, b.widths[j], false)
}
} else { // align right
if j < len(b.widths) {
b.writePadding(c.width, b.widths[j], false)
}
b.write0(b.buf[pos : pos+c.size])
pos += c.size
}
}
}
if i+1 == len(b.lines) {
// last buffered line - we don't have a newline, so just write
// any outstanding buffered data
b.write0(b.buf[pos : pos+b.cell.size])
pos += b.cell.size
} else {
// not the last line - write newline
b.write0(newline)
}
}
return
}
// Format the text between line0 and line1 (excluding line1); pos
// is the buffer position corresponding to the beginning of line0.
// Returns the buffer position corresponding to the beginning of
// line1 and an error, if any.
func (b *Writer) format(pos0 int, line0, line1 int) (pos int) {
pos = pos0
column := len(b.widths)
for this := line0; this < line1; this++ {
line := b.lines[this]
if column >= len(line)-1 {
continue
}
// cell exists in this column => this line
// has more cells than the previous line
// (the last cell per line is ignored because cells are
// tab-terminated; the last cell per line describes the
// text before the newline/formfeed and does not belong
// to a column)
// print unprinted lines until beginning of block
pos = b.writeLines(pos, line0, this)
line0 = this
// column block begin
width := b.minwidth // minimal column width
discardable := true // true if all cells in this column are empty and "soft"
for ; this < line1; this++ {
line = b.lines[this]
if column >= len(line)-1 {
break
}
// cell exists in this column
c := line[column]
// update width
if w := c.width + b.padding; w > width {
width = w
}
// update discardable
if c.width > 0 || c.htab {
discardable = false
}
}
// column block end
// discard empty columns if necessary
if discardable && b.flags&DiscardEmptyColumns != 0 {
width = 0
}
// format and print all columns to the right of this column
// (we know the widths of this column and all columns to the left)
b.widths = append(b.widths, width) // push width
pos = b.format(pos, line0, this)
b.widths = b.widths[0 : len(b.widths)-1] // pop width
line0 = this
}
// print unprinted lines until end
return b.writeLines(pos, line0, line1)
}
// Append text to current cell.
func (b *Writer) append(text []byte) {
b.buf = append(b.buf, text...)
b.cell.size += len(text)
}
// Update the cell width.
func (b *Writer) updateWidth() {
b.cell.width += runewidth.StringWidth(string(b.buf[b.pos:]))
b.pos = len(b.buf)
}
// To escape a text segment, bracket it with Escape characters.
// For instance, the tab in this string "Ignore this tab: \xff\t\xff"
// does not terminate a cell and constitutes a single character of
// width one for formatting purposes.
//
// The value 0xff was chosen because it cannot appear in a valid UTF-8 sequence.
const Escape = '\xff'
// Start escaped mode.
func (b *Writer) startEscape(ch byte) {
switch ch {
case Escape:
b.endChar = Escape
case '<':
b.endChar = '>'
case '&':
b.endChar = ';'
}
}
// Terminate escaped mode. If the escaped text was an HTML tag, its width
// is assumed to be zero for formatting purposes; if it was an HTML entity,
// its width is assumed to be one. In all other cases, the width is the
// unicode width of the text.
func (b *Writer) endEscape() {
switch b.endChar {
case Escape:
b.updateWidth()
if b.flags&StripEscape == 0 {
b.cell.width -= 2 // don't count the Escape chars
}
case '>': // tag of zero width
case ';':
b.cell.width++ // entity, count as one rune
}
b.pos = len(b.buf)
b.endChar = 0
}
// Terminate the current cell by adding it to the list of cells of the
// current line. Returns the number of cells in that line.
func (b *Writer) terminateCell(htab bool) int {
b.cell.htab = htab
line := &b.lines[len(b.lines)-1]
*line = append(*line, b.cell)
b.cell = cell{}
return len(*line)
}
func (b *Writer) handlePanic(err *error, op string) {
if e := recover(); e != nil {
if op == "Flush" {
// If Flush ran into a panic, we still need to reset.
b.reset()
}
if nerr, ok := e.(osError); ok {
*err = nerr.err
return
}
panic("tabwriter: panic during " + op)
}
}
// Flush should be called after the last call to Write to ensure
// that any data buffered in the Writer is written to output. Any
// incomplete escape sequence at the end is considered
// complete for formatting purposes.
func (b *Writer) Flush() error {
return b.flush()
}
// flush is the internal version of Flush, with a named return value which we
// don't want to expose.
func (b *Writer) flush() (err error) {
defer b.handlePanic(&err, "Flush")
b.flushNoDefers()
return nil
}
// flushNoDefers is like flush, but without a deferred handlePanic call. This
// can be called from other methods which already have their own deferred
// handlePanic calls, such as Write, and avoid the extra defer work.
func (b *Writer) flushNoDefers() {
// add current cell if not empty
if b.cell.size > 0 {
if b.endChar != 0 {
// inside escape - terminate it even if incomplete
b.endEscape()
}
b.terminateCell(false)
}
// format contents of buffer
b.format(0, 0, len(b.lines))
b.reset()
}
var hbar = []byte("---\n")
// Write writes buf to the writer b.
// The only errors returned are ones encountered
// while writing to the underlying output stream.
func (b *Writer) Write(buf []byte) (n int, err error) {
defer b.handlePanic(&err, "Write")
// split text into cells
n = 0
for i, ch := range buf {
if b.endChar == 0 {
// outside escape
switch ch {
case '\t', '\v', '\n', '\f':
// end of cell
b.append(buf[n:i])
b.updateWidth()
n = i + 1 // ch consumed
ncells := b.terminateCell(ch == '\t')
if ch == '\n' || ch == '\f' {
// terminate line
b.addLine(ch == '\f')
if ch == '\f' || ncells == 1 {
// A '\f' always forces a flush. Otherwise, if the previous
// line has only one cell which does not have an impact on
// the formatting of the following lines (the last cell per
// line is ignored by format()), thus we can flush the
// Writer contents.
b.flushNoDefers()
if ch == '\f' && b.flags&Debug != 0 {
// indicate section break
b.write0(hbar)
}
}
}
case Escape:
// start of escaped sequence
b.append(buf[n:i])
b.updateWidth()
n = i
if b.flags&StripEscape != 0 {
n++ // strip Escape
}
b.startEscape(Escape)
case '<', '&':
// possibly an html tag/entity
if b.flags&FilterHTML != 0 {
// begin of tag/entity
b.append(buf[n:i])
b.updateWidth()
n = i
b.startEscape(ch)
}
}
} else {
// inside escape
if ch == b.endChar {
// end of tag/entity
j := i + 1
if ch == Escape && b.flags&StripEscape != 0 {
j = i // strip Escape
}
b.append(buf[n:j])
n = i + 1 // ch consumed
b.endEscape()
}
}
}
// append leftover text
b.append(buf[n:])
n = len(buf)
return
}
// NewWriter allocates and initializes a new tabwriter.Writer.
// The parameters are the same as for the Init function.
func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer {
return new(Writer).Init(output, minwidth, tabwidth, padding, padchar, flags)
}

View File

@@ -0,0 +1,165 @@
package formatter
import (
"fmt"
"strconv"
"strings"
"github.com/docker/docker/api/types/volume"
units "github.com/docker/go-units"
)
const (
defaultVolumeQuietFormat = "{{.Name}}"
defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
idHeader = "ID"
volumeNameHeader = "VOLUME NAME"
mountpointHeader = "MOUNTPOINT"
linksHeader = "LINKS"
groupHeader = "GROUP"
availabilityHeader = "AVAILABILITY"
statusHeader = "STATUS"
)
// NewVolumeFormat returns a format for use with a volume Context
func NewVolumeFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultVolumeQuietFormat
}
return defaultVolumeTableFormat
case RawFormatKey:
if quiet {
return `name: {{.Name}}`
}
return `name: {{.Name}}\ndriver: {{.Driver}}\n`
}
return Format(source)
}
// VolumeWrite writes formatted volumes using the Context
func VolumeWrite(ctx Context, volumes []*volume.Volume) error {
render := func(format func(subContext SubContext) error) error {
for _, vol := range volumes {
if err := format(&volumeContext{v: *vol}); err != nil {
return err
}
}
return nil
}
return ctx.Write(newVolumeContext(), render)
}
type volumeContext struct {
HeaderContext
v volume.Volume
}
func newVolumeContext() *volumeContext {
volumeCtx := volumeContext{}
volumeCtx.Header = SubHeaderContext{
"ID": idHeader,
"Name": volumeNameHeader,
"Group": groupHeader,
"Driver": DriverHeader,
"Scope": ScopeHeader,
"Availability": availabilityHeader,
"Mountpoint": mountpointHeader,
"Labels": LabelsHeader,
"Links": linksHeader,
"Size": SizeHeader,
"Status": statusHeader,
}
return &volumeCtx
}
func (c *volumeContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *volumeContext) Name() string {
return c.v.Name
}
func (c *volumeContext) Driver() string {
return c.v.Driver
}
func (c *volumeContext) Scope() string {
return c.v.Scope
}
func (c *volumeContext) Mountpoint() string {
return c.v.Mountpoint
}
func (c *volumeContext) Labels() string {
if c.v.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.v.Labels {
joinLabels = append(joinLabels, k+"="+v)
}
return strings.Join(joinLabels, ",")
}
func (c *volumeContext) Label(name string) string {
if c.v.Labels == nil {
return ""
}
return c.v.Labels[name]
}
func (c *volumeContext) Links() string {
if c.v.UsageData == nil {
return "N/A"
}
return strconv.FormatInt(c.v.UsageData.RefCount, 10)
}
func (c *volumeContext) Size() string {
if c.v.UsageData == nil {
return "N/A"
}
return units.HumanSize(float64(c.v.UsageData.Size))
}
func (c *volumeContext) Group() string {
if c.v.ClusterVolume == nil {
return "N/A"
}
return c.v.ClusterVolume.Spec.Group
}
func (c *volumeContext) Availability() string {
if c.v.ClusterVolume == nil {
return "N/A"
}
return string(c.v.ClusterVolume.Spec.Availability)
}
func (c *volumeContext) Status() string {
if c.v.ClusterVolume == nil {
return "N/A"
}
if c.v.ClusterVolume.Info == nil || c.v.ClusterVolume.Info.VolumeID == "" {
return "pending creation"
}
l := len(c.v.ClusterVolume.PublishStatus)
switch l {
case 0:
return "created"
case 1:
return "in use (1 node)"
default:
return fmt.Sprintf("in use (%d nodes)", l)
}
}

96
vendor/github.com/docker/cli/templates/templates.go generated vendored Normal file
View File

@@ -0,0 +1,96 @@
package templates
import (
"bytes"
"encoding/json"
"strings"
"text/template"
)
// basicFunctions are the set of initial
// functions provided to every template.
var basicFunctions = template.FuncMap{
"json": func(v interface{}) string {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.Encode(v)
// Remove the trailing new line added by the encoder
return strings.TrimSpace(buf.String())
},
"split": strings.Split,
"join": strings.Join,
"title": strings.Title, //nolint:staticcheck // strings.Title is deprecated, but we only use it for ASCII, so replacing with golang.org/x/text is out of scope
"lower": strings.ToLower,
"upper": strings.ToUpper,
"pad": padWithSpace,
"truncate": truncateWithLength,
}
// HeaderFunctions are used to created headers of a table.
// This is a replacement of basicFunctions for header generation
// because we want the header to remain intact.
// Some functions like `pad` are not overridden (to preserve alignment
// with the columns).
var HeaderFunctions = template.FuncMap{
"json": func(v string) string {
return v
},
"split": func(v string, _ string) string {
// we want the table header to show the name of the column, and not
// split the table header itself. Using a different signature
// here, and return a string instead of []string
return v
},
"join": func(v string, _ string) string {
// table headers are always a string, so use a different signature
// for the "join" function (string instead of []string)
return v
},
"title": func(v string) string {
return v
},
"lower": func(v string) string {
return v
},
"upper": func(v string) string {
return v
},
"truncate": func(v string, _ int) string {
return v
},
}
// Parse creates a new anonymous template with the basic functions
// and parses the given format.
func Parse(format string) (*template.Template, error) {
return NewParse("", format)
}
// New creates a new empty template with the provided tag and built-in
// template functions.
func New(tag string) *template.Template {
return template.New(tag).Funcs(basicFunctions)
}
// NewParse creates a new tagged template with the basic functions
// and parses the given format.
func NewParse(tag, format string) (*template.Template, error) {
return New(tag).Parse(format)
}
// padWithSpace adds whitespace to the input if the input is non-empty
func padWithSpace(source string, prefix, suffix int) string {
if source == "" {
return source
}
return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix)
}
// truncateWithLength truncates the source string up to the length provided by the input
func truncateWithLength(source string, length int) string {
if len(source) < length {
return source
}
return source[:length]
}

View File

@@ -0,0 +1,73 @@
// Package stringid provides helper functions for dealing with string identifiers
package stringid // import "github.com/docker/docker/pkg/stringid"
import (
"crypto/rand"
"encoding/hex"
"errors"
"regexp"
"strconv"
"strings"
)
const (
shortLen = 12
fullLen = 64
)
var (
validShortID = regexp.MustCompile("^[a-f0-9]{12}$")
validHex = regexp.MustCompile(`^[a-f0-9]{64}$`)
)
// IsShortID determines if id has the correct format and length for a short ID.
// It checks the IDs length and if it consists of valid characters for IDs (a-f0-9).
func IsShortID(id string) bool {
if len(id) != shortLen {
return false
}
return validShortID.MatchString(id)
}
// TruncateID returns a shorthand version of a string identifier for convenience.
// A collision with other shorthands is very unlikely, but possible.
// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller
// will need to use a longer prefix, or the full-length Id.
func TruncateID(id string) string {
if i := strings.IndexRune(id, ':'); i >= 0 {
id = id[i+1:]
}
if len(id) > shortLen {
id = id[:shortLen]
}
return id
}
// GenerateRandomID returns a unique id.
func GenerateRandomID() string {
b := make([]byte, 32)
for {
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
id := hex.EncodeToString(b)
// if we try to parse the truncated for as an int and we don't have
// an error then the value is all numeric and causes issues when
// used as a hostname. ref #3869
if _, err := strconv.ParseInt(TruncateID(id), 10, 64); err == nil {
continue
}
return id
}
}
// ValidateID checks whether an ID string is a valid, full-length image ID.
func ValidateID(id string) error {
if len(id) != fullLen {
return errors.New("image ID '" + id + "' is invalid")
}
if !validHex.MatchString(id) {
return errors.New("image ID '" + id + "' is invalid")
}
return nil
}