diff --git a/probe/docker/container.go b/probe/docker/container.go index ca11c3883..a93604431 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -379,8 +379,8 @@ func (c *container) getBaseNode() report.Node { }).WithParents(report.EmptySets. Add(report.ContainerImage, report.MakeStringSet(report.MakeContainerImageNodeID(c.Image()))), ) - result = result.AddTable(LabelPrefix, c.container.Config.Labels) - result = result.AddTable(EnvPrefix, c.env()) + result = result.AddPrefixTable(LabelPrefix, c.container.Config.Labels) + result = result.AddPrefixTable(EnvPrefix, c.env()) return result } diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index 48971061c..b1e2eb75c 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -4,6 +4,7 @@ import ( "net" "strings" + humanize "github.com/dustin/go-humanize" docker_client "github.com/fsouza/go-dockerclient" "github.com/weaveworks/scope/probe" @@ -15,23 +16,26 @@ import ( const ( ImageID = "docker_image_id" ImageName = "docker_image_name" + ImageSize = "docker_image_size" + ImageVirtualSize = "docker_image_virtual_size" ImageLabelPrefix = "docker_image_label_" IsInHostNetwork = "docker_is_in_host_network" + ImageTableID = "image_table" ) // Exposed for testing var ( ContainerMetadataTemplates = report.MetadataTemplates{ - ContainerID: {ID: ContainerID, Label: "ID", From: report.FromLatest, Truncate: 12, Priority: 1}, - ContainerStateHuman: {ID: ContainerStateHuman, Label: "State", From: report.FromLatest, Priority: 2}, - ContainerCommand: {ID: ContainerCommand, Label: "Command", From: report.FromLatest, Priority: 3}, - ImageID: {ID: ImageID, Label: "Image ID", From: report.FromLatest, Truncate: 12, Priority: 11}, - ContainerUptime: {ID: ContainerUptime, Label: "Uptime", From: report.FromLatest, Priority: 12}, - ContainerRestartCount: {ID: ContainerRestartCount, Label: "Restart #", From: report.FromLatest, Priority: 13}, - ContainerNetworks: {ID: ContainerNetworks, Label: "Networks", From: report.FromSets, Priority: 14}, - ContainerIPs: {ID: ContainerIPs, Label: "IPs", From: report.FromSets, Priority: 15}, - ContainerPorts: {ID: ContainerPorts, Label: "Ports", From: report.FromSets, Priority: 16}, - ContainerCreated: {ID: ContainerCreated, Label: "Created", From: report.FromLatest, Datatype: "datetime", Priority: 17}, + ImageName: {ID: ImageName, Label: "Image", From: report.FromLatest, Priority: 1}, + ContainerCommand: {ID: ContainerCommand, Label: "Command", From: report.FromLatest, Priority: 2}, + ContainerStateHuman: {ID: ContainerStateHuman, Label: "State", From: report.FromLatest, Priority: 3}, + ContainerUptime: {ID: ContainerUptime, Label: "Uptime", From: report.FromLatest, Priority: 4}, + ContainerRestartCount: {ID: ContainerRestartCount, Label: "Restart #", From: report.FromLatest, Priority: 5}, + ContainerNetworks: {ID: ContainerNetworks, Label: "Networks", From: report.FromSets, Priority: 6}, + ContainerIPs: {ID: ContainerIPs, Label: "IPs", From: report.FromSets, Priority: 7}, + ContainerPorts: {ID: ContainerPorts, Label: "Ports", From: report.FromSets, Priority: 8}, + ContainerCreated: {ID: ContainerCreated, Label: "Created", From: report.FromLatest, Datatype: "datetime", Priority: 9}, + ContainerID: {ID: ContainerID, Label: "ID", From: report.FromLatest, Truncate: 12, Priority: 10}, } ContainerMetricTemplates = report.MetricTemplates{ @@ -44,6 +48,14 @@ var ( } ContainerTableTemplates = report.TableTemplates{ + ImageTableID: {ID: ImageTableID, Label: "Image", + FixedRows: map[string]string{ + ImageID: "ID", + ImageName: "Name", + ImageSize: "Size", + ImageVirtualSize: "Virtual Size", + }, + }, LabelPrefix: {ID: LabelPrefix, Label: "Docker Labels", Prefix: LabelPrefix}, EnvPrefix: {ID: EnvPrefix, Label: "Environment Variables", Prefix: EnvPrefix}, } @@ -236,16 +248,17 @@ func (r *Reporter) containerImageTopology() report.Topology { r.registry.WalkImages(func(image docker_client.APIImages) { imageID := trimImageID(image.ID) - nodeID := report.MakeContainerImageNodeID(imageID) - node := report.MakeNodeWith(nodeID, map[string]string{ - ImageID: imageID, - }) - node = node.AddTable(ImageLabelPrefix, image.Labels) - - if len(image.RepoTags) > 0 { - node = node.WithLatests(map[string]string{ImageName: image.RepoTags[0]}) + latests := map[string]string{ + ImageID: imageID, + ImageSize: humanize.Bytes(uint64(image.Size)), + ImageVirtualSize: humanize.Bytes(uint64(image.VirtualSize)), } - + if len(image.RepoTags) > 0 { + latests[ImageName] = image.RepoTags[0] + } + nodeID := report.MakeContainerImageNodeID(imageID) + node := report.MakeNodeWith(nodeID, latests) + node = node.AddPrefixTable(ImageLabelPrefix, image.Labels) result.AddNode(node) }) diff --git a/probe/kubernetes/meta.go b/probe/kubernetes/meta.go index d8109867e..c7db23d0c 100644 --- a/probe/kubernetes/meta.go +++ b/probe/kubernetes/meta.go @@ -56,5 +56,5 @@ func (m meta) MetaNode(id string) report.Node { Name: m.Name(), Namespace: m.Namespace(), Created: m.Created(), - }).AddTable(LabelPrefix, m.Labels()) + }).AddPrefixTable(LabelPrefix, m.Labels()) } diff --git a/render/container.go b/render/container.go index eec54e872..82974107d 100644 --- a/render/container.go +++ b/render/container.go @@ -192,6 +192,8 @@ func (r containerWithImageNameRenderer) Render(rpt report.Report, dct Decorator) imageNodeID := report.MakeContainerImageNodeID(imageNameWithoutVersion) c = propagateLatest(docker.ImageName, image, c) + c = propagateLatest(docker.ImageSize, image, c) + c = propagateLatest(docker.ImageVirtualSize, image, c) c = propagateLatest(docker.ImageLabelPrefix+"works.weave.role", image, c) c.Parents = c.Parents. Delete(report.ContainerImage). diff --git a/render/detailed/metadata_test.go b/render/detailed/metadata_test.go index d77740bc5..38a9fc597 100644 --- a/render/detailed/metadata_test.go +++ b/render/detailed/metadata_test.go @@ -27,9 +27,9 @@ func TestNodeMetadata(t *testing.T) { Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")), ), want: []report.MetadataRow{ - {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1, Truncate: 12}, - {ID: docker.ContainerStateHuman, Label: "State", Value: "running", Priority: 2}, - {ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24", Priority: 15}, + {ID: docker.ContainerStateHuman, Label: "State", Value: "running", Priority: 3}, + {ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24", Priority: 7}, + {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 10, Truncate: 12}, }, }, { diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go index fce374776..1402e73de 100644 --- a/render/detailed/node_test.go +++ b/render/detailed/node_test.go @@ -199,9 +199,9 @@ func TestMakeDetailedContainerNode(t *testing.T) { Linkable: true, Pseudo: false, Metadata: []report.MetadataRow{ - {ID: "docker_container_id", Label: "ID", Value: fixture.ServerContainerID, Priority: 1, Truncate: 12}, - {ID: "docker_container_state_human", Label: "State", Value: "running", Priority: 2}, - {ID: "docker_image_id", Label: "Image ID", Value: fixture.ServerContainerImageID, Priority: 11, Truncate: 12}, + {ID: "docker_image_name", Label: "Image", Value: fixture.ServerContainerImageName, Priority: 1}, + {ID: "docker_container_state_human", Label: "State", Value: "running", Priority: 3}, + {ID: "docker_container_id", Label: "ID", Value: fixture.ServerContainerID, Priority: 10, Truncate: 12}, }, Metrics: []report.MetricRow{ { diff --git a/render/detailed/summary_test.go b/render/detailed/summary_test.go index 8910b11b1..9bf78c128 100644 --- a/render/detailed/summary_test.go +++ b/render/detailed/summary_test.go @@ -127,7 +127,8 @@ func TestMakeNodeSummary(t *testing.T) { Shape: "hexagon", Linkable: true, Metadata: []report.MetadataRow{ - {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 1, Truncate: 12}, + {ID: docker.ImageName, Label: "Image", Value: fixture.ClientContainerImageName, Priority: 1}, + {ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID, Priority: 10, Truncate: 12}, }, Adjacency: report.MakeIDList(fixture.ServerContainerNodeID), }, diff --git a/render/detailed/tables_test.go b/render/detailed/tables_test.go index ebdf3b2ee..17c4c6b3d 100644 --- a/render/detailed/tables_test.go +++ b/render/detailed/tables_test.go @@ -48,6 +48,11 @@ func TestNodeTables(t *testing.T) { }, }, }, + { + ID: docker.ImageTableID, + Label: "Image", + Rows: []report.MetadataRow{}, + }, }, }, { diff --git a/report/table.go b/report/table.go index feaec789d..04d2abd05 100644 --- a/report/table.go +++ b/report/table.go @@ -17,8 +17,8 @@ const ( TruncationCountPrefix = "table_truncation_count_" ) -// AddTable appends arbirary key-value pairs to the Node, returning a new node. -func (node Node) AddTable(prefix string, labels map[string]string) Node { +// AddPrefixTable appends arbitrary key-value pairs to the Node, returning a new node. +func (node Node) AddPrefixTable(prefix string, labels map[string]string) Node { count := 0 for key, value := range labels { if count >= MaxTableRows { @@ -34,17 +34,20 @@ func (node Node) AddTable(prefix string, labels map[string]string) Node { return node } -// ExtractTable returns the key-value pairs with the given prefix from this Node, -func (node Node) ExtractTable(prefix string) (rows map[string]string, truncationCount int) { +// ExtractTable returns the key-value pairs to build a table from this node +func (node Node) ExtractTable(template TableTemplate) (rows map[string]string, truncationCount int) { rows = map[string]string{} truncationCount = 0 node.Latest.ForEach(func(key string, _ time.Time, value string) { - if strings.HasPrefix(key, prefix) { - label := key[len(prefix):] + if label, ok := template.FixedRows[key]; ok { + rows[label] = value + } + if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) { + label := key[len(template.Prefix):] rows[label] = value } }) - if str, ok := node.Latest.Lookup(TruncationCountPrefix + prefix); ok { + if str, ok := node.Latest.Lookup(TruncationCountPrefix + template.Prefix); ok { if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil { log.Warn("Unexpected truncation count format %q", str) } @@ -79,15 +82,31 @@ func (t Table) Copy() Table { return result } +// FixedRow describes a row which is part of a TableTemplate and whose value is extracted +// from a predetermined key +type FixedRow struct { + Label string `json:"label"` + Key string `json:"key"` +} + // TableTemplate describes how to render a table for the UI. type TableTemplate struct { ID string `json:"id"` Label string `json:"label"` Prefix string `json:"prefix"` + // FixedRows indicates what predetermined rows to render each entry is + // indexed by the key to extract the row value is mapped to the row + // label + FixedRows map[string]string `json:"fixedRows"` } // Copy returns a value-copy of the TableTemplate func (t TableTemplate) Copy() TableTemplate { + fixedRowsCopy := make(map[string]string, len(t.FixedRows)) + for key, value := range t.FixedRows { + fixedRowsCopy[key] = value + } + t.FixedRows = fixedRowsCopy return t } @@ -102,10 +121,16 @@ func (t TableTemplate) Merge(other TableTemplate) TableTemplate { return s2 } + fixedRows := t.FixedRows + if len(other.FixedRows) > len(fixedRows) { + fixedRows = other.FixedRows + } + return TableTemplate{ - ID: max(t.ID, other.ID), - Label: max(t.Label, other.Label), - Prefix: max(t.Prefix, other.Prefix), + ID: max(t.ID, other.ID), + Label: max(t.Label, other.Label), + Prefix: max(t.Prefix, other.Prefix), + FixedRows: fixedRows, } } @@ -116,7 +141,7 @@ type TableTemplates map[string]TableTemplate func (t TableTemplates) Tables(node Node) []Table { var result []Table for _, template := range t { - rows, truncationCount := node.ExtractTable(template.Prefix) + rows, truncationCount := node.ExtractTable(template) table := Table{ ID: template.ID, Label: template.Label, diff --git a/report/table_test.go b/report/table_test.go index cf276ba75..e7dedf000 100644 --- a/report/table_test.go +++ b/report/table_test.go @@ -9,15 +9,15 @@ import ( "github.com/weaveworks/scope/test" ) -func TestTables(t *testing.T) { +func TestPrefixTables(t *testing.T) { want := map[string]string{ "foo1": "bar1", "foo2": "bar2", } nmd := report.MakeNode("foo1") - nmd = nmd.AddTable("foo_", want) - have, truncationCount := nmd.ExtractTable("foo_") + nmd = nmd.AddPrefixTable("foo_", want) + have, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"}) if truncationCount != 0 { t.Error("Table shouldn't had been truncated") @@ -28,6 +28,29 @@ func TestTables(t *testing.T) { } } +func TestFixedTables(t *testing.T) { + want := map[string]string{ + "foo1": "bar1", + "foo2": "bar2", + } + nmd := report.MakeNodeWith("foo1", map[string]string{ + "foo1key": "bar1", + "foo2key": "bar2", + }) + + template := report.TableTemplate{FixedRows: map[string]string{ + "foo1key": "foo1", + "foo2key": "foo2", + }, + } + + have, _ := nmd.ExtractTable(template) + + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + func TestTruncation(t *testing.T) { wantTruncationCount := 1 want := map[string]string{} @@ -39,8 +62,8 @@ func TestTruncation(t *testing.T) { nmd := report.MakeNode("foo1") - nmd = nmd.AddTable("foo_", want) - _, truncationCount := nmd.ExtractTable("foo_") + nmd = nmd.AddPrefixTable("foo_", want) + _, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"}) if truncationCount != wantTruncationCount { t.Error( diff --git a/vendor/github.com/dustin/go-humanize/LICENSE b/vendor/github.com/dustin/go-humanize/LICENSE new file mode 100644 index 000000000..8d9a94a90 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2005-2008 Dustin Sallings + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + diff --git a/vendor/github.com/dustin/go-humanize/big.go b/vendor/github.com/dustin/go-humanize/big.go new file mode 100644 index 000000000..f49dc337d --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/big.go @@ -0,0 +1,31 @@ +package humanize + +import ( + "math/big" +) + +// order of magnitude (to a max order) +func oomm(n, b *big.Int, maxmag int) (float64, int) { + mag := 0 + m := &big.Int{} + for n.Cmp(b) >= 0 { + n.DivMod(n, b, m) + mag++ + if mag == maxmag && maxmag >= 0 { + break + } + } + return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag +} + +// total order of magnitude +// (same as above, but with no upper limit) +func oom(n, b *big.Int) (float64, int) { + mag := 0 + m := &big.Int{} + for n.Cmp(b) >= 0 { + n.DivMod(n, b, m) + mag++ + } + return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag +} diff --git a/vendor/github.com/dustin/go-humanize/bigbytes.go b/vendor/github.com/dustin/go-humanize/bigbytes.go new file mode 100644 index 000000000..d4fb37142 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/bigbytes.go @@ -0,0 +1,173 @@ +package humanize + +import ( + "fmt" + "math/big" + "strings" + "unicode" +) + +var ( + bigIECExp = big.NewInt(1024) + + // BigByte is one byte in bit.Ints + BigByte = big.NewInt(1) + // BigKiByte is 1,024 bytes in bit.Ints + BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp) + // BigMiByte is 1,024 k bytes in bit.Ints + BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp) + // BigGiByte is 1,024 m bytes in bit.Ints + BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp) + // BigTiByte is 1,024 g bytes in bit.Ints + BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp) + // BigPiByte is 1,024 t bytes in bit.Ints + BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp) + // BigEiByte is 1,024 p bytes in bit.Ints + BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp) + // BigZiByte is 1,024 e bytes in bit.Ints + BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp) + // BigYiByte is 1,024 z bytes in bit.Ints + BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp) +) + +var ( + bigSIExp = big.NewInt(1000) + + // BigSIByte is one SI byte in big.Ints + BigSIByte = big.NewInt(1) + // BigKByte is 1,000 SI bytes in big.Ints + BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp) + // BigMByte is 1,000 SI k bytes in big.Ints + BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp) + // BigGByte is 1,000 SI m bytes in big.Ints + BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp) + // BigTByte is 1,000 SI g bytes in big.Ints + BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp) + // BigPByte is 1,000 SI t bytes in big.Ints + BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp) + // BigEByte is 1,000 SI p bytes in big.Ints + BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp) + // BigZByte is 1,000 SI e bytes in big.Ints + BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp) + // BigYByte is 1,000 SI z bytes in big.Ints + BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp) +) + +var bigBytesSizeTable = map[string]*big.Int{ + "b": BigByte, + "kib": BigKiByte, + "kb": BigKByte, + "mib": BigMiByte, + "mb": BigMByte, + "gib": BigGiByte, + "gb": BigGByte, + "tib": BigTiByte, + "tb": BigTByte, + "pib": BigPiByte, + "pb": BigPByte, + "eib": BigEiByte, + "eb": BigEByte, + "zib": BigZiByte, + "zb": BigZByte, + "yib": BigYiByte, + "yb": BigYByte, + // Without suffix + "": BigByte, + "ki": BigKiByte, + "k": BigKByte, + "mi": BigMiByte, + "m": BigMByte, + "gi": BigGiByte, + "g": BigGByte, + "ti": BigTiByte, + "t": BigTByte, + "pi": BigPiByte, + "p": BigPByte, + "ei": BigEiByte, + "e": BigEByte, + "z": BigZByte, + "zi": BigZiByte, + "y": BigYByte, + "yi": BigYiByte, +} + +var ten = big.NewInt(10) + +func humanateBigBytes(s, base *big.Int, sizes []string) string { + if s.Cmp(ten) < 0 { + return fmt.Sprintf("%d B", s) + } + c := (&big.Int{}).Set(s) + val, mag := oomm(c, base, len(sizes)-1) + suffix := sizes[mag] + f := "%.0f %s" + if val < 10 { + f = "%.1f %s" + } + + return fmt.Sprintf(f, val, suffix) + +} + +// BigBytes produces a human readable representation of an SI size. +// +// See also: ParseBigBytes. +// +// BigBytes(82854982) -> 83MB +func BigBytes(s *big.Int) string { + sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + return humanateBigBytes(s, bigSIExp, sizes) +} + +// BigIBytes produces a human readable representation of an IEC size. +// +// See also: ParseBigBytes. +// +// BigIBytes(82854982) -> 79MiB +func BigIBytes(s *big.Int) string { + sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} + return humanateBigBytes(s, bigIECExp, sizes) +} + +// ParseBigBytes parses a string representation of bytes into the number +// of bytes it represents. +// +// See also: BigBytes, BigIBytes. +// +// ParseBigBytes("42MB") -> 42000000, nil +// ParseBigBytes("42mib") -> 44040192, nil +func ParseBigBytes(s string) (*big.Int, error) { + lastDigit := 0 + hasComma := false + for _, r := range s { + if !(unicode.IsDigit(r) || r == '.' || r == ',') { + break + } + if r == ',' { + hasComma = true + } + lastDigit++ + } + + num := s[:lastDigit] + if hasComma { + num = strings.Replace(num, ",", "", -1) + } + + val := &big.Rat{} + _, err := fmt.Sscanf(num, "%f", val) + if err != nil { + return nil, err + } + + extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) + if m, ok := bigBytesSizeTable[extra]; ok { + mv := (&big.Rat{}).SetInt(m) + val.Mul(val, mv) + rv := &big.Int{} + rv.Div(val.Num(), val.Denom()) + return rv, nil + } + + return nil, fmt.Errorf("unhandled size name: %v", extra) +} diff --git a/vendor/github.com/dustin/go-humanize/bytes.go b/vendor/github.com/dustin/go-humanize/bytes.go new file mode 100644 index 000000000..190ab65f3 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/bytes.go @@ -0,0 +1,143 @@ +package humanize + +import ( + "fmt" + "math" + "strconv" + "strings" + "unicode" +) + +// IEC Sizes. +// kibis of bits +const ( + Byte = 1 << (iota * 10) + KiByte + MiByte + GiByte + TiByte + PiByte + EiByte +) + +// SI Sizes. +const ( + IByte = 1 + KByte = IByte * 1000 + MByte = KByte * 1000 + GByte = MByte * 1000 + TByte = GByte * 1000 + PByte = TByte * 1000 + EByte = PByte * 1000 +) + +var bytesSizeTable = map[string]uint64{ + "b": Byte, + "kib": KiByte, + "kb": KByte, + "mib": MiByte, + "mb": MByte, + "gib": GiByte, + "gb": GByte, + "tib": TiByte, + "tb": TByte, + "pib": PiByte, + "pb": PByte, + "eib": EiByte, + "eb": EByte, + // Without suffix + "": Byte, + "ki": KiByte, + "k": KByte, + "mi": MiByte, + "m": MByte, + "gi": GiByte, + "g": GByte, + "ti": TiByte, + "t": TByte, + "pi": PiByte, + "p": PByte, + "ei": EiByte, + "e": EByte, +} + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanateBytes(s uint64, base float64, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[int(e)] + val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 + f := "%.0f %s" + if val < 10 { + f = "%.1f %s" + } + + return fmt.Sprintf(f, val, suffix) +} + +// Bytes produces a human readable representation of an SI size. +// +// See also: ParseBytes. +// +// Bytes(82854982) -> 83MB +func Bytes(s uint64) string { + sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(s, 1000, sizes) +} + +// IBytes produces a human readable representation of an IEC size. +// +// See also: ParseBytes. +// +// IBytes(82854982) -> 79MiB +func IBytes(s uint64) string { + sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} + return humanateBytes(s, 1024, sizes) +} + +// ParseBytes parses a string representation of bytes into the number +// of bytes it represents. +// +// See Also: Bytes, IBytes. +// +// ParseBytes("42MB") -> 42000000, nil +// ParseBytes("42mib") -> 44040192, nil +func ParseBytes(s string) (uint64, error) { + lastDigit := 0 + hasComma := false + for _, r := range s { + if !(unicode.IsDigit(r) || r == '.' || r == ',') { + break + } + if r == ',' { + hasComma = true + } + lastDigit++ + } + + num := s[:lastDigit] + if hasComma { + num = strings.Replace(num, ",", "", -1) + } + + f, err := strconv.ParseFloat(num, 64) + if err != nil { + return 0, err + } + + extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) + if m, ok := bytesSizeTable[extra]; ok { + f *= float64(m) + if f >= math.MaxUint64 { + return 0, fmt.Errorf("too large: %v", s) + } + return uint64(f), nil + } + + return 0, fmt.Errorf("unhandled size name: %v", extra) +} diff --git a/vendor/github.com/dustin/go-humanize/comma.go b/vendor/github.com/dustin/go-humanize/comma.go new file mode 100644 index 000000000..74f446b67 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/comma.go @@ -0,0 +1,101 @@ +package humanize + +import ( + "bytes" + "math/big" + "strconv" + "strings" +) + +// Comma produces a string form of the given number in base 10 with +// commas after every three orders of magnitude. +// +// e.g. Comma(834142) -> 834,142 +func Comma(v int64) string { + sign := "" + if v < 0 { + sign = "-" + v = 0 - v + } + + parts := []string{"", "", "", "", "", "", ""} + j := len(parts) - 1 + + for v > 999 { + parts[j] = strconv.FormatInt(v%1000, 10) + switch len(parts[j]) { + case 2: + parts[j] = "0" + parts[j] + case 1: + parts[j] = "00" + parts[j] + } + v = v / 1000 + j-- + } + parts[j] = strconv.Itoa(int(v)) + return sign + strings.Join(parts[j:], ",") +} + +// Commaf produces a string form of the given number in base 10 with +// commas after every three orders of magnitude. +// +// e.g. Commaf(834142.32) -> 834,142.32 +func Commaf(v float64) string { + buf := &bytes.Buffer{} + if v < 0 { + buf.Write([]byte{'-'}) + v = 0 - v + } + + comma := []byte{','} + + parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") + pos := 0 + if len(parts[0])%3 != 0 { + pos += len(parts[0]) % 3 + buf.WriteString(parts[0][:pos]) + buf.Write(comma) + } + for ; pos < len(parts[0]); pos += 3 { + buf.WriteString(parts[0][pos : pos+3]) + buf.Write(comma) + } + buf.Truncate(buf.Len() - 1) + + if len(parts) > 1 { + buf.Write([]byte{'.'}) + buf.WriteString(parts[1]) + } + return buf.String() +} + +// BigComma produces a string form of the given big.Int in base 10 +// with commas after every three orders of magnitude. +func BigComma(b *big.Int) string { + sign := "" + if b.Sign() < 0 { + sign = "-" + b.Abs(b) + } + + athousand := big.NewInt(1000) + c := (&big.Int{}).Set(b) + _, m := oom(c, athousand) + parts := make([]string, m+1) + j := len(parts) - 1 + + mod := &big.Int{} + for b.Cmp(athousand) >= 0 { + b.DivMod(b, athousand, mod) + parts[j] = strconv.FormatInt(mod.Int64(), 10) + switch len(parts[j]) { + case 2: + parts[j] = "0" + parts[j] + case 1: + parts[j] = "00" + parts[j] + } + j-- + } + parts[j] = strconv.Itoa(int(b.Int64())) + return sign + strings.Join(parts[j:], ",") +} diff --git a/vendor/github.com/dustin/go-humanize/commaf.go b/vendor/github.com/dustin/go-humanize/commaf.go new file mode 100644 index 000000000..620690dec --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/commaf.go @@ -0,0 +1,40 @@ +// +build go1.6 + +package humanize + +import ( + "bytes" + "math/big" + "strings" +) + +// BigCommaf produces a string form of the given big.Float in base 10 +// with commas after every three orders of magnitude. +func BigCommaf(v *big.Float) string { + buf := &bytes.Buffer{} + if v.Sign() < 0 { + buf.Write([]byte{'-'}) + v.Abs(v) + } + + comma := []byte{','} + + parts := strings.Split(v.Text('f', -1), ".") + pos := 0 + if len(parts[0])%3 != 0 { + pos += len(parts[0]) % 3 + buf.WriteString(parts[0][:pos]) + buf.Write(comma) + } + for ; pos < len(parts[0]); pos += 3 { + buf.WriteString(parts[0][pos : pos+3]) + buf.Write(comma) + } + buf.Truncate(buf.Len() - 1) + + if len(parts) > 1 { + buf.Write([]byte{'.'}) + buf.WriteString(parts[1]) + } + return buf.String() +} diff --git a/vendor/github.com/dustin/go-humanize/ftoa.go b/vendor/github.com/dustin/go-humanize/ftoa.go new file mode 100644 index 000000000..c76190b10 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/ftoa.go @@ -0,0 +1,23 @@ +package humanize + +import "strconv" + +func stripTrailingZeros(s string) string { + offset := len(s) - 1 + for offset > 0 { + if s[offset] == '.' { + offset-- + break + } + if s[offset] != '0' { + break + } + offset-- + } + return s[:offset+1] +} + +// Ftoa converts a float to a string with no trailing zeros. +func Ftoa(num float64) string { + return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) +} diff --git a/vendor/github.com/dustin/go-humanize/humanize.go b/vendor/github.com/dustin/go-humanize/humanize.go new file mode 100644 index 000000000..a69540a06 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/humanize.go @@ -0,0 +1,8 @@ +/* +Package humanize converts boring ugly numbers to human-friendly strings and back. + +Durations can be turned into strings such as "3 days ago", numbers +representing sizes like 82854982 into useful strings like, "83MB" or +"79MiB" (whichever you prefer). +*/ +package humanize diff --git a/vendor/github.com/dustin/go-humanize/number.go b/vendor/github.com/dustin/go-humanize/number.go new file mode 100644 index 000000000..32141348c --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/number.go @@ -0,0 +1,192 @@ +package humanize + +/* +Slightly adapted from the source to fit go-humanize. + +Author: https://github.com/gorhill +Source: https://gist.github.com/gorhill/5285193 + +*/ + +import ( + "math" + "strconv" +) + +var ( + renderFloatPrecisionMultipliers = [...]float64{ + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + } + + renderFloatPrecisionRounders = [...]float64{ + 0.5, + 0.05, + 0.005, + 0.0005, + 0.00005, + 0.000005, + 0.0000005, + 0.00000005, + 0.000000005, + 0.0000000005, + } +) + +// FormatFloat produces a formatted number as string based on the following user-specified criteria: +// * thousands separator +// * decimal separator +// * decimal precision +// +// Usage: s := RenderFloat(format, n) +// The format parameter tells how to render the number n. +// +// See examples: http://play.golang.org/p/LXc1Ddm1lJ +// +// Examples of format strings, given n = 12345.6789: +// "#,###.##" => "12,345.67" +// "#,###." => "12,345" +// "#,###" => "12345,678" +// "#\u202F###,##" => "12 345,68" +// "#.###,###### => 12.345,678900 +// "" (aka default format) => 12,345.67 +// +// The highest precision allowed is 9 digits after the decimal symbol. +// There is also a version for integer number, FormatInteger(), +// which is convenient for calls within template. +func FormatFloat(format string, n float64) string { + // Special cases: + // NaN = "NaN" + // +Inf = "+Infinity" + // -Inf = "-Infinity" + if math.IsNaN(n) { + return "NaN" + } + if n > math.MaxFloat64 { + return "Infinity" + } + if n < -math.MaxFloat64 { + return "-Infinity" + } + + // default format + precision := 2 + decimalStr := "." + thousandStr := "," + positiveStr := "" + negativeStr := "-" + + if len(format) > 0 { + format := []rune(format) + + // If there is an explicit format directive, + // then default values are these: + precision = 9 + thousandStr = "" + + // collect indices of meaningful formatting directives + formatIndx := []int{} + for i, char := range format { + if char != '#' && char != '0' { + formatIndx = append(formatIndx, i) + } + } + + if len(formatIndx) > 0 { + // Directive at index 0: + // Must be a '+' + // Raise an error if not the case + // index: 0123456789 + // +0.000,000 + // +000,000.0 + // +0000.00 + // +0000 + if formatIndx[0] == 0 { + if format[formatIndx[0]] != '+' { + panic("RenderFloat(): invalid positive sign directive") + } + positiveStr = "+" + formatIndx = formatIndx[1:] + } + + // Two directives: + // First is thousands separator + // Raise an error if not followed by 3-digit + // 0123456789 + // 0.000,000 + // 000,000.00 + if len(formatIndx) == 2 { + if (formatIndx[1] - formatIndx[0]) != 4 { + panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers") + } + thousandStr = string(format[formatIndx[0]]) + formatIndx = formatIndx[1:] + } + + // One directive: + // Directive is decimal separator + // The number of digit-specifier following the separator indicates wanted precision + // 0123456789 + // 0.00 + // 000,0000 + if len(formatIndx) == 1 { + decimalStr = string(format[formatIndx[0]]) + precision = len(format) - formatIndx[0] - 1 + } + } + } + + // generate sign part + var signStr string + if n >= 0.000000001 { + signStr = positiveStr + } else if n <= -0.000000001 { + signStr = negativeStr + n = -n + } else { + signStr = "" + n = 0.0 + } + + // split number into integer and fractional parts + intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) + + // generate integer part string + intStr := strconv.Itoa(int(intf)) + + // add thousand separator if required + if len(thousandStr) > 0 { + for i := len(intStr); i > 3; { + i -= 3 + intStr = intStr[:i] + thousandStr + intStr[i:] + } + } + + // no fractional part, we can leave now + if precision == 0 { + return signStr + intStr + } + + // generate fractional part + fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) + // may need padding + if len(fracStr) < precision { + fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr + } + + return signStr + intStr + decimalStr + fracStr +} + +// FormatInteger produces a formatted number as string. +// See FormatFloat. +func FormatInteger(format string, n int) string { + return FormatFloat(format, float64(n)) +} diff --git a/vendor/github.com/dustin/go-humanize/ordinals.go b/vendor/github.com/dustin/go-humanize/ordinals.go new file mode 100644 index 000000000..43d88a861 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/ordinals.go @@ -0,0 +1,25 @@ +package humanize + +import "strconv" + +// Ordinal gives you the input number in a rank/ordinal format. +// +// Ordinal(3) -> 3rd +func Ordinal(x int) string { + suffix := "th" + switch x % 10 { + case 1: + if x%100 != 11 { + suffix = "st" + } + case 2: + if x%100 != 12 { + suffix = "nd" + } + case 3: + if x%100 != 13 { + suffix = "rd" + } + } + return strconv.Itoa(x) + suffix +} diff --git a/vendor/github.com/dustin/go-humanize/si.go b/vendor/github.com/dustin/go-humanize/si.go new file mode 100644 index 000000000..9cce4e8d3 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/si.go @@ -0,0 +1,113 @@ +package humanize + +import ( + "errors" + "math" + "regexp" + "strconv" +) + +var siPrefixTable = map[float64]string{ + -24: "y", // yocto + -21: "z", // zepto + -18: "a", // atto + -15: "f", // femto + -12: "p", // pico + -9: "n", // nano + -6: "µ", // micro + -3: "m", // milli + 0: "", + 3: "k", // kilo + 6: "M", // mega + 9: "G", // giga + 12: "T", // tera + 15: "P", // peta + 18: "E", // exa + 21: "Z", // zetta + 24: "Y", // yotta +} + +var revSIPrefixTable = revfmap(siPrefixTable) + +// revfmap reverses the map and precomputes the power multiplier +func revfmap(in map[float64]string) map[string]float64 { + rv := map[string]float64{} + for k, v := range in { + rv[v] = math.Pow(10, k) + } + return rv +} + +var riParseRegex *regexp.Regexp + +func init() { + ri := `^([\-0-9.]+)\s?([` + for _, v := range siPrefixTable { + ri += v + } + ri += `]?)(.*)` + + riParseRegex = regexp.MustCompile(ri) +} + +// ComputeSI finds the most appropriate SI prefix for the given number +// and returns the prefix along with the value adjusted to be within +// that prefix. +// +// See also: SI, ParseSI. +// +// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p") +func ComputeSI(input float64) (float64, string) { + if input == 0 { + return 0, "" + } + mag := math.Abs(input) + exponent := math.Floor(logn(mag, 10)) + exponent = math.Floor(exponent/3) * 3 + + value := mag / math.Pow(10, exponent) + + // Handle special case where value is exactly 1000.0 + // Should return 1M instead of 1000k + if value == 1000.0 { + exponent += 3 + value = mag / math.Pow(10, exponent) + } + + value = math.Copysign(value, input) + + prefix := siPrefixTable[exponent] + return value, prefix +} + +// SI returns a string with default formatting. +// +// SI uses Ftoa to format float value, removing trailing zeros. +// +// See also: ComputeSI, ParseSI. +// +// e.g. SI(1000000, B) -> 1MB +// e.g. SI(2.2345e-12, "F") -> 2.2345pF +func SI(input float64, unit string) string { + value, prefix := ComputeSI(input) + return Ftoa(value) + " " + prefix + unit +} + +var errInvalid = errors.New("invalid input") + +// ParseSI parses an SI string back into the number and unit. +// +// See also: SI, ComputeSI. +// +// e.g. ParseSI(2.2345pF) -> (2.2345e-12, "F", nil) +func ParseSI(input string) (float64, string, error) { + found := riParseRegex.FindStringSubmatch(input) + if len(found) != 4 { + return 0, "", errInvalid + } + mag := revSIPrefixTable[found[2]] + unit := found[3] + + base, err := strconv.ParseFloat(found[1], 64) + return base * mag, unit, err +} diff --git a/vendor/github.com/dustin/go-humanize/times.go b/vendor/github.com/dustin/go-humanize/times.go new file mode 100644 index 000000000..815f63079 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/times.go @@ -0,0 +1,114 @@ +package humanize + +import ( + "fmt" + "math" + "sort" + "time" +) + +// Seconds-based time units +const ( + Day = 24 * time.Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month + LongTime = 37 * Year +) + +// Time formats a time into a relative string. +// +// Time(someT) -> "3 weeks ago" +func Time(then time.Time) string { + return RelTime(then, time.Now(), "ago", "from now") +} + +// A RelTimeMagnitude struct contains a relative time point at which +// the relative format of time will switch to a new format string. A +// slice of these in ascending order by their "D" field is passed to +// CustomRelTime to format durations. +// +// The Format field is a string that may contain a "%s" which will be +// replaced with the appropriate signed label (e.g. "ago" or "from +// now") and a "%d" that will be replaced by the quantity. +// +// The DivBy field is the amount of time the time difference must be +// divided by in order to display correctly. +// +// e.g. if D is 2*time.Minute and you want to display "%d minutes %s" +// DivBy should be time.Minute so whatever the duration is will be +// expressed in minutes. +type RelTimeMagnitude struct { + D time.Duration + Format string + DivBy time.Duration +} + +var defaultMagnitudes = []RelTimeMagnitude{ + {time.Second, "now", time.Second}, + {2 * time.Second, "1 second %s", 1}, + {time.Minute, "%d seconds %s", time.Second}, + {2 * time.Minute, "1 minute %s", 1}, + {time.Hour, "%d minutes %s", time.Minute}, + {2 * time.Hour, "1 hour %s", 1}, + {Day, "%d hours %s", time.Hour}, + {2 * Day, "1 day %s", 1}, + {Week, "%d days %s", Day}, + {2 * Week, "1 week %s", 1}, + {Month, "%d weeks %s", Week}, + {2 * Month, "1 month %s", 1}, + {Year, "%d months %s", Month}, + {18 * Month, "1 year %s", 1}, + {2 * Year, "2 years %s", 1}, + {LongTime, "%d years %s", Year}, + {math.MaxInt64, "a long while %s", 1}, +} + +// RelTime formats a time into a relative string. +// +// It takes two times and two labels. In addition to the generic time +// delta string (e.g. 5 minutes), the labels are used applied so that +// the label corresponding to the smaller time is applied. +// +// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" +func RelTime(a, b time.Time, albl, blbl string) string { + return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) +} + +// CustomRelTime formats a time into a relative string. +// +// It takes two times two labels and a table of relative time formats. +// In addition to the generic time delta string (e.g. 5 minutes), the +// labels are used applied so that the label corresponding to the +// smaller time is applied. +func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { + lbl := albl + diff := b.Sub(a) + + if a.After(b) { + lbl = blbl + diff = a.Sub(b) + } + + n := sort.Search(len(magnitudes), func(i int) bool { + return magnitudes[i].D >= diff + }) + + mag := magnitudes[n] + args := []interface{}{} + escaped := false + for _, ch := range mag.Format { + if escaped { + switch ch { + case 's': + args = append(args, lbl) + case 'd': + args = append(args, diff/mag.DivBy) + } + escaped = false + } else { + escaped = ch == '%' + } + } + return fmt.Sprintf(mag.Format, args...) +} diff --git a/vendor/manifest b/vendor/manifest index 4f86d50bb..7e8372eff 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -760,6 +760,14 @@ "branch": "master", "path": "/system" }, + { + "importpath": "github.com/dustin/go-humanize", + "repository": "https://github.com/dustin/go-humanize", + "vcs": "git", + "revision": "bd88f87ad3a420f7bcf05e90566fd1ceb351fa7f", + "branch": "master", + "notests": true + }, { "importpath": "github.com/emicklei/go-restful", "repository": "https://github.com/emicklei/go-restful",