diff --git a/docs/deployment/methods/buildpacks.md b/docs/deployment/methods/buildpacks.md index 5114b7567..57a036240 100644 --- a/docs/deployment/methods/buildpacks.md +++ b/docs/deployment/methods/buildpacks.md @@ -1,13 +1,165 @@ # Buildpack Deployment +> Subcommands new as of 0.15.0 + +``` +buildpacks:add [--index 1] # Add new app buildpack while inserting into list of buildpacks if necessary +buildpacks:clear # Clear all buildpacks set on the app +buildpacks:list # List all buildpacks for an app +buildpacks:remove # Remove a buildpack set on the app +buildpacks:report [] [] # Displays a buildpack report for one or more apps +buildpacks:set [--index 1] # 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). diff --git a/plugins/buildpacks/.gitignore b/plugins/buildpacks/.gitignore new file mode 100644 index 000000000..56aebe3b7 --- /dev/null +++ b/plugins/buildpacks/.gitignore @@ -0,0 +1,6 @@ +/commands +/subcommands/* +/triggers/* +/install +/post-delete +/report diff --git a/plugins/buildpacks/Makefile b/plugins/buildpacks/Makefile new file mode 100644 index 000000000..635403632 --- /dev/null +++ b/plugins/buildpacks/Makefile @@ -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/* . diff --git a/plugins/buildpacks/buildpacks.go b/plugins/buildpacks/buildpacks.go new file mode 100644 index 000000000..be98fa556 --- /dev/null +++ b/plugins/buildpacks/buildpacks.go @@ -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)) +} diff --git a/plugins/buildpacks/glide.yaml b/plugins/buildpacks/glide.yaml new file mode 100644 index 000000000..3f7a96657 --- /dev/null +++ b/plugins/buildpacks/glide.yaml @@ -0,0 +1,3 @@ +package: . +import: +- package: github.com/ryanuber/columnize diff --git a/plugins/buildpacks/plugin.toml b/plugins/buildpacks/plugin.toml new file mode 100644 index 000000000..0b6f00dab --- /dev/null +++ b/plugins/buildpacks/plugin.toml @@ -0,0 +1,4 @@ +[plugin] +description = "dokku core buildpacks plugin" +version = "0.14.2" +[plugin.config] diff --git a/plugins/buildpacks/src/commands/commands.go b/plugins/buildpacks/src/commands/commands.go new file mode 100644 index 000000000..b4e836a12 --- /dev/null +++ b/plugins/buildpacks/src/commands/commands.go @@ -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] , Add new app buildpack while inserting into list of buildpacks if necessary + buildpacks:clear , Clear all buildpacks set on the app + buildpacks:list , List all buildpacks for an app + buildpacks:remove , Remove a buildpack set on the app + buildpacks:report [] [], Displays a buildpack report for one or more apps + buildpacks:set [--index 1] , 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)) +} diff --git a/plugins/buildpacks/src/glide.lock b/plugins/buildpacks/src/glide.lock new file mode 100644 index 000000000..fe4a98f07 --- /dev/null +++ b/plugins/buildpacks/src/glide.lock @@ -0,0 +1,6 @@ +hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149 +updated: 2017-01-03T17:16:50.97156327-08:00 +imports: +- name: github.com/ryanuber/columnize + version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360 +testImports: [] diff --git a/plugins/buildpacks/src/glide.yaml b/plugins/buildpacks/src/glide.yaml new file mode 100644 index 000000000..3f7a96657 --- /dev/null +++ b/plugins/buildpacks/src/glide.yaml @@ -0,0 +1,3 @@ +package: . +import: +- package: github.com/ryanuber/columnize diff --git a/plugins/buildpacks/src/subcommands/add/add.go b/plugins/buildpacks/src/subcommands/add/add.go new file mode 100644 index 000000000..8aa71e01b --- /dev/null +++ b/plugins/buildpacks/src/subcommands/add/add.go @@ -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()) + } +} diff --git a/plugins/buildpacks/src/subcommands/clear/clear.go b/plugins/buildpacks/src/subcommands/clear/clear.go new file mode 100644 index 000000000..32b60ba10 --- /dev/null +++ b/plugins/buildpacks/src/subcommands/clear/clear.go @@ -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()) + } +} diff --git a/plugins/buildpacks/src/subcommands/list/list.go b/plugins/buildpacks/src/subcommands/list/list.go new file mode 100644 index 000000000..44d56967e --- /dev/null +++ b/plugins/buildpacks/src/subcommands/list/list.go @@ -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()) +} diff --git a/plugins/buildpacks/src/subcommands/remove/remove.go b/plugins/buildpacks/src/subcommands/remove/remove.go new file mode 100644 index 000000000..b10875caa --- /dev/null +++ b/plugins/buildpacks/src/subcommands/remove/remove.go @@ -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()) + } +} diff --git a/plugins/buildpacks/src/subcommands/report/report.go b/plugins/buildpacks/src/subcommands/report/report.go new file mode 100644 index 000000000..507ae416d --- /dev/null +++ b/plugins/buildpacks/src/subcommands/report/report.go @@ -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) +} diff --git a/plugins/buildpacks/src/subcommands/set/set.go b/plugins/buildpacks/src/subcommands/set/set.go new file mode 100644 index 000000000..cf97ca5ce --- /dev/null +++ b/plugins/buildpacks/src/subcommands/set/set.go @@ -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()) + } +} diff --git a/plugins/buildpacks/src/triggers/install/install.go b/plugins/buildpacks/src/triggers/install/install.go new file mode 100644 index 000000000..9c3a8e092 --- /dev/null +++ b/plugins/buildpacks/src/triggers/install/install.go @@ -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())) + } +} diff --git a/plugins/buildpacks/src/triggers/post-delete/post-delete.go b/plugins/buildpacks/src/triggers/post-delete/post-delete.go new file mode 100644 index 000000000..3535d48bf --- /dev/null +++ b/plugins/buildpacks/src/triggers/post-delete/post-delete.go @@ -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()) + } +} diff --git a/plugins/buildpacks/src/triggers/post-extract/post-extract.go b/plugins/buildpacks/src/triggers/post-extract/post-extract.go new file mode 100644 index 000000000..67ec7bf85 --- /dev/null +++ b/plugins/buildpacks/src/triggers/post-extract/post-extract.go @@ -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) +} diff --git a/plugins/buildpacks/src/triggers/report/report.go b/plugins/buildpacks/src/triggers/report/report.go new file mode 100644 index 000000000..7f1e74440 --- /dev/null +++ b/plugins/buildpacks/src/triggers/report/report.go @@ -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, "") +} diff --git a/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/LICENSE @@ -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. diff --git a/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/README.md b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/README.md @@ -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. diff --git a/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/columnize.go @@ -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) +} diff --git a/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/buildpacks/src/vendor/github.com/ryanuber/columnize/columnize_test.go @@ -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 = "" + output := Format(input, config) + + expected := "Column A Column B Column C\n" + expected += "x 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) + } +} diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go new file mode 100644 index 000000000..88a3e0856 --- /dev/null +++ b/plugins/buildpacks/subcommands.go @@ -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 +} diff --git a/plugins/buildpacks/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/buildpacks/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/LICENSE @@ -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. diff --git a/plugins/buildpacks/vendor/github.com/ryanuber/columnize/README.md b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/README.md @@ -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. diff --git a/plugins/buildpacks/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/columnize.go @@ -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) +} diff --git a/plugins/buildpacks/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/buildpacks/vendor/github.com/ryanuber/columnize/columnize_test.go @@ -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 = "" + output := Format(input, config) + + expected := "Column A Column B Column C\n" + expected += "x 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) + } +} diff --git a/plugins/common/log.go b/plugins/common/log.go index dd3992e83..68f130ba0 100644 --- a/plugins/common/log.go +++ b/plugins/common/log.go @@ -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) } } diff --git a/plugins/common/properties.go b/plugins/common/properties.go index 61da772f3..f09d02b84 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -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 diff --git a/plugins/config/src/commands/commands.go b/plugins/config/src/commands/commands.go index 8d24c953c..04465d531 100644 --- a/plugins/config/src/commands/commands.go +++ b/plugins/config/src/commands/commands.go @@ -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)) } diff --git a/plugins/network/src/commands/commands.go b/plugins/network/src/commands/commands.go index e8d1fdcf8..b18de6202 100644 --- a/plugins/network/src/commands/commands.go +++ b/plugins/network/src/commands/commands.go @@ -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)) } diff --git a/plugins/network/src/triggers/install/install.go b/plugins/network/src/triggers/install/install.go index f78133512..cb4931ef7 100644 --- a/plugins/network/src/triggers/install/install.go +++ b/plugins/network/src/triggers/install/install.go @@ -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()) + } } } } diff --git a/plugins/network/src/triggers/post-create/post-create.go b/plugins/network/src/triggers/post-create/post-create.go index 4d03f0df3..c71d07b9c 100644 --- a/plugins/network/src/triggers/post-create/post-create.go +++ b/plugins/network/src/triggers/post-create/post-create.go @@ -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()) + } } diff --git a/plugins/network/src/triggers/post-delete/post-delete.go b/plugins/network/src/triggers/post-delete/post-delete.go index 841c5c84c..7d29808b5 100644 --- a/plugins/network/src/triggers/post-delete/post-delete.go +++ b/plugins/network/src/triggers/post-delete/post-delete.go @@ -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()) + } } diff --git a/plugins/repo/src/commands/commands.go b/plugins/repo/src/commands/commands.go index 8abaada25..03213873f 100644 --- a/plugins/repo/src/commands/commands.go +++ b/plugins/repo/src/commands/commands.go @@ -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)) } diff --git a/tests/unit/30_buildpacks.bats b/tests/unit/30_buildpacks.bats new file mode 100644 index 000000000..687da8f41 --- /dev/null +++ b/tests/unit/30_buildpacks.bats @@ -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 +} diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index e09af6ab0..d9e747382 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -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() {