Merge pull request #3413 from dokku/3256-buildpack-management

Implement buildpacks plugin
This commit is contained in:
Jose Diaz-Gonzalez
2019-03-05 22:22:56 -05:00
committed by GitHub
40 changed files with 2363 additions and 78 deletions

View File

@@ -1,13 +1,165 @@
# Buildpack Deployment
> Subcommands new as of 0.15.0
```
buildpacks:add [--index 1] <app> <buildpack> # Add new app buildpack while inserting into list of buildpacks if necessary
buildpacks:clear <app> # Clear all buildpacks set on the app
buildpacks:list <app> # List all buildpacks for an app
buildpacks:remove <app> <buildpack> # Remove a buildpack set on the app
buildpacks:report [<app>] [<flag>] # Displays a buildpack report for one or more apps
buildpacks:set [--index 1] <app> <buildpack> # Set new app buildpack at a given position defaulting to the first buildpack if no index is specified
```
> Warning: If using the `buildpacks` plugin, be sure to unset any `BUILDPACK_URL` and remove any such entries from a committed `.env` file. A specified `BUILDPACK_URL` will always override a `.buildpacks` file or the buildpacks plugin.
Dokku normally defaults to using [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) for deployment, though this may be overridden by committing a valid `Dockerfile` to the root of your repository and pushing the repository to your Dokku installation. To avoid this automatic `Dockerfile` deployment detection, you may do one of the following:
- Use `dokku config:set` to set the `BUILDPACK_URL` environment variable.
- Add `BUILDPACK_URL` to a committed `.env` file in the root of your repository.
- See the [environment variable documentation](/docs/configuration/environment-variables.md) for more details.
- Set a `BUILDPACK_URL` environment variable
- This can be done via `dokku config:set` or via a committed `.env` file in the root of the repository. See the [environment variable documentation](/docs/configuration/environment-variables.md) for more details.
- Create a `.buildpacks` file in the root of your repository.
- This can be via a committed `.buildpacks` file or managed via the `buildpacks` plugin commands.
## Switching from Dockerfile deployments
This page will cover usage of the `buildpacks` plugin.
## Usage
### Listing Buildpacks in Use
The `buildpacks:list` command can be used to show buildpacks that have been set for an app. This will omit any auto-detected buildpacks.
```shell
# running for an app with no buildpacks specified
dokku buildpacks:list node-js-app
```
```
-----> test buildpack urls
```
```shell
# running for an app with two buildpacks specified
dokku buildpacks:list node-js-app
```
```
-----> test buildpack urls
https://github.com/heroku/heroku-buildpack-python.git
https://github.com/heroku/heroku-buildpack-nodejs.git
```
### Adding custom buildpacks
> Please check the documentation for your particular buildpack as you may need to include configuration files (such as a Procfile) in your project root.
To add a custom buildpack, use the `buildpacks:add` command:
```shell
dokku buildpacks:add node-js-app https://github.com/heroku/heroku-buildpack-nodejs.git
```
When no buildpacks are currently specified, the specified buildpack will be the only one executed for detection and compilation.
Multiple buildpacks may be specified by using the `buildpacks:add` command multiple times.
```shell
dokku buildpacks:add node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
dokku buildpacks:add node-js-app https://github.com/heroku/heroku-buildpack-nodejs.git
```
Buildpacks are executed in order, may be inserted at a specified index via the `--index` flag. This flag is specified starting at a 1-index value.
```shell
# will add the golang buildpack at the second position, bumping all proceeding ones by 1 position
dokku buildpacks:add --index 2 node-js-app https://github.com/heroku/heroku-buildpack-golang.git
```
### Overwriting a buildpack position
In some cases, it may be necessary to swap out a given buildpack. Rather than needing to re-specify each buildpack, the `buildpacks:set` command can be used to overwrite a buildpack at a given position.
```shell
dokku buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
```
By default, this will overwrite the _first_ buildpack specified. To specify an index, the `--index` flag may be used. This flag is specified starting at a 1-index value, and defaults to `1`.
```shell
# the following are equivalent commands
dokku buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
dokku buildpacks:set --index 1 node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
```
If the index specified is larger than the number of buildpacks currently configured, the buildpack will be appended to the end of the list.
```shell
dokku buildpacks:set --index 99 node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
```
### Removing a buildpack
> At least one of a buildpack or index must be specified
A single buildpack can be removed by name via the `buildpacks:remove` command.
```shell
dokku buildpacks:remove node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
```
Buildpacks can also be removed by index via the `--index` flag. This flag is specified starting at a 1-index value.
```shell
dokku buildpacks:remove node-js-app --index 1
```
### Clearing all buildpacks
> This does not affect automatically detected buildpacks, nor does it impact any specified `BUILDPACK_URL` environment variable.
The `buildpacks:clear` command can be used to clear all configured buildpacks for a specified app.
```shell
dokku buildpacks:clear node-js-app
```
### Displaying buildpack reports about an app
You can get a report about the app's buildpacks status using the `buildpacks:report` command:
```shell
dokku buildpacks:report
```
```
=====> node-js-app buildpacks information
Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git
=====> python-sample buildpacks information
Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git,https://github.com/heroku/heroku-buildpack-python.git
=====> ruby-sample buildpacks information
Buildpacks list:
```
You can run the command for a specific app also.
```shell
dokku buildpacks:report node-js-app
```
```
=====> node-js-app buildpacks information
Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git
```
You can pass flags which will output only the value of the specific information you want. For example:
```shell
dokku buildpacks:report node-js-app --buildpacks-list
```
## Errata
### Switching from Dockerfile deployments
If an application was previously deployed via Dockerfile, the following commands should be run before a buildpack deploy will succeed:
@@ -15,54 +167,25 @@ If an application was previously deployed via Dockerfile, the following commands
dokku config:unset --no-restart node-js-app DOKKU_DOCKERFILE_CMD DOKKU_DOCKERFILE_ENTRYPOINT DOKKU_PROXY_PORT_MAP
```
## Specifying a custom buildpack
In certain cases you may want to specify a custom buildpack. While Dokku uses Herokuish to support all the [official Heroku buildpacks](https://github.com/gliderlabs/herokuish#buildpacks), it is possible that the buildpack detection does not work well for your application. As well, you may wish to use a custom buildpack to handle specific application logic.
To use a specific buildpack, you can run the following Dokku command:
```shell
# replace REPOSITORY_URL with your buildpack's url
dokku config:set node-js-app BUILDPACK_URL=REPOSITORY_URL
# example: using a specific ruby buildpack version
dokku config:set node-js-app BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git#v142
```
Please check the documentation for your particular buildpack as you may need to include configuration files (such as a Procfile) in your project root.
## Using multiple buildpacks
You can only set a single buildpack using the `BUILDPACK_URL`, though there may be times when you wish to use multiple buildpacks. To do so, simply create a `.buildpacks` file in the base of your repository. This file should list all the buildpacks, one-per-line. For instance, if you wish to use both the `nodejs` and `ruby` buildpacks, your `.buildpacks` file should contain the following:
```shell
https://github.com/heroku/heroku-buildpack-nodejs.git#v87
https://github.com/heroku/heroku-buildpack-ruby.git#v142
```
### Using a specific buildpack version
> Always remember to pin your buildpack versions when using the multi-buildpacks method, or you may find deploys changing your deployed environment.
You may also choose to set just a single buildpack in this file, though that is up to you.
Please check the documentation for your particular buildpack(s) as you may need to include configuration files (such as a Procfile) in your project root.
## Using a specific buildpack version
As Dokku pins all buildpacks via Herokuish releases, there may be occasions where a local buildpack version is out of date. If you wish to use a more recent version of the buildpack, you may use any of the above methods to specify a buildpack *without* the Git SHA attached like so:
By default, Dokku uses the [gliderlabs/herokuish](https://github.com/gliderlabs/herokuish/) project, which pins all of it's vendored buildpacks. There may be occasions where the pinned version results in a broken deploy, or does not have a particular feature that is required to build your project. To use a more recent version of a given buildpack, the buildpack may be specified *without* a Git commit SHA like so:
```shell
# using the latest nodejs buildpack
dokku config:set node-js-app BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-nodejs
dokku buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs
```
You may also wish to use a *specific* version of a buildpack, which is also simple
This will use the latest commit on the `master` branch of the specified buildpack. To pin to a newer version of a buildpack, a sha may also be specified by using the form `REPOSITORY_URL#COMMIT_SHA`, where `COMMIT_SHA` is any tree-ish git object - usually a git tag.
```shell
# using v87 of the nodejs buildpack
dokku config:set node-js-app BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-nodejs#v87
dokku buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs#v87
```
## Specifying commands via Procfile
### Specifying commands via Procfile
While many buildpacks have a default command that is run when a detected repository is pushed, it is possible to override this command via a Procfile. A Procfile can also be used to specify multiple commands, each of which is subject to process scaling. See the [process scaling documentation](/docs/deployment/process-management.md) for more details around scaling individual processes.
@@ -87,17 +210,18 @@ importantworker: env QUEUE=important bundle exec rake resque:work
The `web` process type holds some significance in that it is the only process type that is automatically scaled to `1` on the initial application deploy. See the [process scaling documentation](/docs/deployment/process-management.md) for more details around scaling individual processes.
## cURL build timeouts
Certain buildpacks may time out in retrieving dependencies via cURL. This can happen when your network connection is poor or if there is significant network congestion. You may see a message similar to `gzip: stdin: unexpected end of file` after a cURL command.
### `curl` build timeouts
If you see output similar this when deploying , you may need to override the cURL timeouts to increase the length of time allotted to those tasks. You can do so via the `config` plugin:
Certain buildpacks may time out in retrieving dependencies via `curl`. This can happen when your network connection is poor or if there is significant network congestion. You may see a message similar to `gzip: stdin: unexpected end of file` after a `curl` command.
If you see output similar this when deploying , you may need to override the `curl` timeouts to increase the length of time allotted to those tasks. You can do so via the `config` plugin:
```shell
dokku config:set --global CURL_TIMEOUT=1200
dokku config:set --global CURL_CONNECT_TIMEOUT=180
```
## Clearing buildpack cache
### Clearing buildpack cache
See the [repository management documentation](/docs/advanced-usage/repository-management.md#clearing-app-cache).

6
plugins/buildpacks/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/commands
/subcommands/*
/triggers/*
/install
/post-delete
/report

View File

@@ -0,0 +1,37 @@
include ../../common.mk
GO_ARGS ?= -a
SUBCOMMANDS = subcommands/add subcommands/clear subcommands/list subcommands/remove subcommands/report subcommands/set
TRIGGERS = triggers/install triggers/post-delete triggers/post-extract triggers/report
build-in-docker: clean
docker run --rm \
-v $$PWD/../..:$(GO_REPO_ROOT) \
-w $(GO_REPO_ROOT)/plugins/buildpacks \
$(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 post-extract report
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/* .

View File

@@ -0,0 +1,61 @@
package buildpacks
import (
"fmt"
"os"
"reflect"
"strings"
"github.com/dokku/dokku/plugins/common"
)
// ReportSingleApp is an internal function that displays the app report for one or more apps
func ReportSingleApp(appName, infoFlag string) {
if err := common.VerifyAppName(appName); err != nil {
common.LogFail(err.Error())
}
buildpacks, err := common.PropertyListGet("buildpacks", appName, "buildpacks")
if err != nil {
common.LogFail(err.Error())
}
infoFlags := map[string]string{
"--buildpacks-list": strings.Join(buildpacks, ","),
}
if len(infoFlag) == 0 {
common.LogInfo2Quiet(fmt.Sprintf("%s buildpacks information", appName))
for k, v := range infoFlags {
key := common.UcFirst(strings.Replace(strings.TrimPrefix(k, "--"), "-", " ", -1))
common.LogVerbose(fmt.Sprintf("%s%s", Right(fmt.Sprintf("%s:", key), 31, " "), v))
}
return
}
for k, v := range infoFlags {
if infoFlag == k {
fmt.Fprintln(os.Stdout, v)
return
}
}
keys := reflect.ValueOf(infoFlags).MapKeys()
strkeys := make([]string, len(keys))
for i := 0; i < len(keys); i++ {
strkeys[i] = keys[i].String()
}
common.LogFail(fmt.Sprintf("Invalid flag passed, valid flags: %s", strings.Join(strkeys, ", ")))
}
func times(str string, n int) (out string) {
for i := 0; i < n; i++ {
out += str
}
return
}
// Right right-pads the string with pad up to len runes
func Right(str string, length int, pad string) string {
return str + times(pad, length-len(str))
}

View File

@@ -0,0 +1,3 @@
package: .
import:
- package: github.com/ryanuber/columnize

View File

@@ -0,0 +1,4 @@
[plugin]
description = "dokku core buildpacks plugin"
version = "0.14.2"
[plugin.config]

View File

@@ -0,0 +1,67 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
columnize "github.com/ryanuber/columnize"
)
const (
helpHeader = `Usage: dokku buildpacks[:COMMAND]
Manages buildpacks settings for an app
Additional commands:`
helpContent = `
buildpacks:add [--index 1] <app> <buildpack>, Add new app buildpack while inserting into list of buildpacks if necessary
buildpacks:clear <app>, Clear all buildpacks set on the app
buildpacks:list <app>, List all buildpacks for an app
buildpacks:remove <app> <buildpack>, Remove a buildpack set on the app
buildpacks:report [<app>] [<flag>], Displays a buildpack report for one or more apps
buildpacks:set [--index 1] <app> <buildpack>, Set new app buildpack at a given position defaulting to the first buildpack if no index is specified
`
)
func main() {
flag.Usage = usage
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "buildpacks", "buildpacks: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 buildpacks, Manages buildpack 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/buildpacks/src/glide.lock generated Normal file
View 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: []

View File

@@ -0,0 +1,3 @@
package: .
import:
- package: github.com/ryanuber/columnize

View File

@@ -0,0 +1,19 @@
package main
import (
"flag"
"os"
"github.com/dokku/dokku/plugins/buildpacks"
"github.com/dokku/dokku/plugins/common"
)
func main() {
args := flag.NewFlagSet("buildpacks:add", flag.ExitOnError)
index := args.Int("index", 0, "--index: the 1-based index of the URL in the list of URLs")
args.Parse(os.Args[2:])
err := buildpacks.CommandAdd(args.Args(), *index)
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,18 @@
package main
import (
"flag"
"os"
"github.com/dokku/dokku/plugins/buildpacks"
"github.com/dokku/dokku/plugins/common"
)
func main() {
args := flag.NewFlagSet("buildpacks:clear", flag.ExitOnError)
args.Parse(os.Args[2:])
err := buildpacks.CommandClear(args.Args())
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,14 @@
package main
import (
"flag"
"os"
"github.com/dokku/dokku/plugins/buildpacks"
)
func main() {
args := flag.NewFlagSet("buildpacks:list", flag.ExitOnError)
args.Parse(os.Args[2:])
buildpacks.CommandList(args.Args())
}

View File

@@ -0,0 +1,19 @@
package main
import (
"flag"
"os"
"github.com/dokku/dokku/plugins/buildpacks"
"github.com/dokku/dokku/plugins/common"
)
func main() {
args := flag.NewFlagSet("buildpacks:remove", flag.ExitOnError)
index := args.Int("index", 0, "--index: the 1-based index of the URL in the list of URLs")
args.Parse(os.Args[2:])
err := buildpacks.CommandRemove(args.Args(), *index)
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,34 @@
package main
import (
"flag"
"strings"
"github.com/dokku/dokku/plugins/buildpacks"
"github.com/dokku/dokku/plugins/common"
)
// displays a buildpacks report for one or more apps
func main() {
flag.Parse()
appName := flag.Arg(1)
infoFlag := flag.Arg(2)
if strings.HasPrefix(appName, "--") {
infoFlag = appName
appName = ""
}
if len(appName) == 0 {
apps, err := common.DokkuApps()
if err != nil {
return
}
for _, appName := range apps {
buildpacks.ReportSingleApp(appName, infoFlag)
}
return
}
buildpacks.ReportSingleApp(appName, infoFlag)
}

View File

@@ -0,0 +1,19 @@
package main
import (
"flag"
"os"
"github.com/dokku/dokku/plugins/buildpacks"
"github.com/dokku/dokku/plugins/common"
)
func main() {
args := flag.NewFlagSet("buildpacks:set", flag.ExitOnError)
index := args.Int("index", 0, "--index: the 1-based index of the URL in the list of URLs")
args.Parse(os.Args[2:])
err := buildpacks.CommandSet(args.Args(), *index)
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,14 @@
package main
import (
"fmt"
"github.com/dokku/dokku/plugins/common"
)
// runs the install step for the buildpacks plugin
func main() {
if err := common.PropertySetup("buildpacks"); err != nil {
common.LogFail(fmt.Sprintf("Unable to install the buildpacks plugin: %s", err.Error()))
}
}

View File

@@ -0,0 +1,18 @@
package main
import (
"flag"
"github.com/dokku/dokku/plugins/common"
)
// destroys the buildpacks property for a given app container
func main() {
flag.Parse()
appName := flag.Arg(0)
err := common.PropertyDestroy("buildpacks", appName)
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,45 @@
package main
import (
"bufio"
"flag"
"fmt"
"os"
"path"
"github.com/dokku/dokku/plugins/common"
)
// writes a .buildpacks file into the app
func main() {
flag.Parse()
appName := flag.Arg(0)
tmpWorkDir := flag.Arg(1)
buildpacks, err := common.PropertyListGet("buildpacks", appName, "buildpacks")
if err != nil {
return
}
if len(buildpacks) == 0 {
return
}
buildpacksPath := path.Join(tmpWorkDir, ".buildpacks")
file, err := os.OpenFile(buildpacksPath, os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
common.LogFail(fmt.Sprintf("Error writing .buildpacks file: %s", err.Error()))
return
}
w := bufio.NewWriter(file)
for _, buildpack := range buildpacks {
fmt.Fprintln(w, buildpack)
}
if err = w.Flush(); err != nil {
common.LogFail(fmt.Sprintf("Error writing .buildpacks file: %s", err.Error()))
return
}
file.Chmod(0600)
}

View File

@@ -0,0 +1,15 @@
package main
import (
"flag"
"github.com/dokku/dokku/plugins/buildpacks"
)
// displays a buildpacks report for one or more apps
func main() {
flag.Parse()
appName := flag.Arg(0)
buildpacks.ReportSingleApp(appName, "")
}

View File

@@ -0,0 +1,3 @@
language: go
go:
- tip

View 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.

View File

@@ -0,0 +1,69 @@
Columnize
=========
Easy column-formatted output for golang
[![Build Status](https://travis-ci.org/ryanuber/columnize.svg)](https://travis-ci.org/ryanuber/columnize)
[![GoDoc](https://godoc.org/github.com/ryanuber/columnize?status.svg)](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.

View 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)
}

View 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)
}
}

View File

@@ -0,0 +1,154 @@
package buildpacks
import (
"errors"
"fmt"
"github.com/dokku/dokku/plugins/common"
)
// CommandAdd implements buildpacks:add
func CommandAdd(args []string, index int) (err error) {
var appName string
appName, err = getAppName(args)
if err != nil {
return
}
buildpack := ""
if len(args) >= 2 {
buildpack = args[1]
}
if buildpack == "" {
return errors.New("Must specify a buildpack to add")
}
err = common.PropertyListAdd("buildpacks", appName, "buildpacks", buildpack, index)
return
}
// CommandClear implements buildpacks:clear
func CommandClear(args []string) (err error) {
var appName string
appName, err = getAppName(args)
if err != nil {
return
}
common.PropertyDelete("buildpacks", appName, "buildpacks")
return
}
// CommandList implements buildpacks:list
func CommandList(args []string) (err error) {
var appName string
appName, err = getAppName(args)
if err != nil {
return
}
buildpacks, err := common.PropertyListGet("buildpacks", appName, "buildpacks")
if err != nil {
return
}
common.LogInfo1Quiet(fmt.Sprintf("%s buildpack urls", appName))
for _, buildpack := range buildpacks {
common.LogVerbose(buildpack)
}
return nil
}
// CommandRemove implements buildpacks:remove
func CommandRemove(args []string, index int) (err error) {
var appName string
appName, err = getAppName(args)
if err != nil {
return
}
buildpack := ""
if len(args) >= 2 {
buildpack = args[1]
}
if index != 0 && buildpack != "" {
err = errors.New("Please choose either index or Buildpack, but not both")
return
}
if index == 0 && buildpack == "" {
err = errors.New("Must specify a buildpack to remove, either by index or URL")
return
}
var buildpacks []string
buildpacks, err = common.PropertyListGet("buildpacks", appName, "buildpacks")
if err != nil {
return
}
if len(buildpacks) == 0 {
err = fmt.Errorf("No buildpacks were found, next release on %s will detect buildpack normally", appName)
return
}
if index != 0 {
var value string
value, err = common.PropertyListGetByIndex("buildpacks", appName, "buildpacks", index-1)
if err != nil {
return
}
buildpack = value
} else {
_, err = common.PropertyListGetByValue("buildpacks", appName, "buildpacks", buildpack)
if err != nil {
return
}
}
common.LogInfo1Quiet(fmt.Sprintf("Removing %s", buildpack))
err = common.PropertyListRemove("buildpacks", appName, "buildpacks", buildpack)
if err != nil {
return
}
return
}
// CommandSet implements buildpacks:set
func CommandSet(args []string, index int) (err error) {
var appName string
appName, err = getAppName(args)
if err != nil {
return
}
buildpack := ""
if len(args) >= 2 {
buildpack = args[1]
}
if buildpack == "" {
return errors.New("Must specify a buildpack to add")
}
if index > 0 {
index--
}
err = common.PropertyListSet("buildpacks", appName, "buildpacks", buildpack, index)
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
}

View File

@@ -0,0 +1,3 @@
language: go
go:
- tip

View 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.

View File

@@ -0,0 +1,69 @@
Columnize
=========
Easy column-formatted output for golang
[![Build Status](https://travis-ci.org/ryanuber/columnize.svg)](https://travis-ci.org/ryanuber/columnize)
[![GoDoc](https://godoc.org/github.com/ryanuber/columnize?status.svg)](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.

View 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)
}

View 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)
}
}

View File

@@ -19,7 +19,7 @@ func LogInfo1(text string) {
// LogInfo1Quiet is the info1 header formatter (with quiet option)
func LogInfo1Quiet(text string) {
if os.Getenv("DOKKU_QUIET_OUTPUT") != "" {
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
LogInfo1(text)
}
}
@@ -45,7 +45,7 @@ func LogVerbose(text string) {
// LogVerboseQuiet is the verbose log formatter
// prints indented text to stdout (with quiet option)
func LogVerboseQuiet(text string) {
if os.Getenv("DOKKU_QUIET_OUTPUT") != "" {
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
LogVerbose(text)
}
}

View File

@@ -1,10 +1,13 @@
package common
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"os"
"os/user"
"path"
"reflect"
"strconv"
"strings"
@@ -34,34 +37,37 @@ func CommandPropertySet(pluginName, appName, property, value string, properties
PropertyWrite(pluginName, appName, property, value)
} else {
LogInfo2Quiet(fmt.Sprintf("Unsetting %s", property))
PropertyDelete(pluginName, appName, property)
err := PropertyDelete(pluginName, appName, property)
if err != nil {
LogFail(err.Error())
}
}
}
// PropertyDelete deletes a property from the plugin properties for an app
func PropertyDelete(pluginName string, appName string, property string) {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/")
func PropertyDelete(pluginName string, appName string, property string) error {
propertyPath := getPropertyPath(pluginName, appName, property)
if err := os.Remove(propertyPath); err != nil {
LogFail(fmt.Sprintf("Unable to remove %s property %s.%s", pluginName, appName, property))
return fmt.Errorf("Unable to remove %s property %s.%s", pluginName, appName, property)
}
return nil
}
// PropertyDestroy destroys the plugin properties for an app
func PropertyDestroy(pluginName string, appName string) {
func PropertyDestroy(pluginName string, appName string) error {
if appName == "_all_" {
pluginConfigPath := getPluginConfigPath(pluginName)
os.RemoveAll(pluginConfigPath)
} else {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
os.RemoveAll(pluginAppConfigRoot)
return os.RemoveAll(pluginConfigPath)
}
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
return os.RemoveAll(pluginAppConfigRoot)
}
// PropertyExists returns whether a property exists or not
func PropertyExists(pluginName string, appName string, property string) bool {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/")
propertyPath := getPropertyPath(pluginName, appName, property)
_, err := os.Stat(propertyPath)
return !os.IsNotExist(err)
}
@@ -77,9 +83,7 @@ func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val
return
}
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/")
propertyPath := getPropertyPath(pluginName, appName, property)
b, err := ioutil.ReadFile(propertyPath)
if err != nil {
LogWarn(fmt.Sprintf("Unable to read %s property %s.%s", pluginName, appName, property))
@@ -89,23 +93,240 @@ func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val
return
}
// PropertyWrite writes a value for a given application property
func PropertyWrite(pluginName string, appName string, property string, value string) {
if err := makePropertyPath(pluginName, appName); err != nil {
LogFail(fmt.Sprintf("Unable to create %s config directory for %s: %s", pluginName, appName, err.Error()))
// PropertyListAdd adds a property to a list at an optionally specified index
func PropertyListAdd(pluginName string, appName string, property string, value string, index int) error {
if err := PropertyTouch(pluginName, appName, property); err != nil {
return err
}
scannedLines, err := PropertyListGet(pluginName, appName, property)
if err != nil {
return err
}
value = strings.TrimSpace(value)
var lines []string
for i, line := range scannedLines {
if index != 0 && i == (index-1) {
lines = append(lines, value)
}
lines = append(lines, line)
}
if index == 0 || index > len(scannedLines) {
lines = append(lines, value)
}
propertyPath := getPropertyPath(pluginName, appName, property)
file, err := os.OpenFile(propertyPath, os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
return err
}
w := bufio.NewWriter(file)
for _, line := range lines {
fmt.Fprintln(w, line)
}
if err = w.Flush(); err != nil {
return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
return nil
}
// PropertyListGet returns a property list
func PropertyListGet(pluginName string, appName string, property string) (lines []string, err error) {
if !PropertyExists(pluginName, appName, property) {
return lines, nil
}
propertyPath := getPropertyPath(pluginName, appName, property)
file, err := os.Open(propertyPath)
if err != nil {
return lines, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err = scanner.Err(); err != nil {
return lines, fmt.Errorf("Unable to read %s config value for %s.%s: %s", pluginName, appName, property, err.Error())
}
return lines, nil
}
// PropertyListGetByIndex returns an entry within property list by index
func PropertyListGetByIndex(pluginName string, appName string, property string, index int) (propertyValue string, err error) {
lines, err := PropertyListGet(pluginName, appName, property)
if err != nil {
return
}
found := false
for i, line := range lines {
if i == index {
propertyValue = line
found = true
}
}
if !found {
err = errors.New("Index not found")
}
return
}
// PropertyListGetByValue returns an entry within property list by value
func PropertyListGetByValue(pluginName string, appName string, property string, value string) (propertyValue string, err error) {
lines, err := PropertyListGet(pluginName, appName, property)
if err != nil {
return
}
found := false
for _, line := range lines {
if line == value {
propertyValue = line
found = true
}
}
if !found {
err = errors.New("Value not found")
}
return
}
// PropertyListRemove removes a value from a property list
func PropertyListRemove(pluginName string, appName string, property string, value string) error {
lines, err := PropertyListGet(pluginName, appName, property)
if err != nil {
return err
}
propertyPath := getPropertyPath(pluginName, appName, property)
file, err := os.OpenFile(propertyPath, os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
return err
}
found := false
w := bufio.NewWriter(file)
for _, line := range lines {
if line == value {
found = true
continue
}
fmt.Fprintln(w, line)
}
if err = w.Flush(); err != nil {
return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
if !found {
return errors.New("Property not found, nothing was removed")
}
return nil
}
// PropertyListSet sets a value within a property list at a specified index
func PropertyListSet(pluginName string, appName string, property string, value string, index int) error {
if err := PropertyTouch(pluginName, appName, property); err != nil {
return err
}
scannedLines, err := PropertyListGet(pluginName, appName, property)
if err != nil {
return err
}
value = strings.TrimSpace(value)
var lines []string
if index >= len(scannedLines) {
for _, line := range scannedLines {
lines = append(lines, line)
}
lines = append(lines, value)
} else {
for i, line := range scannedLines {
if i == index {
lines = append(lines, value)
} else {
lines = append(lines, line)
}
}
}
propertyPath := getPropertyPath(pluginName, appName, property)
file, err := os.OpenFile(propertyPath, os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
return err
}
w := bufio.NewWriter(file)
for _, line := range lines {
fmt.Fprintln(w, line)
}
if err = w.Flush(); err != nil {
return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
return nil
}
// PropertyTouch ensures a given application property file exists
func PropertyTouch(pluginName string, appName string, property string) error {
if err := makePluginAppPropertyPath(pluginName, appName); err != nil {
return fmt.Errorf("Unable to create %s config directory for %s: %s", pluginName, appName, err.Error())
}
propertyPath := getPropertyPath(pluginName, appName, property)
if PropertyExists(pluginName, appName, property) {
return nil
}
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/")
file, err := os.Create(propertyPath)
if err != nil {
LogFail(fmt.Sprintf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error()))
return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())
}
defer file.Close()
return nil
}
// PropertyWrite writes a value for a given application property
func PropertyWrite(pluginName string, appName string, property string, value string) error {
if err := PropertyTouch(pluginName, appName, property); err != nil {
return err
}
propertyPath := getPropertyPath(pluginName, appName, property)
file, err := os.Create(propertyPath)
if err != nil {
return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())
}
defer file.Close()
fmt.Fprintf(file, value)
file.Chmod(0600)
setPermissions(propertyPath, 0600)
return nil
}
// PropertySetup creates the plugin config root
@@ -117,18 +338,23 @@ func PropertySetup(pluginName string) (err error) {
return setPermissions(pluginConfigRoot, 0755)
}
func getPropertyPath(pluginName string, appName string, property string) string {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
return path.Join(pluginAppConfigRoot, property)
}
// getPluginAppPropertyPath returns the plugin property path for a given plugin/app combination
func getPluginAppPropertyPath(pluginName string, appName string) string {
return strings.Join([]string{getPluginConfigPath(pluginName), appName}, "/")
return path.Join(getPluginConfigPath(pluginName), appName)
}
// getPluginConfigPath returns the plugin property path for a given plugin
func getPluginConfigPath(pluginName string) string {
return strings.Join([]string{MustGetEnv("DOKKU_LIB_ROOT"), "config", pluginName}, "/")
return path.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config", pluginName)
}
// makePropertyPath ensures that a property path exists
func makePropertyPath(pluginName string, appName string) (err error) {
// makePluginAppPropertyPath ensures that a property path exists
func makePluginAppPropertyPath(pluginName string, appName string) (err error) {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
if err = os.MkdirAll(pluginAppConfigRoot, 0755); err != nil {
return

View File

@@ -67,11 +67,11 @@ func main() {
}
func usage() {
fmt.Println(helpHeader)
config := columnize.DefaultConfig()
config.Delim = ","
config.Prefix = " "
config.Empty = ""
content := strings.Split(helpContent, "\n")[1:]
fmt.Println(helpHeader)
fmt.Println(columnize.Format(content, config))
}

View File

@@ -55,11 +55,11 @@ func main() {
}
func usage() {
fmt.Println(helpHeader)
config := columnize.DefaultConfig()
config.Delim = ","
config.Prefix = " "
config.Empty = ""
content := strings.Split(helpContent, "\n")[1:]
fmt.Println(helpHeader)
fmt.Println(columnize.Format(content, config))
}

View File

@@ -23,10 +23,14 @@ func main() {
}
if proxy.IsAppProxyEnabled(appName) {
common.LogVerboseQuiet("Setting %s network property 'bind-all-interfaces' to false")
common.PropertyWrite("network", appName, "bind-all-interfaces", "false")
if err := common.PropertyWrite("network", appName, "bind-all-interfaces", "false"); err != nil {
common.LogWarn(err.Error())
}
} else {
common.LogVerboseQuiet("Setting %s network property 'bind-all-interfaces' to true")
common.PropertyWrite("network", appName, "bind-all-interfaces", "true")
if err := common.PropertyWrite("network", appName, "bind-all-interfaces", "true"); err != nil {
common.LogWarn(err.Error())
}
}
}
}

View File

@@ -11,5 +11,8 @@ func main() {
flag.Parse()
appName := flag.Arg(0)
common.PropertyWrite("network", appName, "bind-all-interfaces", "false")
err := common.PropertyWrite("network", appName, "bind-all-interfaces", "false")
if err != nil {
common.LogWarn(err.Error())
}
}

View File

@@ -11,5 +11,8 @@ func main() {
flag.Parse()
appName := flag.Arg(0)
common.PropertyDestroy("network", appName)
err := common.PropertyDestroy("network", appName)
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -53,11 +53,11 @@ func main() {
}
func usage() {
fmt.Println(helpHeader)
config := columnize.DefaultConfig()
config.Delim = ","
config.Prefix = " "
config.Empty = ""
content := strings.Split(helpContent, "\n")[1:]
fmt.Println(helpHeader)
fmt.Println(columnize.Format(content, config))
}

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
deploy_app
}
teardown() {
destroy_app
global_teardown
}
@test "(buildpacks) buildpacks:help" {
run /bin/bash -c "dokku buildpacks"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manages buildpacks settings for an app"
help_output="$output"
run /bin/bash -c "dokku buildpacks:help"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manages buildpacks settings for an app"
assert_output "$help_output"
}
@test "(buildpacks) buildpacks:add" {
run /bin/bash -c "dokku buildpacks:add $TEST_APP heroku/nodejs"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output_contains "heroku/nodejs"
run /bin/bash -c "dokku buildpacks:add $TEST_APP heroku/ruby"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/nodejs heroku/ruby"
run /bin/bash -c "dokku buildpacks:add --index 1 $TEST_APP heroku/golang"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/golang heroku/nodejs heroku/ruby"
run /bin/bash -c "dokku buildpacks:add --index 2 $TEST_APP heroku/python"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/golang heroku/python heroku/nodejs heroku/ruby"
run /bin/bash -c "dokku buildpacks:add --index 100 $TEST_APP heroku/php"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/golang heroku/python heroku/nodejs heroku/ruby heroku/php"
}
@test "(buildpacks) buildpacks:set" {
run /bin/bash -c "dokku buildpacks:set $TEST_APP heroku/nodejs"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output_contains "heroku/nodejs"
run /bin/bash -c "dokku buildpacks:set $TEST_APP heroku/ruby"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/ruby"
run /bin/bash -c "dokku buildpacks:set --index 1 $TEST_APP heroku/golang"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/golang"
run /bin/bash -c "dokku buildpacks:set --index 2 $TEST_APP heroku/python"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/golang heroku/python"
run /bin/bash -c "dokku buildpacks:set --index 100 $TEST_APP heroku/php"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output "heroku/golang heroku/python heroku/php"
}
@test "(buildpacks) buildpacks:remove" {
run /bin/bash -c "dokku buildpacks:set $TEST_APP heroku/nodejs"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku buildpacks:set --index 2 $TEST_APP heroku/ruby"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output_contains "heroku/nodejs heroku/ruby"
run /bin/bash -c "dokku buildpacks:remove $TEST_APP heroku/nodejs"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
echo "output: $output"
echo "status: $status"
assert_output_contains "heroku/ruby"
run /bin/bash -c "dokku buildpacks:remove $TEST_APP heroku/php"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku buildpacks:remove $TEST_APP heroku/ruby"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_output_not_exists
}
@test "(buildpacks) buildpacks:clear" {
run /bin/bash -c "dokku buildpacks:set $TEST_APP heroku/nodejs"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku buildpacks:set --index 2 $TEST_APP heroku/ruby"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku buildpacks:clear $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_output_not_exists
run /bin/bash -c "dokku buildpacks:clear $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku buildpacks:clear $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_output_not_exists
}

View File

@@ -82,6 +82,12 @@ assert_output_exists() {
[[ -n "$output" ]] || flunk "expected output, found none"
}
# ShellCheck doesn't know about $output from Bats
# shellcheck disable=SC2154
assert_output_not_exists() {
[[ -z "$output" ]] || flunk "expected no output, found some"
}
# ShellCheck doesn't know about $output from Bats
# shellcheck disable=SC2154
assert_output_contains() {