mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-07-20 18:28:03 +08:00
ls: format opt
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
183
vendor/github.com/docker/cli/cli/command/formatter/buildcache.go
generated
vendored
Normal file
183
vendor/github.com/docker/cli/cli/command/formatter/buildcache.go
generated
vendored
Normal 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)
|
||||
}
|
392
vendor/github.com/docker/cli/cli/command/formatter/container.go
generated
vendored
Normal file
392
vendor/github.com/docker/cli/cli/command/formatter/container.go
generated
vendored
Normal 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
|
||||
}
|
86
vendor/github.com/docker/cli/cli/command/formatter/context.go
generated
vendored
Normal file
86
vendor/github.com/docker/cli/cli/command/formatter/context.go
generated
vendored
Normal 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)
|
||||
}
|
48
vendor/github.com/docker/cli/cli/command/formatter/custom.go
generated
vendored
Normal file
48
vendor/github.com/docker/cli/cli/command/formatter/custom.go
generated
vendored
Normal 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
|
||||
}
|
477
vendor/github.com/docker/cli/cli/command/formatter/disk_usage.go
generated
vendored
Normal file
477
vendor/github.com/docker/cli/cli/command/formatter/disk_usage.go
generated
vendored
Normal 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))
|
||||
}
|
61
vendor/github.com/docker/cli/cli/command/formatter/displayutils.go
generated
vendored
Normal file
61
vendor/github.com/docker/cli/cli/command/formatter/displayutils.go
generated
vendored
Normal 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
|
||||
}
|
128
vendor/github.com/docker/cli/cli/command/formatter/formatter.go
generated
vendored
Normal file
128
vendor/github.com/docker/cli/cli/command/formatter/formatter.go
generated
vendored
Normal 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
|
||||
}
|
283
vendor/github.com/docker/cli/cli/command/formatter/image.go
generated
vendored
Normal file
283
vendor/github.com/docker/cli/cli/command/formatter/image.go
generated
vendored
Normal 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))
|
||||
}
|
68
vendor/github.com/docker/cli/cli/command/formatter/reflect.go
generated
vendored
Normal file
68
vendor/github.com/docker/cli/cli/command/formatter/reflect.go
generated
vendored
Normal 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
|
||||
}
|
604
vendor/github.com/docker/cli/cli/command/formatter/tabwriter/tabwriter.go
generated
vendored
Normal file
604
vendor/github.com/docker/cli/cli/command/formatter/tabwriter/tabwriter.go
generated
vendored
Normal 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)
|
||||
}
|
165
vendor/github.com/docker/cli/cli/command/formatter/volume.go
generated
vendored
Normal file
165
vendor/github.com/docker/cli/cli/command/formatter/volume.go
generated
vendored
Normal 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
96
vendor/github.com/docker/cli/templates/templates.go
generated
vendored
Normal 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]
|
||||
}
|
73
vendor/github.com/docker/docker/pkg/stringid/stringid.go
generated
vendored
Normal file
73
vendor/github.com/docker/docker/pkg/stringid/stringid.go
generated
vendored
Normal 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
|
||||
}
|
Reference in New Issue
Block a user