feat: Implement resource:limit and resource:reserve
Also add the install and post-delete hooks
This commit is contained in:
6
plugins/resource/.gitignore
vendored
Normal file
6
plugins/resource/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/commands
|
||||||
|
/subcommands/*
|
||||||
|
/triggers/*
|
||||||
|
/install
|
||||||
|
/post-delete
|
||||||
|
/report
|
||||||
37
plugins/resource/Makefile
Normal file
37
plugins/resource/Makefile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
include ../../common.mk
|
||||||
|
|
||||||
|
GO_ARGS ?= -a
|
||||||
|
|
||||||
|
SUBCOMMANDS = subcommands/limit subcommands/reserve
|
||||||
|
TRIGGERS = triggers/install triggers/post-delete
|
||||||
|
build-in-docker: clean
|
||||||
|
docker run --rm \
|
||||||
|
-v $$PWD/../..:$(GO_REPO_ROOT) \
|
||||||
|
-w $(GO_REPO_ROOT)/plugins/resource \
|
||||||
|
$(BUILD_IMAGE) \
|
||||||
|
bash -c "GO_ARGS='$(GO_ARGS)' make -j4 build" || exit $$?
|
||||||
|
|
||||||
|
build: commands subcommands triggers
|
||||||
|
$(MAKE) triggers-copy
|
||||||
|
|
||||||
|
commands: **/**/commands.go
|
||||||
|
go build $(GO_ARGS) -o commands src/commands/commands.go
|
||||||
|
|
||||||
|
subcommands: $(SUBCOMMANDS)
|
||||||
|
|
||||||
|
subcommands/%: src/subcommands/*/%.go
|
||||||
|
go build $(GO_ARGS) -o $@ $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf commands subcommands triggers install post-delete
|
||||||
|
|
||||||
|
src-clean:
|
||||||
|
rm -rf .gitignore src triggers vendor Makefile *.go
|
||||||
|
|
||||||
|
triggers: $(TRIGGERS)
|
||||||
|
|
||||||
|
triggers/%: src/triggers/*/%.go
|
||||||
|
go build $(GO_ARGS) -o $@ $<
|
||||||
|
|
||||||
|
triggers-copy:
|
||||||
|
cp triggers/* .
|
||||||
3
plugins/resource/glide.yaml
Normal file
3
plugins/resource/glide.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package: .
|
||||||
|
import:
|
||||||
|
- package: github.com/ryanuber/columnize
|
||||||
4
plugins/resource/plugin.toml
Normal file
4
plugins/resource/plugin.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[plugin]
|
||||||
|
description = "dokku core resource plugin"
|
||||||
|
version = "0.14.6"
|
||||||
|
[plugin.config]
|
||||||
12
plugins/resource/resource.go
Normal file
12
plugins/resource/resource.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package resource
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
Cpu string `json:"cpu"`
|
||||||
|
Memory string `json:"memory"`
|
||||||
|
MemorySwap string `json:"memory-swap"`
|
||||||
|
Network string `json:"network"`
|
||||||
|
NetworkIngress string `json:"network-ingress"`
|
||||||
|
NetworkEgress string `json:"network-egress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginName = "resource"
|
||||||
63
plugins/resource/src/commands/commands.go
Normal file
63
plugins/resource/src/commands/commands.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dokku/dokku/plugins/common"
|
||||||
|
columnize "github.com/ryanuber/columnize"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
helpHeader = `Usage: dokku resource[:COMMAND]
|
||||||
|
|
||||||
|
Manages resource settings for an app
|
||||||
|
|
||||||
|
Additional commands:`
|
||||||
|
|
||||||
|
helpContent = `
|
||||||
|
resource:limit [--process-type process-type] [RESOURCE_OPTS...] <app>, Limit resources for a given app/process-type combination
|
||||||
|
resource:reserve [--process-type process-type] [RESOURCE_OPTS...] <app>, Reserve resources for a given app/process-type combination
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cmd := flag.Arg(0)
|
||||||
|
switch cmd {
|
||||||
|
case "resource", "resource:help":
|
||||||
|
usage()
|
||||||
|
case "help":
|
||||||
|
command := common.NewShellCmd(fmt.Sprintf("ps -o command= %d", os.Getppid()))
|
||||||
|
command.ShowOutput = false
|
||||||
|
output, err := command.Output()
|
||||||
|
|
||||||
|
if err == nil && strings.Contains(string(output), "--all") {
|
||||||
|
fmt.Println(helpContent)
|
||||||
|
} else {
|
||||||
|
fmt.Print("\n resource, Manages resource settings for an app\n")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable")
|
||||||
|
dokkuNotImplementExitCode = 10
|
||||||
|
}
|
||||||
|
os.Exit(dokkuNotImplementExitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
config := columnize.DefaultConfig()
|
||||||
|
config.Delim = ","
|
||||||
|
config.Prefix = " "
|
||||||
|
config.Empty = ""
|
||||||
|
content := strings.Split(helpContent, "\n")[1:]
|
||||||
|
fmt.Println(helpHeader)
|
||||||
|
fmt.Println(columnize.Format(content, config))
|
||||||
|
}
|
||||||
6
plugins/resource/src/glide.lock
generated
Normal file
6
plugins/resource/src/glide.lock
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149
|
||||||
|
updated: 2017-01-03T17:16:50.97156327-08:00
|
||||||
|
imports:
|
||||||
|
- name: github.com/ryanuber/columnize
|
||||||
|
version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360
|
||||||
|
testImports: []
|
||||||
3
plugins/resource/src/glide.yaml
Normal file
3
plugins/resource/src/glide.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package: .
|
||||||
|
import:
|
||||||
|
- package: github.com/ryanuber/columnize
|
||||||
35
plugins/resource/src/subcommands/limit/limit.go
Normal file
35
plugins/resource/src/subcommands/limit/limit.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/dokku/dokku/plugins/common"
|
||||||
|
"github.com/dokku/dokku/plugins/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := flag.NewFlagSet("resource:limit", flag.ExitOnError)
|
||||||
|
processeType := args.String("process-type", "", "process-type: A process type to manage")
|
||||||
|
cpu := args.String("cpu", "", "cpu: The amount of cpu to limit processes by")
|
||||||
|
memory := args.String("memory", "", "memory: The amount of memory to limit processes by")
|
||||||
|
memorySwap := args.String("memory-swap", "", "memory-swap: The amount of swap memory to limit processes by")
|
||||||
|
network := args.String("network", "", "network: The amount of network bandwidth to limit processes by")
|
||||||
|
networkIngress := args.String("network-ingress", "", "network-ingress: The amount of ingress network bandwidth to limit processes by")
|
||||||
|
networkEgress := args.String("network-egress", "", "network-egress: The amount of egress network bandwidth to limit processes by")
|
||||||
|
args.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
resources := resource.Resource{
|
||||||
|
Cpu: *cpu,
|
||||||
|
Memory: *memory,
|
||||||
|
MemorySwap: *memorySwap,
|
||||||
|
Network: *network,
|
||||||
|
NetworkIngress: *networkIngress,
|
||||||
|
NetworkEgress: *networkEgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := resource.CommandLimit(args.Args(), *processeType, resources)
|
||||||
|
if err != nil {
|
||||||
|
common.LogFail(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
35
plugins/resource/src/subcommands/reserve/reserve.go
Normal file
35
plugins/resource/src/subcommands/reserve/reserve.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/dokku/dokku/plugins/common"
|
||||||
|
"github.com/dokku/dokku/plugins/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := flag.NewFlagSet("resource:reserve", flag.ExitOnError)
|
||||||
|
processeType := args.String("process-type", "", "process-type: A process type to manage")
|
||||||
|
cpu := args.String("cpu", "", "cpu: The amount of cpu to reserve for processes")
|
||||||
|
memory := args.String("memory", "", "memory: The amount of memory to reserve for processes")
|
||||||
|
memorySwap := args.String("memory-swap", "", "memory-swap: The amount of swap memory to reserve for processes")
|
||||||
|
network := args.String("network", "", "network: The amount of network bandwidth to reserve for processes")
|
||||||
|
networkIngress := args.String("network-ingress", "", "network-ingress: The amount of ingress network bandwidth to reserve for processes")
|
||||||
|
networkEgress := args.String("network-egress", "", "network-egress: The amount of egress network bandwidth to reserve for processes")
|
||||||
|
args.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
resources := resource.Resource{
|
||||||
|
Cpu: *cpu,
|
||||||
|
Memory: *memory,
|
||||||
|
MemorySwap: *memorySwap,
|
||||||
|
Network: *network,
|
||||||
|
NetworkIngress: *networkIngress,
|
||||||
|
NetworkEgress: *networkEgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := resource.CommandReserve(args.Args(), *processeType, resources)
|
||||||
|
if err != nil {
|
||||||
|
common.LogFail(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
14
plugins/resource/src/triggers/install/install.go
Normal file
14
plugins/resource/src/triggers/install/install.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dokku/dokku/plugins/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runs the install step for the resource plugin
|
||||||
|
func main() {
|
||||||
|
if err := common.PropertySetup("resource"); err != nil {
|
||||||
|
common.LogFail(fmt.Sprintf("Unable to install the resource plugin: %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
18
plugins/resource/src/triggers/post-delete/post-delete.go
Normal file
18
plugins/resource/src/triggers/post-delete/post-delete.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"github.com/dokku/dokku/plugins/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// destroys the resource property for a given app container
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
appName := flag.Arg(0)
|
||||||
|
|
||||||
|
err := common.PropertyDestroy("resource", appName)
|
||||||
|
if err != nil {
|
||||||
|
common.LogFail(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
3
plugins/resource/src/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
3
plugins/resource/src/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
20
plugins/resource/src/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
20
plugins/resource/src/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2016 Ryan Uber
|
||||||
|
|
||||||
|
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.
|
||||||
69
plugins/resource/src/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
69
plugins/resource/src/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
Columnize
|
||||||
|
=========
|
||||||
|
|
||||||
|
Easy column-formatted output for golang
|
||||||
|
|
||||||
|
[](https://travis-ci.org/ryanuber/columnize)
|
||||||
|
[](https://godoc.org/github.com/ryanuber/columnize)
|
||||||
|
|
||||||
|
Columnize is a really small Go package that makes building CLI's a little bit
|
||||||
|
easier. In some CLI designs, you want to output a number similar items in a
|
||||||
|
human-readable way with nicely aligned columns. However, figuring out how wide
|
||||||
|
to make each column is a boring problem to solve and eats your valuable time.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ryanuber/columnize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
output := []string{
|
||||||
|
"Name | Gender | Age",
|
||||||
|
"Bob | Male | 38",
|
||||||
|
"Sally | Female | 26",
|
||||||
|
}
|
||||||
|
result := columnize.SimpleFormat(output)
|
||||||
|
fmt.Println(result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, you just pass in a list of strings. And the result:
|
||||||
|
|
||||||
|
```
|
||||||
|
Name Gender Age
|
||||||
|
Bob Male 38
|
||||||
|
Sally Female 26
|
||||||
|
```
|
||||||
|
|
||||||
|
Columnize is tolerant of missing or empty fields, or even empty lines, so
|
||||||
|
passing in extra lines for spacing should show up as you would expect.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Columnize is configured using a `Config`, which can be obtained by calling the
|
||||||
|
`DefaultConfig()` method. You can then tweak the settings in the resulting
|
||||||
|
`Config`:
|
||||||
|
|
||||||
|
```
|
||||||
|
config := columnize.DefaultConfig()
|
||||||
|
config.Delim = "|"
|
||||||
|
config.Glue = " "
|
||||||
|
config.Prefix = ""
|
||||||
|
config.Empty = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
* `Delim` is the string by which columns of **input** are delimited
|
||||||
|
* `Glue` is the string by which columns of **output** are delimited
|
||||||
|
* `Prefix` is a string by which each line of **output** is prefixed
|
||||||
|
* `Empty` is a string used to replace blank values found in output
|
||||||
|
|
||||||
|
You can then pass the `Config` in using the `Format` method (signature below) to
|
||||||
|
have text formatted to your liking.
|
||||||
|
|
||||||
|
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.
|
||||||
178
plugins/resource/src/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
178
plugins/resource/src/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package columnize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config can be used to tune certain parameters which affect the way
|
||||||
|
// in which Columnize will format output text.
|
||||||
|
type Config struct {
|
||||||
|
// The string by which the lines of input will be split.
|
||||||
|
Delim string
|
||||||
|
|
||||||
|
// The string by which columns of output will be separated.
|
||||||
|
Glue string
|
||||||
|
|
||||||
|
// The string by which columns of output will be prefixed.
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
// A replacement string to replace empty fields
|
||||||
|
Empty string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a *Config with default values.
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Delim: "|",
|
||||||
|
Glue: " ",
|
||||||
|
Prefix: "",
|
||||||
|
Empty: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeConfig merges two config objects together and returns the resulting
|
||||||
|
// configuration. Values from the right take precedence over the left side.
|
||||||
|
func MergeConfig(a, b *Config) *Config {
|
||||||
|
var result Config = *a
|
||||||
|
|
||||||
|
// Return quickly if either side was nil
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Delim != "" {
|
||||||
|
result.Delim = b.Delim
|
||||||
|
}
|
||||||
|
if b.Glue != "" {
|
||||||
|
result.Glue = b.Glue
|
||||||
|
}
|
||||||
|
if b.Prefix != "" {
|
||||||
|
result.Prefix = b.Prefix
|
||||||
|
}
|
||||||
|
if b.Empty != "" {
|
||||||
|
result.Empty = b.Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringFormat, given a set of column widths and the number of columns in
|
||||||
|
// the current line, returns a sprintf-style format string which can be used
|
||||||
|
// to print output aligned properly with other lines using the same widths set.
|
||||||
|
func stringFormat(c *Config, widths []int, columns int) string {
|
||||||
|
// Create the buffer with an estimate of the length
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
|
||||||
|
|
||||||
|
// Start with the prefix, if any was given. The buffer will not return an
|
||||||
|
// error so it does not need to be handled
|
||||||
|
buf.WriteString(c.Prefix)
|
||||||
|
|
||||||
|
// Create the format string from the discovered widths
|
||||||
|
for i := 0; i < columns && i < len(widths); i++ {
|
||||||
|
if i == columns-1 {
|
||||||
|
buf.WriteString("%s\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// elementsFromLine returns a list of elements, each representing a single
|
||||||
|
// item which will belong to a column of output.
|
||||||
|
func elementsFromLine(config *Config, line string) []interface{} {
|
||||||
|
seperated := strings.Split(line, config.Delim)
|
||||||
|
elements := make([]interface{}, len(seperated))
|
||||||
|
for i, field := range seperated {
|
||||||
|
value := strings.TrimSpace(field)
|
||||||
|
|
||||||
|
// Apply the empty value, if configured.
|
||||||
|
if value == "" && config.Empty != "" {
|
||||||
|
value = config.Empty
|
||||||
|
}
|
||||||
|
elements[i] = value
|
||||||
|
}
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
// runeLen calculates the number of visible "characters" in a string
|
||||||
|
func runeLen(s string) int {
|
||||||
|
l := 0
|
||||||
|
for _ = range s {
|
||||||
|
l++
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// widthsFromLines examines a list of strings and determines how wide each
|
||||||
|
// column should be considering all of the elements that need to be printed
|
||||||
|
// within it.
|
||||||
|
func widthsFromLines(config *Config, lines []string) []int {
|
||||||
|
widths := make([]int, 0, 8)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
elems := elementsFromLine(config, line)
|
||||||
|
for i := 0; i < len(elems); i++ {
|
||||||
|
l := runeLen(elems[i].(string))
|
||||||
|
if len(widths) <= i {
|
||||||
|
widths = append(widths, l)
|
||||||
|
} else if widths[i] < l {
|
||||||
|
widths[i] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widths
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format is the public-facing interface that takes a list of strings and
|
||||||
|
// returns nicely aligned column-formatted text.
|
||||||
|
func Format(lines []string, config *Config) string {
|
||||||
|
conf := MergeConfig(DefaultConfig(), config)
|
||||||
|
widths := widthsFromLines(conf, lines)
|
||||||
|
|
||||||
|
// Estimate the buffer size
|
||||||
|
glueSize := len(conf.Glue)
|
||||||
|
var size int
|
||||||
|
for _, w := range widths {
|
||||||
|
size += w + glueSize
|
||||||
|
}
|
||||||
|
size *= len(lines)
|
||||||
|
|
||||||
|
// Create the buffer
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, size))
|
||||||
|
|
||||||
|
// Create a cache for the string formats
|
||||||
|
fmtCache := make(map[int]string, 16)
|
||||||
|
|
||||||
|
// Create the formatted output using the format string
|
||||||
|
for _, line := range lines {
|
||||||
|
elems := elementsFromLine(conf, line)
|
||||||
|
|
||||||
|
// Get the string format using cache
|
||||||
|
numElems := len(elems)
|
||||||
|
stringfmt, ok := fmtCache[numElems]
|
||||||
|
if !ok {
|
||||||
|
stringfmt = stringFormat(conf, widths, numElems)
|
||||||
|
fmtCache[numElems] = stringfmt
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(buf, stringfmt, elems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the string result
|
||||||
|
result := buf.String()
|
||||||
|
|
||||||
|
// Remove trailing newline without removing leading/trailing space
|
||||||
|
if n := len(result); n > 0 && result[n-1] == '\n' {
|
||||||
|
result = result[:n-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleFormat is a convenience function to format text with the defaults.
|
||||||
|
func SimpleFormat(lines []string) string {
|
||||||
|
return Format(lines, nil)
|
||||||
|
}
|
||||||
306
plugins/resource/src/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
306
plugins/resource/src/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package columnize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
crand "crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListOfStringsInput(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyLinesOutput(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLeadingSpacePreserved(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"| Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := " Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnWidthCalculator(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"Longer than A | Longer than B | Longer than C",
|
||||||
|
"short | short | short",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "Longer than A Longer than B Longer than C\n"
|
||||||
|
expected += "short short short"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||||
|
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||||
|
t.Fatalf("\n%s", printableProof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
|
||||||
|
"short | short | short",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
|
||||||
|
expected += "short short short"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||||
|
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||||
|
t.Fatalf("\n%s", printableProof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkColumnWidthCalculator(b *testing.B) {
|
||||||
|
// Generate the input
|
||||||
|
input := []string{
|
||||||
|
"UUID A | UUID B | UUID C | Column D | Column E",
|
||||||
|
}
|
||||||
|
|
||||||
|
format := "%s|%s|%s|%s"
|
||||||
|
short := "short"
|
||||||
|
|
||||||
|
uuid := func() string {
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
if _, err := crand.Read(buf); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to read random bytes: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
|
||||||
|
buf[0:4],
|
||||||
|
buf[4:6],
|
||||||
|
buf[6:8],
|
||||||
|
buf[8:10],
|
||||||
|
buf[10:16])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
|
||||||
|
input = append(input, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Format(input, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVariedInputSpacing(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A |Column B| Column C",
|
||||||
|
"x|y| z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmatchedColumnCounts(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"Value A | Value B",
|
||||||
|
"Value A | Value B | Value C | Value D",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "Value A Value B\n"
|
||||||
|
expected += "Value A Value B Value C Value D"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlternateDelimiter(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column | A % Column | B % Column | C",
|
||||||
|
"Value A % Value B % Value C",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Delim = "%"
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column | A Column | B Column | C\n"
|
||||||
|
expected += "Value A Value B Value C"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlternateSpacingString(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Glue = " "
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleFormat(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
output := SimpleFormat(input)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlternatePrefixString(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Prefix = " "
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := " Column A Column B Column C\n"
|
||||||
|
expected += " x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyFieldReplacement(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Empty = "<none>"
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x <none> z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyConfigValues(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Config{}
|
||||||
|
output := Format(input, &config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeConfig(t *testing.T) {
|
||||||
|
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
|
||||||
|
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
|
||||||
|
conf3 := &Config{Delim: "c", Prefix: "c"}
|
||||||
|
|
||||||
|
m := MergeConfig(conf1, conf2)
|
||||||
|
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = MergeConfig(conf1, conf3)
|
||||||
|
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = MergeConfig(conf1, nil)
|
||||||
|
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = MergeConfig(conf1, &Config{})
|
||||||
|
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
83
plugins/resource/subcommands.go
Normal file
83
plugins/resource/subcommands.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/dokku/dokku/plugins/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandLimit implements resource:limit
|
||||||
|
func CommandLimit(args []string, processType string, r Resource) (err error) {
|
||||||
|
return setRequestType(args, processType, r, "limit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandReserve implements resource:reserve
|
||||||
|
func CommandReserve(args []string, processType string, r Resource) (err error) {
|
||||||
|
return setRequestType(args, processType, r, "reserve")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRequestType(args []string, processType string, r Resource, requestType string) (err error) {
|
||||||
|
var appName string
|
||||||
|
appName, err = getAppName(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = common.VerifyAppName(appName); err != nil {
|
||||||
|
common.LogFail(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(processType) == 0 {
|
||||||
|
processType = "_all_"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := map[string]string{
|
||||||
|
"cpu": r.Cpu,
|
||||||
|
"memory": r.Memory,
|
||||||
|
"memory-swap": r.MemorySwap,
|
||||||
|
"network": r.Network,
|
||||||
|
"network-ingress": r.NetworkIngress,
|
||||||
|
"network-egress": r.NetworkEgress,
|
||||||
|
}
|
||||||
|
|
||||||
|
hasValues := false
|
||||||
|
for _, value := range resources {
|
||||||
|
if value != "" {
|
||||||
|
hasValues = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasValues {
|
||||||
|
return errors.New("Please specify a resource to modify")
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestType == "limit" {
|
||||||
|
common.LogInfo2Quiet(fmt.Sprintf("Setting resource limits for %v", appName))
|
||||||
|
} else if requestType == "reserve" {
|
||||||
|
common.LogInfo2Quiet(fmt.Sprintf("Setting resource reservation for %v", appName))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range resources {
|
||||||
|
if value != "" {
|
||||||
|
common.LogVerbose(fmt.Sprintf("%v: %v", key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
property := fmt.Sprintf("%v.%v.%v", processType, requestType, key)
|
||||||
|
err = common.PropertyWrite(PluginName, appName, property, value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppName(args []string) (appName string, err error) {
|
||||||
|
if len(args) >= 1 {
|
||||||
|
appName = args[0]
|
||||||
|
} else {
|
||||||
|
err = errors.New("Please specify an app to run the command on")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
3
plugins/resource/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
3
plugins/resource/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
20
plugins/resource/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
20
plugins/resource/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2016 Ryan Uber
|
||||||
|
|
||||||
|
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.
|
||||||
69
plugins/resource/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
69
plugins/resource/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
Columnize
|
||||||
|
=========
|
||||||
|
|
||||||
|
Easy column-formatted output for golang
|
||||||
|
|
||||||
|
[](https://travis-ci.org/ryanuber/columnize)
|
||||||
|
[](https://godoc.org/github.com/ryanuber/columnize)
|
||||||
|
|
||||||
|
Columnize is a really small Go package that makes building CLI's a little bit
|
||||||
|
easier. In some CLI designs, you want to output a number similar items in a
|
||||||
|
human-readable way with nicely aligned columns. However, figuring out how wide
|
||||||
|
to make each column is a boring problem to solve and eats your valuable time.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ryanuber/columnize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
output := []string{
|
||||||
|
"Name | Gender | Age",
|
||||||
|
"Bob | Male | 38",
|
||||||
|
"Sally | Female | 26",
|
||||||
|
}
|
||||||
|
result := columnize.SimpleFormat(output)
|
||||||
|
fmt.Println(result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, you just pass in a list of strings. And the result:
|
||||||
|
|
||||||
|
```
|
||||||
|
Name Gender Age
|
||||||
|
Bob Male 38
|
||||||
|
Sally Female 26
|
||||||
|
```
|
||||||
|
|
||||||
|
Columnize is tolerant of missing or empty fields, or even empty lines, so
|
||||||
|
passing in extra lines for spacing should show up as you would expect.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Columnize is configured using a `Config`, which can be obtained by calling the
|
||||||
|
`DefaultConfig()` method. You can then tweak the settings in the resulting
|
||||||
|
`Config`:
|
||||||
|
|
||||||
|
```
|
||||||
|
config := columnize.DefaultConfig()
|
||||||
|
config.Delim = "|"
|
||||||
|
config.Glue = " "
|
||||||
|
config.Prefix = ""
|
||||||
|
config.Empty = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
* `Delim` is the string by which columns of **input** are delimited
|
||||||
|
* `Glue` is the string by which columns of **output** are delimited
|
||||||
|
* `Prefix` is a string by which each line of **output** is prefixed
|
||||||
|
* `Empty` is a string used to replace blank values found in output
|
||||||
|
|
||||||
|
You can then pass the `Config` in using the `Format` method (signature below) to
|
||||||
|
have text formatted to your liking.
|
||||||
|
|
||||||
|
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.
|
||||||
178
plugins/resource/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
178
plugins/resource/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package columnize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config can be used to tune certain parameters which affect the way
|
||||||
|
// in which Columnize will format output text.
|
||||||
|
type Config struct {
|
||||||
|
// The string by which the lines of input will be split.
|
||||||
|
Delim string
|
||||||
|
|
||||||
|
// The string by which columns of output will be separated.
|
||||||
|
Glue string
|
||||||
|
|
||||||
|
// The string by which columns of output will be prefixed.
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
// A replacement string to replace empty fields
|
||||||
|
Empty string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a *Config with default values.
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Delim: "|",
|
||||||
|
Glue: " ",
|
||||||
|
Prefix: "",
|
||||||
|
Empty: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeConfig merges two config objects together and returns the resulting
|
||||||
|
// configuration. Values from the right take precedence over the left side.
|
||||||
|
func MergeConfig(a, b *Config) *Config {
|
||||||
|
var result Config = *a
|
||||||
|
|
||||||
|
// Return quickly if either side was nil
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Delim != "" {
|
||||||
|
result.Delim = b.Delim
|
||||||
|
}
|
||||||
|
if b.Glue != "" {
|
||||||
|
result.Glue = b.Glue
|
||||||
|
}
|
||||||
|
if b.Prefix != "" {
|
||||||
|
result.Prefix = b.Prefix
|
||||||
|
}
|
||||||
|
if b.Empty != "" {
|
||||||
|
result.Empty = b.Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringFormat, given a set of column widths and the number of columns in
|
||||||
|
// the current line, returns a sprintf-style format string which can be used
|
||||||
|
// to print output aligned properly with other lines using the same widths set.
|
||||||
|
func stringFormat(c *Config, widths []int, columns int) string {
|
||||||
|
// Create the buffer with an estimate of the length
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
|
||||||
|
|
||||||
|
// Start with the prefix, if any was given. The buffer will not return an
|
||||||
|
// error so it does not need to be handled
|
||||||
|
buf.WriteString(c.Prefix)
|
||||||
|
|
||||||
|
// Create the format string from the discovered widths
|
||||||
|
for i := 0; i < columns && i < len(widths); i++ {
|
||||||
|
if i == columns-1 {
|
||||||
|
buf.WriteString("%s\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// elementsFromLine returns a list of elements, each representing a single
|
||||||
|
// item which will belong to a column of output.
|
||||||
|
func elementsFromLine(config *Config, line string) []interface{} {
|
||||||
|
seperated := strings.Split(line, config.Delim)
|
||||||
|
elements := make([]interface{}, len(seperated))
|
||||||
|
for i, field := range seperated {
|
||||||
|
value := strings.TrimSpace(field)
|
||||||
|
|
||||||
|
// Apply the empty value, if configured.
|
||||||
|
if value == "" && config.Empty != "" {
|
||||||
|
value = config.Empty
|
||||||
|
}
|
||||||
|
elements[i] = value
|
||||||
|
}
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
// runeLen calculates the number of visible "characters" in a string
|
||||||
|
func runeLen(s string) int {
|
||||||
|
l := 0
|
||||||
|
for _ = range s {
|
||||||
|
l++
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// widthsFromLines examines a list of strings and determines how wide each
|
||||||
|
// column should be considering all of the elements that need to be printed
|
||||||
|
// within it.
|
||||||
|
func widthsFromLines(config *Config, lines []string) []int {
|
||||||
|
widths := make([]int, 0, 8)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
elems := elementsFromLine(config, line)
|
||||||
|
for i := 0; i < len(elems); i++ {
|
||||||
|
l := runeLen(elems[i].(string))
|
||||||
|
if len(widths) <= i {
|
||||||
|
widths = append(widths, l)
|
||||||
|
} else if widths[i] < l {
|
||||||
|
widths[i] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widths
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format is the public-facing interface that takes a list of strings and
|
||||||
|
// returns nicely aligned column-formatted text.
|
||||||
|
func Format(lines []string, config *Config) string {
|
||||||
|
conf := MergeConfig(DefaultConfig(), config)
|
||||||
|
widths := widthsFromLines(conf, lines)
|
||||||
|
|
||||||
|
// Estimate the buffer size
|
||||||
|
glueSize := len(conf.Glue)
|
||||||
|
var size int
|
||||||
|
for _, w := range widths {
|
||||||
|
size += w + glueSize
|
||||||
|
}
|
||||||
|
size *= len(lines)
|
||||||
|
|
||||||
|
// Create the buffer
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, size))
|
||||||
|
|
||||||
|
// Create a cache for the string formats
|
||||||
|
fmtCache := make(map[int]string, 16)
|
||||||
|
|
||||||
|
// Create the formatted output using the format string
|
||||||
|
for _, line := range lines {
|
||||||
|
elems := elementsFromLine(conf, line)
|
||||||
|
|
||||||
|
// Get the string format using cache
|
||||||
|
numElems := len(elems)
|
||||||
|
stringfmt, ok := fmtCache[numElems]
|
||||||
|
if !ok {
|
||||||
|
stringfmt = stringFormat(conf, widths, numElems)
|
||||||
|
fmtCache[numElems] = stringfmt
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(buf, stringfmt, elems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the string result
|
||||||
|
result := buf.String()
|
||||||
|
|
||||||
|
// Remove trailing newline without removing leading/trailing space
|
||||||
|
if n := len(result); n > 0 && result[n-1] == '\n' {
|
||||||
|
result = result[:n-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleFormat is a convenience function to format text with the defaults.
|
||||||
|
func SimpleFormat(lines []string) string {
|
||||||
|
return Format(lines, nil)
|
||||||
|
}
|
||||||
306
plugins/resource/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
306
plugins/resource/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package columnize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
crand "crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListOfStringsInput(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyLinesOutput(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLeadingSpacePreserved(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"| Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := " Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnWidthCalculator(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"Longer than A | Longer than B | Longer than C",
|
||||||
|
"short | short | short",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "Longer than A Longer than B Longer than C\n"
|
||||||
|
expected += "short short short"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||||
|
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||||
|
t.Fatalf("\n%s", printableProof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
|
||||||
|
"short | short | short",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
|
||||||
|
expected += "short short short"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||||
|
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||||
|
t.Fatalf("\n%s", printableProof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkColumnWidthCalculator(b *testing.B) {
|
||||||
|
// Generate the input
|
||||||
|
input := []string{
|
||||||
|
"UUID A | UUID B | UUID C | Column D | Column E",
|
||||||
|
}
|
||||||
|
|
||||||
|
format := "%s|%s|%s|%s"
|
||||||
|
short := "short"
|
||||||
|
|
||||||
|
uuid := func() string {
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
if _, err := crand.Read(buf); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to read random bytes: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
|
||||||
|
buf[0:4],
|
||||||
|
buf[4:6],
|
||||||
|
buf[6:8],
|
||||||
|
buf[8:10],
|
||||||
|
buf[10:16])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
|
||||||
|
input = append(input, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Format(input, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVariedInputSpacing(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A |Column B| Column C",
|
||||||
|
"x|y| z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmatchedColumnCounts(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"Value A | Value B",
|
||||||
|
"Value A | Value B | Value C | Value D",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "Value A Value B\n"
|
||||||
|
expected += "Value A Value B Value C Value D"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlternateDelimiter(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column | A % Column | B % Column | C",
|
||||||
|
"Value A % Value B % Value C",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Delim = "%"
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column | A Column | B Column | C\n"
|
||||||
|
expected += "Value A Value B Value C"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlternateSpacingString(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Glue = " "
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleFormat(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
output := SimpleFormat(input)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlternatePrefixString(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Prefix = " "
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := " Column A Column B Column C\n"
|
||||||
|
expected += " x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyFieldReplacement(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.Empty = "<none>"
|
||||||
|
output := Format(input, config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x <none> z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyConfigValues(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"Column A | Column B | Column C",
|
||||||
|
"x | y | z",
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Config{}
|
||||||
|
output := Format(input, &config)
|
||||||
|
|
||||||
|
expected := "Column A Column B Column C\n"
|
||||||
|
expected += "x y z"
|
||||||
|
|
||||||
|
if output != expected {
|
||||||
|
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeConfig(t *testing.T) {
|
||||||
|
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
|
||||||
|
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
|
||||||
|
conf3 := &Config{Delim: "c", Prefix: "c"}
|
||||||
|
|
||||||
|
m := MergeConfig(conf1, conf2)
|
||||||
|
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = MergeConfig(conf1, conf3)
|
||||||
|
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = MergeConfig(conf1, nil)
|
||||||
|
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = MergeConfig(conf1, &Config{})
|
||||||
|
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
|
||||||
|
t.Fatalf("bad: %#v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user